0.0.3-desirable
Some checks failed
deploy / deploy (push) Failing after 4s

This commit is contained in:
Untone 2024-08-31 03:32:37 +03:00
parent 9b1c2060d6
commit 3ff90ba4f3
8 changed files with 1192 additions and 480 deletions

521
Cargo.lock generated
View File

@ -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",
]

View File

@ -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"

263
src/app_state.rs Normal file
View File

@ -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<String> = 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<String> {
let mut redis = self.redis.clone();
// Пытаемся получить кэшированный список из Redis
let cached_list: Option<String> = 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<Option<String>, actix_web::Error> {
let mut redis = self.redis.clone();
let new_path: Option<String> = 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<u64, actix_web::Error> {
let mut redis = self.redis.clone();
let quota_key = format!("quota:{}", user_id);
// Попытка получить квоту из Redis
let quota: u64 = redis.get(&quota_key).await.unwrap_or(0);
if quota == 0 {
// Если квота не найдена, устанавливаем её в 0 байт и задаем TTL на одну неделю
redis
.set_ex(&quota_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<u64, actix_web::Error> {
let mut redis = self.redis.clone();
let quota_key = format!("quota:{}", user_id);
// Проверяем, существует ли ключ в Redis
let exists: bool = redis.exists(&quota_key).await.map_err(|_| {
ErrorInternalServerError("Failed to check if user quota exists in Redis")
})?;
// Если ключ не существует, создаем его с начальным значением и устанавливаем TTL
if !exists {
redis
.set_ex(&quota_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(&quota_key, bytes)
.await
.map_err(|_| ErrorInternalServerError("Failed to increment user quota in Redis"))?;
Ok(new_quota)
}
}

97
src/auth.rs Normal file
View File

@ -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<AuthData>,
}
#[derive(Deserialize)]
struct AuthData {
validate_jwt_token: Option<ValidateJWTToken>,
}
#[derive(Deserialize)]
struct ValidateJWTToken {
is_valid: bool,
claims: Option<Claims>,
}
#[derive(Deserialize)]
struct Claims {
sub: Option<String>,
}
/// Получает айди пользователя из токена в заголовке
pub async fn get_id_by_token(token: &str) -> Result<String, Box<dyn Error>> {
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::<String, HashMap<String, String>>::new();
let mut params = HashMap::<String, String>::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(())
}

183
src/handlers.rs Normal file
View File

@ -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<HttpResponse, actix_web::Error> {
// Проверяем наличие файла в 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<AppState>,
) -> Result<HttpResponse, actix_web::Error> {
// Получаем токен из заголовка авторизации
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<String>,
state: web::Data<AppState>,
) -> Result<HttpResponse, actix_web::Error> {
// весь запрошенный путь
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
}

View File

@ -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<String> = 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<String> {
let mut redis = self.redis.clone();
// Пытаемся получить кэшированный список из Redis
let cached_list: Option<String> = 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<Option<String>, actix_web::Error> {
let mut redis = self.redis.clone();
let new_path: Option<String> = 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<Vec<u8>, 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<u8>,
content_type: &str,
) -> Result<String, actix_web::Error> {
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<bool, actix_web::Error> {
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<AuthData>,
}
#[derive(Deserialize)]
struct AuthData {
validate_jwt_token: Option<ValidateJWTToken>,
}
#[derive(Deserialize)]
struct ValidateJWTToken {
is_valid: bool,
claims: Option<Claims>,
}
#[derive(Deserialize)]
struct Claims {
sub: Option<String>,
}
pub async fn get_id_by_token(token: &str) -> Result<String, Box<dyn Error>> {
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::<String, HashMap<String, String>>::new();
let mut params = HashMap::<String, String>::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<String>,
state: web::Data<AppState>,
) -> Result<HttpResponse, actix_web::Error> {
// Получаем токен из заголовка авторизации
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<HttpResponse, actix_web::Error> {
// Проверяем наличие файла в 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()

77
src/s3_utils.rs Normal file
View File

@ -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<u8>,
content_type: &str,
) -> Result<String, actix_web::Error> {
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<bool, actix_web::Error> {
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<Vec<u8>, 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
}

47
src/thumbnail.rs Normal file
View File

@ -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::<u32>() {
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<HashMap<u32, Vec<u8>>, 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)
}