diff --git a/Cargo.lock b/Cargo.lock index 12d1862..eda4e90 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -107,6 +107,7 @@ dependencies = [ "matchit", "memchr", "mime", + "multer", "percent-encoding", "pin-project-lite", "serde", @@ -388,6 +389,15 @@ version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" +[[package]] +name = "encoding_rs" +version = "0.8.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9852635589dc9f9ea1b6fe9f05b50ef208c85c834a562f0c6abb1c475736ec2b" +dependencies = [ + "cfg-if", +] + [[package]] name = "event-listener" version = "2.5.2" @@ -434,6 +444,21 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futures" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f73fe65f54d1e12b726f517d3e2135ca3125a437b6d998caf1962961f7172d9e" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.21" @@ -472,6 +497,12 @@ dependencies = [ "parking_lot 0.11.2", ] +[[package]] +name = "futures-io" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b" + [[package]] name = "futures-macro" version = "0.3.21" @@ -501,10 +532,13 @@ version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a" dependencies = [ + "futures-channel", "futures-core", + "futures-io", "futures-macro", "futures-sink", "futures-task", + "memchr", "pin-project-lite", "pin-utils", "slab", @@ -633,6 +667,7 @@ dependencies = [ "axum-auth", "base64", "byte-unit", + "futures", "homedisk-database", "homedisk-types", "hyper", @@ -915,6 +950,24 @@ dependencies = [ "winapi", ] +[[package]] +name = "multer" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f8f35e687561d5c1667590911e6698a8cb714a134a7505718a182e7bc9d3836" +dependencies = [ + "bytes", + "encoding_rs", + "futures-util", + "http", + "httparse", + "log", + "memchr", + "mime", + "spin 0.9.3", + "version_check", +] + [[package]] name = "nom" version = "7.1.1" diff --git a/server/Cargo.toml b/server/Cargo.toml index f05e827..0ab4ce2 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -4,7 +4,7 @@ version = "0.0.0" edition = "2021" [dependencies] -axum = "0.5.4" +axum = { version = "0.5.4", features = ["multipart"] } log = "0.4.17" thiserror = "1.0.31" serde = { version = "1.0.137", features = ["derive"] } @@ -17,3 +17,4 @@ axum-auth = "0.2.0" jsonwebtoken = "8.1.0" base64 = "0.13.0" byte-unit = "4.0.14" +futures = "0.3.21" diff --git a/server/src/fs/upload.rs b/server/src/fs/upload.rs index e9ef2de..a26132c 100644 --- a/server/src/fs/upload.rs +++ b/server/src/fs/upload.rs @@ -1,59 +1,76 @@ +use std::io::Write; use std::{fs, path::Path}; -use crate::fs::validate_path; -use axum::{extract::rejection::JsonRejection, Extension, Json}; +use axum::extract::{Multipart, Query}; +use axum::{Extension, Json}; use axum_auth::AuthBearer; +use futures::TryStreamExt; use homedisk_database::Database; use homedisk_types::{ config::types::Config, errors::{FsError, ServerError}, - fs::upload::{Request, Response}, + fs::upload::{Pagination, Response}, }; -use crate::middleware::{find_user, validate_json, validate_jwt}; +use crate::fs::validate_path; +use crate::middleware::{find_user, validate_jwt}; pub async fn handle( Extension(db): Extension, Extension(config): Extension, AuthBearer(token): AuthBearer, - request: Result, JsonRejection>, + mut multipart: Multipart, + query: Query, ) -> Result, ServerError> { - let Json(request) = validate_json::(request)?; let token = validate_jwt(config.jwt.secret.as_bytes(), &token)?; // validate the `path` can be used - validate_path(&request.path)?; + validate_path(&query.path)?; // search for a user by UUID from a token let user = find_user(db, token.claims.sub).await?; - // get file content - let content = base64::decode(request.content) - .map_err(|err| ServerError::FsError(FsError::Base64(err.to_string())))?; - - // directory where the file will be placed - let dir = format!( - "{user_dir}/{req_dir}", + // path to the file + let file_path = format!( + "{user_dir}/{request_path}", user_dir = user.user_dir(&config.storage.path), - req_dir = request.path + request_path = query.path ); - let path = Path::new(&dir); + let file_path = Path::new(&file_path); // check if the file currently exists to avoid overwriting it - if path.exists() { + if file_path.exists() { return Err(ServerError::FsError(FsError::FileAlreadyExists)); } // create a directory where the file will be placed // e.g. path ==> `/secret/files/images/screenshot.png` // directories up to `/home/homedisk/{username}/secret/files/images/` will be created - match path.parent() { - Some(prefix) => fs::create_dir_all(&prefix).unwrap(), + match file_path.parent() { + Some(prefix) => fs::create_dir_all(&prefix) + .map_err(|err| ServerError::FsError(FsError::CreateFile(err.to_string())))?, None => (), } + // get multipart field + let field = multipart + .next_field() + .await + .map_err(|_| ServerError::FsError(FsError::MultipartError))? + .ok_or(ServerError::FsError(FsError::MultipartError))?; + + // create file + let file = std::fs::File::create(&file_path) + .map_err(|err| ServerError::FsError(FsError::CreateFile(err.to_string())))?; + // write file - fs::write(&path, &content) + field + .try_fold((file, 0u64), |(mut file, written_len), bytes| async move { + file.write_all(bytes.as_ref()).expect("write file error"); + + Ok((file, written_len + bytes.len() as u64)) + }) + .await .map_err(|err| ServerError::FsError(FsError::WriteFile(err.to_string())))?; Ok(Json(Response { uploaded: true })) diff --git a/types/src/errors/fs.rs b/types/src/errors/fs.rs index ab99b0a..2825685 100644 --- a/types/src/errors/fs.rs +++ b/types/src/errors/fs.rs @@ -5,13 +5,19 @@ pub enum Error { #[error("file already exists")] FileAlreadyExists, - #[error("write file error - {0}")] + #[error("unexpected multipart error")] + MultipartError, + + #[error("create file - {0}")] + CreateFile(String), + + #[error("write file - {0}")] WriteFile(String), #[error("base64 - {0}")] Base64(String), - #[error("read dir error - {0}")] + #[error("read dir - {0}")] ReadDir(String), #[error("unknow error")] diff --git a/types/src/errors/mod.rs b/types/src/errors/mod.rs index 087a1a9..f266c7f 100644 --- a/types/src/errors/mod.rs +++ b/types/src/errors/mod.rs @@ -51,7 +51,9 @@ impl axum::response::IntoResponse for ServerError { AuthError::UnknownError(_) => StatusCode::INTERNAL_SERVER_ERROR, }, Self::FsError(ref err) => match err { + FsError::MultipartError => StatusCode::BAD_REQUEST, FsError::FileAlreadyExists => StatusCode::BAD_REQUEST, + FsError::CreateFile(_) => StatusCode::INTERNAL_SERVER_ERROR, FsError::WriteFile(_) => StatusCode::INTERNAL_SERVER_ERROR, FsError::Base64(_) => StatusCode::BAD_REQUEST, FsError::ReadDir(_) => StatusCode::BAD_REQUEST, diff --git a/types/src/fs/upload.rs b/types/src/fs/upload.rs index 498c22e..758c08e 100644 --- a/types/src/fs/upload.rs +++ b/types/src/fs/upload.rs @@ -1,8 +1,7 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, Serialize, Deserialize, Clone)] -pub struct Request { - pub content: String, +pub struct Pagination { pub path: String, }