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",
"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"

View File

@ -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"

View File

@ -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<Database>,
Extension(config): Extension<Config>,
AuthBearer(token): AuthBearer,
request: Result<Json<Request>, JsonRejection>,
mut multipart: Multipart,
query: Query<Pagination>,
) -> Result<Json<Response>, ServerError> {
let Json(request) = validate_json::<Request>(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 }))

View File

@ -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")]

View File

@ -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,

View File

@ -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,
}