server (/fs/upload): rewrite upload functional to use multipart
This commit is contained in:
parent
7b05a6d407
commit
03af3e63b2
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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 }))
|
||||||
|
|
|
@ -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")]
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue