diff --git a/backend/Cargo.lock b/backend/Cargo.lock index ae9dd5e..fc12f9a 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -53,6 +53,28 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "aws-lc-rs" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ec2f1fc3ec205783a5da9a7e6c1509cc69dedf09a1949e412c1e18469326d00" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a2f9779ce85b93ab6170dd940ad0169b5766ff848247aff13bb788b832fe3f4" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", +] + [[package]] name = "axum" version = "0.8.9" @@ -60,6 +82,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" dependencies = [ "axum-core", + "axum-macros", "bytes", "form_urlencoded", "futures-util", @@ -72,6 +95,7 @@ dependencies = [ "matchit", "memchr", "mime", + "multer", "percent-encoding", "pin-project-lite", "serde_core", @@ -105,6 +129,17 @@ dependencies = [ "tracing", ] +[[package]] +name = "axum-macros" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aa268c23bfbbd2c4363b9cd302a4f504fb2a9dfe7e3451d66f35dd392e20aca" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "axum-test" version = "20.0.0" @@ -143,6 +178,7 @@ dependencies = [ "chrono", "dotenvy", "lazy-regex", + "opendal", "serde", "serde_json", "serial_test", @@ -154,6 +190,17 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "backon" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cffb0e931875b666fc4fcb20fee52e9bbd1ef836fd9e9e04ec21555f9f85f7ef" +dependencies = [ + "fastrand", + "gloo-timers", + "tokio", +] + [[package]] name = "base64" version = "0.22.1" @@ -215,6 +262,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7a4d3ec6524d28a329fc53654bbadc9bdd7b0431f5d65f1a56ffb28a1ee5283" dependencies = [ "find-msvc-tools", + "jobserver", + "libc", "shlex", ] @@ -224,6 +273,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chacha20" version = "0.10.0" @@ -249,6 +304,25 @@ dependencies = [ "windows-link", ] +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -275,6 +349,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -339,6 +423,22 @@ dependencies = [ "typenum", ] +[[package]] +name = "ctor" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "424e0138278faeb2b401f174ad17e715c829512d74f3d1e81eb43365c2e0590e" +dependencies = [ + "ctor-proc-macro", + "dtor", +] + +[[package]] +name = "ctor-proc-macro" +version = "0.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1" + [[package]] name = "der" version = "0.7.10" @@ -394,6 +494,27 @@ version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" +[[package]] +name = "dtor" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "404d02eeb088a82cfd873006cb713fe411306c7d182c344905e101fb1167d301" +dependencies = [ + "dtor-proc-macro", +] + +[[package]] +name = "dtor-proc-macro" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5" + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + [[package]] name = "either" version = "1.15.0" @@ -412,6 +533,15 @@ 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 = "equivalent" version = "1.0.2" @@ -490,6 +620,12 @@ dependencies = [ "syn", ] +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -522,6 +658,27 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.32" @@ -595,6 +752,7 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ + "futures-channel", "futures-core", "futures-io", "futures-macro", @@ -622,8 +780,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 5.3.0", + "wasip2", + "wasm-bindgen", ] [[package]] @@ -634,12 +808,24 @@ checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 6.0.0", "rand_core 0.10.0", "wasip2", "wasip3", ] +[[package]] +name = "gloo-timers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + [[package]] name = "hashbrown" version = "0.15.5" @@ -777,19 +963,37 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", +] + [[package]] name = "hyper-util" version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ + "base64", "bytes", "futures-channel", "futures-util", "http", "http-body", "hyper", + "ipnet", "libc", + "percent-encoding", "pin-project-lite", "socket2", "tokio", @@ -951,18 +1155,128 @@ dependencies = [ "rustversion", ] +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + [[package]] name = "itoa" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "jiff" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f00b5dbd620d61dfdcb6007c9c1f6054ebd75319f163d886a9055cec1155073d" +dependencies = [ + "jiff-static", + "jiff-tzdb-platform", + "js-sys", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", + "wasm-bindgen", + "windows-sys 0.61.2", +] + +[[package]] +name = "jiff-static" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e000de030ff8022ea1da3f466fbb0f3a809f5e51ed31f6dd931c35181ad8e6d7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "jiff-tzdb" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c900ef84826f1338a557697dc8fc601df9ca9af4ac137c7fb61d4c6f2dfd3076" + +[[package]] +name = "jiff-tzdb-platform" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" +dependencies = [ + "jiff-tzdb", +] + +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys", + "log", + "simd_cesu8", + "thiserror", + "walkdir", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + [[package]] name = "js-sys" version = "0.3.94" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e04e2ef80ce82e13552136fabeef8a5ed1f985a96805761cbb9a2c34e7664d9" dependencies = [ + "cfg-if", + "futures-util", "once_cell", "wasm-bindgen", ] @@ -1039,6 +1353,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + [[package]] name = "litemap" version = "0.8.2" @@ -1060,6 +1380,12 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + [[package]] name = "matchers" version = "0.2.0" @@ -1085,6 +1411,15 @@ dependencies = [ "digest", ] +[[package]] +name = "mea" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6747f54621d156e1b47eb6b25f39a941b9fc347f98f67d25d8881ff99e8ed832" +dependencies = [ + "slab", +] + [[package]] name = "memchr" version = "2.8.0" @@ -1118,6 +1453,23 @@ dependencies = [ "windows-sys 0.61.2", ] +[[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.50.3" @@ -1229,6 +1581,131 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "opendal" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97b31d3d8e99a85d83b73ec26647f5607b80578ed9375810b6e44ffa3590a236" +dependencies = [ + "ctor", + "opendal-core", + "opendal-layer-concurrent-limit", + "opendal-layer-logging", + "opendal-layer-retry", + "opendal-layer-timeout", + "opendal-service-fs", + "opendal-testkit", +] + +[[package]] +name = "opendal-core" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1849dd2687e173e776d3af5fce1ba3ae47b9dd37a09d1c4deba850ef45fe00ca" +dependencies = [ + "anyhow", + "base64", + "bytes", + "futures", + "http", + "http-body", + "jiff", + "log", + "md-5", + "mea", + "percent-encoding", + "quick-xml", + "reqsign-core", + "reqwest", + "serde", + "serde_json", + "tokio", + "url", + "uuid", + "web-time", +] + +[[package]] +name = "opendal-layer-concurrent-limit" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "048b1b29c503263bdd80a9afe46a68cd02ea9bd361185b1feab4b151078998e9" +dependencies = [ + "futures", + "http", + "mea", + "opendal-core", +] + +[[package]] +name = "opendal-layer-logging" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2645adc988b12eda106e2679ae529facfbbaa868ceb706f6f8125c6af15c47b" +dependencies = [ + "log", + "opendal-core", +] + +[[package]] +name = "opendal-layer-retry" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4eac134ffa4ddda6131a640a84a5315996424b9416c85052f8c64c1a33b70ad4" +dependencies = [ + "backon", + "log", + "opendal-core", +] + +[[package]] +name = "opendal-layer-timeout" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "619586ab7480c2e3009f6d18eabab18957bc094778fd130bcc38924970a90f4c" +dependencies = [ + "opendal-core", + "tokio", +] + +[[package]] +name = "opendal-service-fs" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf0be0417abeeb0053376d816b90fceb9ca98f20dfb54ebf1f2a282729f83663" +dependencies = [ + "bytes", + "log", + "opendal-core", + "serde", + "tokio", + "xattr", +] + +[[package]] +name = "opendal-testkit" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd74c7a2655c98d8147dfb1cf3d753112b8b9631fc5a50a4bdf5d42613acb04e" +dependencies = [ + "bytes", + "dotenvy", + "opendal-core", + "opendal-layer-logging", + "opendal-layer-retry", + "opendal-layer-timeout", + "rand 0.8.6", + "sha2", + "tokio", + "uuid", +] + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + [[package]] name = "parking" version = "2.2.1" @@ -1312,6 +1789,21 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "portable-atomic-util" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" +dependencies = [ + "portable-atomic", +] + [[package]] name = "potential_utf" version = "0.1.5" @@ -1365,6 +1857,72 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "quick-xml" +version = "0.38.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +dependencies = [ + "aws-lc-rs", + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.4", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.52.0", +] + [[package]] name = "quote" version = "1.0.45" @@ -1374,6 +1932,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "r-efi" version = "6.0.0" @@ -1387,10 +1951,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" dependencies = [ "libc", - "rand_chacha", + "rand_chacha 0.3.1", "rand_core 0.6.4", ] +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + [[package]] name = "rand" version = "0.10.0" @@ -1412,6 +1986,16 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + [[package]] name = "rand_core" version = "0.6.4" @@ -1421,6 +2005,15 @@ dependencies = [ "getrandom 0.2.17", ] +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + [[package]] name = "rand_core" version = "0.10.0" @@ -1474,6 +2067,66 @@ version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +[[package]] +name = "reqsign-core" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b10302cf0a7d7e7352ba211fc92c3c5bebf1286153e49cc5aa87348078a8e102" +dependencies = [ + "anyhow", + "base64", + "bytes", + "form_urlencoded", + "futures", + "hex", + "hmac", + "http", + "jiff", + "log", + "percent-encoding", + "sha1", + "sha2", + "windows-sys 0.61.2", +] + +[[package]] +name = "reqwest" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62e0021ea2c22aed41653bc7e1419abb2c97e038ff2c33d0e1309e49a97deec0" +dependencies = [ + "base64", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + [[package]] name = "reserve-port" version = "2.4.0" @@ -1532,12 +2185,41 @@ dependencies = [ "thiserror", ] +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + [[package]] name = "rustls" version = "0.23.39" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c2c118cb077cca2822033836dfb1b975355dfb784b5e8da48f7b6c5db74e60e" dependencies = [ + "aws-lc-rs", "once_cell", "ring", "rustls-pki-types", @@ -1546,21 +2228,62 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-native-certs" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + [[package]] name = "rustls-pki-types" version = "1.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" dependencies = [ + "web-time", "zeroize", ] +[[package]] +name = "rustls-platform-verifier" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" +dependencies = [ + "core-foundation", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + [[package]] name = "rustls-webpki" version = "0.103.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" dependencies = [ + "aws-lc-rs", "ring", "rustls-pki-types", "untrusted", @@ -1578,6 +2301,15 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "scc" version = "2.4.0" @@ -1587,6 +2319,15 @@ dependencies = [ "sdd", ] +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -1599,6 +2340,29 @@ version = "3.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "490dcfcbfef26be6800d11870ff2df8774fa6e86d047e3e8c8a76b25655e41ca" +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "semver" version = "1.0.28" @@ -1754,6 +2518,22 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "simd_cesu8" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "slab" version = "0.4.12" @@ -2035,6 +2815,9 @@ name = "sync_wrapper" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] [[package]] name = "synstructure" @@ -2134,9 +2917,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.52.1" +version = "1.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" dependencies = [ "bytes", "libc", @@ -2160,6 +2943,16 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "tokio-stream" version = "0.1.18" @@ -2218,9 +3011,9 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.8" +version = "0.6.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +checksum = "68d6fdd9f81c2819c9a8b0e0cd91660e7746a8e6ea2ba7c6b2b057985f6bcb51" dependencies = [ "bitflags", "bytes", @@ -2237,9 +3030,11 @@ dependencies = [ "pin-project-lite", "tokio", "tokio-util", + "tower", "tower-layer", "tower-service", "tracing", + "url", ] [[package]] @@ -2427,7 +3222,9 @@ version = "1.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" dependencies = [ + "getrandom 0.4.2", "js-sys", + "serde_core", "wasm-bindgen", ] @@ -2449,6 +3246,16 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "want" version = "0.3.1" @@ -2501,6 +3308,16 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03623de6905b7206edd0a75f69f747f134b7f0a2323392d664448bf2d3c5d87e" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.117" @@ -2555,6 +3372,19 @@ dependencies = [ "wasmparser", ] +[[package]] +name = "wasm-streams" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "wasmparser" version = "0.244.0" @@ -2567,6 +3397,35 @@ dependencies = [ "semver", ] +[[package]] +name = "web-sys" +version = "0.3.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd70027e39b12f0849461e08ffc50b9cd7688d942c1c8e3c7b22273236b4dd0a" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-root-certs" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "webpki-roots" version = "0.26.11" @@ -2595,6 +3454,15 @@ dependencies = [ "wasite", ] +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "windows-core" version = "0.62.2" @@ -2896,6 +3764,16 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix", +] + [[package]] name = "yansi" version = "1.0.1" diff --git a/backend/Cargo.toml b/backend/Cargo.toml index af961b2..78ecad1 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -5,11 +5,11 @@ edition = "2024" [dependencies] anyhow = "1.0.102" -axum = { version = "0.8.9", features = ["tracing"] } +axum = { version = "0.8.9", features = ["tracing", "multipart", "macros"] } serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.149" -tokio = { version = "1.52.1", features = ["full"] } -tower-http = { version = "0.6.8", features = ["fs", "trace"] } +tokio = { version = "1.52.3", features = ["full"] } +tower-http = { version = "0.6.10", features = ["fs", "trace", "cors"] } tower-cookies = "0.11.0" tracing = "0.1.44" tracing-subscriber = { version = "0.3.23", features = ["env-filter"] } @@ -17,6 +17,7 @@ lazy-regex = "3.6.0" sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "postgres", "uuid", "chrono"] } chrono = { version = "0.4.44", features = ["serde"] } dotenvy = "0.15.7" +opendal = { version = "0.56.0", features = ["tests", "services-fs"] } [dev-dependencies] axum-test = "20.0.0" diff --git a/backend/migrations/20260511201435_add_storage_path_to_file_records.sql b/backend/migrations/20260511201435_add_storage_path_to_file_records.sql new file mode 100644 index 0000000..1842f51 --- /dev/null +++ b/backend/migrations/20260511201435_add_storage_path_to_file_records.sql @@ -0,0 +1 @@ +ALTER TABLE file_records ADD COLUMN storage_path TEXT NOT NULL DEFAULT ''; diff --git a/backend/src/error.rs b/backend/src/error.rs index c48319b..afd02b3 100644 --- a/backend/src/error.rs +++ b/backend/src/error.rs @@ -10,6 +10,7 @@ pub enum LoftError { AuthFailTokenWrongFormat, AuthFailCtxNotInRequestExt, FileIdNotFound, + UndefinedErrorType, } impl fmt::Display for LoftError { @@ -34,6 +35,10 @@ impl IntoResponse for LoftError { info!("NOT_FOUND"); StatusCode::NOT_FOUND.into_response() } + Self::UndefinedErrorType => { + info!("INTERNAL_SERVER_ERROR"); + StatusCode::INTERNAL_SERVER_ERROR.into_response() + } } } } diff --git a/backend/src/main.rs b/backend/src/main.rs index 6e5c992..e14143f 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -4,9 +4,15 @@ mod model; mod web; use anyhow::Result; -use axum::{Router, middleware, response::Response}; +use axum::{ + Router, + extract::DefaultBodyLimit, + http::{HeaderValue, Method, header}, + middleware, + response::Response, +}; use tower_cookies::CookieManagerLayer; -use tower_http::{services::ServeDir, trace::TraceLayer}; +use tower_http::{cors::CorsLayer, services::ServeDir, trace::TraceLayer}; use tracing::info; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; @@ -33,8 +39,9 @@ async fn main() -> Result<()> { let file_repository = FileRepository::new().await?; - let routes_file = - routes_file(file_repository.clone()).route_layer(middleware::from_fn(mw_require_auth)); + let routes_file = routes_file(file_repository.clone()) + .route_layer(middleware::from_fn(mw_require_auth)) + .layer(DefaultBodyLimit::disable()); let app = Router::new() .nest("/api", routes_file) @@ -47,6 +54,13 @@ async fn main() -> Result<()> { mw_ctx_resolver, )) .layer(CookieManagerLayer::new()) + .layer( + CorsLayer::new() + .allow_origin("http://localhost:5173".parse::().unwrap()) + .allow_methods([Method::GET, Method::POST, Method::DELETE]) + .allow_credentials(true) + .allow_headers([header::CONTENT_TYPE]), + ) .fallback_service(ServeDir::new("./")); let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?; diff --git a/backend/src/model.rs b/backend/src/model.rs index 7b88b14..cbbf4ab 100644 --- a/backend/src/model.rs +++ b/backend/src/model.rs @@ -1,26 +1,24 @@ +use opendal::{Operator, layers::LoggingLayer, services}; use serde::{Deserialize, Serialize}; -use sqlx::{PgPool, prelude::FromRow}; +use sqlx::{PgPool, prelude::FromRow, types::uuid}; use std::fmt::Display; +use tracing::info; use crate::error::LoftError; #[derive(Clone, Debug, Serialize, FromRow)] +#[serde(rename_all = "camelCase")] pub struct FileRecord { pub id: i64, // pub user_id: i64, pub name: String, pub file_type: String, pub size: i64, + #[serde(skip_serializing)] + pub storage_path: String, pub uploaded_at: chrono::DateTime, } -#[derive(Clone, Debug, Deserialize)] -pub struct FileToCreate { - pub name: String, - pub file_type: FileType, - pub size: i64, -} - #[derive(Clone, Debug, Serialize, Deserialize)] pub enum FileType { Image, @@ -41,6 +39,7 @@ impl Display for FileType { #[derive(Clone)] pub struct FileRepository { pub pool: PgPool, + pub op: Operator, } impl FileRepository { @@ -49,20 +48,39 @@ impl FileRepository { let database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set"); let pool = PgPool::connect(&database_url).await.unwrap(); - Ok(Self { pool }) + let storage_path = std::env::var("STORAGE_PATH").expect("STORAGE_PATH must be set"); + let op = Operator::new(services::Fs::default().root(&storage_path)) + .unwrap() + .layer(LoggingLayer::default()) + .finish(); + //.map_err(|x| LoftError::customerror)?; + + Ok(Self { pool, op }) } - pub async fn upload_file(&self, file_to_create: FileToCreate) -> Result { + pub async fn upload_file( + &self, + bytes: Vec, + file_name: String, + ) -> Result { + let storage_path_name = format!("{}-{}", file_name, uuid::Uuid::new_v4()); + let bytes_length = bytes.len(); + + info!("Uploading file \"{}\"", file_name); + self.op.write(&storage_path_name, bytes).await.unwrap(); + + info!("Saving metadata of file \"{}\" in file_records", file_name); let file = sqlx::query_as!( FileRecord, r#" - INSERT INTO file_records (name, file_type, size) - VALUES ($1, $2, $3) + INSERT INTO file_records (name, file_type, size, storage_path) + VALUES ($1, $2, $3, $4) RETURNING * "#, - file_to_create.name, - file_to_create.file_type.to_string(), - file_to_create.size as i64 + file_name, + "TODO-file_type".to_string(), + bytes_length as i64, + storage_path_name ) .fetch_one(&self.pool) .await @@ -71,8 +89,24 @@ impl FileRepository { Ok(file) } - pub async fn download_file(&self, file_id: i64) -> Result { - sqlx::query_as!( + pub async fn download_file(&self, file_id: i64) -> Result, LoftError> { + info!( + "Fetching metadata of file \"{}\" from file_records", + file_id + ); + let record = self.get_file(file_id).await?; + info!("Downloading file \"{}\"", file_id); + let bytes = self.op.read(&record.storage_path).await.unwrap(); + + Ok(bytes.to_vec()) + } + + pub async fn get_file(&self, file_id: i64) -> Result { + info!( + "Fetching metadata of file \"{}\" from file_records", + file_id + ); + let record = sqlx::query_as!( FileRecord, r#" SELECT * @@ -84,10 +118,21 @@ impl FileRepository { .fetch_optional(&self.pool) .await .unwrap() - .ok_or(LoftError::FileIdNotFound) + .ok_or(LoftError::FileIdNotFound)?; + + Ok(record) } pub async fn delete_file(&self, file_id: i64) -> Result { + info!( + "Fetching metadata of file \"{}\" from file_records", + file_id + ); + let record = self.get_file(file_id).await?; + info!("Downloading file bytes \"{}\"", file_id); + self.op.delete(&record.storage_path).await.unwrap(); + + info!("Downloading file record \"{}\"", file_id); sqlx::query_as!( FileRecord, r#" @@ -123,57 +168,61 @@ impl FileRepository { mod tests { use super::*; - async fn file_repository() -> Result { - Ok(FileRepository::new().await.unwrap()) + async fn truncate(pool: &PgPool) { + sqlx::query!("TRUNCATE TABLE file_records") + .execute(pool) + .await + .unwrap(); } - fn new_file_record(name: &str, file_type: FileType, size: i64) -> FileToCreate { - FileToCreate { - name: name.to_string(), - file_type, - size, - } + async fn file_repository() -> Result { + Ok(FileRepository::new().await.unwrap()) } #[tokio::test] #[serial_test::serial] async fn test_upload_and_list() { let file_repository = file_repository().await.unwrap(); - sqlx::query!("TRUNCATE TABLE file_records") - .execute(&file_repository.pool) + truncate(&file_repository.pool).await; + + file_repository + .upload_file(vec![0u8; 10], "a.jpg".to_string()) + .await + .unwrap(); + file_repository + .upload_file(vec![0u8; 10], "b.jpg".to_string()) .await .unwrap(); - - let file_a = new_file_record("a", FileType::Document, 10); - let file_b = new_file_record("b", FileType::Document, 10); - file_repository.upload_file(file_a).await.unwrap(); - file_repository.upload_file(file_b).await.unwrap(); let files = file_repository.list_files().await.unwrap(); + assert_eq!(files.len(), 2); + truncate(&file_repository.pool).await; } #[tokio::test] #[serial_test::serial] async fn test_download() { let file_repository = file_repository().await.unwrap(); - sqlx::query!("TRUNCATE TABLE file_records") - .execute(&file_repository.pool) + truncate(&file_repository.pool).await; + + let uploaded = file_repository + .upload_file(vec![0u8; 10], "a.jpg".to_string()) .await .unwrap(); - - let file_a = new_file_record("a", FileType::Document, 10); - let uploaded = file_repository.upload_file(file_a).await.unwrap(); let downloaded = file_repository.download_file(uploaded.id).await.unwrap(); - assert_eq!(downloaded.name, "a"); + + assert!(!downloaded.is_empty()); + truncate(&file_repository.pool).await; } #[tokio::test] #[serial_test::serial] async fn test_download_not_found() { let file_repository = file_repository().await.unwrap(); + truncate(&file_repository.pool).await; assert!(matches!( - file_repository.download_file(99).await, + file_repository.download_file(i64::MAX).await, Err(LoftError::FileIdNotFound) )); } @@ -182,18 +231,19 @@ mod tests { #[serial_test::serial] async fn test_delete() { let file_repository = file_repository().await.unwrap(); - sqlx::query!("TRUNCATE TABLE file_records") - .execute(&file_repository.pool) + truncate(&file_repository.pool).await; + + let uploaded = file_repository + .upload_file(vec![0u8; 10], "a.jpg".to_string()) .await .unwrap(); - - let file_a = new_file_record("a", FileType::Document, 10); - let uploaded = file_repository.upload_file(file_a).await.unwrap(); file_repository.delete_file(uploaded.id).await.unwrap(); + assert!(matches!( file_repository.download_file(uploaded.id).await, Err(LoftError::FileIdNotFound) )); + truncate(&file_repository.pool).await; } #[tokio::test] diff --git a/backend/src/web/routes_file.rs b/backend/src/web/routes_file.rs index 49f9c76..75b932a 100644 --- a/backend/src/web/routes_file.rs +++ b/backend/src/web/routes_file.rs @@ -1,40 +1,69 @@ use axum::{ Json, Router, - extract::{Path, State}, + extract::{Multipart, Path, State}, + response::IntoResponse, routing::get, }; use tracing::info; use crate::{ error::LoftError, - model::{FileRecord, FileRepository, FileToCreate}, + model::{FileRecord, FileRepository}, }; pub fn routes_file(file_repository: FileRepository) -> Router { Router::new() .route("/files", get(list_files).post(upload_file)) - .route("/files/{id}", get(download_file).delete(delete_file)) + .route("/files/{id}", get(get_file).delete(delete_file)) + .route("/files/{id}/download", get(download_file)) .with_state(file_repository) } async fn upload_file( State(file_repository): State, - Json(file_to_create): Json, + mut multipart: Multipart, ) -> Result, LoftError> { info!("handler: upload_file"); - let file = file_repository.upload_file(file_to_create).await?; - Ok(Json(file)) + let mut file_name = None; + let mut file_type: Option = None; + let mut bytes = None; + + while let Some(field) = multipart.next_field().await.unwrap() { + match field.name().unwrap() { + // "file_type" => file_type = Some(field.text().await.unwrap().parse().unwrap()), + "file" => { + file_name = field.file_name().map(|s| s.to_string()); + bytes = Some(field.bytes().await.unwrap().to_vec()) + } + _ => (), + } + } + + if let (Some(bytes), Some(file_name)) = (bytes, file_name) { + let file = file_repository.upload_file(bytes, file_name).await?; + return Ok(Json(file)); + } + + Err(LoftError::UndefinedErrorType) } -async fn download_file( +#[axum::debug_handler] +async fn get_file( State(file_repository): State, Path(file_id): Path, ) -> Result, LoftError> { - info!("handler: download_file"); + let record = file_repository.get_file(file_id as i64).await?; + Ok(Json(record)) +} - let file = file_repository.download_file(file_id as i64).await?; - Ok(Json(file)) +#[axum::debug_handler] +async fn download_file( + State(file_repository): State, + Path(file_id): Path, +) -> Result { + let bytes = file_repository.download_file(file_id as i64).await?; + Ok(bytes) } async fn delete_file( @@ -60,8 +89,12 @@ async fn list_files( #[cfg(test)] mod tests { use axum::{Router, middleware}; - use axum_test::TestServer; + use axum_test::{ + TestServer, + multipart::{MultipartForm, Part}, + }; use serde_json::json; + use sqlx::PgPool; use tower_cookies::CookieManagerLayer; use crate::{ @@ -90,6 +123,13 @@ mod tests { TestServer::new(app) } + async fn truncate(pool: &PgPool) { + sqlx::query!("TRUNCATE TABLE file_records") + .execute(pool) + .await + .unwrap(); + } + #[tokio::test] async fn test_requires_auth() { let server = test_server().await; @@ -107,23 +147,28 @@ mod tests { } #[tokio::test] + #[serial_test::serial] async fn test_requires_auth_post() { + let file_repository = FileRepository::new().await.unwrap(); + truncate(&file_repository.pool).await; + let server = test_server().await; server .post("/api/files") - .json(&json!({"name": "a", "file_type": "Document"})) + .multipart(MultipartForm::new().add_part( + "file", + Part::bytes(b"fake_bytes".to_vec()).file_name("a.jpg"), + )) .await .assert_status_unauthorized(); + truncate(&file_repository.pool).await; } #[tokio::test] #[serial_test::serial] async fn test_list_files_empty() { let file_repository = FileRepository::new().await.unwrap(); - sqlx::query!("TRUNCATE TABLE file_records") - .execute(&file_repository.pool) - .await - .unwrap(); + truncate(&file_repository.pool).await; let server = test_server().await; server @@ -138,47 +183,56 @@ mod tests { #[serial_test::serial] async fn test_upload_and_list_files() { let file_repository = FileRepository::new().await.unwrap(); - sqlx::query!("TRUNCATE TABLE file_records") - .execute(&file_repository.pool) - .await - .unwrap(); + truncate(&file_repository.pool).await; let server = test_server().await; let res = server .post("/api/files") .add_header(axum::http::header::COOKIE, AUTH_COOKIE) - .json(&json!({"name": "a", "file_type": "Document", "size": 10})) + .multipart(MultipartForm::new().add_part( + "file", + Part::bytes(b"fake_bytes".to_vec()).file_name("a.jpg"), + )) .await; + res.assert_status_ok(); let file = res.json::(); - assert_eq!(file["name"], "a"); - assert_eq!(file["file_type"], "Document"); - assert_eq!(file["size"], 10); + assert_eq!(file["name"], "a.jpg"); let list = server .get("/api/files") .add_header(axum::http::header::COOKIE, AUTH_COOKIE) .await .json::(); + assert_eq!(list.as_array().unwrap().len(), 1); + truncate(&file_repository.pool).await; } #[tokio::test] #[serial_test::serial] async fn test_download_file() { + let file_repository = FileRepository::new().await.unwrap(); + truncate(&file_repository.pool).await; + let server = test_server().await; let post_res = server .post("/api/files") .add_header(axum::http::header::COOKIE, AUTH_COOKIE) - .json(&json!({"name": "b", "file_type": "Document", "size": 10})) + .multipart(MultipartForm::new().add_part( + "file", + Part::bytes(b"fake_bytes".to_vec()).file_name("a.jpg"), + )) .await; let id = post_res.json::()["id"].as_i64().unwrap(); let res = server - .get(&format!("/api/files/{id}")) + .get(&format!("/api/files/{id}/download")) .add_header(axum::http::header::COOKIE, AUTH_COOKIE) .await; + res.assert_status_ok(); - assert_eq!(res.json::()["name"], "b"); + assert_eq!(res.as_bytes(), b"fake_bytes".as_ref()); + truncate(&file_repository.pool).await; } #[tokio::test] @@ -186,7 +240,7 @@ mod tests { async fn test_download_file_not_found() { let server = test_server().await; server - .get("/api/files/99") + .get("/api/files/99/download") .add_header(axum::http::header::COOKIE, AUTH_COOKIE) .await .assert_status_not_found(); @@ -195,11 +249,17 @@ mod tests { #[tokio::test] #[serial_test::serial] async fn test_delete_file() { + let file_repository = FileRepository::new().await.unwrap(); + truncate(&file_repository.pool).await; + let server = test_server().await; let post_res = server .post("/api/files") .add_header(axum::http::header::COOKIE, AUTH_COOKIE) - .json(&json!({"name": "c", "file_type": "Document", "size": 10})) + .multipart(MultipartForm::new().add_part( + "file", + Part::bytes(b"fake_bytes".to_vec()).file_name("a.jpg"), + )) .await; let id = post_res.json::()["id"].as_i64().unwrap(); @@ -214,6 +274,8 @@ mod tests { .add_header(axum::http::header::COOKIE, AUTH_COOKIE) .await .assert_status_not_found(); + + truncate(&file_repository.pool).await; } #[tokio::test]