server (/fs/upload): rewrite upload functional to use multipart

This commit is contained in:
MedzikUser 2022-05-04 21:46:07 +02:00
parent 7b05a6d407
commit 03af3e63b2
No known key found for this signature in database
GPG Key ID: A5FAC1E185C112DB
6 changed files with 103 additions and 25 deletions

53
Cargo.lock generated
View File

@ -107,6 +107,7 @@ dependencies = [
"matchit", "matchit",
"memchr", "memchr",
"mime", "mime",
"multer",
"percent-encoding", "percent-encoding",
"pin-project-lite", "pin-project-lite",
"serde", "serde",
@ -388,6 +389,15 @@ version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" 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]] [[package]]
name = "event-listener" name = "event-listener"
version = "2.5.2" version = "2.5.2"
@ -434,6 +444,21 @@ dependencies = [
"percent-encoding", "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]] [[package]]
name = "futures-channel" name = "futures-channel"
version = "0.3.21" version = "0.3.21"
@ -472,6 +497,12 @@ dependencies = [
"parking_lot 0.11.2", "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]] [[package]]
name = "futures-macro" name = "futures-macro"
version = "0.3.21" version = "0.3.21"
@ -501,10 +532,13 @@ version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a" checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a"
dependencies = [ dependencies = [
"futures-channel",
"futures-core", "futures-core",
"futures-io",
"futures-macro", "futures-macro",
"futures-sink", "futures-sink",
"futures-task", "futures-task",
"memchr",
"pin-project-lite", "pin-project-lite",
"pin-utils", "pin-utils",
"slab", "slab",
@ -633,6 +667,7 @@ dependencies = [
"axum-auth", "axum-auth",
"base64", "base64",
"byte-unit", "byte-unit",
"futures",
"homedisk-database", "homedisk-database",
"homedisk-types", "homedisk-types",
"hyper", "hyper",
@ -915,6 +950,24 @@ dependencies = [
"winapi", "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]] [[package]]
name = "nom" name = "nom"
version = "7.1.1" version = "7.1.1"

View File

@ -4,7 +4,7 @@ version = "0.0.0"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
axum = "0.5.4" axum = { version = "0.5.4", features = ["multipart"] }
log = "0.4.17" log = "0.4.17"
thiserror = "1.0.31" thiserror = "1.0.31"
serde = { version = "1.0.137", features = ["derive"] } serde = { version = "1.0.137", features = ["derive"] }
@ -17,3 +17,4 @@ axum-auth = "0.2.0"
jsonwebtoken = "8.1.0" jsonwebtoken = "8.1.0"
base64 = "0.13.0" base64 = "0.13.0"
byte-unit = "4.0.14" byte-unit = "4.0.14"
futures = "0.3.21"

View File

@ -1,59 +1,76 @@
use std::io::Write;
use std::{fs, path::Path}; use std::{fs, path::Path};
use crate::fs::validate_path; use axum::extract::{Multipart, Query};
use axum::{extract::rejection::JsonRejection, Extension, Json}; use axum::{Extension, Json};
use axum_auth::AuthBearer; use axum_auth::AuthBearer;
use futures::TryStreamExt;
use homedisk_database::Database; use homedisk_database::Database;
use homedisk_types::{ use homedisk_types::{
config::types::Config, config::types::Config,
errors::{FsError, ServerError}, 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( pub async fn handle(
Extension(db): Extension<Database>, Extension(db): Extension<Database>,
Extension(config): Extension<Config>, Extension(config): Extension<Config>,
AuthBearer(token): AuthBearer, AuthBearer(token): AuthBearer,
request: Result<Json<Request>, JsonRejection>, mut multipart: Multipart,
query: Query<Pagination>,
) -> Result<Json<Response>, ServerError> { ) -> Result<Json<Response>, ServerError> {
let Json(request) = validate_json::<Request>(request)?;
let token = validate_jwt(config.jwt.secret.as_bytes(), &token)?; let token = validate_jwt(config.jwt.secret.as_bytes(), &token)?;
// validate the `path` can be used // validate the `path` can be used
validate_path(&request.path)?; validate_path(&query.path)?;
// search for a user by UUID from a token // search for a user by UUID from a token
let user = find_user(db, token.claims.sub).await?; let user = find_user(db, token.claims.sub).await?;
// get file content // path to the file
let content = base64::decode(request.content) let file_path = format!(
.map_err(|err| ServerError::FsError(FsError::Base64(err.to_string())))?; "{user_dir}/{request_path}",
// directory where the file will be placed
let dir = format!(
"{user_dir}/{req_dir}",
user_dir = user.user_dir(&config.storage.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 // check if the file currently exists to avoid overwriting it
if path.exists() { if file_path.exists() {
return Err(ServerError::FsError(FsError::FileAlreadyExists)); return Err(ServerError::FsError(FsError::FileAlreadyExists));
} }
// create a directory where the file will be placed // create a directory where the file will be placed
// e.g. path ==> `/secret/files/images/screenshot.png` // e.g. path ==> `/secret/files/images/screenshot.png`
// directories up to `/home/homedisk/{username}/secret/files/images/` will be created // directories up to `/home/homedisk/{username}/secret/files/images/` will be created
match path.parent() { match file_path.parent() {
Some(prefix) => fs::create_dir_all(&prefix).unwrap(), Some(prefix) => fs::create_dir_all(&prefix)
.map_err(|err| ServerError::FsError(FsError::CreateFile(err.to_string())))?,
None => (), 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 // 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())))?; .map_err(|err| ServerError::FsError(FsError::WriteFile(err.to_string())))?;
Ok(Json(Response { uploaded: true })) Ok(Json(Response { uploaded: true }))

View File

@ -5,13 +5,19 @@ pub enum Error {
#[error("file already exists")] #[error("file already exists")]
FileAlreadyExists, FileAlreadyExists,
#[error("write file error - {0}")] #[error("unexpected multipart error")]
MultipartError,
#[error("create file - {0}")]
CreateFile(String),
#[error("write file - {0}")]
WriteFile(String), WriteFile(String),
#[error("base64 - {0}")] #[error("base64 - {0}")]
Base64(String), Base64(String),
#[error("read dir error - {0}")] #[error("read dir - {0}")]
ReadDir(String), ReadDir(String),
#[error("unknow error")] #[error("unknow error")]

View File

@ -51,7 +51,9 @@ impl axum::response::IntoResponse for ServerError {
AuthError::UnknownError(_) => StatusCode::INTERNAL_SERVER_ERROR, AuthError::UnknownError(_) => StatusCode::INTERNAL_SERVER_ERROR,
}, },
Self::FsError(ref err) => match err { Self::FsError(ref err) => match err {
FsError::MultipartError => StatusCode::BAD_REQUEST,
FsError::FileAlreadyExists => StatusCode::BAD_REQUEST, FsError::FileAlreadyExists => StatusCode::BAD_REQUEST,
FsError::CreateFile(_) => StatusCode::INTERNAL_SERVER_ERROR,
FsError::WriteFile(_) => StatusCode::INTERNAL_SERVER_ERROR, FsError::WriteFile(_) => StatusCode::INTERNAL_SERVER_ERROR,
FsError::Base64(_) => StatusCode::BAD_REQUEST, FsError::Base64(_) => StatusCode::BAD_REQUEST,
FsError::ReadDir(_) => StatusCode::BAD_REQUEST, FsError::ReadDir(_) => StatusCode::BAD_REQUEST,

View File

@ -1,8 +1,7 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Request { pub struct Pagination {
pub content: String,
pub path: String, pub path: String,
} }