WIP: v1.0.0
This commit is contained in:
parent
cadb66a730
commit
78d247ef3e
12 changed files with 658 additions and 891 deletions
648
Cargo.lock
generated
648
Cargo.lock
generated
|
@ -103,28 +103,6 @@ version = "1.0.97"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f"
|
||||
|
||||
[[package]]
|
||||
name = "async-stream"
|
||||
version = "0.3.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476"
|
||||
dependencies = [
|
||||
"async-stream-impl",
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-stream-impl"
|
||||
version = "0.3.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-trait"
|
||||
version = "0.1.88"
|
||||
|
@ -145,12 +123,6 @@ dependencies = [
|
|||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "atomic"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba"
|
||||
|
||||
[[package]]
|
||||
name = "atomic"
|
||||
version = "0.6.0"
|
||||
|
@ -166,6 +138,68 @@ version = "1.4.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
|
||||
|
||||
[[package]]
|
||||
name = "axum"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "de45108900e1f9b9242f7f2e254aa3e2c029c921c258fe9e6b4217eeebd54288"
|
||||
dependencies = [
|
||||
"axum-core",
|
||||
"axum-macros",
|
||||
"bytes",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
"itoa",
|
||||
"matchit",
|
||||
"memchr",
|
||||
"mime",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"rustversion",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_path_to_error",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tower",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "axum-core"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68464cd0412f486726fb3373129ef5d2993f90c34bc2bc1c1e9943b2f4fc7ca6"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"http",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"mime",
|
||||
"pin-project-lite",
|
||||
"rustversion",
|
||||
"sync_wrapper",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "backon"
|
||||
version = "1.4.1"
|
||||
|
@ -204,12 +238,6 @@ version = "1.7.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89e25b6adfb930f02d1981565a6e5d9c547ac15a96606256d3b59040e5cd4ca3"
|
||||
|
||||
[[package]]
|
||||
name = "binascii"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "383d29d513d8764dcdc42ea295d979eb99c3c9f00607b3692cf68a431f7dca72"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.9.0"
|
||||
|
@ -348,17 +376,6 @@ version = "0.4.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b"
|
||||
|
||||
[[package]]
|
||||
name = "cookie"
|
||||
version = "0.18.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747"
|
||||
dependencies = [
|
||||
"percent-encoding",
|
||||
"time",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.10.0"
|
||||
|
@ -484,48 +501,6 @@ dependencies = [
|
|||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deranged"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e"
|
||||
dependencies = [
|
||||
"powerfmt",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "devise"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1d90b0c4c777a2cad215e3c7be59ac7c15adf45cf76317009b7d096d46f651d"
|
||||
dependencies = [
|
||||
"devise_codegen",
|
||||
"devise_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "devise_codegen"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "71b28680d8be17a570a2334922518be6adc3f58ecc880cbb404eaeb8624fd867"
|
||||
dependencies = [
|
||||
"devise_core",
|
||||
"quote",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "devise_core"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b035a542cf7abf01f2e3c4d5a7acbaebfefe120ae4efc7bde3df98186e4b8af7"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"proc-macro2",
|
||||
"proc-macro2-diagnostics",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.7"
|
||||
|
@ -564,15 +539,6 @@ dependencies = [
|
|||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "encoding_rs"
|
||||
version = "0.8.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "env_filter"
|
||||
version = "0.1.3"
|
||||
|
@ -656,7 +622,7 @@ version = "0.10.19"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8cb01cd46b0cf372153850f4c6c272d9cbea2da513e07538405148f95bd789f3"
|
||||
dependencies = [
|
||||
"atomic 0.6.0",
|
||||
"atomic",
|
||||
"parking_lot",
|
||||
"pear",
|
||||
"serde",
|
||||
|
@ -714,6 +680,7 @@ checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
|
|||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-executor",
|
||||
"futures-io",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
|
@ -805,19 +772,6 @@ dependencies = [
|
|||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generator"
|
||||
version = "0.7.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5cc16584ff22b460a382b7feec54b23d2908d858152e5739a120b949293bd74e"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"log",
|
||||
"rustversion",
|
||||
"windows",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.7"
|
||||
|
@ -861,12 +815,6 @@ version = "0.28.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253"
|
||||
|
||||
[[package]]
|
||||
name = "glob"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
|
||||
|
||||
[[package]]
|
||||
name = "gloo-timers"
|
||||
version = "0.3.0"
|
||||
|
@ -879,25 +827,6 @@ dependencies = [
|
|||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "0.3.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"fnv",
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"futures-util",
|
||||
"http 0.2.12",
|
||||
"indexmap",
|
||||
"slab",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.2"
|
||||
|
@ -924,18 +853,6 @@ version = "0.5.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fbd780fe5cc30f81464441920d82ac8740e2e46b29a6fad543ddd075229ce37e"
|
||||
|
||||
[[package]]
|
||||
name = "hex"
|
||||
version = "0.4.3"
|
||||
|
@ -969,17 +886,6 @@ dependencies = [
|
|||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "0.2.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"fnv",
|
||||
"itoa",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "1.3.1"
|
||||
|
@ -991,17 +897,6 @@ dependencies = [
|
|||
"itoa",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http-body"
|
||||
version = "0.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"http 0.2.12",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http-body"
|
||||
version = "1.0.1"
|
||||
|
@ -1009,7 +904,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"http 1.3.1",
|
||||
"http",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1020,8 +915,8 @@ checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
|
|||
dependencies = [
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"http 1.3.1",
|
||||
"http-body 1.0.1",
|
||||
"http",
|
||||
"http-body",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
|
@ -1037,30 +932,6 @@ version = "1.0.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "0.14.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"h2",
|
||||
"http 0.2.12",
|
||||
"http-body 0.4.6",
|
||||
"httparse",
|
||||
"httpdate",
|
||||
"itoa",
|
||||
"pin-project-lite",
|
||||
"socket2",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
"want",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "1.6.0"
|
||||
|
@ -1070,9 +941,10 @@ dependencies = [
|
|||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-util",
|
||||
"http 1.3.1",
|
||||
"http-body 1.0.1",
|
||||
"http",
|
||||
"http-body",
|
||||
"httparse",
|
||||
"httpdate",
|
||||
"itoa",
|
||||
"pin-project-lite",
|
||||
"smallvec",
|
||||
|
@ -1087,8 +959,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"http 1.3.1",
|
||||
"hyper 1.6.0",
|
||||
"http",
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
|
@ -1107,9 +979,9 @@ dependencies = [
|
|||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-util",
|
||||
"http 1.3.1",
|
||||
"http-body 1.0.1",
|
||||
"hyper 1.6.0",
|
||||
"http",
|
||||
"http-body",
|
||||
"hyper",
|
||||
"pin-project-lite",
|
||||
"socket2",
|
||||
"tokio",
|
||||
|
@ -1299,7 +1171,6 @@ checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058"
|
|||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1314,17 +1185,6 @@ version = "2.11.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
|
||||
|
||||
[[package]]
|
||||
name = "is-terminal"
|
||||
version = "0.4.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9"
|
||||
dependencies = [
|
||||
"hermit-abi 0.5.0",
|
||||
"libc",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.1"
|
||||
|
@ -1432,28 +1292,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e"
|
||||
|
||||
[[package]]
|
||||
name = "loom"
|
||||
version = "0.5.6"
|
||||
name = "matchit"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff50ecb28bb86013e935fb6683ab1f6d3a20016f123c76fd4c27470076ac30f5"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"generator",
|
||||
"scoped-tls",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "matchers"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
|
||||
dependencies = [
|
||||
"regex-automata 0.1.10",
|
||||
]
|
||||
checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
|
||||
|
||||
[[package]]
|
||||
name = "md-5"
|
||||
|
@ -1491,6 +1333,7 @@ name = "minna_caos"
|
|||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum",
|
||||
"camino",
|
||||
"color-eyre",
|
||||
"constant_time_eq",
|
||||
|
@ -1498,13 +1341,13 @@ dependencies = [
|
|||
"env_logger",
|
||||
"figment",
|
||||
"fstr",
|
||||
"futures",
|
||||
"log",
|
||||
"once_cell",
|
||||
"opendal",
|
||||
"rand 0.9.0",
|
||||
"regex",
|
||||
"replace_with",
|
||||
"rocket",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sqlx",
|
||||
|
@ -1524,35 +1367,6 @@ dependencies = [
|
|||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[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 1.3.1",
|
||||
"httparse",
|
||||
"memchr",
|
||||
"mime",
|
||||
"spin",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"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]]
|
||||
name = "num-bigint-dig"
|
||||
version = "0.8.4"
|
||||
|
@ -1570,12 +1384,6 @@ dependencies = [
|
|||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-conv"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
|
||||
|
||||
[[package]]
|
||||
name = "num-integer"
|
||||
version = "0.1.46"
|
||||
|
@ -1606,16 +1414,6 @@ dependencies = [
|
|||
"libm",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num_cpus"
|
||||
version = "1.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
|
||||
dependencies = [
|
||||
"hermit-abi 0.3.9",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "object"
|
||||
version = "0.32.2"
|
||||
|
@ -1645,7 +1443,7 @@ dependencies = [
|
|||
"chrono",
|
||||
"futures",
|
||||
"getrandom 0.2.15",
|
||||
"http 1.3.1",
|
||||
"http",
|
||||
"log",
|
||||
"md-5",
|
||||
"once_cell",
|
||||
|
@ -1664,12 +1462,6 @@ version = "0.1.6"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
|
||||
|
||||
[[package]]
|
||||
name = "overload"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
|
||||
|
||||
[[package]]
|
||||
name = "owo-colors"
|
||||
version = "3.5.0"
|
||||
|
@ -1797,12 +1589,6 @@ dependencies = [
|
|||
"portable-atomic",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "powerfmt"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.21"
|
||||
|
@ -2004,26 +1790,6 @@ dependencies = [
|
|||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ref-cast"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4a0ae411dbe946a674d89546582cea4ba2bb8defac896622d6496f14c23ba5cf"
|
||||
dependencies = [
|
||||
"ref-cast-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ref-cast-impl"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.11.1"
|
||||
|
@ -2032,17 +1798,8 @@ checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
|
|||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-automata 0.4.9",
|
||||
"regex-syntax 0.8.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
|
||||
dependencies = [
|
||||
"regex-syntax 0.6.29",
|
||||
"regex-automata",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -2053,15 +1810,9 @@ checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
|
|||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax 0.8.5",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.6.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.5"
|
||||
|
@ -2084,10 +1835,10 @@ dependencies = [
|
|||
"bytes",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"http 1.3.1",
|
||||
"http-body 1.0.1",
|
||||
"http",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"hyper 1.6.0",
|
||||
"hyper",
|
||||
"hyper-rustls",
|
||||
"hyper-util",
|
||||
"ipnet",
|
||||
|
@ -2133,88 +1884,6 @@ dependencies = [
|
|||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rocket"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a516907296a31df7dc04310e7043b61d71954d703b603cc6867a026d7e72d73f"
|
||||
dependencies = [
|
||||
"async-stream",
|
||||
"async-trait",
|
||||
"atomic 0.5.3",
|
||||
"binascii",
|
||||
"bytes",
|
||||
"either",
|
||||
"figment",
|
||||
"futures",
|
||||
"indexmap",
|
||||
"log",
|
||||
"memchr",
|
||||
"multer",
|
||||
"num_cpus",
|
||||
"parking_lot",
|
||||
"pin-project-lite",
|
||||
"rand 0.8.5",
|
||||
"ref-cast",
|
||||
"rocket_codegen",
|
||||
"rocket_http",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"state",
|
||||
"tempfile",
|
||||
"time",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"tokio-util",
|
||||
"ubyte",
|
||||
"version_check",
|
||||
"yansi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rocket_codegen"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "575d32d7ec1a9770108c879fc7c47815a80073f96ca07ff9525a94fcede1dd46"
|
||||
dependencies = [
|
||||
"devise",
|
||||
"glob",
|
||||
"indexmap",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rocket_http",
|
||||
"syn",
|
||||
"unicode-xid",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rocket_http"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e274915a20ee3065f611c044bd63c40757396b6dbc057d6046aec27f14f882b9"
|
||||
dependencies = [
|
||||
"cookie",
|
||||
"either",
|
||||
"futures",
|
||||
"http 0.2.12",
|
||||
"hyper 0.14.32",
|
||||
"indexmap",
|
||||
"log",
|
||||
"memchr",
|
||||
"pear",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"ref-cast",
|
||||
"serde",
|
||||
"smallvec",
|
||||
"stable-pattern",
|
||||
"state",
|
||||
"time",
|
||||
"tokio",
|
||||
"uncased",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rsa"
|
||||
version = "0.9.8"
|
||||
|
@ -2336,12 +2005,6 @@ dependencies = [
|
|||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scoped-tls"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.2.0"
|
||||
|
@ -2403,6 +2066,16 @@ dependencies = [
|
|||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_path_to_error"
|
||||
version = "0.1.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "59fab13f937fa393d08645bf3a84bdfe86e296747b506ada67bb15f10f218b2a"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_spanned"
|
||||
version = "0.6.8"
|
||||
|
@ -2461,15 +2134,6 @@ version = "1.3.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signature"
|
||||
version = "2.2.0"
|
||||
|
@ -2717,30 +2381,12 @@ dependencies = [
|
|||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "stable-pattern"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4564168c00635f88eaed410d5efa8131afa8d8699a612c80c455a0ba05c21045"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "stable_deref_trait"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
|
||||
|
||||
[[package]]
|
||||
name = "state"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b8c4a4445d81357df8b1a650d0d0d6fbbbfe99d064aa5e02f3e4022061476d8"
|
||||
dependencies = [
|
||||
"loom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "stringprep"
|
||||
version = "0.1.5"
|
||||
|
@ -2838,37 +2484,6 @@ dependencies = [
|
|||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.3.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d9c75b47bdff86fa3334a3db91356b8d7d86a9b839dab7d0bdc5c3d3a077618"
|
||||
dependencies = [
|
||||
"deranged",
|
||||
"itoa",
|
||||
"num-conv",
|
||||
"powerfmt",
|
||||
"serde",
|
||||
"time-core",
|
||||
"time-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time-core"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c"
|
||||
|
||||
[[package]]
|
||||
name = "time-macros"
|
||||
version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29aa485584182073ed57fd5004aa09c371f021325014694e432313345865fd04"
|
||||
dependencies = [
|
||||
"num-conv",
|
||||
"time-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinystr"
|
||||
version = "0.7.6"
|
||||
|
@ -2906,7 +2521,6 @@ dependencies = [
|
|||
"mio",
|
||||
"parking_lot",
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
"socket2",
|
||||
"tokio-macros",
|
||||
"windows-sys 0.52.0",
|
||||
|
@ -3061,33 +2675,15 @@ dependencies = [
|
|||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[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 = [
|
||||
"matchers",
|
||||
"nu-ansi-term",
|
||||
"once_cell",
|
||||
"regex",
|
||||
"sharded-slab",
|
||||
"smallvec",
|
||||
"thread_local",
|
||||
"tracing",
|
||||
"tracing-core",
|
||||
"tracing-log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -3102,22 +2698,12 @@ version = "1.18.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f"
|
||||
|
||||
[[package]]
|
||||
name = "ubyte"
|
||||
version = "0.10.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f720def6ce1ee2fc44d40ac9ed6d3a59c361c80a75a7aa8e75bb9baed31cf2ea"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uncased"
|
||||
version = "0.9.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1b88fcfe09e89d3866a5c11019378088af2d24c3fbd4f0543f96b479ec90697"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
|
@ -3148,12 +2734,6 @@ version = "0.1.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||
|
||||
[[package]]
|
||||
name = "untrusted"
|
||||
version = "0.9.0"
|
||||
|
@ -3400,37 +2980,6 @@ dependencies = [
|
|||
"wasite",
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "windows"
|
||||
version = "0.48.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f"
|
||||
dependencies = [
|
||||
"windows-targets 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.52.0"
|
||||
|
@ -3722,9 +3271,6 @@ name = "yansi"
|
|||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
|
||||
dependencies = [
|
||||
"is-terminal",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yoke"
|
||||
|
|
|
@ -8,7 +8,7 @@ opt-level = 3
|
|||
|
||||
[dependencies]
|
||||
sqlx = { version = "0.8.3", features = ["tls-rustls-ring-native-roots", "sqlite", "runtime-tokio"] }
|
||||
rocket = { version = "0.5.1", default-features = false, features = ["http2", "json"] }
|
||||
axum = { version = "0.8.3", default-features = false, features = ["json", "http1", "tokio", "macros"] }
|
||||
opendal = { version = "0.52.0", features = ["services-fs"] }
|
||||
tokio = { version = "1.44.1", features = ["rt-multi-thread", "macros", "parking_lot"] }
|
||||
color-eyre = "0.6.3"
|
||||
|
@ -27,4 +27,5 @@ dashmap = "7.0.0-rc2"
|
|||
tokio-util = "0.7.14"
|
||||
replace_with = "0.1.7"
|
||||
async-trait = "0.1.88"
|
||||
rand = "0.9.0"
|
||||
rand = "0.9.0"
|
||||
futures = "0.3.31"
|
|
@ -15,8 +15,6 @@ use validator::{Validate, ValidationError};
|
|||
pub struct Config {
|
||||
pub http_address: IpAddr,
|
||||
pub http_port: u16,
|
||||
#[serde(default)]
|
||||
pub trust_http_reverse_proxy: bool,
|
||||
pub api_secret: FStr<64>,
|
||||
pub database_file: Utf8PathBuf,
|
||||
pub staging_directory: Utf8PathBuf,
|
||||
|
|
|
@ -1,14 +1,17 @@
|
|||
use axum::Json;
|
||||
use axum::http::{HeaderName, StatusCode};
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use color_eyre::Report;
|
||||
use rocket::response::Responder;
|
||||
use rocket::{Request, response};
|
||||
use serde_json::json;
|
||||
use std::borrow::Cow;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ApiError {
|
||||
Internal { report: Report },
|
||||
HeaderValidationFailed { name: Cow<'static, str>, message: Cow<'static, str> },
|
||||
BodyValidationFailed { path: Cow<'static, str>, message: Cow<'static, str> },
|
||||
ResourceNotFound { resource_type: Cow<'static, str>, id: Cow<'static, str> },
|
||||
Forbidden,
|
||||
InvalidRequestHeader { name: &'static HeaderName, message: Cow<'static, str> },
|
||||
InvalidRequestContent { path: Cow<'static, str>, message: Cow<'static, str> },
|
||||
UnknownResource { resource_type: Cow<'static, str>, id: Cow<'static, str> },
|
||||
}
|
||||
|
||||
impl From<Report> for ApiError {
|
||||
|
@ -17,8 +20,50 @@ impl From<Report> for ApiError {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'r> Responder<'r, 'static> for ApiError {
|
||||
fn respond_to(self, _: &Request<'_>) -> response::Result<'static> {
|
||||
todo!()
|
||||
impl From<std::io::Error> for ApiError {
|
||||
fn from(error: std::io::Error) -> Self {
|
||||
ApiError::Internal { report: Report::new(error) }
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoResponse for ApiError {
|
||||
fn into_response(self) -> Response {
|
||||
match self {
|
||||
ApiError::Internal { report } => {
|
||||
log::error!("Internal error in request handler: {:#}", report);
|
||||
StatusCode::INTERNAL_SERVER_ERROR.into_response()
|
||||
}
|
||||
ApiError::Forbidden => StatusCode::FORBIDDEN.into_response(),
|
||||
ApiError::InvalidRequestHeader { name, message } => (
|
||||
StatusCode::UNPROCESSABLE_ENTITY,
|
||||
Json(json!({
|
||||
"type": "https://minna.media/api-problems/general/invalid-request-header",
|
||||
"title": "A specific request header value is invalid.",
|
||||
"detail": format!("The value of `{}` is invalid: {}", name.as_str(), message)
|
||||
})),
|
||||
)
|
||||
.into_response(),
|
||||
ApiError::InvalidRequestContent { path, message } => (
|
||||
StatusCode::UNPROCESSABLE_ENTITY,
|
||||
Json(json!({
|
||||
"type": "https://minna.media/api-problems/general/invalid-request-content",
|
||||
"title": "The request content is semantically invalid.",
|
||||
"detail": format!("`{path}`: {message}"),
|
||||
"path": path
|
||||
})),
|
||||
)
|
||||
.into_response(),
|
||||
ApiError::UnknownResource { resource_type, id } => (
|
||||
StatusCode::NOT_FOUND,
|
||||
Json(json!({
|
||||
"type": "https://minna.media/api-problems/general/unknown-resource",
|
||||
"title": "The requested resource is unknown.",
|
||||
"detail": format!("There is no {resource_type} resource with this ID: {id}"),
|
||||
"resource_type": resource_type,
|
||||
"resource_id": id
|
||||
})),
|
||||
)
|
||||
.into_response(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,33 +1,31 @@
|
|||
use fstr::FStr;
|
||||
use rocket::Request;
|
||||
use rocket::form::validate::Len;
|
||||
use rocket::http::Status;
|
||||
use rocket::outcome::Outcome::Success;
|
||||
use rocket::request::{FromRequest, Outcome};
|
||||
use crate::http_api::Context;
|
||||
use crate::http_api::api_error::ApiError;
|
||||
use crate::http_api::headers::HeaderMapExt;
|
||||
use axum::extract::FromRequestParts;
|
||||
use axum::http::request::Parts;
|
||||
|
||||
pub struct CorrectApiSecret(pub FStr<64>);
|
||||
pub struct AppAuthorization;
|
||||
|
||||
pub struct AuthorizedApiAccessor();
|
||||
impl FromRequestParts<Context> for AppAuthorization {
|
||||
type Rejection = ApiError;
|
||||
|
||||
#[rocket::async_trait]
|
||||
impl<'r> FromRequest<'r> for AuthorizedApiAccessor {
|
||||
type Error = ();
|
||||
|
||||
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
|
||||
let provided_secret = request
|
||||
.headers()
|
||||
.get_one("Authorization")
|
||||
async fn from_request_parts(parts: &mut Parts, state: &Context) -> Result<Self, Self::Rejection> {
|
||||
let provided_secret = parts
|
||||
.headers
|
||||
.get_at_most_once(&axum::http::header::AUTHORIZATION)?
|
||||
.map(|v| v.to_str().ok())
|
||||
.flatten()
|
||||
.map(|v| v.strip_prefix("Bearer "))
|
||||
.take_if(|v| v.len() == 64)
|
||||
.flatten();
|
||||
.flatten()
|
||||
.take_if(|v| v.len() == 64);
|
||||
|
||||
let correct_secret = request.rocket().state::<CorrectApiSecret>().unwrap().0;
|
||||
let correct_secret = state.api_secret;
|
||||
if let Some(provided_secret) = provided_secret {
|
||||
if constant_time_eq::constant_time_eq(provided_secret.as_bytes(), correct_secret.as_bytes()) {
|
||||
return Success(AuthorizedApiAccessor());
|
||||
return Ok(AppAuthorization);
|
||||
}
|
||||
}
|
||||
|
||||
Outcome::Error((Status::Forbidden, ()))
|
||||
Err(ApiError::Forbidden)
|
||||
}
|
||||
}
|
||||
|
|
85
src/http_api/headers.rs
Normal file
85
src/http_api/headers.rs
Normal file
|
@ -0,0 +1,85 @@
|
|||
use crate::http_api::api_error::ApiError;
|
||||
use axum::http::{HeaderMap, HeaderName, HeaderValue};
|
||||
|
||||
pub mod upload_headers {
|
||||
use axum::http::HeaderName;
|
||||
use once_cell::sync::Lazy;
|
||||
use std::str::FromStr;
|
||||
|
||||
pub static UPLOAD_OFFSET: Lazy<HeaderName> = Lazy::new(|| HeaderName::from_str("upload-offset").unwrap());
|
||||
pub static UPLOAD_COMPLETE: Lazy<HeaderName> = Lazy::new(|| HeaderName::from_str("upload-complete").unwrap());
|
||||
}
|
||||
|
||||
pub trait HeaderMapExt {
|
||||
fn get_exactly_once(&self, key: &'static HeaderName) -> Result<&HeaderValue, ApiError>;
|
||||
fn get_at_most_once(&self, key: &'static HeaderName) -> Result<Option<&HeaderValue>, ApiError>;
|
||||
}
|
||||
|
||||
impl HeaderMapExt for HeaderMap {
|
||||
fn get_exactly_once(&self, key: &'static HeaderName) -> Result<&HeaderValue, ApiError> {
|
||||
let mut values_iterator = self.get_all(key).into_iter();
|
||||
|
||||
if let Some(value) = values_iterator.next() {
|
||||
if values_iterator.next().is_none() {
|
||||
return Ok(value);
|
||||
}
|
||||
}
|
||||
|
||||
Err(ApiError::InvalidRequestHeader {
|
||||
name: key,
|
||||
message: "must be specified exactly once".into(),
|
||||
})
|
||||
}
|
||||
|
||||
fn get_at_most_once(&self, key: &'static HeaderName) -> Result<Option<&HeaderValue>, ApiError> {
|
||||
let mut values_iterator = self.get_all(key).into_iter();
|
||||
|
||||
if let Some(value) = values_iterator.next() {
|
||||
if values_iterator.next().is_none() {
|
||||
Ok(Some(value))
|
||||
} else {
|
||||
Err(ApiError::InvalidRequestHeader {
|
||||
name: key,
|
||||
message: "must be specified at most once".into(),
|
||||
})
|
||||
}
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait HeaderValueExt {
|
||||
fn get_unsigned_decimal_number(&self, header_name_for_error: &'static HeaderName) -> Result<u64, ApiError>;
|
||||
fn get_boolean(&self, header_name_for_error: &'static HeaderName) -> Result<bool, ApiError>;
|
||||
}
|
||||
|
||||
impl HeaderValueExt for HeaderValue {
|
||||
fn get_unsigned_decimal_number(&self, header_name_for_error: &'static HeaderName) -> Result<u64, ApiError> {
|
||||
self.to_str()
|
||||
.ok()
|
||||
.map(|v| v.parse::<u64>().ok())
|
||||
.flatten()
|
||||
.ok_or(ApiError::InvalidRequestHeader {
|
||||
name: header_name_for_error,
|
||||
message: "must be an unsigned 64-bit decimal number".into(),
|
||||
})
|
||||
}
|
||||
|
||||
fn get_boolean(&self, header_name_for_error: &'static HeaderName) -> Result<bool, ApiError> {
|
||||
if let Ok(value) = self.to_str() {
|
||||
if value == "?1" {
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
if value == "?0" {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
|
||||
Err(ApiError::InvalidRequestHeader {
|
||||
name: header_name_for_error,
|
||||
message: "must be `?1` (true) or `?0` (false)".into(),
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,173 +1,31 @@
|
|||
mod api_error;
|
||||
mod auth;
|
||||
mod stream_upload_payload_to_file;
|
||||
mod upload_headers;
|
||||
mod headers;
|
||||
mod upload;
|
||||
|
||||
use crate::http_api::api_error::ApiError;
|
||||
use crate::http_api::auth::{AuthorizedApiAccessor, CorrectApiSecret};
|
||||
use crate::http_api::stream_upload_payload_to_file::{StreamUploadPayloadToFileOutcome, stream_upload_payload_to_file};
|
||||
use crate::http_api::upload_headers::{SuppliedOptionalContentLength, SuppliedUploadComplete, SuppliedUploadOffset};
|
||||
use crate::upload_manager::{UploadId, UploadManager};
|
||||
use color_eyre::{Report, Result};
|
||||
use crate::http_api::upload::create_uploads_router;
|
||||
use crate::upload_manager::UploadManager;
|
||||
use axum::Router;
|
||||
use color_eyre::Result;
|
||||
use fstr::FStr;
|
||||
use rocket::data::{DataStream, ToByteUnit};
|
||||
use rocket::http::{ContentType, MediaType, Status};
|
||||
use rocket::serde::json::Json;
|
||||
use rocket::{Data, Request, Response, State, patch, post, routes};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use std::borrow::Cow;
|
||||
use std::io::ErrorKind;
|
||||
use std::net::IpAddr;
|
||||
use tokio::fs::File;
|
||||
use tokio::io::AsyncSeekExt;
|
||||
use tokio_util::bytes::Buf;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub async fn start_http_api_server(upload_manager: UploadManager, address: IpAddr, port: u16, trust_reverse_proxy: bool, api_secret: FStr<64>) -> Result<()> {
|
||||
let rocket_app = rocket::custom(rocket::config::Config {
|
||||
address,
|
||||
port,
|
||||
ident: rocket::config::Ident::try_new("minna-caos".to_owned()).unwrap(),
|
||||
ip_header: if trust_reverse_proxy { Some("X-Forwarded-For".into()) } else { None },
|
||||
shutdown: rocket::config::Shutdown {
|
||||
grace: 5,
|
||||
mercy: 5,
|
||||
..rocket::config::Shutdown::default()
|
||||
},
|
||||
keep_alive: 10,
|
||||
..rocket::Config::default()
|
||||
});
|
||||
#[derive(Debug)]
|
||||
struct ContextInner {
|
||||
pub upload_manager: UploadManager,
|
||||
pub api_secret: FStr<64>,
|
||||
}
|
||||
|
||||
rocket_app
|
||||
.manage(CorrectApiSecret(api_secret))
|
||||
.manage(upload_manager)
|
||||
.mount("/", routes![create_upload, append_upload])
|
||||
.launch()
|
||||
.await?;
|
||||
type Context = Arc<ContextInner>;
|
||||
|
||||
pub async fn start_http_api_server(upload_manager: UploadManager, address: IpAddr, port: u16, api_secret: FStr<64>) -> Result<()> {
|
||||
let router = Router::new()
|
||||
.nest("/uploads", create_uploads_router())
|
||||
.with_state(Arc::new(ContextInner { upload_manager, api_secret }));
|
||||
|
||||
let listener = tokio::net::TcpListener::bind((address, port)).await?;
|
||||
axum::serve(listener, router).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct CreateUploadPayload {
|
||||
size: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct CreateUploadResponse {
|
||||
upload_id: UploadId,
|
||||
}
|
||||
|
||||
#[post("/uploads", data = "<payload>")]
|
||||
async fn create_upload(
|
||||
_accessor: AuthorizedApiAccessor,
|
||||
upload_manager: &State<UploadManager>,
|
||||
payload: Json<CreateUploadPayload>,
|
||||
) -> Result<Json<CreateUploadResponse>, ApiError> {
|
||||
if payload.size < 1 || payload.size > (2 ^ 63 - 1) {
|
||||
return Err(ApiError::BodyValidationFailed {
|
||||
path: "size".into(),
|
||||
message: "size must be in 1..(2^63 - 1)".into(),
|
||||
});
|
||||
}
|
||||
|
||||
let upload = upload_manager.create_upload(payload.size).await?;
|
||||
|
||||
Ok(Json(CreateUploadResponse { upload_id: *upload.id() }))
|
||||
}
|
||||
|
||||
const PARTIAL_UPLOAD_MEDIA_TYPE: MediaType = MediaType::const_new("application", "partial-upload", &[]);
|
||||
|
||||
#[derive(Debug)]
|
||||
enum AppendUploadResponse {
|
||||
RequestSuperseded,
|
||||
UploadOffsetMismatch { expected: u64 },
|
||||
InconsistentUploadLength { expected: u64, detail: Cow<'static, str> },
|
||||
StreamToFileOutcome(StreamUploadPayloadToFileOutcome),
|
||||
}
|
||||
|
||||
#[patch("/uploads/<upload_id>", data = "<payload>")]
|
||||
async fn append_upload(
|
||||
upload_id: &str,
|
||||
upload_manager: &State<UploadManager>,
|
||||
payload: Data<'_>,
|
||||
supplied_content_type: Option<&ContentType>,
|
||||
supplied_content_length: SuppliedOptionalContentLength,
|
||||
supplied_upload_offset: SuppliedUploadOffset,
|
||||
supplied_upload_complete: SuppliedUploadComplete,
|
||||
) -> Result<AppendUploadResponse, ApiError> {
|
||||
if !supplied_content_type.map(|c| c.exact_eq(&PARTIAL_UPLOAD_MEDIA_TYPE)).unwrap_or(false) {
|
||||
return Err(ApiError::HeaderValidationFailed {
|
||||
name: "content-type".into(),
|
||||
message: format!("must be {}", PARTIAL_UPLOAD_MEDIA_TYPE.to_string()).into(),
|
||||
});
|
||||
}
|
||||
|
||||
let upload = if let Some(upload) = upload_manager.get_upload_by_id(upload_id) {
|
||||
upload
|
||||
} else {
|
||||
return Err(ApiError::ResourceNotFound {
|
||||
resource_type: "upload".into(),
|
||||
id: upload_id.to_owned().into(),
|
||||
});
|
||||
};
|
||||
|
||||
let mut file_acquisition = if let Some(file) = upload.file().acquire().await {
|
||||
file
|
||||
} else {
|
||||
return Ok(AppendUploadResponse::RequestSuperseded);
|
||||
};
|
||||
|
||||
let release_request_token = file_acquisition.release_request_token();
|
||||
let mut file = file_acquisition.inner().get_or_open().await.map_err(Report::new)?;
|
||||
|
||||
let total_size = upload.total_size();
|
||||
let current_offset = file.stream_position().await.map_err(Report::new)?;
|
||||
let remaining_content_length = total_size - current_offset;
|
||||
|
||||
if supplied_upload_offset.0 != current_offset {
|
||||
return Ok(AppendUploadResponse::UploadOffsetMismatch { expected: current_offset });
|
||||
}
|
||||
|
||||
let payload_length_limit = if let Some(supplied_content_length) = supplied_content_length.0 {
|
||||
if supplied_upload_complete.0 {
|
||||
if remaining_content_length != supplied_content_length {
|
||||
return Ok(AppendUploadResponse::InconsistentUploadLength {
|
||||
expected: total_size,
|
||||
detail: "Upload-Complete is set to true, and Content-Length is set, \
|
||||
but the value of Content-Length does not equal the length of the remaining content."
|
||||
.into(),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if supplied_content_length >= remaining_content_length {
|
||||
return Ok(AppendUploadResponse::InconsistentUploadLength {
|
||||
expected: total_size,
|
||||
detail: "Upload-Complete is set to false, and Content-Length is set, \
|
||||
but the value of Content-Length is not smaller than the length of the remaining content."
|
||||
.into(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
supplied_content_length
|
||||
} else {
|
||||
remaining_content_length
|
||||
};
|
||||
|
||||
let outcome = tokio::select! {
|
||||
o = stream_upload_payload_to_file(
|
||||
payload.open(payload_length_limit.bytes()),
|
||||
&mut file,
|
||||
remaining_content_length,
|
||||
supplied_content_length.0,
|
||||
supplied_upload_complete.0
|
||||
) => Some(o),
|
||||
_ = release_request_token.cancelled() => None
|
||||
};
|
||||
|
||||
file.sync_all().await.map_err(Report::new)?;
|
||||
file_acquisition.release().await;
|
||||
|
||||
todo!()
|
||||
}
|
||||
|
|
|
@ -1,46 +0,0 @@
|
|||
use rocket::data::DataStream;
|
||||
use std::io::ErrorKind;
|
||||
use tokio::fs::File;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum StreamUploadPayloadToFileOutcome {
|
||||
StoppedUnexpectedly,
|
||||
TooMuchData,
|
||||
Success,
|
||||
}
|
||||
|
||||
pub async fn stream_upload_payload_to_file(
|
||||
stream: DataStream<'_>,
|
||||
file: &mut File,
|
||||
remaining_content_length: u64,
|
||||
supplied_content_length: Option<u64>,
|
||||
supplied_upload_complete: bool,
|
||||
) -> Result<StreamUploadPayloadToFileOutcome, std::io::Error> {
|
||||
match stream.stream_to(file).await {
|
||||
Ok(n) => {
|
||||
if let Some(supplied_content_length) = supplied_content_length {
|
||||
if n.written < supplied_content_length {
|
||||
return Ok(StreamUploadPayloadToFileOutcome::StoppedUnexpectedly);
|
||||
}
|
||||
} else {
|
||||
if supplied_upload_complete {
|
||||
if n.written < remaining_content_length {
|
||||
return Ok(StreamUploadPayloadToFileOutcome::StoppedUnexpectedly);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !n.complete {
|
||||
return Ok(StreamUploadPayloadToFileOutcome::TooMuchData);
|
||||
}
|
||||
|
||||
Ok(StreamUploadPayloadToFileOutcome::Success)
|
||||
}
|
||||
Err(error) => match error.kind() {
|
||||
ErrorKind::TimedOut => Ok(StreamUploadPayloadToFileOutcome::StoppedUnexpectedly),
|
||||
ErrorKind::BrokenPipe => Ok(StreamUploadPayloadToFileOutcome::StoppedUnexpectedly),
|
||||
ErrorKind::ConnectionReset => Ok(StreamUploadPayloadToFileOutcome::StoppedUnexpectedly),
|
||||
_ => Err(error),
|
||||
},
|
||||
}
|
||||
}
|
308
src/http_api/upload/append_to_upload.rs
Normal file
308
src/http_api/upload/append_to_upload.rs
Normal file
|
@ -0,0 +1,308 @@
|
|||
use crate::http_api::Context;
|
||||
use crate::http_api::api_error::ApiError;
|
||||
use crate::http_api::headers::{HeaderMapExt, HeaderValueExt, upload_headers};
|
||||
use crate::http_api::upload::{PARTIAL_UPLOAD_MEDIA_TYPE, UploadCompleteResponseHeader, UploadOffsetResponseHeader};
|
||||
use crate::upload_manager::{UnfinishedUpload, UploadId, UploadManager};
|
||||
use axum::Json;
|
||||
use axum::body::{Body, BodyDataStream};
|
||||
use axum::extract::{Path, State};
|
||||
use axum::http::{HeaderMap, StatusCode};
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use color_eyre::Report;
|
||||
use futures::TryStreamExt;
|
||||
use serde::Deserialize;
|
||||
use serde_json::json;
|
||||
use std::borrow::Cow;
|
||||
use std::io::ErrorKind;
|
||||
use std::sync::Arc;
|
||||
use tokio::fs::File;
|
||||
use tokio::io::{AsyncReadExt, AsyncSeekExt};
|
||||
use tokio_util::io::StreamReader;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(super) struct AppendToUploadPathParameters {
|
||||
upload_id: UploadId,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum HandleAppendOutcome {
|
||||
RequestSuperseded,
|
||||
UploadOffsetMismatch { expected: u64, provided: u64 },
|
||||
InconsistentUploadLength { expected: u64, detail: Cow<'static, str> },
|
||||
ContentStreamStoppedUnexpectedly,
|
||||
TooMuchContent,
|
||||
UploadIncomplete { offset: u64 },
|
||||
UploadComplete,
|
||||
}
|
||||
|
||||
impl IntoResponse for HandleAppendOutcome {
|
||||
fn into_response(self) -> Response {
|
||||
match self {
|
||||
HandleAppendOutcome::RequestSuperseded => (
|
||||
StatusCode::CONFLICT,
|
||||
UploadCompleteResponseHeader(false),
|
||||
Json(json!({
|
||||
"type": "https://minna.media/api-problems/caos/request-superseded",
|
||||
"title": "Another request superseded the current request.",
|
||||
})),
|
||||
)
|
||||
.into_response(),
|
||||
HandleAppendOutcome::UploadOffsetMismatch { expected, provided } => (
|
||||
StatusCode::CONFLICT,
|
||||
UploadCompleteResponseHeader(false),
|
||||
UploadOffsetResponseHeader(expected),
|
||||
Json(json!({
|
||||
"type": "https://iana.org/assignments/http-problem-types#mismatching-upload-offset",
|
||||
"title": "The upload offset provided in the request does not match the actual offset of the resource.",
|
||||
"expected-offset": expected,
|
||||
"provided-offset": provided,
|
||||
})),
|
||||
)
|
||||
.into_response(),
|
||||
HandleAppendOutcome::InconsistentUploadLength { expected, detail } => (
|
||||
StatusCode::CONFLICT,
|
||||
UploadCompleteResponseHeader(false),
|
||||
UploadOffsetResponseHeader(expected),
|
||||
Json(json!({
|
||||
"type": "https://iana.org/assignments/http-problem-types#inconsistent-upload-length",
|
||||
"title": "The provided upload lengths are inconsistent with one another or a previously established total length.",
|
||||
"detail": detail,
|
||||
})),
|
||||
)
|
||||
.into_response(),
|
||||
HandleAppendOutcome::ContentStreamStoppedUnexpectedly => (
|
||||
StatusCode::BAD_REQUEST,
|
||||
UploadCompleteResponseHeader(false),
|
||||
Json(json!({
|
||||
"type": "https://minna.media/api-problems/caos/content-stream-stopped-unexpectedly",
|
||||
"title": "The content stream stopped unexpectedly.",
|
||||
})),
|
||||
)
|
||||
.into_response(),
|
||||
HandleAppendOutcome::TooMuchContent => (
|
||||
StatusCode::BAD_REQUEST,
|
||||
UploadCompleteResponseHeader(false),
|
||||
Json(json!({
|
||||
"type": "https://minna.media/api-problems/caos/too-much-content",
|
||||
"title": "The request contained more content than it should.",
|
||||
})),
|
||||
)
|
||||
.into_response(),
|
||||
HandleAppendOutcome::UploadIncomplete { offset } => (
|
||||
StatusCode::NO_CONTENT,
|
||||
UploadCompleteResponseHeader(false),
|
||||
UploadOffsetResponseHeader(offset),
|
||||
Body::empty(),
|
||||
)
|
||||
.into_response(),
|
||||
HandleAppendOutcome::UploadComplete => (StatusCode::NO_CONTENT, UploadCompleteResponseHeader(true), Body::empty()).into_response(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) async fn append_to_upload(
|
||||
State(context): State<Context>,
|
||||
Path(AppendToUploadPathParameters { upload_id }): Path<AppendToUploadPathParameters>,
|
||||
headers: HeaderMap,
|
||||
request_body: Body,
|
||||
) -> Result<impl IntoResponse, (UploadCompleteResponseHeader, ApiError)> {
|
||||
let parameters = parse_request_parameters(&context.upload_manager, upload_id, &headers)
|
||||
.await
|
||||
.map_err(|e| (UploadCompleteResponseHeader(false), e))?;
|
||||
|
||||
do_append(parameters, request_body.into_data_stream()).await.map_err(|e| {
|
||||
(
|
||||
UploadCompleteResponseHeader(false),
|
||||
ApiError::Internal {
|
||||
report: Report::new(e).wrap_err(format!("IO error during file upload ({upload_id})")),
|
||||
},
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
struct RequestParameters {
|
||||
pub upload: Arc<UnfinishedUpload>,
|
||||
pub supplied_content_length: Option<u64>,
|
||||
pub supplied_upload_offset: u64,
|
||||
pub supplied_upload_complete: bool,
|
||||
}
|
||||
|
||||
async fn parse_request_parameters(upload_manager: &UploadManager, upload_id: UploadId, headers: &HeaderMap) -> Result<RequestParameters, ApiError> {
|
||||
let upload = if let Some(upload) = upload_manager.get_upload_by_id(&upload_id) {
|
||||
upload
|
||||
} else {
|
||||
return Err(ApiError::UnknownResource {
|
||||
resource_type: "upload".into(),
|
||||
id: upload_id.to_string().into(),
|
||||
});
|
||||
};
|
||||
|
||||
if !headers
|
||||
.get_exactly_once(&axum::http::header::CONTENT_TYPE)?
|
||||
.to_str()
|
||||
.ok()
|
||||
.map(|v| v == PARTIAL_UPLOAD_MEDIA_TYPE)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return Err(ApiError::InvalidRequestHeader {
|
||||
name: &axum::http::header::CONTENT_TYPE,
|
||||
message: format!("must be {}", PARTIAL_UPLOAD_MEDIA_TYPE.to_string()).into(),
|
||||
});
|
||||
}
|
||||
|
||||
let supplied_content_length = headers
|
||||
.get_at_most_once(&axum::http::header::CONTENT_LENGTH)?
|
||||
.map(|v| v.get_unsigned_decimal_number(&axum::http::header::CONTENT_LENGTH))
|
||||
.transpose()?;
|
||||
|
||||
let supplied_upload_offset = headers
|
||||
.get_exactly_once(&upload_headers::UPLOAD_OFFSET)?
|
||||
.get_unsigned_decimal_number(&upload_headers::UPLOAD_OFFSET)?;
|
||||
|
||||
let supplied_upload_complete = headers
|
||||
.get_exactly_once(&upload_headers::UPLOAD_COMPLETE)?
|
||||
.get_boolean(&upload_headers::UPLOAD_COMPLETE)?;
|
||||
|
||||
Ok(RequestParameters {
|
||||
upload,
|
||||
supplied_content_length,
|
||||
supplied_upload_offset,
|
||||
supplied_upload_complete,
|
||||
})
|
||||
}
|
||||
|
||||
async fn do_append(parameters: RequestParameters, content_stream: BodyDataStream) -> Result<HandleAppendOutcome, std::io::Error> {
|
||||
let mut file_acquisition = if let Some(file) = parameters.upload.file().acquire().await {
|
||||
file
|
||||
} else {
|
||||
return Ok(HandleAppendOutcome::RequestSuperseded);
|
||||
};
|
||||
|
||||
let release_request_token = file_acquisition.release_request_token();
|
||||
let mut file = file_acquisition.inner().get_or_open().await?;
|
||||
|
||||
let total_size = parameters.upload.total_size();
|
||||
let current_offset = file.stream_position().await?;
|
||||
let remaining_content_length = total_size - current_offset;
|
||||
|
||||
if parameters.supplied_upload_offset != current_offset {
|
||||
return Ok(HandleAppendOutcome::UploadOffsetMismatch {
|
||||
expected: current_offset,
|
||||
provided: parameters.supplied_upload_offset,
|
||||
});
|
||||
}
|
||||
|
||||
let payload_length_limit = if let Some(supplied_content_length) = parameters.supplied_content_length {
|
||||
if parameters.supplied_upload_complete {
|
||||
if remaining_content_length != supplied_content_length {
|
||||
return Ok(HandleAppendOutcome::InconsistentUploadLength {
|
||||
expected: total_size,
|
||||
detail: "Upload-Complete is set to true, and Content-Length is set, \
|
||||
but the value of Content-Length does not equal the length of the remaining content."
|
||||
.into(),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if supplied_content_length >= remaining_content_length {
|
||||
return Ok(HandleAppendOutcome::InconsistentUploadLength {
|
||||
expected: total_size,
|
||||
detail: "Upload-Complete is set to false, and Content-Length is set, \
|
||||
but the value of Content-Length is not smaller than the length of the remaining content."
|
||||
.into(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
supplied_content_length
|
||||
} else {
|
||||
remaining_content_length
|
||||
};
|
||||
|
||||
let outcome = tokio::select! {
|
||||
o = stream_to_file(
|
||||
content_stream,
|
||||
&mut file,
|
||||
remaining_content_length,
|
||||
parameters.supplied_content_length,
|
||||
parameters.supplied_upload_complete,
|
||||
payload_length_limit
|
||||
) => Some(o?),
|
||||
_ = release_request_token.cancelled() => None
|
||||
};
|
||||
|
||||
file.sync_all().await?;
|
||||
|
||||
let is_upload_complete = parameters.supplied_upload_complete && outcome.as_ref().map(|o| matches!(o, StreamToFileOutcome::Success { .. })).unwrap_or(false);
|
||||
|
||||
if is_upload_complete {
|
||||
parameters.upload.mark_as_finished(file_acquisition).await;
|
||||
} else {
|
||||
file_acquisition.release().await;
|
||||
}
|
||||
|
||||
Ok(if let Some(outcome) = outcome {
|
||||
match outcome {
|
||||
StreamToFileOutcome::StoppedUnexpectedly => HandleAppendOutcome::ContentStreamStoppedUnexpectedly,
|
||||
StreamToFileOutcome::TooMuchContent => HandleAppendOutcome::TooMuchContent,
|
||||
StreamToFileOutcome::Success { written_bytes } => {
|
||||
if is_upload_complete {
|
||||
HandleAppendOutcome::UploadComplete
|
||||
} else {
|
||||
HandleAppendOutcome::UploadIncomplete {
|
||||
offset: current_offset + written_bytes,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
HandleAppendOutcome::RequestSuperseded
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum StreamToFileOutcome {
|
||||
StoppedUnexpectedly,
|
||||
TooMuchContent,
|
||||
Success { written_bytes: u64 },
|
||||
}
|
||||
|
||||
async fn stream_to_file(
|
||||
content_stream: BodyDataStream,
|
||||
file: &mut File,
|
||||
remaining_content_length: u64,
|
||||
supplied_content_length: Option<u64>,
|
||||
supplied_upload_complete: bool,
|
||||
payload_length_limit: u64,
|
||||
) -> Result<StreamToFileOutcome, std::io::Error> {
|
||||
let body_with_io_error = content_stream.into_stream().map_err(|err| std::io::Error::new(ErrorKind::Other, err));
|
||||
let stream = StreamReader::new(body_with_io_error).take(payload_length_limit + 1);
|
||||
futures::pin_mut!(stream);
|
||||
|
||||
match tokio::io::copy(&mut stream, file).await {
|
||||
Ok(n) => {
|
||||
if let Some(supplied_content_length) = supplied_content_length {
|
||||
if n < supplied_content_length {
|
||||
return Ok(StreamToFileOutcome::StoppedUnexpectedly);
|
||||
}
|
||||
} else {
|
||||
if supplied_upload_complete {
|
||||
if n < remaining_content_length {
|
||||
return Ok(StreamToFileOutcome::StoppedUnexpectedly);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if n > payload_length_limit {
|
||||
return Ok(StreamToFileOutcome::TooMuchContent);
|
||||
}
|
||||
|
||||
Ok(StreamToFileOutcome::Success { written_bytes: n })
|
||||
}
|
||||
Err(error) => match error.kind() {
|
||||
ErrorKind::TimedOut => Ok(StreamToFileOutcome::StoppedUnexpectedly),
|
||||
ErrorKind::BrokenPipe => Ok(StreamToFileOutcome::StoppedUnexpectedly),
|
||||
ErrorKind::ConnectionReset => Ok(StreamToFileOutcome::StoppedUnexpectedly),
|
||||
_ => Err(error),
|
||||
},
|
||||
}
|
||||
}
|
71
src/http_api/upload/mod.rs
Normal file
71
src/http_api/upload/mod.rs
Normal file
|
@ -0,0 +1,71 @@
|
|||
use crate::http_api::Context;
|
||||
use crate::http_api::api_error::ApiError;
|
||||
use crate::http_api::auth::AppAuthorization;
|
||||
use crate::http_api::headers::upload_headers;
|
||||
use crate::http_api::upload::append_to_upload::append_to_upload;
|
||||
use crate::upload_manager::UploadId;
|
||||
use axum::extract::State;
|
||||
use axum::http::HeaderValue;
|
||||
use axum::response::{IntoResponseParts, ResponseParts};
|
||||
use axum::{Json, Router, routing};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub mod append_to_upload;
|
||||
|
||||
const PARTIAL_UPLOAD_MEDIA_TYPE: &'static str = "application/partial-upload";
|
||||
|
||||
struct UploadCompleteResponseHeader(bool);
|
||||
|
||||
impl IntoResponseParts for UploadCompleteResponseHeader {
|
||||
type Error = ();
|
||||
|
||||
fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseParts, Self::Error> {
|
||||
res.headers_mut()
|
||||
.insert(&*upload_headers::UPLOAD_COMPLETE, HeaderValue::from_static(if self.0 { "?1" } else { "?0" }));
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
struct UploadOffsetResponseHeader(u64);
|
||||
|
||||
impl IntoResponseParts for UploadOffsetResponseHeader {
|
||||
type Error = ();
|
||||
|
||||
fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseParts, Self::Error> {
|
||||
res.headers_mut().insert(&*upload_headers::UPLOAD_OFFSET, self.0.into());
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct CreateUploadPayload {
|
||||
size: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct CreateUploadResponseBody {
|
||||
upload_id: UploadId,
|
||||
}
|
||||
|
||||
pub fn create_uploads_router() -> Router<Context> {
|
||||
Router::new()
|
||||
.route("/", routing::post(create_upload))
|
||||
.route("/{upload_id}", routing::post(append_to_upload))
|
||||
}
|
||||
|
||||
async fn create_upload(
|
||||
State(context): State<Context>,
|
||||
_: AppAuthorization,
|
||||
payload: Json<CreateUploadPayload>,
|
||||
) -> color_eyre::Result<Json<CreateUploadResponseBody>, ApiError> {
|
||||
if payload.size < 1 || payload.size > (2 ^ 63 - 1) {
|
||||
return Err(ApiError::InvalidRequestContent {
|
||||
path: "size".into(),
|
||||
message: "size must be in 1..(2^63 - 1)".into(),
|
||||
});
|
||||
}
|
||||
|
||||
let upload = context.upload_manager.create_upload(payload.size).await?;
|
||||
|
||||
Ok(Json(CreateUploadResponseBody { upload_id: *upload.id() }))
|
||||
}
|
|
@ -1,90 +0,0 @@
|
|||
use crate::http_api::api_error::ApiError;
|
||||
use rocket::Request;
|
||||
use rocket::http::Status;
|
||||
use rocket::request::{FromRequest, Outcome};
|
||||
use std::str::FromStr;
|
||||
|
||||
pub struct SuppliedUploadOffset(pub u64);
|
||||
|
||||
#[rocket::async_trait]
|
||||
impl<'r> FromRequest<'r> for SuppliedUploadOffset {
|
||||
type Error = ApiError;
|
||||
|
||||
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
|
||||
let mut value_iterator = request.headers().get("upload-offset");
|
||||
|
||||
if let Some(value) = value_iterator.next() {
|
||||
if let Ok(value) = u64::from_str(value) {
|
||||
if value_iterator.next().is_none() {
|
||||
return Outcome::Success(SuppliedUploadOffset(value));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Outcome::Error((
|
||||
Status::BadRequest,
|
||||
ApiError::HeaderValidationFailed {
|
||||
name: "Upload-Offset".into(),
|
||||
message: "must be a single 64-bit unsigned decimal number".into(),
|
||||
},
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SuppliedOptionalContentLength(pub Option<u64>);
|
||||
|
||||
#[rocket::async_trait]
|
||||
impl<'r> FromRequest<'r> for SuppliedOptionalContentLength {
|
||||
type Error = ApiError;
|
||||
|
||||
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
|
||||
let mut value_iterator = request.headers().get("content-length");
|
||||
|
||||
if let Some(value) = value_iterator.next() {
|
||||
if let Ok(value) = u64::from_str(value) {
|
||||
if value_iterator.next().is_none() {
|
||||
return Outcome::Success(SuppliedOptionalContentLength(Some(value)));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return Outcome::Success(SuppliedOptionalContentLength(None));
|
||||
};
|
||||
|
||||
Outcome::Error((
|
||||
Status::BadRequest,
|
||||
ApiError::HeaderValidationFailed {
|
||||
name: "Content-Length".into(),
|
||||
message: "must be a single 64-bit unsigned decimal number".into(),
|
||||
},
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SuppliedUploadComplete(pub bool);
|
||||
|
||||
#[rocket::async_trait]
|
||||
impl<'r> FromRequest<'r> for SuppliedUploadComplete {
|
||||
type Error = ApiError;
|
||||
|
||||
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
|
||||
let mut value_iterator = request.headers().get("upload-complete");
|
||||
|
||||
if let Some(value) = value_iterator.next() {
|
||||
if value_iterator.next().is_none() {
|
||||
if value == "?1" {
|
||||
return Outcome::Success(SuppliedUploadComplete(true));
|
||||
} else if value == "?0" {
|
||||
return Outcome::Success(SuppliedUploadComplete(false));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Outcome::Error((
|
||||
Status::BadRequest,
|
||||
ApiError::HeaderValidationFailed {
|
||||
name: "Upload-Complete".into(),
|
||||
message: "must be `?1` (true) or `?0` (false)".into(),
|
||||
},
|
||||
))
|
||||
}
|
||||
}
|
|
@ -36,14 +36,7 @@ async fn main() -> Result<()> {
|
|||
|
||||
log::info!("Initialization successful.");
|
||||
|
||||
start_http_api_server(
|
||||
upload_manager,
|
||||
config.http_address,
|
||||
config.http_port,
|
||||
config.trust_http_reverse_proxy,
|
||||
config.api_secret,
|
||||
)
|
||||
.await?;
|
||||
start_http_api_server(upload_manager, config.http_address, config.http_port, config.api_secret).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue