Compare commits

...

6 Commits

14 changed files with 759 additions and 30 deletions

307
Cargo.lock generated
View File

@ -39,6 +39,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d6fd624c75e18b3b4c6b9caf42b1afe24437daaee904069137d8bab077be8b8" checksum = "6d6fd624c75e18b3b4c6b9caf42b1afe24437daaee904069137d8bab077be8b8"
dependencies = [ dependencies = [
"axum-core", "axum-core",
"axum-macros",
"bytes", "bytes",
"form_urlencoded", "form_urlencoded",
"futures-util", "futures-util",
@ -51,6 +52,7 @@ dependencies = [
"matchit", "matchit",
"memchr", "memchr",
"mime", "mime",
"multer",
"percent-encoding", "percent-encoding",
"pin-project-lite", "pin-project-lite",
"rustversion", "rustversion",
@ -86,6 +88,17 @@ dependencies = [
"tracing", "tracing",
] ]
[[package]]
name = "axum-macros"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "backtrace" name = "backtrace"
version = "0.3.74" version = "0.3.74"
@ -125,8 +138,15 @@ version = "0.1.0"
dependencies = [ dependencies = [
"axum", "axum",
"figment", "figment",
"futures",
"futures-util",
"problem_details",
"serde", "serde",
"tokio", "tokio",
"tokio-stream",
"tracing",
"tracing-log",
"tracing-subscriber",
] ]
[[package]] [[package]]
@ -135,6 +155,15 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "encoding_rs"
version = "0.8.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
dependencies = [
"cfg-if",
]
[[package]] [[package]]
name = "equivalent" name = "equivalent"
version = "1.0.1" version = "1.0.1"
@ -148,6 +177,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8cb01cd46b0cf372153850f4c6c272d9cbea2da513e07538405148f95bd789f3" checksum = "8cb01cd46b0cf372153850f4c6c272d9cbea2da513e07538405148f95bd789f3"
dependencies = [ dependencies = [
"atomic", "atomic",
"pear",
"serde", "serde",
"toml", "toml",
"uncased", "uncased",
@ -169,6 +199,21 @@ dependencies = [
"percent-encoding", "percent-encoding",
] ]
[[package]]
name = "futures"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
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.31" version = "0.3.31"
@ -176,6 +221,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
dependencies = [ dependencies = [
"futures-core", "futures-core",
"futures-sink",
] ]
[[package]] [[package]]
@ -184,6 +230,40 @@ version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
[[package]]
name = "futures-executor"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
dependencies = [
"futures-core",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-io"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
[[package]]
name = "futures-macro"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "futures-sink"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
[[package]] [[package]]
name = "futures-task" name = "futures-task"
version = "0.3.31" version = "0.3.31"
@ -196,10 +276,16 @@ version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
dependencies = [ dependencies = [
"futures-channel",
"futures-core", "futures-core",
"futures-io",
"futures-macro",
"futures-sink",
"futures-task", "futures-task",
"memchr",
"pin-project-lite", "pin-project-lite",
"pin-utils", "pin-utils",
"slab",
] ]
[[package]] [[package]]
@ -248,6 +334,16 @@ dependencies = [
"pin-project-lite", "pin-project-lite",
] ]
[[package]]
name = "http-serde"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f056c8559e3757392c8d091e796416e4649d8e49e88b8d76df6c002f05027fd"
dependencies = [
"http",
"serde",
]
[[package]] [[package]]
name = "httparse" name = "httparse"
version = "1.9.5" version = "1.9.5"
@ -305,12 +401,24 @@ dependencies = [
"hashbrown", "hashbrown",
] ]
[[package]]
name = "inlinable_string"
version = "0.1.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb"
[[package]] [[package]]
name = "itoa" name = "itoa"
version = "1.0.14" version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674"
[[package]]
name = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.169" version = "0.2.169"
@ -371,6 +479,33 @@ dependencies = [
"windows-sys", "windows-sys",
] ]
[[package]]
name = "multer"
version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b"
dependencies = [
"bytes",
"encoding_rs",
"futures-util",
"http",
"httparse",
"memchr",
"mime",
"spin",
"version_check",
]
[[package]]
name = "nu-ansi-term"
version = "0.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
dependencies = [
"overload",
"winapi",
]
[[package]] [[package]]
name = "object" name = "object"
version = "0.36.7" version = "0.36.7"
@ -386,6 +521,12 @@ version = "1.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
[[package]]
name = "overload"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[package]] [[package]]
name = "parking_lot" name = "parking_lot"
version = "0.12.3" version = "0.12.3"
@ -409,6 +550,29 @@ dependencies = [
"windows-targets", "windows-targets",
] ]
[[package]]
name = "pear"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61a386cd715229d399604b50d1361683fe687066f42d56f54be995bc6868f71c"
dependencies = [
"inlinable_string",
"pear_codegen",
"yansi",
]
[[package]]
name = "pear_codegen"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da9f0f13dac8069c139e8300a6510e3f4143ecf5259c60b116a9b271b4ca0d54"
dependencies = [
"proc-macro2",
"proc-macro2-diagnostics",
"quote",
"syn",
]
[[package]] [[package]]
name = "percent-encoding" name = "percent-encoding"
version = "2.3.1" version = "2.3.1"
@ -427,6 +591,19 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "problem_details"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ee2055bf7d8f34bd11bf85388b878bf244256015f1ed290b6b717c0e97bb478"
dependencies = [
"axum",
"http",
"http-serde",
"serde",
"serde_json",
]
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.92" version = "1.0.92"
@ -436,6 +613,19 @@ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]]
name = "proc-macro2-diagnostics"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8"
dependencies = [
"proc-macro2",
"quote",
"syn",
"version_check",
"yansi",
]
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.38" version = "1.0.38"
@ -541,6 +731,15 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "sharded-slab"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
dependencies = [
"lazy_static",
]
[[package]] [[package]]
name = "signal-hook-registry" name = "signal-hook-registry"
version = "1.4.2" version = "1.4.2"
@ -550,6 +749,15 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "slab"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
dependencies = [
"autocfg",
]
[[package]] [[package]]
name = "smallvec" name = "smallvec"
version = "1.13.2" version = "1.13.2"
@ -566,6 +774,12 @@ dependencies = [
"windows-sys", "windows-sys",
] ]
[[package]]
name = "spin"
version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.95" version = "2.0.95"
@ -583,6 +797,16 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
[[package]]
name = "thread_local"
version = "1.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c"
dependencies = [
"cfg-if",
"once_cell",
]
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.42.0" version = "1.42.0"
@ -612,6 +836,17 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "tokio-stream"
version = "0.1.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047"
dependencies = [
"futures-core",
"pin-project-lite",
"tokio",
]
[[package]] [[package]]
name = "toml" name = "toml"
version = "0.8.19" version = "0.8.19"
@ -682,9 +917,21 @@ checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
dependencies = [ dependencies = [
"log", "log",
"pin-project-lite", "pin-project-lite",
"tracing-attributes",
"tracing-core", "tracing-core",
] ]
[[package]]
name = "tracing-attributes"
version = "0.1.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "tracing-core" name = "tracing-core"
version = "0.1.33" version = "0.1.33"
@ -692,6 +939,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c"
dependencies = [ dependencies = [
"once_cell", "once_cell",
"valuable",
]
[[package]]
name = "tracing-log"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
dependencies = [
"log",
"once_cell",
"tracing-core",
]
[[package]]
name = "tracing-subscriber"
version = "0.3.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
dependencies = [
"nu-ansi-term",
"sharded-slab",
"smallvec",
"thread_local",
"tracing-core",
"tracing-log",
] ]
[[package]] [[package]]
@ -709,6 +982,12 @@ version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83"
[[package]]
name = "valuable"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
[[package]] [[package]]
name = "version_check" name = "version_check"
version = "0.9.5" version = "0.9.5"
@ -721,6 +1000,28 @@ version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.52.0" version = "0.52.0"
@ -802,3 +1103,9 @@ checksum = "39281189af81c07ec09db316b302a3e67bf9bd7cbf6c820b50e35fee9c2fa980"
dependencies = [ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "yansi"
version = "1.0.0-rc"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ee746ad3851dd3bc40e4a028ab3b00b99278d929e48957bcb2d111874a7e43e"

View File

@ -1,2 +0,0 @@
port = 3000
listen = "0.0.0.0"

View File

@ -2,9 +2,18 @@
name = "casket-backend" name = "casket-backend"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2021"
description = "Backend for the casket file cloud."
repository = "https://git.nifni.eu/nif/casket"
[dependencies] [dependencies]
axum = { version = "0.8.1" } axum = { version = "0.8.1", features = ["multipart", "macros"] }
tokio = { version = "1.42.0", features = ["full"] } tokio = { version = "1.42.0", features = ["full"] }
figment = { version = "0.10.19", features = ["toml"] } tokio-stream = "0.1.17"
serde = {version = "1.0.217", features = ["derive"]} futures = "0.3"
futures-util = "0.3"
figment = { version = "0.10.19", features = ["toml", "env"] }
serde = { version = "1.0.217", features = ["derive"] }
tracing = "0.1.41"
tracing-log = "0.2.0"
tracing-subscriber = "0.3.19"
problem_details = { version = "0.7.0", features = ["axum"] }

25
casket-backend/README.md Normal file
View File

@ -0,0 +1,25 @@
# Casket backend
Backend server for casket.
## Running
```bash
cd casket-backend
cargo run
```
## Configuration
See [casket.toml](./casket.toml) for an example config file. By default, the following files are checked
to exist:
- `./casket-backend/casket.toml`
- `/config/casket.toml`
Additionally, it is possible to provide configs via environment variables. Use the prefix `CASKET_`.
For nested configs use two underscores to mark the place to split:
```bash
CASKET_SERVER__port=1234
```

View File

@ -0,0 +1,6 @@
[server]
port = 3000
bind_address = "127.0.0.1"
[files]
directory = "/tmp/casket"

View File

@ -1,15 +1,32 @@
use figment::providers::{Env, Format};
use figment::{providers::Toml, Figment}; use figment::{providers::Toml, Figment};
use figment::providers::Format;
use serde::Deserialize; use serde::Deserialize;
use std::path::PathBuf;
pub const CONFIG_LOCATIONS: [&str; 2] = ["casket-backend/casket.toml", "/config/casket.toml"];
#[derive(Deserialize, Clone, Debug)] #[derive(Deserialize, Clone, Debug)]
pub struct Config { pub struct Config {
port: u16, pub server: Server,
listen: String, pub files: Files,
}
#[derive(Deserialize, Clone, Debug)]
pub struct Server {
pub port: u16,
pub bind_address: String,
}
#[derive(Deserialize, Clone, Debug)]
pub struct Files {
pub directory: PathBuf,
} }
pub fn get_config() -> figment::Result<Config> { pub fn get_config() -> figment::Result<Config> {
Figment::new() CONFIG_LOCATIONS
.merge(Toml::file("application.toml")) .iter()
.fold(Figment::new(), |figment, location| {
figment.merge(Toml::file(location))
})
.join(Env::prefixed("CASKET_").split("__"))
.extract() .extract()
} }

View File

@ -0,0 +1,17 @@
use axum::http::StatusCode;
use problem_details::ProblemDetails;
use std::fmt::Debug;
use tracing::error;
pub const ERROR_DETAILS_PATH_TRAVERSAL: &str = "You must not traverse!";
pub const ERROR_DETAILS_INTERNAL_ERROR: &str =
"Internal Error! Check the logs of the backend service for details.";
pub const ERROR_DETAILS_ABSOLUTE_PATH_NOT_ALLOWED: &str = "Absolute paths are not allowed.";
pub const ERROR_DETAILS_NO_NAME_PROVIDED_MULTIPART_FIELD: &str =
"No name provided for multipart form field";
pub fn to_internal_error(error: impl Debug) -> ProblemDetails {
error!("{:?}", error);
ProblemDetails::from_status_code(StatusCode::INTERNAL_SERVER_ERROR)
.with_detail(ERROR_DETAILS_INTERNAL_ERROR)
}

View File

@ -0,0 +1,24 @@
use axum::extract::FromRequestParts;
use axum::http::request::Parts;
use axum::http::StatusCode;
use problem_details::ProblemDetails;
use std::fmt::Debug;
pub struct WithProblemDetails<T>(pub T);
impl<T, S> FromRequestParts<S> for WithProblemDetails<T>
where
T: FromRequestParts<S>,
T::Rejection: Debug,
S: Sync,
{
type Rejection = ProblemDetails;
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
match T::from_request_parts(parts, state).await {
Ok(inner) => Ok(Self(inner)),
Err(rejection) => Err(ProblemDetails::from_status_code(StatusCode::BAD_REQUEST)
.with_detail(format!("{rejection:?}"))),
}
}
}

View File

@ -0,0 +1,107 @@
use crate::errors::to_internal_error;
use crate::extractor_helper::WithProblemDetails;
use crate::files::build_system_path;
use crate::{errors, AppState};
use axum::extract::{Query, State};
use axum::http::StatusCode;
use axum::{debug_handler, Json};
use problem_details::ProblemDetails;
use serde::{Deserialize, Serialize};
use std::io::Error;
use std::path::PathBuf;
use tokio::fs::{read_dir, DirEntry};
use tracing::{debug, error};
#[debug_handler]
pub async fn query_file_tree(
State(state): State<AppState>,
axum::extract::Path(user_id): axum::extract::Path<String>,
WithProblemDetails(Query(query)): WithProblemDetails<Query<FileQuery>>,
) -> Result<Json<PathElements>, ProblemDetails> {
let path = build_system_path(&state.config.files.directory, &user_id, &query.path)?;
debug!("Loading path: {:?}", path);
Ok(PathElements::new(load_directory(path, query.nesting).await?).into())
}
async fn load_directory(path: PathBuf, nesting: u32) -> Result<Vec<PathElement>, ProblemDetails> {
match read_dir(path).await {
Err(error) => Err(map_io_error(error)),
Ok(mut value) => {
let mut result = vec![];
while let Some(next) = value.next_entry().await.map_err(map_io_error)? {
result.push(PathElement::from(next, nesting).await?);
}
Ok(result)
}
}
}
fn map_io_error(error: Error) -> ProblemDetails {
match error.kind() {
std::io::ErrorKind::NotFound => ProblemDetails::from_status_code(StatusCode::NOT_FOUND),
_ => to_internal_error(error),
}
}
#[derive(Deserialize)]
pub struct FileQuery {
path: PathBuf,
#[serde(default = "u32::min_value")]
nesting: u32,
}
#[derive(Serialize)]
pub struct PathElements {
files: Vec<PathElement>,
}
impl PathElements {
pub fn new(files: Vec<PathElement>) -> Self {
Self { files }
}
}
#[derive(Serialize)]
pub struct PathElement {
name: String,
is_dir: bool,
children: Option<Vec<PathElement>>,
}
impl PathElement {
async fn from(value: DirEntry, nesting: u32) -> Result<Self, ProblemDetails> {
let metadata = value.metadata().await.unwrap();
debug!("File type: {:?}", value.file_type().await.unwrap());
let is_dir = metadata.is_dir();
if is_dir || metadata.is_file() {
Ok(PathElement {
name: value.file_name().into_string().unwrap(),
is_dir,
children: load_children(value.path(), nesting, is_dir).await?,
})
} else {
error!(
"File <{:?}> is neither a directory nor a file. ",
value.file_name()
);
Err(
ProblemDetails::from_status_code(StatusCode::INTERNAL_SERVER_ERROR)
.with_detail(errors::ERROR_DETAILS_INTERNAL_ERROR),
)
}
}
}
async fn load_children(
path: PathBuf,
nesting: u32,
is_dir: bool,
) -> Result<Option<Vec<PathElement>>, ProblemDetails> {
if nesting == 0 {
Ok(None)
} else if !is_dir {
Ok(Some(vec![]))
} else {
Ok(Some(Box::pin(load_directory(path, nesting - 1)).await?))
}
}

View File

@ -0,0 +1,121 @@
use crate::errors;
use axum::http::StatusCode;
use problem_details::ProblemDetails;
use std::path::{Component, Path, PathBuf};
pub mod list;
pub mod upload;
fn build_system_path(
base_directory: &Path,
user_id: &str,
user_path: &PathBuf,
) -> Result<PathBuf, ProblemDetails> {
if user_path.is_absolute() {
Err(
ProblemDetails::from_status_code(StatusCode::BAD_REQUEST).with_detail(
errors::ERROR_DETAILS_ABSOLUTE_PATH_NOT_ALLOWED.to_owned()
+ format!(" Provided Path: {user_path:?}",).as_str(),
),
)
} else {
sanitize_path(&base_directory.join(user_id).join(user_path))
}
}
pub fn sanitize_path(path: &Path) -> Result<PathBuf, ProblemDetails> {
let mut ret = PathBuf::new();
for component in path.components().peekable() {
match component {
Component::Prefix(..) => unreachable!(),
Component::RootDir => {
ret.push(Component::RootDir);
}
Component::CurDir => {}
Component::ParentDir => {
return Err(ProblemDetails::from_status_code(StatusCode::BAD_REQUEST)
.with_detail(errors::ERROR_DETAILS_PATH_TRAVERSAL));
}
Component::Normal(c) => {
ret.push(c);
}
}
}
Ok(ret)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sanitize_path_success() {
let cases = &[
("", ""),
(".", ""),
(".////./.", ""),
("/", "/"),
("/foo/bar", "/foo/bar"),
("/foo/bar/", "/foo/bar"),
("/foo/bar/./././///", "/foo/bar"),
("foo/bar", "foo/bar"),
("foo/bar/", "foo/bar"),
("foo/bar/./././///", "foo/bar"),
];
for (input, expected) in cases {
let actual = sanitize_path(&PathBuf::from(input));
assert_eq!(actual, Ok(PathBuf::from(expected)), "input: {input}");
}
}
#[test]
fn test_sanitize_path_error() {
let cases = &[
"..",
"/../",
"../../foo/bar",
"../../foo/bar/",
"../../foo/bar/./././///",
"../../foo/bar/..",
"../../foo/bar/../..",
"../../foo/bar/../../..",
"/foo/bar/../../..",
"/foo/bar/../..",
"/foo/bar/..",
];
for input in cases {
let actual = sanitize_path(&PathBuf::from(input));
assert_eq!(
actual,
Err(ProblemDetails::from_status_code(StatusCode::BAD_REQUEST)
.with_detail(errors::ERROR_DETAILS_PATH_TRAVERSAL)),
"input: {input}"
);
}
}
#[test]
fn test_build_system_path_success() {
let input_path = PathBuf::from("tmp/blub");
let user_id = "bla";
assert_eq!(
build_system_path(&PathBuf::from("/tmp/bla"), user_id, &input_path).unwrap(),
PathBuf::from("/tmp/bla/bla/tmp/blub")
);
}
#[test]
fn test_build_system_path_error_with_absolute_user_path_returns_error() {
let input_path = PathBuf::from("tmp/blub");
let user_id = "bla";
assert_eq!(
build_system_path(&PathBuf::from("/tmp/bla"), user_id, &input_path),
Err(
ProblemDetails::from_status_code(StatusCode::BAD_REQUEST).with_detail(
errors::ERROR_DETAILS_ABSOLUTE_PATH_NOT_ALLOWED.to_owned()
+ format!(" Provided Path: {:?}", input_path).as_str()
)
)
);
}
}

View File

@ -0,0 +1,70 @@
use crate::errors::to_internal_error;
use crate::files::build_system_path;
use crate::{errors, AppState};
use axum::body::Bytes;
use axum::debug_handler;
use axum::extract::multipart::{Field, MultipartError};
use axum::extract::{Multipart, State};
use axum::http::StatusCode;
use futures::Stream;
use futures_util::stream::MapErr;
use futures_util::{StreamExt, TryStreamExt};
use problem_details::ProblemDetails;
use std::io;
use std::io::Error;
use std::path::PathBuf;
use tokio::fs::{create_dir_all, File};
use tokio::io::AsyncWriteExt;
#[debug_handler]
pub async fn handle_file_uploads(
State(state): State<AppState>,
axum::extract::Path(user_id): axum::extract::Path<String>,
mut multipart: Multipart,
) -> Result<(), ProblemDetails> {
loop {
let next = multipart.next_field().await;
match next {
Err(error) => return Err(to_internal_error(error)),
Ok(Some(field)) => handle_single_file_upload(&state, &user_id, field).await?,
Ok(None) => return Ok(()),
}
}
}
// clippy behaves weird. When removing the lifetime the compilation fails since the Field type has
// a generic lifetime around the multipart.
#[allow(clippy::needless_lifetimes)]
async fn handle_single_file_upload<'field_lifetime>(
state: &AppState,
user_id: &str,
field: Field<'field_lifetime>,
) -> Result<(), ProblemDetails> {
match field.name() {
None | Some("") => Err(ProblemDetails::from_status_code(StatusCode::BAD_REQUEST)
.with_detail(errors::ERROR_DETAILS_NO_NAME_PROVIDED_MULTIPART_FIELD)),
Some(field_name) => {
let path = PathBuf::from(field_name);
let filesystem_path = build_system_path(&state.config.files.directory, user_id, &path)?;
save_file(filesystem_path, map_error_to_io_error(field))
.await
.map_err(to_internal_error)
}
}
}
pub async fn save_file(
path: PathBuf,
mut content: impl Stream<Item = Result<Bytes, Error>> + Unpin,
) -> Result<(), Error> {
create_dir_all(path.parent().unwrap()).await?;
let mut file = File::create(path).await?;
while let Some(bytes) = content.next().await {
file.write_all(&bytes?).await?;
}
Ok(())
}
fn map_error_to_io_error(field: Field) -> MapErr<Field, fn(MultipartError) -> Error> {
field.map_err(|err| Error::new(io::ErrorKind::Other, err))
}

View File

@ -1,33 +1,56 @@
#![warn(clippy::pedantic)]
mod config; mod config;
mod errors;
mod files;
mod routes; mod routes;
mod extractor_helper;
use axum::Router; use axum::Router;
use std::env;
use std::process::exit;
use std::str::FromStr;
use tracing::{debug, error, info};
#[derive(Clone)] #[derive(Clone, Debug)]
pub struct AppState { pub struct AppState {
config: config::Config, config: config::Config,
} }
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
tracing_subscriber::fmt()
.with_max_level(get_log_level())
.init();
let config = config::get_config(); let config = config::get_config();
match config { match config {
Ok(config) => { Ok(config) => {
println!("Loaded config: {:#?}", &config); debug!("Config loaded {:?}", &config,);
// build our application with a route let bind = format!("{}:{}", &config.server.bind_address, &config.server.port);
let app = Router::new() let app = Router::new()
.merge(routes::routes()) .merge(routes::routes())
.with_state(AppState { config }); .with_state(AppState { config });
// run it let listener = tokio::net::TcpListener::bind(bind).await.unwrap();
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000") info!("listening on {}", listener.local_addr().unwrap());
.await
.unwrap();
println!("listening on {}", listener.local_addr().unwrap());
axum::serve(listener, app).await.unwrap(); axum::serve(listener, app).await.unwrap();
} }
Err(error) => { Err(error) => {
println!("Error while loading config: {}", error); error!(
"Error while loading config: {}. Tried loading from the following files {:?} and env variables prefixed by CASKET_.",
error,
config::CONFIG_LOCATIONS
);
exit(1)
} }
} }
} }
fn get_log_level() -> tracing::Level {
let env = env::var("CASKET_LOG_LEVEL");
match env {
Ok(value) => tracing::Level::from_str(&value)
.unwrap_or_else(|_| panic!("Failed to parse log level env {value}")),
Err(_) => tracing::Level::INFO,
}
}

View File

@ -0,0 +1,16 @@
use crate::files;
use crate::AppState;
use axum::routing::post;
use axum::{response::Html, routing::get, Router};
use files::list::query_file_tree;
pub fn routes() -> Router<AppState> {
Router::new().route("/", get(handler)).route(
"/api/v1/user/{:user_id}/files",
post(files::upload::handle_file_uploads).get(query_file_tree),
)
}
async fn handler() -> Html<&'static str> {
Html("<h1>Hello, World!</h1>")
}

View File

@ -1,11 +0,0 @@
use axum::{response::Html, routing::get, Router};
use crate::AppState;
pub fn routes() -> Router<AppState> {
Router::new().route("/", get(handler))
}
async fn handler() -> Html<&'static str> {
Html("<h1>Hello, World!</h1>")
}