From 3ff90ba4f3ea0e0db01377939d4ad02e065f0563 Mon Sep 17 00:00:00 2001 From: Untone Date: Sat, 31 Aug 2024 03:32:37 +0300 Subject: [PATCH] 0.0.3-desirable --- Cargo.lock | 521 ++++++++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 7 +- src/app_state.rs | 263 ++++++++++++++++++++++++ src/auth.rs | 97 +++++++++ src/handlers.rs | 183 +++++++++++++++++ src/main.rs | 477 +------------------------------------------ src/s3_utils.rs | 77 +++++++ src/thumbnail.rs | 47 +++++ 8 files changed, 1192 insertions(+), 480 deletions(-) create mode 100644 src/app_state.rs create mode 100644 src/auth.rs create mode 100644 src/handlers.rs create mode 100644 src/s3_utils.rs create mode 100644 src/thumbnail.rs diff --git a/Cargo.lock b/Cargo.lock index 35165f1..d9a6e6f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -68,6 +68,44 @@ dependencies = [ "syn 2.0.66", ] +[[package]] +name = "actix-multipart" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5118a26dee7e34e894f7e85aa0ee5080ae4c18bf03c0e30d49a80e418f00a53" +dependencies = [ + "actix-multipart-derive", + "actix-utils", + "actix-web", + "derive_more", + "futures-core", + "futures-util", + "httparse", + "local-waker", + "log", + "memchr", + "mime", + "rand", + "serde", + "serde_json", + "serde_plain", + "tempfile", + "tokio", +] + +[[package]] +name = "actix-multipart-derive" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e11eb847f49a700678ea2fa73daeb3208061afa2b9d1a8527c03390f4c4a1c6b" +dependencies = [ + "darling", + "parse-size", + "proc-macro2", + "quote", + "syn 2.0.66", +] + [[package]] name = "actix-router" version = "0.5.3" @@ -221,6 +259,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "aligned-vec" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aa90d7ce82d4be67b64039a3d588d38dbcc6736577de4a847025ce5b0c468d1" + [[package]] name = "alloc-no-stdlib" version = "2.0.4" @@ -242,12 +286,41 @@ version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" +[[package]] +name = "anyhow" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" + +[[package]] +name = "arbitrary" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" + [[package]] name = "arc-swap" version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" +[[package]] +name = "arg_enum_proc_macro" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", +] + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + [[package]] name = "async-trait" version = "0.1.80" @@ -271,6 +344,29 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" +[[package]] +name = "av1-grain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6678909d8c5d46a42abcf571271e15fdbc0a225e3646cf23762cd415046c78bf" +dependencies = [ + "anyhow", + "arrayvec", + "log", + "nom", + "num-rational", + "v_frame", +] + +[[package]] +name = "avif-serialize" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "876c75a42f6364451a033496a14c44bffe41f5f4a8236f697391f11024e596d2" +dependencies = [ + "arrayvec", +] + [[package]] name = "aws-config" version = "1.5.5" @@ -713,6 +809,12 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" +[[package]] +name = "bitstream-io" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b81e1519b0d82120d2fd469d5bfb2919a9361c48b02d82d04befc1cdd2002452" + [[package]] name = "block-buffer" version = "0.10.4" @@ -743,6 +845,12 @@ dependencies = [ "alloc-stdlib", ] +[[package]] +name = "built" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "236e6289eda5a812bc6b53c3b024039382a2895fbbeef2d748b2931546d392c4" + [[package]] name = "bumpalo" version = "3.16.0" @@ -756,10 +864,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773d90827bc3feecfb67fab12e24de0749aad83c74b9504ecde46237b5cd24e2" [[package]] -name = "byteorder" -version = "1.5.0" +name = "byteorder-lite" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" [[package]] name = "bytes" @@ -797,6 +905,16 @@ dependencies = [ "once_cell", ] +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + [[package]] name = "cfg-if" version = "1.0.0" @@ -952,6 +1070,41 @@ dependencies = [ "typenum", ] +[[package]] +name = "darling" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.66", +] + +[[package]] +name = "darling_macro" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.66", +] + [[package]] name = "debugid" version = "0.8.0" @@ -1007,8 +1160,9 @@ dependencies = [ [[package]] name = "discoursio-quoter" -version = "0.0.2" +version = "0.0.3" dependencies = [ + "actix-multipart", "actix-web", "aws-config", "aws-sdk-s3", @@ -1385,6 +1539,12 @@ dependencies = [ "allocator-api2", ] +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hermit-abi" version = "0.3.9" @@ -1581,6 +1741,12 @@ dependencies = [ "tracing", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "0.5.0" @@ -1593,22 +1759,43 @@ dependencies = [ [[package]] name = "image" -version = "0.24.9" +version = "0.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d" +checksum = "99314c8a2152b8ddb211f924cdae532d8c5e4c8bb54728e12fff1b0cd5963a10" dependencies = [ "bytemuck", - "byteorder", + "byteorder-lite", "color_quant", "exr", "gif", - "jpeg-decoder", + "image-webp", "num-traits", "png", "qoi", + "ravif", + "rayon", + "rgb", "tiff", + "zune-core", + "zune-jpeg", ] +[[package]] +name = "image-webp" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f79afb8cbee2ef20f59ccd477a218c12a93943d075b492015ecb1bb81f8ee904" +dependencies = [ + "byteorder-lite", + "quick-error", +] + +[[package]] +name = "imgref" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44feda355f4159a7c757171a77de25daf6411e217b4cabd03bd6650690468126" + [[package]] name = "indexmap" version = "2.2.6" @@ -1619,12 +1806,32 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "interpolate_name" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", +] + [[package]] name = "ipnet" version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.11" @@ -1645,9 +1852,6 @@ name = "jpeg-decoder" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" -dependencies = [ - "rayon", -] [[package]] name = "js-sys" @@ -1682,6 +1886,17 @@ version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +[[package]] +name = "libfuzzer-sys" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a96cfd5557eb82f2b83fed4955246c988d331975a002961b07c81584d107e7f7" +dependencies = [ + "arbitrary", + "cc", + "once_cell", +] + [[package]] name = "linux-raw-sys" version = "0.4.14" @@ -1721,6 +1936,15 @@ version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" +[[package]] +name = "loop9" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" +dependencies = [ + "imgref", +] + [[package]] name = "lru" version = "0.12.4" @@ -1730,6 +1954,15 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "maybe-rayon" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" +dependencies = [ + "cfg-if", +] + [[package]] name = "md-5" version = "0.10.6" @@ -1762,6 +1995,12 @@ dependencies = [ "unicase", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.7.3" @@ -1801,6 +2040,28 @@ dependencies = [ "tempfile", ] +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "noop_proc_macro" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" + [[package]] name = "num-bigint" version = "0.4.6" @@ -1817,6 +2078,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", +] + [[package]] name = "num-integer" version = "0.1.46" @@ -1826,6 +2098,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -1955,6 +2238,12 @@ dependencies = [ "windows-targets 0.52.5", ] +[[package]] +name = "parse-size" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "944553dd59c802559559161f9816429058b869003836120e262e8caec061b7ae" + [[package]] name = "paste" version = "1.0.15" @@ -2049,6 +2338,25 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "profiling" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d84d1d7a6ac92673717f9f6d1518374ef257669c24ebc5ac25d5033828be58" +dependencies = [ + "profiling-procmacros", +] + +[[package]] +name = "profiling-procmacros" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8021cf59c8ec9c432cfc2526ac6b8aa508ecaf29cd415f271b8406c1b851c3fd" +dependencies = [ + "quote", + "syn 2.0.66", +] + [[package]] name = "qoi" version = "0.4.1" @@ -2058,6 +2366,12 @@ dependencies = [ "bytemuck", ] +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + [[package]] name = "quote" version = "1.0.36" @@ -2097,6 +2411,55 @@ dependencies = [ "getrandom", ] +[[package]] +name = "rav1e" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd87ce80a7665b1cce111f8a16c1f3929f6547ce91ade6addf4ec86a8dda5ce9" +dependencies = [ + "arbitrary", + "arg_enum_proc_macro", + "arrayvec", + "av1-grain", + "bitstream-io", + "built", + "cfg-if", + "interpolate_name", + "itertools", + "libc", + "libfuzzer-sys", + "log", + "maybe-rayon", + "new_debug_unreachable", + "noop_proc_macro", + "num-derive", + "num-traits", + "once_cell", + "paste", + "profiling", + "rand", + "rand_chacha", + "simd_helpers", + "system-deps", + "thiserror", + "v_frame", + "wasm-bindgen", +] + +[[package]] +name = "ravif" +version = "0.11.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f0bfd976333248de2078d350bfdf182ff96e168a24d23d2436cef320dd4bdd" +dependencies = [ + "avif-serialize", + "imgref", + "loop9", + "quick-error", + "rav1e", + "rgb", +] + [[package]] name = "rayon" version = "1.10.0" @@ -2238,6 +2601,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rgb" +version = "0.8.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cd5a1e95672f201913966f39baf355b53b5d92833431847295ae0346a5b939" +dependencies = [ + "bytemuck", +] + [[package]] name = "ring" version = "0.17.8" @@ -2564,6 +2936,24 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_plain" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce1fc6db65a611022b23a0dec6975d63fb80a302cb3388835ff02c097258d50" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -2629,6 +3019,15 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +[[package]] +name = "simd_helpers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" +dependencies = [ + "quote", +] + [[package]] name = "slab" version = "0.4.9" @@ -2673,6 +3072,12 @@ dependencies = [ "der", ] +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "subtle" version = "2.6.1" @@ -2728,6 +3133,25 @@ dependencies = [ "libc", ] +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck", + "pkg-config", + "toml", + "version-compare", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + [[package]] name = "tempfile" version = "3.10.1" @@ -2880,6 +3304,40 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + [[package]] name = "tower" version = "0.4.13" @@ -3047,6 +3505,17 @@ dependencies = [ "serde", ] +[[package]] +name = "v_frame" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f32aaa24bacd11e488aa9ba66369c7cd514885742c9fe08cfe85884db3e92b" +dependencies = [ + "aligned-vec", + "num-traits", + "wasm-bindgen", +] + [[package]] name = "valuable" version = "0.1.0" @@ -3059,6 +3528,12 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "version-compare" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" + [[package]] name = "version_check" version = "0.9.4" @@ -3348,6 +3823,15 @@ version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" +[[package]] +name = "winnow" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" +dependencies = [ + "memchr", +] + [[package]] name = "winreg" version = "0.52.0" @@ -3418,6 +3902,12 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "zune-core" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" + [[package]] name = "zune-inflate" version = "0.2.54" @@ -3426,3 +3916,12 @@ checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" dependencies = [ "simd-adler32", ] + +[[package]] +name = "zune-jpeg" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16099418600b4d8f028622f73ff6e3deaabdff330fb9a2a131dea781ee8b0768" +dependencies = [ + "zune-core", +] diff --git a/Cargo.toml b/Cargo.toml index dbb55b1..8e29667 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "discoursio-quoter" -version = "0.0.2" +version = "0.0.3" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -16,10 +16,11 @@ redis = { version = "0.26.1", features = ["tokio-comp"] } tokio = { version = "1.37.0", features = ["full"] } serde = { version = "1.0.209", features = ["derive"] } sentry-actix = "0.34.0" -aws-sdk-s3 = "1.47.0" # AWS SDK для работы с S3 -image = "0.24.7" # Библиотека для работы с изображениями (генерация миниатюр) +aws-sdk-s3 = "1.47.0" +image = "0.25.2" mime_guess = "2.0.5" aws-config = "1.5.5" +actix-multipart = "0.7.2" [[bin]] name = "quoter" diff --git a/src/app_state.rs b/src/app_state.rs new file mode 100644 index 0000000..0f8a6fb --- /dev/null +++ b/src/app_state.rs @@ -0,0 +1,263 @@ +// app_state.rs + +use actix_web::error::ErrorInternalServerError; +use aws_config::BehaviorVersion; +use aws_sdk_s3::{config::Credentials, Client as S3Client}; +use redis::{aio::MultiplexedConnection, AsyncCommands, Client as RedisClient}; +use std::{env, time::Duration}; +use tokio::time::interval; + +use crate::s3_utils::check_file_exists; + +#[derive(Clone)] +pub struct AppState { + pub redis: MultiplexedConnection, + pub s3_client: S3Client, + pub s3_bucket: String, + pub aws_client: S3Client, + pub aws_bucket: String, +} + +const FILE_LIST_CACHE_KEY: &str = "s3_file_list_cache"; // Ключ для хранения списка файлов в Redis +const PATH_MAPPING_KEY: &str = "path_mapping"; // Ключ для хранения маппинга путей +const CHECK_INTERVAL_SECONDS: u64 = 60 * 60; // Интервал обновления списка файлов: 1 час +const WEEK_SECONDS: u64 = 604800; + +impl AppState { + /// Инициализация нового состояния приложения. + pub async fn new() -> Self { + // Получаем конфигурацию для Redis + let redis_url = env::var("REDIS_URL").expect("REDIS_URL must be set"); + let redis_client = RedisClient::open(redis_url).expect("Invalid Redis URL"); + let redis_connection = redis_client + .get_multiplexed_async_connection() + .await + .unwrap(); + + // Получаем конфигурацию для S3 (Storj) + let s3_access_key = env::var("STORJ_ACCESS_KEY").expect("STORJ_ACCESS_KEY must be set"); + let s3_secret_key = env::var("STORJ_SECRET_KEY").expect("STORJ_SECRET_KEY must be set"); + let s3_endpoint = env::var("STORJ_END_POINT").expect("STORJ_END_POINT must be set"); + let s3_bucket = env::var("STORJ_BUCKET_NAME").expect("STORJ_BUCKET_NAME must be set"); + + // Получаем конфигурацию для AWS S3 + let aws_access_key = env::var("AWS_ACCESS_KEY").expect("AWS_ACCESS_KEY must be set"); + let aws_secret_key = env::var("AWS_SECRET_KEY").expect("AWS_SECRET_KEY must be set"); + let aws_endpoint = env::var("AWS_END_POINT").expect("AWS_END_POINT must be set"); + let aws_bucket = env::var("AWS_BUCKET_NAME").expect("AWS_BUCKET_NAME must be set"); + + // Конфигурируем клиент S3 для Storj + let storj_config = aws_config::defaults(BehaviorVersion::latest()) + .region("eu-west-1") + .endpoint_url(s3_endpoint) + .credentials_provider(Credentials::new( + s3_access_key, + s3_secret_key, + None, + None, + "rust-s3-client", + )) + .load() + .await; + + let s3_client = S3Client::new(&storj_config); + + // Конфигурируем клиент S3 для AWS + let aws_config = aws_config::defaults(BehaviorVersion::latest()) + .region("us-east-1") + .endpoint_url(aws_endpoint) + .credentials_provider(Credentials::new( + aws_access_key, + aws_secret_key, + None, + None, + "rust-aws-client", + )) + .load() + .await; + + let aws_client = S3Client::new(&aws_config); + + let app_state = AppState { + redis: redis_connection, + s3_client, + s3_bucket, + aws_client, + aws_bucket, + }; + + // Кэшируем список файлов из S3 при старте приложения + app_state.cache_file_list().await; + + app_state + } + + /// Кэширует список файлов из Storj S3 в Redis. + pub async fn cache_file_list(&self) { + let mut redis = self.redis.clone(); + + // Запрашиваем список файлов из Storj S3 + let list_objects_v2 = self.s3_client.list_objects_v2(); + let list_response = list_objects_v2 + .bucket(&self.s3_bucket) + .send() + .await + .expect("Failed to list files from S3"); + + if let Some(objects) = list_response.contents { + // Формируем список файлов + let file_list: Vec = objects + .iter() + .filter_map(|object| object.key.clone()) + .collect(); + + // Сохраняем список файлов в Redis в формате JSON + let _: () = redis + .set( + FILE_LIST_CACHE_KEY, + serde_json::to_string(&file_list).unwrap(), + ) + .await + .expect("Failed to cache file list in Redis"); + } + } + + /// Получает кэшированный список файлов из Redis. + pub async fn get_cached_file_list(&self) -> Vec { + let mut redis = self.redis.clone(); + + // Пытаемся получить кэшированный список из Redis + let cached_list: Option = redis.get(FILE_LIST_CACHE_KEY).await.unwrap_or(None); + + if let Some(cached_list) = cached_list { + // Если список найден, возвращаем его в виде вектора строк + serde_json::from_str(&cached_list).unwrap_or_else(|_| vec![]) + } else { + vec![] + } + } + + /// Периодически обновляет кэшированный список файлов из Storj S3. + pub async fn refresh_file_list_periodically(&self) { + let mut interval = interval(Duration::from_secs(CHECK_INTERVAL_SECONDS)); + loop { + interval.tick().await; + self.cache_file_list().await; + } + } + + /// Сохраняет маппинг старого пути из AWS S3 на новый путь в Storj S3. + async fn save_path_by_filekey( + &self, + filekey: &str, + path: &str, + ) -> Result<(), actix_web::Error> { + let mut redis = self.redis.clone(); + // Храним маппинг в формате Hash: old_path -> new_path + redis + .hset(PATH_MAPPING_KEY, filekey, path) + .await + .map_err(|_| ErrorInternalServerError("Failed to save path mapping in Redis"))?; + Ok(()) + } + + /// Получает путь в хранилище из ключа (имени файла) в Redis. + pub async fn get_path(&self, filekey: &str) -> Result, actix_web::Error> { + let mut redis = self.redis.clone(); + let new_path: Option = redis + .hget(PATH_MAPPING_KEY, filekey) + .await + .map_err(|_| ErrorInternalServerError("Failed to get path mapping from Redis"))?; + Ok(new_path) + } + + /// Обновляет Storj S3 данными из Amazon S3 + pub async fn update_filelist_from_aws(&self) { + // Получаем список объектов из AWS S3 + let list_objects_v2 = self.aws_client.list_objects_v2(); + let list_response = list_objects_v2 + .bucket(&self.aws_bucket) + .send() + .await + .expect("Failed to list files from AWS S3"); + + // перебор списка файлов + if let Some(objects) = list_response.contents { + for object in objects { + if let Some(key) = object.key { + let filename_with_extension = key.split('/').last().unwrap(); + + // Убираем расширение файла + let filename = filename_with_extension + .rsplit_once('.') + .map(|(name, _ext)| name) + .unwrap_or(filename_with_extension); // Если расширение отсутствует, возвращаем оригинальное имя + + // Проверяем, существует ли файл на Storj S3 + if !check_file_exists(&self.s3_client, &self.s3_bucket, filename) + .await + .unwrap_or(false) + { + // Сохраняем маппинг пути + self.save_path_by_filekey(filename, &key).await.unwrap(); + } + } + } + } + } + + pub async fn get_or_create_quota(&self, user_id: &str) -> Result { + let mut redis = self.redis.clone(); + let quota_key = format!("quota:{}", user_id); + + // Попытка получить квоту из Redis + let quota: u64 = redis.get("a_key).await.unwrap_or(0); + + if quota == 0 { + // Если квота не найдена, устанавливаем её в 0 байт и задаем TTL на одну неделю + redis + .set_ex("a_key, 0, WEEK_SECONDS) + .await + .map_err(|_| { + ErrorInternalServerError("Failed to set initial user quota in Redis") + })?; + + Ok(0) // Возвращаем 0 как начальную квоту + } else { + Ok(quota) + } + } + + pub async fn increment_uploaded_bytes( + &self, + user_id: &str, + bytes: u64, + ) -> Result { + let mut redis = self.redis.clone(); + let quota_key = format!("quota:{}", user_id); + + // Проверяем, существует ли ключ в Redis + let exists: bool = redis.exists("a_key).await.map_err(|_| { + ErrorInternalServerError("Failed to check if user quota exists in Redis") + })?; + + // Если ключ не существует, создаем его с начальным значением и устанавливаем TTL + if !exists { + redis + .set_ex("a_key, bytes, WEEK_SECONDS) + .await + .map_err(|_| { + ErrorInternalServerError("Failed to set initial user quota in Redis") + })?; + return Ok(bytes); + } + + // Если ключ существует, инкрементируем его значение на заданное количество байт + let new_quota: u64 = redis + .incr("a_key, bytes) + .await + .map_err(|_| ErrorInternalServerError("Failed to increment user quota in Redis"))?; + + Ok(new_quota) + } +} diff --git a/src/auth.rs b/src/auth.rs new file mode 100644 index 0000000..30b0ccc --- /dev/null +++ b/src/auth.rs @@ -0,0 +1,97 @@ +// auth.rs + +use actix_web::error::ErrorInternalServerError; +use redis::{aio::MultiplexedConnection, AsyncCommands}; +use reqwest::header::{HeaderMap, HeaderValue, CONTENT_TYPE}; +use reqwest::Client as HTTPClient; +use serde::Deserialize; +use serde_json::json; +use std::{collections::HashMap, env, error::Error}; + +// Структура для десериализации ответа от сервиса аутентификации +#[derive(Deserialize)] +struct AuthResponse { + data: Option, +} + +#[derive(Deserialize)] +struct AuthData { + validate_jwt_token: Option, +} + +#[derive(Deserialize)] +struct ValidateJWTToken { + is_valid: bool, + claims: Option, +} + +#[derive(Deserialize)] +struct Claims { + sub: Option, +} + +/// Получает айди пользователя из токена в заголовке +pub async fn get_id_by_token(token: &str) -> Result> { + let auth_api_base = env::var("AUTH_URL")?; + let query_name = "validate_jwt_token"; + let operation = "ValidateToken"; + let mut headers = HeaderMap::new(); + headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); + + let mut variables = HashMap::>::new(); + let mut params = HashMap::::new(); + params.insert("token".to_string(), token.to_string()); + params.insert("token_type".to_string(), "access_token".to_string()); + variables.insert("params".to_string(), params); + + let gql = json!({ + "query": format!("query {}($params: ValidateJWTTokenInput!) {{ {}(params: $params) {{ is_valid claims }} }}", operation, query_name), + "operationName": operation, + "variables": variables + }); + + let client = HTTPClient::new(); + let response = client + .post(&auth_api_base) + .headers(headers) + .json(&gql) + .send() + .await?; + + if response.status().is_success() { + let auth_response: AuthResponse = response.json().await?; + if let Some(auth_data) = auth_response.data { + if let Some(validate_jwt_token) = auth_data.validate_jwt_token { + if validate_jwt_token.is_valid { + if let Some(claims) = validate_jwt_token.claims { + if let Some(sub) = claims.sub { + return Ok(sub); + } + } + } + } + } + Err(Box::new(std::io::Error::new( + std::io::ErrorKind::Other, + "Invalid token response", + ))) + } else { + Err(Box::new(std::io::Error::new( + std::io::ErrorKind::Other, + format!("Request failed with status: {}", response.status()), + ))) + } +} + +/// Сохраняет имя файла в Redis для пользователя +pub async fn user_added_file( + redis: &mut MultiplexedConnection, + user_id: &str, + filename: &str, +) -> Result<(), actix_web::Error> { + redis + .sadd(user_id, filename) + .await + .map_err(|_| ErrorInternalServerError("Failed to save filename in Redis"))?; // Добавляем имя файла в набор пользователя + Ok(()) +} diff --git a/src/handlers.rs b/src/handlers.rs new file mode 100644 index 0000000..140f993 --- /dev/null +++ b/src/handlers.rs @@ -0,0 +1,183 @@ +// handlers.rs + +use crate::app_state::AppState; +use crate::auth::{get_id_by_token, user_added_file}; +use crate::s3_utils::{ + check_file_exists, generate_key_with_extension, load_file_from_s3, upload_to_s3, +}; +use crate::thumbnail::{find_closest_width, generate_thumbnails, parse_thumbnail_request, ALLOWED_THUMBNAIL_WIDTHS}; +use actix_multipart::Multipart; +use actix_web::error::ErrorInternalServerError; +use actix_web::{web, HttpRequest, HttpResponse, Result}; +use futures::StreamExt; +use mime_guess::MimeGuess; + +pub const MAX_WEEK_BYTES: u64 = 2 * 1024 * 1024 * 1024; // Лимит квоты на пользователя: 2 ГБ в неделю + +/// Функция для обслуживания файла по заданному пути. +async fn serve_file(file_key: &str, state: &AppState) -> Result { + // Проверяем наличие файла в Storj S3 + if !check_file_exists(&state.s3_client, &state.s3_bucket, file_key).await? { + return Err(ErrorInternalServerError("File not found in S3")); + } + + let checked_filekey = state.get_path(file_key).await.unwrap().unwrap(); + + // Получаем объект из Storj S3 + let get_object_output = state + .s3_client + .get_object() + .bucket(&state.s3_bucket) + .key(checked_filekey) + .send() + .await + .map_err(|_| ErrorInternalServerError("Failed to get object from S3"))?; + + let data: aws_sdk_s3::primitives::AggregatedBytes = get_object_output + .body + .collect() + .await + .map_err(|_| ErrorInternalServerError("Failed to read object body"))?; + + let data_bytes = data.into_bytes(); + let mime_type = MimeGuess::from_path(file_key).first_or_octet_stream(); // Определяем MIME-тип файла + + Ok(HttpResponse::Ok() + .content_type(mime_type.as_ref()) + .body(data_bytes)) +} + +/// Обработчик для аплоада файлов. +pub async fn upload_handler( + req: HttpRequest, + mut payload: Multipart, + state: web::Data, +) -> Result { + // Получаем токен из заголовка авторизации + let token = req + .headers() + .get("Authorization") + .and_then(|header_value| header_value.to_str().ok()); + if token.is_none() { + return Err(actix_web::error::ErrorUnauthorized("Unauthorized")); // Если токен отсутствует, возвращаем ошибку + } + + let user_id = get_id_by_token(token.unwrap()).await?; + + // Получаем текущую квоту пользователя + let this_week_amount: u64 = state.get_or_create_quota(&user_id).await.unwrap_or(0); + + while let Some(field) = payload.next().await { + let mut field = field?; + let content_type = field.content_type().unwrap().to_string(); + let file_name = field + .content_disposition() + .unwrap() + .get_filename() + .map(|f| f.to_string()); + + if let Some(name) = file_name { + let mut file_bytes = Vec::new(); + let mut file_size: u64 = 0; + + // Читаем данные файла + while let Some(chunk) = field.next().await { + let data = chunk?; + file_size += data.len() as u64; + file_bytes.extend_from_slice(&data); + } + + // Проверяем, что добавление файла не превышает лимит квоты + if this_week_amount + file_size > MAX_WEEK_BYTES { + return Err(actix_web::error::ErrorUnauthorized("Quota exceeded")); + // Квота превышена + } + + // Инкрементируем квоту пользователя + let _ = state.increment_uploaded_bytes(&user_id, file_size).await?; + + // Определяем правильное расширение и ключ для S3 + let file_key = generate_key_with_extension(name, content_type.to_owned()); + + // Загружаем файл в S3 + upload_to_s3( + &state.s3_client, + &state.s3_bucket, + &file_key, + file_bytes, + &content_type, + ) + .await?; + + // Сохраняем информацию о загруженном файле для пользователя + user_added_file(&mut state.redis.clone(), &user_id, &file_key).await?; + } + } + + Ok(HttpResponse::Ok().json("File uploaded successfully")) +} + +/// Обработчик для скачивания файла и генерации миниатюры, если она недоступна. +pub async fn proxy_handler( + _req: HttpRequest, + path: web::Path, + state: web::Data, +) -> Result { + // весь запрошенный путь + let requested_path = state.get_path(&path).await.unwrap().unwrap(); + + // имя файла + let filename_with_extension = requested_path.split("/").last().unwrap(); + + // убираем расширение файла + let requested_filekey = filename_with_extension + .rsplit_once('.') + .map(|(name, _ext)| name) + .unwrap_or(filename_with_extension); // Если расширение отсутствует, возвращаем оригинальное имя + + // Проверяем, запрошена ли миниатюра + if let Some((base_filename, requested_width, _ext)) = + parse_thumbnail_request(&requested_filekey) + { + // Находим ближайший подходящий размер + let closest_width = find_closest_width(requested_width); + let thumbnail_key = format!("{}_{}", base_filename, closest_width); + + // Проверяем наличие миниатюры в кэше + let cached_files = state.get_cached_file_list().await; + if !cached_files.contains(&thumbnail_key) { + if cached_files.contains(&base_filename) { + // Загружаем оригинальный файл из S3 + let original_data = + load_file_from_s3(&state.s3_client, &state.s3_bucket, &base_filename).await?; + + // Генерируем миниатюру для ближайшего подходящего размера + let image = image::load_from_memory(&original_data).map_err(|_| { + ErrorInternalServerError("Failed to load image for thumbnail generation") + })?; + let thumbnails_bytes = + generate_thumbnails(&image, &ALLOWED_THUMBNAIL_WIDTHS).await?; + let thumbnail_bytes = thumbnails_bytes[&closest_width].clone(); + // Загружаем миниатюру в S3 + upload_to_s3( + &state.s3_client, + &state.s3_bucket, + &thumbnail_key, + thumbnail_bytes.clone(), + "image/jpeg", + ) + .await?; + + return Ok(HttpResponse::Ok() + .content_type("image/jpeg") + .body(thumbnail_bytes)); + } + } else { + // Если миниатюра уже есть в кэше, просто возвращаем её + return serve_file(&thumbnail_key, &state).await; + } + } + + // Если запрошен целый файл + serve_file(&requested_filekey, &state).await +} diff --git a/src/main.rs b/src/main.rs index 1d48540..2095679 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,484 +1,29 @@ -use actix_web::{ - error::{ErrorInternalServerError, ErrorUnauthorized}, - middleware::Logger, - web, App, HttpRequest, HttpResponse, HttpServer, Result, -}; -use aws_config::BehaviorVersion; -use aws_sdk_s3::primitives::ByteStream; -use aws_sdk_s3::{config::Credentials, error::SdkError, Client as S3Client}; -use image::{imageops::FilterType, DynamicImage}; -use mime_guess::MimeGuess; -use redis::Client as RedisClient; -use redis::{aio::MultiplexedConnection, AsyncCommands}; -use reqwest::{ - header::{HeaderMap, HeaderValue, CONTENT_TYPE}, - Client as HTTPClient, -}; -use serde::Deserialize; -use serde_json::json; -use std::path::Path; -use std::{collections::HashMap, error::Error, io::Cursor}; -use std::{env, time::Duration}; -use tokio::time::interval; +mod app_state; +mod auth; +mod handlers; +mod s3_utils; +mod thumbnail; -const MAX_QUOTA_BYTES: u64 = 2 * 1024 * 1024 * 1024; // Лимит квоты на пользователя: 2 ГБ в неделю -const FILE_LIST_CACHE_KEY: &str = "s3_file_list_cache"; // Ключ для хранения списка файлов в Redis -const PATH_MAPPING_KEY: &str = "path_mapping"; // Ключ для хранения маппинга путей -const CHECK_INTERVAL_SECONDS: u64 = 60; // Интервал обновления кэша: 1 минута - -/// Структура состояния приложения, содержащая Redis и S3 клиенты. -#[derive(Clone)] -struct AppState { - redis: MultiplexedConnection, // Подключение к Redis - s3_client: S3Client, // Клиент S3 для Storj - s3_bucket: String, // Название бакета в Storj - aws_client: S3Client, // Клиент S3 для AWS - aws_bucket: String, // Название бакета в AWS -} - -impl AppState { - /// Инициализация нового состояния приложения. - async fn new() -> Self { - // Получаем конфигурацию для Redis - let redis_url = env::var("REDIS_URL").expect("REDIS_URL must be set"); - let redis_client = RedisClient::open(redis_url).expect("Invalid Redis URL"); - let redis_connection = redis_client - .get_multiplexed_async_connection() - .await - .unwrap(); - - // Получаем конфигурацию для S3 (Storj) - let s3_access_key = env::var("STORJ_ACCESS_KEY").expect("STORJ_ACCESS_KEY must be set"); - let s3_secret_key = env::var("STORJ_SECRET_KEY").expect("STORJ_SECRET_KEY must be set"); - let s3_endpoint = env::var("STORJ_END_POINT").expect("STORJ_END_POINT must be set"); - let s3_bucket = env::var("STORJ_BUCKET_NAME").expect("STORJ_BUCKET_NAME must be set"); - - // Получаем конфигурацию для AWS S3 - let aws_access_key = env::var("AWS_ACCESS_KEY").expect("AWS_ACCESS_KEY must be set"); - let aws_secret_key = env::var("AWS_SECRET_KEY").expect("AWS_SECRET_KEY must be set"); - let aws_endpoint = env::var("AWS_END_POINT").expect("AWS_END_POINT must be set"); - let aws_bucket = env::var("AWS_BUCKET_NAME").expect("AWS_BUCKET_NAME must be set"); - - // Конфигурируем клиент S3 для Storj - let storj_config = aws_config::defaults(BehaviorVersion::latest()) - .region("eu-west-1") - .endpoint_url(s3_endpoint) - .credentials_provider(Credentials::new( - s3_access_key, - s3_secret_key, - None, - None, - "rust-s3-client", - )) - .load() - .await; - - let s3_client = S3Client::new(&storj_config); - - // Конфигурируем клиент S3 для AWS - let aws_config = aws_config::defaults(BehaviorVersion::latest()) - .region("us-east-1") - .endpoint_url(aws_endpoint) - .credentials_provider(Credentials::new( - aws_access_key, - aws_secret_key, - None, - None, - "rust-aws-client", - )) - .load() - .await; - - let aws_client = S3Client::new(&aws_config); - - let app_state = AppState { - redis: redis_connection, - s3_client, - s3_bucket, - aws_client, - aws_bucket, - }; - - // Кэшируем список файлов из S3 при старте приложения - app_state.cache_file_list().await; - - app_state - } - - /// Кэширует список файлов из Storj S3 в Redis. - async fn cache_file_list(&self) { - let mut redis = self.redis.clone(); - - // Запрашиваем список файлов из Storj S3 - let list_objects_v2 = self.s3_client.list_objects_v2(); - let list_response = list_objects_v2 - .bucket(&self.s3_bucket) - .send() - .await - .expect("Failed to list files from S3"); - - if let Some(objects) = list_response.contents { - // Формируем список файлов - let file_list: Vec = objects - .iter() - .filter_map(|object| object.key.clone()) - .collect(); - - // Сохраняем список файлов в Redis в формате JSON - let _: () = redis - .set( - FILE_LIST_CACHE_KEY, - serde_json::to_string(&file_list).unwrap(), - ) - .await - .expect("Failed to cache file list in Redis"); - } - } - - /// Получает кэшированный список файлов из Redis. - async fn get_cached_file_list(&self) -> Vec { - let mut redis = self.redis.clone(); - - // Пытаемся получить кэшированный список из Redis - let cached_list: Option = redis.get(FILE_LIST_CACHE_KEY).await.unwrap_or(None); - - if let Some(cached_list) = cached_list { - // Если список найден, возвращаем его в виде вектора строк - serde_json::from_str(&cached_list).unwrap_or_else(|_| vec![]) - } else { - vec![] - } - } - - /// Периодически обновляет кэшированный список файлов из Storj S3. - async fn refresh_file_list_periodically(&self) { - let mut interval = interval(Duration::from_secs(CHECK_INTERVAL_SECONDS)); - loop { - interval.tick().await; - self.cache_file_list().await; - } - } - - /// Сохраняет маппинг старого пути из AWS S3 на новый путь в Storj S3. - async fn save_path_mapping( - &self, - old_path: &str, - new_path: &str, - ) -> Result<(), actix_web::Error> { - let mut redis = self.redis.clone(); - // Храним маппинг в формате Hash: old_path -> new_path - redis - .hset(PATH_MAPPING_KEY, old_path, new_path) - .await - .map_err(|_| ErrorInternalServerError("Failed to save path mapping in Redis"))?; - Ok(()) - } - - /// Получает новый путь для старого пути из маппинга в Redis. - async fn get_new_path(&self, old_path: &str) -> Result, actix_web::Error> { - let mut redis = self.redis.clone(); - let new_path: Option = redis - .hget(PATH_MAPPING_KEY, old_path) - .await - .map_err(|_| ErrorInternalServerError("Failed to get path mapping from Redis"))?; - Ok(new_path) - } -} - -/// Генерирует миниатюру изображения с заданной шириной. -async fn generate_thumbnail(image: &DynamicImage, width: u32) -> Result, actix_web::Error> { - let original_width = image.width(); - let scale_factor = original_width / width; - let height = image.height() / scale_factor; - let thumbnail = image.resize(width, height, FilterType::Lanczos3); // Ресайз изображения с использованием фильтра Lanczos3 - let mut buffer = Vec::new(); - thumbnail - .write_to(&mut Cursor::new(&mut buffer), image::ImageFormat::Jpeg) - .map_err(|_| ErrorInternalServerError("Failed to generate thumbnail"))?; // Сохранение изображения в формате JPEG - Ok(buffer) -} - -/// Загружает файл в S3 хранилище. -async fn upload_to_s3( - s3_client: &S3Client, - bucket: &str, - key: &str, - body: Vec, - content_type: &str, -) -> Result { - let body_stream = ByteStream::from(body); // Преобразуем тело файла в поток байтов - s3_client - .put_object() - .bucket(bucket) - .key(key) - .body(body_stream) - .content_type(content_type) - .send() - .await - .map_err(|_| ErrorInternalServerError("Failed to upload file to S3"))?; // Загрузка файла в S3 - - Ok(key.to_string()) // Возвращаем ключ файла -} - -/// Проверяет, существует ли файл в S3. -async fn check_file_exists( - s3_client: &S3Client, - bucket: &str, - key: &str, -) -> Result { - match s3_client.head_object().bucket(bucket).key(key).send().await { - Ok(_) => Ok(true), // Файл найден - Err(SdkError::ServiceError(service_error)) if service_error.err().is_not_found() => { - Ok(false) // Файл не найден - } - Err(e) => Err(ErrorInternalServerError(e.to_string())), // Ошибка при проверке - } -} - -/// Проверяет и обновляет квоту пользователя. -async fn check_and_update_quota( - redis: &mut MultiplexedConnection, - user_id: &str, - file_size: u64, -) -> Result<(), actix_web::Error> { - let current_quota: u64 = redis.get(user_id).await.unwrap_or(0); // Получаем текущую квоту пользователя - if current_quota + file_size > MAX_QUOTA_BYTES { - return Err(ErrorUnauthorized("Quota exceeded")); // Квота превышена - } - redis - .incr(user_id, file_size) - .await - .map_err(|_| ErrorInternalServerError("Failed to update quota in Redis"))?; // Увеличиваем использованную квоту - Ok(()) -} - -/// Сохраняет имя файла в Redis для пользователя. -async fn save_filename_in_redis( - redis: &mut MultiplexedConnection, - user_id: &str, - filename: &str, -) -> Result<(), actix_web::Error> { - redis - .sadd(user_id, filename) - .await - .map_err(|_| ErrorInternalServerError("Failed to save filename in Redis"))?; // Добавляем имя файла в набор пользователя - Ok(()) -} - -/// Загружает файлы из AWS S3 в Storj S3 и сохраняет маппинг путей. -async fn upload_files_from_aws(app_state: &AppState) -> Result<(), actix_web::Error> { - // Получаем список объектов из AWS S3 - let list_objects_v2 = app_state.aws_client.list_objects_v2(); - let list_response = list_objects_v2 - .bucket(app_state.aws_bucket.clone()) - .send() - .await - .map_err(|_| ErrorInternalServerError("Failed to list files from AWS S3"))?; - - if let Some(objects) = list_response.contents { - for object in objects { - if let Some(key) = object.key { - // Получаем объект из AWS S3 - let object_response = app_state - .aws_client - .get_object() - .bucket(app_state.aws_bucket.clone()) - .key(&key) - .send() - .await - .map_err(|_| ErrorInternalServerError("Failed to get object from AWS S3"))?; - - let body = object_response - .body - .collect() - .await - .map_err(|_| ErrorInternalServerError("Failed to read object body"))?; - let content_type = object_response - .content_type - .unwrap_or_else(|| "application/octet-stream".to_string()); - - // Определяем новый ключ для Storj S3 (например, сохраняем в корне с тем же именем) - let new_key = Path::new(&key) - .file_name() - .and_then(|name| name.to_str()) - .unwrap_or(&key) - .to_string(); - - // Загружаем объект в Storj S3 - let storj_url = upload_to_s3( - &app_state.s3_client, - &app_state.s3_bucket, - &new_key, - body.into_bytes().to_vec(), - &content_type, - ) - .await?; - - // Сохраняем маппинг старого пути на новый - app_state.save_path_mapping(&key, &new_key).await?; - - println!("Uploaded {} to Storj at {}", key, storj_url); - } - } - } - - Ok(()) -} - -// Структура для десериализации ответа от сервиса аутентификации -#[derive(Deserialize)] -struct AuthResponse { - data: Option, -} - -#[derive(Deserialize)] -struct AuthData { - validate_jwt_token: Option, -} - -#[derive(Deserialize)] -struct ValidateJWTToken { - is_valid: bool, - claims: Option, -} - -#[derive(Deserialize)] -struct Claims { - sub: Option, -} - -pub async fn get_id_by_token(token: &str) -> Result> { - let auth_api_base = env::var("AUTH_URL")?; - let query_name = "validate_jwt_token"; - let operation = "ValidateToken"; - let mut headers = HeaderMap::new(); - headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); - - let mut variables = HashMap::>::new(); - let mut params = HashMap::::new(); - params.insert("token".to_string(), token.to_string()); - params.insert("token_type".to_string(), "access_token".to_string()); - variables.insert("params".to_string(), params); - - let gql = json!({ - "query": format!("query {}($params: ValidateJWTTokenInput!) {{ {}(params: $params) {{ is_valid claims }} }}", operation, query_name), - "operationName": operation, - "variables": variables - }); - - let client = HTTPClient::new(); - let response = client - .post(&auth_api_base) - .headers(headers) - .json(&gql) - .send() - .await?; - - if response.status().is_success() { - let auth_response: AuthResponse = response.json().await?; - if let Some(auth_data) = auth_response.data { - if let Some(validate_jwt_token) = auth_data.validate_jwt_token { - if validate_jwt_token.is_valid { - if let Some(claims) = validate_jwt_token.claims { - if let Some(sub) = claims.sub { - return Ok(sub); - } - } - } - } - } - Err(Box::new(std::io::Error::new( - std::io::ErrorKind::Other, - "Invalid token response", - ))) - } else { - Err(Box::new(std::io::Error::new( - std::io::ErrorKind::Other, - format!("Request failed with status: {}", response.status()), - ))) - } -} - -/// Обработчик прокси-запросов. -async fn proxy_handler( - req: HttpRequest, - path: web::Path, - state: web::Data, -) -> Result { - // Получаем токен из заголовка авторизации - let token = req - .headers() - .get("Authorization") - .and_then(|header_value| header_value.to_str().ok()); - if token.is_none() { - return Err(ErrorUnauthorized("Unauthorized")); // Если токен отсутствует, возвращаем ошибку - } - - let user_id = get_id_by_token(token.unwrap()).await?; - let requested_path = path.into_inner(); // Полученный путь из запроса - - // Проверяем, есть ли маппинг для старого пути - if let Some(new_path) = state.get_new_path(&requested_path).await? { - // Используем новый путь для доступа к файлу - return serve_file(&new_path, &state).await; - } - - // Если маппинга нет, предполагаем, что путь является новым - serve_file(&requested_path, &state).await -} - -/// Функция для обслуживания файла по заданному пути. -async fn serve_file(file_key: &str, state: &AppState) -> Result { - // Проверяем наличие файла в Storj S3 - if !check_file_exists(&state.s3_client, &state.s3_bucket, file_key).await? { - return Err(ErrorInternalServerError("File not found in S3")); - } - - // Получаем объект из Storj S3 - let get_object_output = state - .s3_client - .get_object() - .bucket(&state.s3_bucket) - .key(file_key) - .send() - .await - .map_err(|_| ErrorInternalServerError("Failed to get object from S3"))?; - - let data = get_object_output - .body - .collect() - .await - .map_err(|_| ErrorInternalServerError("Failed to read object body"))?; - - let mime_type = MimeGuess::from_path(file_key).first_or_octet_stream(); // Определяем MIME-тип файла - - Ok(HttpResponse::Ok() - .content_type(mime_type.as_ref()) - .body(data.into_bytes())) -} +use actix_web::{middleware::Logger, web, App, HttpServer}; +use app_state::AppState; +use handlers::{proxy_handler, upload_handler}; #[actix_web::main] async fn main() -> std::io::Result<()> { - // Инициализируем состояние приложения let app_state = AppState::new().await; let app_state_clone = app_state.clone(); tokio::spawn(async move { - // Запускаем задачу обновления списка файлов в фоне + app_state_clone.update_filelist_from_aws().await; app_state_clone.refresh_file_list_periodically().await; }); - // Загружаем файлы из AWS S3 в Storj S3 и сохраняем маппинг путей - upload_files_from_aws(&app_state) - .await - .expect("Failed to upload files from AWS to Storj"); - - // Запускаем HTTP сервер HttpServer::new(move || { App::new() .app_data(web::Data::new(app_state.clone())) .wrap(Logger::default()) - .route("/{path:.*}", web::get().to(proxy_handler)) // Маршрутизация всех GET запросов на proxy_handler + .route("/{path:.*}", web::get().to(proxy_handler)) + .route("/", web::post().to(upload_handler)) }) .bind("127.0.0.1:8080")? .run() diff --git a/src/s3_utils.rs b/src/s3_utils.rs new file mode 100644 index 0000000..1600111 --- /dev/null +++ b/src/s3_utils.rs @@ -0,0 +1,77 @@ +use std::str::FromStr; + +use actix_web::error::ErrorInternalServerError; +use aws_sdk_s3::{error::SdkError, primitives::ByteStream, Client as S3Client}; +use mime_guess::mime; + +/// Загружает файл в S3 хранилище. +pub async fn upload_to_s3( + s3_client: &S3Client, + bucket: &str, + key: &str, + body: Vec, + content_type: &str, +) -> Result { + let body_stream = ByteStream::from(body); // Преобразуем тело файла в поток байтов + s3_client + .put_object() + .bucket(bucket) + .key(key) + .body(body_stream) + .content_type(content_type) + .send() + .await + .map_err(|_| ErrorInternalServerError("Failed to upload file to S3"))?; // Загрузка файла в S3 + + Ok(key.to_string()) // Возвращаем ключ файла +} + +/// Проверяет, существует ли файл в S3. +pub async fn check_file_exists( + s3_client: &S3Client, + bucket: &str, + key: &str, +) -> Result { + match s3_client.head_object().bucket(bucket).key(key).send().await { + Ok(_) => Ok(true), // Файл найден + Err(SdkError::ServiceError(service_error)) if service_error.err().is_not_found() => { + Ok(false) // Файл не найден + } + Err(e) => Err(ErrorInternalServerError(e.to_string())), // Ошибка при проверке + } +} + +/// Загружает файл из S3. +pub async fn load_file_from_s3( + s3_client: &S3Client, + bucket: &str, + key: &str, +) -> Result, actix_web::Error> { + let get_object_output = s3_client + .get_object() + .bucket(bucket) + .key(key) + .send() + .await + .map_err(|_| ErrorInternalServerError("Failed to get object from S3"))?; + + let data: aws_sdk_s3::primitives::AggregatedBytes = get_object_output + .body + .collect() + .await + .map_err(|_| ErrorInternalServerError("Failed to read object body"))?; + + Ok(data.to_vec()) +} + +/// Генерирует ключ с правильным расширением на основе MIME-типа. +pub fn generate_key_with_extension(base_key: String, mime_type: String) -> String { + let mime: mime::Mime = + mime::Mime::from_str(&mime_type).unwrap_or(mime::APPLICATION_OCTET_STREAM); + if let Some(extensions) = mime_guess::get_mime_extensions_str(mime.as_ref()) { + if let Some(extension) = extensions.first() { + return format!("{}.{}", base_key, extension); + } + } + base_key +} diff --git a/src/thumbnail.rs b/src/thumbnail.rs new file mode 100644 index 0000000..44d36da --- /dev/null +++ b/src/thumbnail.rs @@ -0,0 +1,47 @@ +// thumbnail.rs + +use actix_web::error::ErrorInternalServerError; +use image::{imageops::FilterType, DynamicImage}; +use std::{collections::HashMap, io::Cursor}; + +pub const ALLOWED_THUMBNAIL_WIDTHS: [u32; 6] = [10, 40, 110, 300, 600, 800]; + +/// Парсит запрос на миниатюру, извлекая оригинальное имя файла и требуемую ширину. +/// Пример: "filename_150.ext" -> ("filename.ext", 150) +pub fn parse_thumbnail_request(path: &str) -> Option<(String, u32, String)> { + if let Some((name_part, ext_part)) = path.rsplit_once('.') { + if let Some((base_name, width_str)) = name_part.rsplit_once('_') { + if let Ok(width) = width_str.parse::() { + return Some((base_name.to_string(), width, ext_part.to_string())); + } + } + } + None +} + +/// Выбирает ближайший подходящий размер из предопределённых. +pub fn find_closest_width(requested_width: u32) -> u32 { + *ALLOWED_THUMBNAIL_WIDTHS + .iter() + .min_by_key(|&&width| (width as i32 - requested_width as i32).abs()) + .unwrap_or(&ALLOWED_THUMBNAIL_WIDTHS[0]) // Возвращаем самый маленький размер, если ничего не подошло +} + +/// Генерирует миниатюры изображения для заданного набора ширин. +pub async fn generate_thumbnails( + image: &DynamicImage, + widths: &[u32], +) -> Result>, actix_web::Error> { + let mut thumbnails = HashMap::new(); + + for &width in widths { + let thumbnail = image.resize(width, u32::MAX, FilterType::Lanczos3); // Ресайз изображения по ширине + let mut buffer = Vec::new(); + thumbnail + .write_to(&mut Cursor::new(&mut buffer), image::ImageFormat::Jpeg) + .map_err(|_| ErrorInternalServerError("Failed to generate thumbnail"))?; // Сохранение изображения в формате JPEG + thumbnails.insert(width, buffer); + } + + Ok(thumbnails) +}