Final preparation
This commit is contained in:
parent
ab138e8536
commit
a3e4ac2b2e
10
.dockerignore
Normal file
10
.dockerignore
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
**/target/
|
||||||
|
**/.vscode/
|
||||||
|
**/.env
|
||||||
|
**/.git/
|
||||||
|
**/.dockerignore
|
||||||
|
**/Dockerfile
|
||||||
|
**/compose.yaml
|
||||||
|
**/LICENSE
|
||||||
|
**/README.md
|
||||||
|
files/
|
8
Cargo.lock
generated
8
Cargo.lock
generated
@ -1732,18 +1732,18 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.207"
|
version = "1.0.208"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5665e14a49a4ea1b91029ba7d3bca9f299e1f7cfa194388ccc20f14743e784f2"
|
checksum = "cff085d2cb684faa248efb494c39b68e522822ac0de72ccf08109abde717cfb2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_derive"
|
name = "serde_derive"
|
||||||
version = "1.0.207"
|
version = "1.0.208"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6aea2634c86b0e8ef2cfdc0c340baede54ec27b1e46febd7f80dffb2aa44a00e"
|
checksum = "24008e81ff7613ed8e5ba0cfaf24e2c2f1e5b8a0495711e44fcd4882fca62bcf"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
@ -22,7 +22,7 @@ axum-extra = { version = "0.9", features = ["typed-header"] }
|
|||||||
chrono = { version = "0.4", features = ["serde"] }
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
dotenvy = "0.15"
|
dotenvy = "0.15"
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
itertools = "0.13.0"
|
itertools = "0.13"
|
||||||
jsonwebtoken = "9"
|
jsonwebtoken = "9"
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
scrypt = { version = "0.11", default-features = false, features = ["std"] }
|
scrypt = { version = "0.11", default-features = false, features = ["std"] }
|
||||||
|
19
Dockerfile
Normal file
19
Dockerfile
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
FROM rust:slim AS chef
|
||||||
|
RUN cargo install cargo-chef
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
FROM chef AS planner
|
||||||
|
COPY . .
|
||||||
|
RUN cargo chef prepare
|
||||||
|
|
||||||
|
FROM chef AS builder
|
||||||
|
COPY --from=planner /app/recipe.json recipe.json
|
||||||
|
RUN cargo chef cook --release
|
||||||
|
COPY . .
|
||||||
|
RUN cargo b -r
|
||||||
|
|
||||||
|
FROM debian:stable-slim
|
||||||
|
EXPOSE 3000
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=builder /app/target/release/project .
|
||||||
|
CMD [ "./project" ]
|
25
compose-dev.yaml
Normal file
25
compose-dev.yaml
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
services:
|
||||||
|
backend:
|
||||||
|
build: .
|
||||||
|
ports:
|
||||||
|
- 3000:3000
|
||||||
|
environment:
|
||||||
|
JWT_SECRET: ${JWT_SECRET}
|
||||||
|
DATABASE_URL: 'postgresql://tester:testing123!@backend_db/backend'
|
||||||
|
depends_on:
|
||||||
|
- backend_db
|
||||||
|
|
||||||
|
backend_db:
|
||||||
|
image: ghcr.io/fboulnois/pg_uuidv7:1.5.0
|
||||||
|
environment:
|
||||||
|
- POSTGRES_USER=tester
|
||||||
|
- POSTGRES_PASSWORD=testing123!
|
||||||
|
- POSTGRES_DB=backend
|
||||||
|
ports:
|
||||||
|
- 5432:5432
|
||||||
|
volumes:
|
||||||
|
- backend_db_data:/var/lib/postgresql/data
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
backend_db_data:
|
19
compose.yaml
19
compose.yaml
@ -1,15 +1,22 @@
|
|||||||
services:
|
services:
|
||||||
db:
|
backend:
|
||||||
|
build: .
|
||||||
|
environment:
|
||||||
|
JWT_SECRET: ${JWT_SECRET}
|
||||||
|
DATABASE_URL: 'postgresql://tester:testing123!@backend_db/backend'
|
||||||
|
depends_on:
|
||||||
|
- backend_db
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
backend_db:
|
||||||
image: ghcr.io/fboulnois/pg_uuidv7:1.5.0
|
image: ghcr.io/fboulnois/pg_uuidv7:1.5.0
|
||||||
environment:
|
environment:
|
||||||
- POSTGRES_USER=tester
|
- POSTGRES_USER=tester
|
||||||
- POSTGRES_PASSWORD=testing123!
|
- POSTGRES_PASSWORD=testing123!
|
||||||
- POSTGRES_DB=testing
|
- POSTGRES_DB=backend
|
||||||
ports:
|
|
||||||
- 5432:5432
|
|
||||||
volumes:
|
volumes:
|
||||||
- postgres_data:/var/lib/postgresql/data
|
- backend_db_data:/var/lib/postgresql/data
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
postgres_data:
|
backend_db_data:
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
use std::collections::{HashMap, HashSet};
|
use std::{
|
||||||
|
collections::{HashMap, HashSet},
|
||||||
|
fmt::Write as _,
|
||||||
|
};
|
||||||
|
|
||||||
use axum::extract::multipart::{self, Multipart};
|
use axum::extract::multipart::{self, Multipart};
|
||||||
use tokio::io::AsyncWrite;
|
|
||||||
|
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
|
||||||
@ -10,25 +12,50 @@ pub struct Params {
|
|||||||
parent_folder: Uuid,
|
parent_folder: Uuid,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Debug, Default)]
|
||||||
|
pub struct Response {
|
||||||
|
success: HashMap<Box<str>, Uuid>,
|
||||||
|
error: HashMap<Box<str>, &'static str>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_name(name: &str, existing_names: &HashSet<String>) -> Result<(), &'static str> {
|
||||||
|
if name.len() > 255 {
|
||||||
|
return Err("Name too long");
|
||||||
|
}
|
||||||
|
if existing_names.contains(name) {
|
||||||
|
return Err("Item with that name already exists");
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
async fn create_file(
|
async fn create_file(
|
||||||
file_id: Uuid,
|
storage: &crate::FileStorage,
|
||||||
file: impl AsyncWrite + Unpin,
|
|
||||||
file_name: &str,
|
file_name: &str,
|
||||||
field: &mut multipart::Field<'_>,
|
field: &mut multipart::Field<'_>,
|
||||||
parent_folder: Uuid,
|
parent_folder: Uuid,
|
||||||
pool: &Pool,
|
pool: &Pool,
|
||||||
) -> bool {
|
) -> anyhow::Result<Uuid> {
|
||||||
let (hash, size) = match crate::FileStorage::write_to_file(file, field).await {
|
let (file_id, file) = storage.create().await?;
|
||||||
Ok(values) => values,
|
let (hash, size) = crate::FileStorage::write_to_file(file, field).await?;
|
||||||
Err(err) => {
|
db::file::insert(file_id, parent_folder, file_name, size, hash, pool).await?;
|
||||||
tracing::warn!(%err);
|
Ok(file_id)
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
db::file::insert(file_id, parent_folder, file_name, size, hash, pool)
|
async fn parse_field(
|
||||||
|
field: &mut multipart::Field<'_>,
|
||||||
|
name: &str,
|
||||||
|
storage: &crate::FileStorage,
|
||||||
|
parent_folder: Uuid,
|
||||||
|
pool: &Pool,
|
||||||
|
existing_names: &HashSet<String>,
|
||||||
|
) -> Result<Uuid, &'static str> {
|
||||||
|
validate_name(name, existing_names)?;
|
||||||
|
create_file(storage, name, field, parent_folder, pool)
|
||||||
.await
|
.await
|
||||||
.inspect_err(|err| tracing::warn!(%err))
|
.map_err(|err| {
|
||||||
.is_ok()
|
tracing::warn!(%err, "Error creating the file");
|
||||||
|
"Error creating the file"
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn upload(
|
pub async fn upload(
|
||||||
@ -36,7 +63,7 @@ pub async fn upload(
|
|||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
claims: Claims,
|
claims: Claims,
|
||||||
mut multi: Multipart,
|
mut multi: Multipart,
|
||||||
) -> GeneralResult<Json<HashMap<String, Uuid>>> {
|
) -> GeneralResult<Json<Response>> {
|
||||||
db::folder::get_permissions(params.parent_folder, claims.user_id, &state.pool)
|
db::folder::get_permissions(params.parent_folder, claims.user_id, &state.pool)
|
||||||
.await
|
.await
|
||||||
.can_write_guard()?;
|
.can_write_guard()?;
|
||||||
@ -46,39 +73,47 @@ pub async fn upload(
|
|||||||
.await
|
.await
|
||||||
.handle_internal("Error getting existing names")?;
|
.handle_internal("Error getting existing names")?;
|
||||||
|
|
||||||
let mut result = HashMap::new();
|
let mut response = Response::default();
|
||||||
while let Ok(Some(mut field)) = multi.next_field().await {
|
while let Ok(Some(mut field)) = multi.next_field().await {
|
||||||
let Some(file_name) = field.file_name().map(ToOwned::to_owned) else {
|
let Some(file_name) = field.file_name().map(Box::<str>::from) else {
|
||||||
continue;
|
|
||||||
};
|
|
||||||
if existing_names.contains(&file_name) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if file_name.len() > 50 {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let Ok((file_id, mut file)) = state.storage.create().await else {
|
|
||||||
tracing::warn!("Couldn't create uuid for new file");
|
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
let is_success = create_file(
|
let parse_result = parse_field(
|
||||||
file_id,
|
|
||||||
&mut file,
|
|
||||||
&file_name,
|
|
||||||
&mut field,
|
&mut field,
|
||||||
|
&file_name,
|
||||||
|
&state.storage,
|
||||||
params.parent_folder,
|
params.parent_folder,
|
||||||
&state.pool,
|
&state.pool,
|
||||||
|
&existing_names,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
if !is_success {
|
|
||||||
let _ = state.storage.delete(file_id).await;
|
match parse_result {
|
||||||
continue;
|
Ok(uuid) => {
|
||||||
|
response.success.insert(file_name, uuid);
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
response.error.insert(file_name, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
result.insert(file_name, file_id);
|
if !response.success.is_empty() {
|
||||||
|
return Ok(Json(response));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Json(result))
|
if response.error.is_empty() {
|
||||||
|
return Err(GeneralError::message(
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
"No files sent",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut message = "No file successfully uploaded:".to_owned();
|
||||||
|
for (key, val) in response.error {
|
||||||
|
write!(message, "\n{key}: {val}").unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(GeneralError::message(StatusCode::BAD_REQUEST, message))
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,13 @@ pub async fn create(
|
|||||||
.await
|
.await
|
||||||
.can_write_guard()?;
|
.can_write_guard()?;
|
||||||
|
|
||||||
|
if params.folder_name.len() > 255 {
|
||||||
|
return Err(GeneralError::message(
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
"Folder name too long",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
let exists = db::folder::name_exists(params.parent_folder_id, ¶ms.folder_name, &pool)
|
let exists = db::folder::name_exists(params.parent_folder_id, ¶ms.folder_name, &pool)
|
||||||
.await
|
.await
|
||||||
.handle_internal("Error getting existing names")?;
|
.handle_internal("Error getting existing names")?;
|
||||||
|
@ -16,7 +16,7 @@ pub async fn get(
|
|||||||
) -> GeneralResult<Json<HashMap<i32, PermissionRaw>>> {
|
) -> GeneralResult<Json<HashMap<i32, PermissionRaw>>> {
|
||||||
db::folder::get_permissions(params.folder_id, claims.user_id, &pool)
|
db::folder::get_permissions(params.folder_id, claims.user_id, &pool)
|
||||||
.await
|
.await
|
||||||
.can_manage_guard()?;
|
.can_read_guard()?;
|
||||||
|
|
||||||
db::permissions::get_all_for_folder(params.folder_id, &pool)
|
db::permissions::get_all_for_folder(params.folder_id, &pool)
|
||||||
.await
|
.await
|
||||||
|
@ -33,11 +33,12 @@ fn validate_password(password: &str) -> Result<(), ValidationError> {
|
|||||||
has_special = true;
|
has_special = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let error_msgs = [has_lower, has_upper, has_number, has_special]
|
let msg = [has_lower, has_upper, has_number, has_special]
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.zip(["No lower", "No upper", "No numbers", "No special"])
|
.zip(["No lower", "No upper", "No numbers", "No special"])
|
||||||
.filter_map(|(param, msg)| (!param).then_some(msg));
|
.filter_map(|(param, msg)| (!param).then_some(msg))
|
||||||
let msg = error_msgs.format(" ").to_string();
|
.format(" ")
|
||||||
|
.to_string();
|
||||||
if !msg.is_empty() {
|
if !msg.is_empty() {
|
||||||
return Err(ValidationError::new("invalid_password").with_message(msg.into()));
|
return Err(ValidationError::new("invalid_password").with_message(msg.into()));
|
||||||
}
|
}
|
||||||
|
54
src/main.rs
54
src/main.rs
@ -44,11 +44,37 @@ async fn create_test_users(pool: &Pool) -> anyhow::Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn init_tracing() {
|
||||||
|
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
||||||
|
|
||||||
|
let mut err = None;
|
||||||
|
|
||||||
|
tracing_subscriber::registry()
|
||||||
|
.with(
|
||||||
|
tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|inner_err| {
|
||||||
|
err = Some(inner_err);
|
||||||
|
"debug,sqlx=info,axum::rejection=trace".parse().unwrap()
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.with(tracing_subscriber::fmt::layer())
|
||||||
|
.init();
|
||||||
|
|
||||||
|
if let Some(err) = err {
|
||||||
|
tracing::info!(
|
||||||
|
%err,
|
||||||
|
"Error constructing EnvFilter, falling back to using the default"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> anyhow::Result<()> {
|
async fn main() -> anyhow::Result<()> {
|
||||||
|
if env::var("RUST_BACKTRACE").is_err() {
|
||||||
|
env::set_var("RUST_BACKTRACE", "1");
|
||||||
|
}
|
||||||
let _ = dotenvy::dotenv();
|
let _ = dotenvy::dotenv();
|
||||||
|
|
||||||
tracing_subscriber::fmt::init();
|
init_tracing();
|
||||||
|
|
||||||
auth::force_init_keys();
|
auth::force_init_keys();
|
||||||
|
|
||||||
@ -109,12 +135,32 @@ fn app(state: AppState) -> Router {
|
|||||||
permissions::{self, get_top_level::get_top_level},
|
permissions::{self, get_top_level::get_top_level},
|
||||||
users,
|
users,
|
||||||
};
|
};
|
||||||
use tower_http::ServiceBuilderExt as _;
|
use tower_http::{
|
||||||
|
trace::{MakeSpan, TraceLayer},
|
||||||
|
ServiceBuilderExt as _,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
struct SpanMaker;
|
||||||
|
|
||||||
|
impl<B> MakeSpan<B> for SpanMaker {
|
||||||
|
fn make_span(&mut self, request: &axum::http::Request<B>) -> tracing::Span {
|
||||||
|
tracing::debug_span!(
|
||||||
|
"request",
|
||||||
|
method = %request.method(),
|
||||||
|
uri = %request.uri(),
|
||||||
|
version = ?request.version(),
|
||||||
|
headers = ?request.headers(),
|
||||||
|
request_id = %uuid::Uuid::now_v7()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const TEN_GIBIBYTES: usize = 10 * 1024 * 1024 * 1024;
|
||||||
let middleware = tower::ServiceBuilder::new()
|
let middleware = tower::ServiceBuilder::new()
|
||||||
.layer(DefaultBodyLimit::disable())
|
.layer(DefaultBodyLimit::max(TEN_GIBIBYTES))
|
||||||
.sensitive_headers([header::AUTHORIZATION, header::COOKIE])
|
.sensitive_headers([header::AUTHORIZATION, header::COOKIE])
|
||||||
.trace_for_http()
|
.layer(TraceLayer::new_for_http().make_span_with(SpanMaker))
|
||||||
.compression();
|
.compression();
|
||||||
|
|
||||||
// Build route service
|
// Build route service
|
||||||
|
Reference in New Issue
Block a user