feat: generate and display thumbnails asynchronously
This commit is contained in:
@@ -37,6 +37,11 @@
|
||||
"ordinal": 6,
|
||||
"name": "user_id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "thumbnail_storage_path",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -51,7 +56,8 @@
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "31e1333f84168ce0ba75c474f2a1d7a0d0c4d04857130da327b07821f20cf07c"
|
||||
|
||||
@@ -37,6 +37,11 @@
|
||||
"ordinal": 6,
|
||||
"name": "user_id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "thumbnail_storage_path",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -55,7 +60,8 @@
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "638f29a940b4514c1ebd047975e1e1fb8d9b647dbe6ef025dbb13b6417b6e91e"
|
||||
|
||||
@@ -37,6 +37,11 @@
|
||||
"ordinal": 6,
|
||||
"name": "user_id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "thumbnail_storage_path",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -52,7 +57,8 @@
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "d059130365f7e4b3a6ae49bee073897a12e58a29b0b2e5c814ff8cb392e32fa0"
|
||||
|
||||
@@ -37,6 +37,11 @@
|
||||
"ordinal": 6,
|
||||
"name": "user_id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "thumbnail_storage_path",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -52,7 +57,8 @@
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "f703be0699214db15f5d65ea7ec3186407f6a72cc1596f652fcdcd7c7f49562d"
|
||||
|
||||
581
backend/Cargo.lock
generated
581
backend/Cargo.lock
generated
@@ -17,6 +17,24 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aligned"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee4508988c62edf04abd8d92897fca0c2995d907ce1dfeaf369dac3716a40685"
|
||||
dependencies = [
|
||||
"as-slice",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aligned-vec"
|
||||
version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b"
|
||||
dependencies = [
|
||||
"equator",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "allocator-api2"
|
||||
version = "0.2.21"
|
||||
@@ -47,6 +65,17 @@ dependencies = [
|
||||
"derive_arbitrary",
|
||||
]
|
||||
|
||||
[[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",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "argon2"
|
||||
version = "0.5.3"
|
||||
@@ -59,6 +88,21 @@ dependencies = [
|
||||
"password-hash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "arrayvec"
|
||||
version = "0.7.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f02882884d3e1bc524fb12c79f107f6ad0e1cfd498c536ffb494301740995dfe"
|
||||
|
||||
[[package]]
|
||||
name = "as-slice"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516"
|
||||
dependencies = [
|
||||
"stable_deref_trait",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "atoi"
|
||||
version = "2.0.0"
|
||||
@@ -80,6 +124,49 @@ version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||
|
||||
[[package]]
|
||||
name = "av-scenechange"
|
||||
version = "0.14.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0f321d77c20e19b92c39e7471cf986812cbb46659d2af674adc4331ef3f18394"
|
||||
dependencies = [
|
||||
"aligned",
|
||||
"anyhow",
|
||||
"arg_enum_proc_macro",
|
||||
"arrayvec",
|
||||
"log",
|
||||
"num-rational",
|
||||
"num-traits",
|
||||
"pastey",
|
||||
"rayon",
|
||||
"thiserror 2.0.18",
|
||||
"v_frame",
|
||||
"y4m",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "av1-grain"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8cfddb07216410377231960af4fcab838eaa12e013417781b78bd95ee22077f8"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arrayvec",
|
||||
"log",
|
||||
"nom",
|
||||
"num-rational",
|
||||
"v_frame",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "avif-serialize"
|
||||
version = "0.8.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7178fe5f7d460b13895ebb9dcb28a3a6216d2df2574a0806cb51b555d297f38"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-lc-rs"
|
||||
version = "1.17.0"
|
||||
@@ -206,6 +293,7 @@ dependencies = [
|
||||
"chrono",
|
||||
"dotenvy",
|
||||
"futures-util",
|
||||
"image",
|
||||
"lazy-regex",
|
||||
"mime_guess",
|
||||
"opendal",
|
||||
@@ -247,6 +335,12 @@ version = "1.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
|
||||
|
||||
[[package]]
|
||||
name = "bit_field"
|
||||
version = "0.10.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.11.0"
|
||||
@@ -256,6 +350,15 @@ dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitstream-io"
|
||||
version = "4.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7eff00be299a18769011411c9def0d827e8f2d7bf0c3dbf53633147a8867fd1f"
|
||||
dependencies = [
|
||||
"no_std_io2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "blake2"
|
||||
version = "0.10.6"
|
||||
@@ -274,18 +377,36 @@ dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "built"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c0e531d93d39c34eef561e929e8a7f86d77a5af08aac4f6d6e39976c51858e9"
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.20.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
|
||||
|
||||
[[package]]
|
||||
name = "bytemuck"
|
||||
version = "1.25.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder-lite"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.11.1"
|
||||
@@ -356,6 +477,12 @@ dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "color_quant"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
|
||||
|
||||
[[package]]
|
||||
name = "combine"
|
||||
version = "4.6.7"
|
||||
@@ -450,6 +577,25 @@ dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-deque"
|
||||
version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
|
||||
dependencies = [
|
||||
"crossbeam-epoch",
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-epoch"
|
||||
version = "0.9.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-queue"
|
||||
version = "0.3.12"
|
||||
@@ -465,6 +611,12 @@ version = "0.8.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
||||
|
||||
[[package]]
|
||||
name = "crunchy"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.1.7"
|
||||
@@ -619,6 +771,26 @@ dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equator"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc"
|
||||
dependencies = [
|
||||
"equator-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equator-macro"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
@@ -697,12 +869,42 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "exr"
|
||||
version = "1.74.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4300e043a56aa2cb633c01af81ca8f699a321879a7854d3896a0ba89056363be"
|
||||
dependencies = [
|
||||
"bit_field",
|
||||
"half",
|
||||
"lebe",
|
||||
"miniz_oxide",
|
||||
"rayon-core",
|
||||
"smallvec",
|
||||
"zune-inflate",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
|
||||
|
||||
[[package]]
|
||||
name = "fax"
|
||||
version = "0.2.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "caf1079563223d5d59d83c85886a56e586cfd5c1a26292e971a0fa266531ac5a"
|
||||
|
||||
[[package]]
|
||||
name = "fdeflate"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c"
|
||||
dependencies = [
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.9"
|
||||
@@ -715,6 +917,7 @@ version = "1.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
|
||||
dependencies = [
|
||||
"crc32fast",
|
||||
"miniz_oxide",
|
||||
"zlib-rs",
|
||||
]
|
||||
@@ -923,6 +1126,16 @@ dependencies = [
|
||||
"wasip3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gif"
|
||||
version = "0.14.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee8cfcc411d9adbbaba82fb72661cc1bcca13e8bba98b364e62b2dba8f960159"
|
||||
dependencies = [
|
||||
"color_quant",
|
||||
"weezl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gloo-timers"
|
||||
version = "0.3.0"
|
||||
@@ -958,6 +1171,17 @@ dependencies = [
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "half"
|
||||
version = "2.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"crunchy",
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.14.5"
|
||||
@@ -1277,6 +1501,46 @@ dependencies = [
|
||||
"icu_properties",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "image"
|
||||
version = "0.25.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"byteorder-lite",
|
||||
"color_quant",
|
||||
"exr",
|
||||
"gif",
|
||||
"image-webp",
|
||||
"moxcms",
|
||||
"num-traits",
|
||||
"png",
|
||||
"qoi",
|
||||
"ravif",
|
||||
"rayon",
|
||||
"rgb",
|
||||
"tiff",
|
||||
"zune-core",
|
||||
"zune-jpeg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "image-webp"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3"
|
||||
dependencies = [
|
||||
"byteorder-lite",
|
||||
"quick-error",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "imgref"
|
||||
version = "1.12.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89194689a993ab15268672e99e7b0e19da2da3268ac682e8f02d29d4d1434cd7"
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.13.1"
|
||||
@@ -1289,6 +1553,17 @@ dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[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",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inventory"
|
||||
version = "0.3.24"
|
||||
@@ -1304,6 +1579,15 @@ version = "2.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.18"
|
||||
@@ -1462,12 +1746,28 @@ version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
|
||||
|
||||
[[package]]
|
||||
name = "lebe"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.184"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af"
|
||||
|
||||
[[package]]
|
||||
name = "libfuzzer-sys"
|
||||
version = "0.4.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a9fd2f41a1cba099f79a0b6b6c35656cf7c03351a7bae8ff0f28f25270f929d2"
|
||||
dependencies = [
|
||||
"arbitrary",
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libm"
|
||||
version = "0.2.16"
|
||||
@@ -1523,6 +1823,15 @@ version = "0.4.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||
|
||||
[[package]]
|
||||
name = "loop9"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062"
|
||||
dependencies = [
|
||||
"imgref",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lru-slab"
|
||||
version = "0.1.2"
|
||||
@@ -1544,6 +1853,16 @@ version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
|
||||
|
||||
[[package]]
|
||||
name = "maybe-rayon"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"rayon",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "md-5"
|
||||
version = "0.10.6"
|
||||
@@ -1606,6 +1925,16 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "moxcms"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
"pxfm",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "multer"
|
||||
version = "3.1.0"
|
||||
@@ -1623,6 +1952,30 @@ dependencies = [
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "new_debug_unreachable"
|
||||
version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
|
||||
|
||||
[[package]]
|
||||
name = "no_std_io2"
|
||||
version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "418abd1b6d34fbf6cae440dc874771b0525a604428704c76e48b29a5e67b8003"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "8.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nonempty"
|
||||
version = "0.7.0"
|
||||
@@ -1635,6 +1988,12 @@ version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21"
|
||||
|
||||
[[package]]
|
||||
name = "noop_proc_macro"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8"
|
||||
|
||||
[[package]]
|
||||
name = "nu-ansi-term"
|
||||
version = "0.50.3"
|
||||
@@ -1699,6 +2058,17 @@ version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967"
|
||||
|
||||
[[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",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-integer"
|
||||
version = "0.1.46"
|
||||
@@ -1911,6 +2281,18 @@ dependencies = [
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "paste"
|
||||
version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
||||
|
||||
[[package]]
|
||||
name = "pastey"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec"
|
||||
|
||||
[[package]]
|
||||
name = "pem-rfc7468"
|
||||
version = "0.7.0"
|
||||
@@ -1985,6 +2367,19 @@ version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
|
||||
|
||||
[[package]]
|
||||
name = "png"
|
||||
version = "0.18.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"crc32fast",
|
||||
"fdeflate",
|
||||
"flate2",
|
||||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic"
|
||||
version = "1.13.1"
|
||||
@@ -2053,6 +2448,40 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "profiling"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5"
|
||||
dependencies = [
|
||||
"profiling-procmacros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "profiling-procmacros"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4488a4a36b9a4ba6b9334a32a39971f77c1436ec82c38707bce707699cc3bbcb"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pxfm"
|
||||
version = "0.1.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f"
|
||||
|
||||
[[package]]
|
||||
name = "qoi"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quanta"
|
||||
version = "0.12.6"
|
||||
@@ -2068,6 +2497,12 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quick-error"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.38.4"
|
||||
@@ -2231,6 +2666,56 @@ version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba"
|
||||
|
||||
[[package]]
|
||||
name = "rav1e"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43b6dd56e85d9483277cde964fd1bdb0428de4fec5ebba7540995639a21cb32b"
|
||||
dependencies = [
|
||||
"aligned-vec",
|
||||
"arbitrary",
|
||||
"arg_enum_proc_macro",
|
||||
"arrayvec",
|
||||
"av-scenechange",
|
||||
"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",
|
||||
"paste",
|
||||
"profiling",
|
||||
"rand 0.9.4",
|
||||
"rand_chacha 0.9.0",
|
||||
"simd_helpers",
|
||||
"thiserror 2.0.18",
|
||||
"v_frame",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ravif"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e52310197d971b0f5be7fe6b57530dcd27beb35c1b013f29d66c1ad73fbbcc45"
|
||||
dependencies = [
|
||||
"avif-serialize",
|
||||
"imgref",
|
||||
"loop9",
|
||||
"quick-error",
|
||||
"rav1e",
|
||||
"rayon",
|
||||
"rgb",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "raw-cpuid"
|
||||
version = "11.6.0"
|
||||
@@ -2240,6 +2725,26 @@ dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rayon"
|
||||
version = "1.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d"
|
||||
dependencies = [
|
||||
"either",
|
||||
"rayon-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rayon-core"
|
||||
version = "1.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
|
||||
dependencies = [
|
||||
"crossbeam-deque",
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.5.18"
|
||||
@@ -2356,6 +2861,12 @@ dependencies = [
|
||||
"thiserror 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rgb"
|
||||
version = "0.8.53"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4"
|
||||
|
||||
[[package]]
|
||||
name = "ring"
|
||||
version = "0.17.14"
|
||||
@@ -2788,6 +3299,15 @@ dependencies = [
|
||||
"simdutf8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simd_helpers"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6"
|
||||
dependencies = [
|
||||
"quote",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simdutf8"
|
||||
version = "0.1.5"
|
||||
@@ -3148,6 +3668,20 @@ dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tiff"
|
||||
version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52"
|
||||
dependencies = [
|
||||
"fax",
|
||||
"flate2",
|
||||
"half",
|
||||
"quick-error",
|
||||
"weezl",
|
||||
"zune-jpeg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.3.47"
|
||||
@@ -3575,6 +4109,17 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "v_frame"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2"
|
||||
dependencies = [
|
||||
"aligned-vec",
|
||||
"num-traits",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "valuable"
|
||||
version = "0.1.1"
|
||||
@@ -3791,6 +4336,12 @@ dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "weezl"
|
||||
version = "0.1.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88"
|
||||
|
||||
[[package]]
|
||||
name = "whoami"
|
||||
version = "1.6.1"
|
||||
@@ -4143,6 +4694,12 @@ dependencies = [
|
||||
"rustix",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "y4m"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448"
|
||||
|
||||
[[package]]
|
||||
name = "yansi"
|
||||
version = "1.0.1"
|
||||
@@ -4289,3 +4846,27 @@ dependencies = [
|
||||
"log",
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zune-core"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9"
|
||||
|
||||
[[package]]
|
||||
name = "zune-inflate"
|
||||
version = "0.2.54"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02"
|
||||
dependencies = [
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zune-jpeg"
|
||||
version = "0.5.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296"
|
||||
dependencies = [
|
||||
"zune-core",
|
||||
]
|
||||
|
||||
@@ -25,6 +25,7 @@ mime_guess = "2.0.5"
|
||||
tower_governor = { version = "0.8.0", default-features = false, features = ["axum", "tracing"] }
|
||||
utoipa = { version = "5.5.0", features = ["axum_extras", "chrono"] }
|
||||
utoipa-swagger-ui = { version = "9.0.2", features = ["axum"] }
|
||||
image = "0.25.10"
|
||||
|
||||
[dev-dependencies]
|
||||
axum-test = "20.0.0"
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE file_records ADD COLUMN thumbnail_storage_path TEXT;
|
||||
@@ -19,6 +19,7 @@ pub enum LoftError {
|
||||
NoFileProvided,
|
||||
MultipartError(String),
|
||||
InvalidRange,
|
||||
ThumbnailGenerationError(String),
|
||||
}
|
||||
|
||||
impl fmt::Display for LoftError {
|
||||
@@ -51,6 +52,12 @@ impl From<argon2::password_hash::Error> for LoftError {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<image::ImageError> for LoftError {
|
||||
fn from(value: image::ImageError) -> Self {
|
||||
LoftError::ThumbnailGenerationError(value.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for LoftError {}
|
||||
|
||||
impl IntoResponse for LoftError {
|
||||
@@ -80,6 +87,10 @@ impl IntoResponse for LoftError {
|
||||
error!("opendal storage error: {e}");
|
||||
StatusCode::INTERNAL_SERVER_ERROR.into_response()
|
||||
}
|
||||
Self::ThumbnailGenerationError(e) => {
|
||||
error!("thumbnail generation error: {e}");
|
||||
StatusCode::INTERNAL_SERVER_ERROR.into_response()
|
||||
}
|
||||
Self::NoFileProvided => StatusCode::BAD_REQUEST.into_response(),
|
||||
Self::MultipartError(e) => {
|
||||
info!("bad request: {e}");
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
mod ctx;
|
||||
mod error;
|
||||
mod model;
|
||||
mod tasks;
|
||||
mod web;
|
||||
|
||||
use std::{net::SocketAddr, time::Duration};
|
||||
@@ -13,6 +14,7 @@ use axum::{
|
||||
middleware,
|
||||
};
|
||||
use sqlx::PgPool;
|
||||
use tokio::sync::mpsc::{self};
|
||||
use tower_cookies::CookieManagerLayer;
|
||||
use tower_governor::{
|
||||
GovernorLayer, governor::GovernorConfigBuilder, key_extractor::SmartIpKeyExtractor,
|
||||
@@ -31,12 +33,15 @@ use crate::{
|
||||
model::{FileRepository, UserRepository},
|
||||
web::{
|
||||
mw_auth::{mw_ctx_resolver, mw_require_auth},
|
||||
routes_file::{FileApi, routes_file},
|
||||
routes_file::{FileApi, FileState, routes_file},
|
||||
routes_health::{HealthApi, routes_health},
|
||||
routes_login::{AuthApi, routes_auth},
|
||||
},
|
||||
};
|
||||
|
||||
const BUFFER_SIZE: usize = 100;
|
||||
const BODY_LIMIT: usize = 1000 * 1000 * 1000 * 5;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
tracing_subscriber::registry()
|
||||
@@ -51,11 +56,16 @@ async fn main() -> Result<()> {
|
||||
dotenvy::dotenv().ok();
|
||||
|
||||
let database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set");
|
||||
const BODY_LIMIT: usize = 1000 * 1000 * 1000 * 5;
|
||||
let pool = PgPool::connect(&database_url).await?;
|
||||
sqlx::migrate!().run(&pool).await?;
|
||||
|
||||
let file_repository = FileRepository::new(pool.clone())?;
|
||||
let routes_file = routes_file(file_repository.clone())
|
||||
|
||||
let (tx, rx) = mpsc::channel(BUFFER_SIZE);
|
||||
tokio::spawn(tasks::run_thumbnail_worker(rx, file_repository.clone()));
|
||||
|
||||
let file_service = FileState::new(file_repository.clone(), tx);
|
||||
let routes_file = routes_file(file_service)
|
||||
.route_layer(middleware::from_fn(mw_require_auth))
|
||||
.layer(DefaultBodyLimit::max(BODY_LIMIT));
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ pub struct FileRecord {
|
||||
#[serde(skip_serializing)]
|
||||
pub storage_key: String,
|
||||
pub uploaded_at: chrono::DateTime<chrono::Utc>,
|
||||
pub thumbnail_storage_path: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -90,6 +91,18 @@ impl FileRepository {
|
||||
Ok(stream)
|
||||
}
|
||||
|
||||
pub async fn thumbnail(
|
||||
&self,
|
||||
file_id: i64,
|
||||
user_id: i64,
|
||||
) -> Result<impl Stream<Item = std::io::Result<Bytes>> + use<>> {
|
||||
let record = self.get_file(file_id, user_id).await?;
|
||||
let path = format!("thumbnail/{}", &record.storage_key);
|
||||
let reader = self.op.reader(&path).await?;
|
||||
let stream = reader.into_bytes_stream(0..).await?;
|
||||
Ok(stream)
|
||||
}
|
||||
|
||||
pub async fn stream_part(
|
||||
&self,
|
||||
file_id: i64,
|
||||
@@ -155,6 +168,29 @@ impl FileRepository {
|
||||
|
||||
Ok(records)
|
||||
}
|
||||
|
||||
pub async fn update_thumbnail_storage_path(
|
||||
&self,
|
||||
path: &str,
|
||||
file_record_id: i64,
|
||||
user_id: i64,
|
||||
) -> Result<()> {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
UPDATE file_records
|
||||
SET thumbnail_storage_path = $1
|
||||
WHERE id = $2
|
||||
AND user_id = $3
|
||||
"#,
|
||||
path,
|
||||
file_record_id,
|
||||
user_id
|
||||
)
|
||||
.execute(&self.pool)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, FromRow)]
|
||||
@@ -383,6 +419,37 @@ mod tests {
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial_test::serial]
|
||||
async fn test_update_thumbnail_storage_path() {
|
||||
let user_repository = user_repository().await.unwrap();
|
||||
let user = user_repository
|
||||
.create_user("username", "password_hash")
|
||||
.await
|
||||
.unwrap();
|
||||
let file_repository = file_repository().await.unwrap();
|
||||
truncate_file_records(&file_repository.pool).await;
|
||||
|
||||
let record = file_repository
|
||||
.create_file_record(user.id, "a.png", 2, "a.png-uuid")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(record.thumbnail_storage_path, None);
|
||||
|
||||
let path = "thumbnail/a.png-uuid";
|
||||
file_repository
|
||||
.update_thumbnail_storage_path(path, record.id, user.id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let updated = file_repository.get_file(record.id, user.id).await.unwrap();
|
||||
assert_eq!(updated.thumbnail_storage_path, Some(path.to_string()));
|
||||
|
||||
truncate_file_records(&file_repository.pool).await;
|
||||
truncate_users(&user_repository.pool).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial_test::serial]
|
||||
async fn test_stream_part() {
|
||||
|
||||
81
backend/src/tasks.rs
Normal file
81
backend/src/tasks.rs
Normal file
@@ -0,0 +1,81 @@
|
||||
use image::codecs::jpeg::JpegEncoder;
|
||||
use tokio::sync::mpsc::Receiver;
|
||||
use tracing::{info, warn};
|
||||
|
||||
use crate::{
|
||||
error::Result,
|
||||
model::{FileRecord, FileRepository},
|
||||
};
|
||||
|
||||
// change thumbnail task to have the file id in the param
|
||||
// with the file id we can call the file repository and get the file record
|
||||
// which will grand us access to the opendal storage... and also the MIME type
|
||||
// so we can have a match statement after a parser that will give us the type of
|
||||
// process we should do in order to create the thumbnail..
|
||||
// for example an image will be processed with image crate but a video will be processed with tokio::command->ffmpeg
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ThumbnailTask {
|
||||
record_id: i64,
|
||||
user_id: i64,
|
||||
}
|
||||
|
||||
impl ThumbnailTask {
|
||||
pub fn new(record_id: i64, user_id: i64) -> Self {
|
||||
Self { record_id, user_id }
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run_thumbnail_worker(
|
||||
mut rx: Receiver<ThumbnailTask>,
|
||||
file_repository: FileRepository,
|
||||
) {
|
||||
while let Some(task) = rx.recv().await {
|
||||
let record_id = task.record_id;
|
||||
|
||||
if let Err(e) = process_task(task, &file_repository).await {
|
||||
warn!("thumbnail task {} failed: {}", record_id, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn process_task(task: ThumbnailTask, file_repository: &FileRepository) -> Result<()> {
|
||||
let record_id = task.record_id;
|
||||
info!("task for file record {} received", record_id);
|
||||
|
||||
let record = file_repository
|
||||
.get_file(task.record_id, task.user_id)
|
||||
.await?;
|
||||
let file_type = &record.file_type;
|
||||
|
||||
// TODO: create enum that maps file_type (which is actually mime_type) into file category (image, video, ...)
|
||||
if file_type.contains("image") {
|
||||
generate_for_image(&record, file_repository).await?;
|
||||
} else {
|
||||
warn!(
|
||||
"thumbnail generation for file type {} not yet supported",
|
||||
file_type
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn generate_for_image(record: &FileRecord, file_repository: &FileRepository) -> Result<()> {
|
||||
let storage_key = &record.storage_key;
|
||||
let bytes = file_repository.op.read(storage_key).await?.to_vec();
|
||||
|
||||
let image = image::load_from_memory(&bytes)?;
|
||||
let thumbnail = image.thumbnail(256, 256);
|
||||
let rgb = thumbnail.to_rgb8();
|
||||
let mut buf = Vec::new();
|
||||
let mut enc = JpegEncoder::new_with_quality(&mut buf, 50);
|
||||
enc.encode_image(&rgb)?;
|
||||
|
||||
let path = format!("thumbnail/{}", storage_key);
|
||||
file_repository.op.write(&path, buf).await?;
|
||||
file_repository
|
||||
.update_thumbnail_storage_path(&path, record.id, record.user_id)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -7,12 +7,15 @@ use axum::{
|
||||
routing::get,
|
||||
};
|
||||
use sqlx::types::uuid;
|
||||
use tokio::sync::mpsc::Sender;
|
||||
use tracing::warn;
|
||||
use utoipa::OpenApi;
|
||||
|
||||
use crate::{
|
||||
ctx::Ctx,
|
||||
error::{LoftError, Result},
|
||||
model::{FileRecord, FileRepository},
|
||||
tasks::ThumbnailTask,
|
||||
};
|
||||
|
||||
#[derive(OpenApi)]
|
||||
@@ -23,19 +26,36 @@ use crate::{
|
||||
get_file,
|
||||
delete_file,
|
||||
download_file,
|
||||
thumbnail,
|
||||
stream_part
|
||||
),
|
||||
components(schemas(FileRecord))
|
||||
)]
|
||||
pub struct FileApi;
|
||||
|
||||
pub fn routes_file(file_repository: FileRepository) -> Router {
|
||||
#[derive(Clone)]
|
||||
pub struct FileState {
|
||||
pub file_repository: FileRepository,
|
||||
pub tx: Sender<ThumbnailTask>,
|
||||
}
|
||||
|
||||
impl FileState {
|
||||
pub fn new(file_repository: FileRepository, tx: Sender<ThumbnailTask>) -> Self {
|
||||
Self {
|
||||
file_repository,
|
||||
tx,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn routes_file(file_service: FileState) -> Router {
|
||||
Router::new()
|
||||
.route("/files", get(list_files).post(upload_file))
|
||||
.route("/files/{id}", get(get_file).delete(delete_file))
|
||||
.route("/files/{id}/download", get(download_file))
|
||||
.route("/files/{id}/thumbnail", get(thumbnail))
|
||||
.route("/files/{id}/stream_part", get(stream_part))
|
||||
.with_state(file_repository)
|
||||
.with_state(file_service)
|
||||
}
|
||||
|
||||
/// Upload a file
|
||||
@@ -53,7 +73,7 @@ pub fn routes_file(file_repository: FileRepository) -> Router {
|
||||
)
|
||||
)]
|
||||
async fn upload_file(
|
||||
State(file_repository): State<FileRepository>,
|
||||
State(service): State<FileState>,
|
||||
ctx: Ctx,
|
||||
mut multipart: Multipart,
|
||||
) -> Result<(StatusCode, Json<FileRecord>)> {
|
||||
@@ -63,18 +83,27 @@ async fn upload_file(
|
||||
if field.name() == Some("file") {
|
||||
let name = field.file_name().map(str::to_string).unwrap_or_default();
|
||||
let key = format!("{name}-{}", uuid::Uuid::new_v4());
|
||||
let size = file_repository.upload_file(field, &key).await?;
|
||||
let size = service.file_repository.upload_file(field, &key).await?;
|
||||
uploaded = Some((name, key, size));
|
||||
}
|
||||
}
|
||||
|
||||
let (name, key, size) = uploaded.ok_or(LoftError::NoFileProvided)?;
|
||||
|
||||
let file_record = file_repository
|
||||
let record = service
|
||||
.file_repository
|
||||
.create_file_record(ctx.user_id(), &name, size, &key)
|
||||
.await?;
|
||||
|
||||
Ok((StatusCode::CREATED, Json(file_record)))
|
||||
if let Err(e) = service
|
||||
.tx
|
||||
.send(ThumbnailTask::new(record.id, ctx.user_id()))
|
||||
.await
|
||||
{
|
||||
warn!("failed to send thumbnail task for file {}: {e}", record.id)
|
||||
}
|
||||
|
||||
Ok((StatusCode::CREATED, Json(record)))
|
||||
}
|
||||
|
||||
/// Get a file's metadata
|
||||
@@ -92,11 +121,12 @@ async fn upload_file(
|
||||
)
|
||||
)]
|
||||
async fn get_file(
|
||||
State(file_repository): State<FileRepository>,
|
||||
State(service): State<FileState>,
|
||||
ctx: Ctx,
|
||||
Path(file_id): Path<u64>,
|
||||
) -> Result<Json<FileRecord>> {
|
||||
let record = file_repository
|
||||
let record = service
|
||||
.file_repository
|
||||
.get_file(file_id as i64, ctx.user_id())
|
||||
.await?;
|
||||
Ok(Json(record))
|
||||
@@ -117,16 +147,43 @@ async fn get_file(
|
||||
)
|
||||
)]
|
||||
async fn download_file(
|
||||
State(file_repository): State<FileRepository>,
|
||||
State(service): State<FileState>,
|
||||
ctx: Ctx,
|
||||
Path(file_id): Path<u64>,
|
||||
) -> Result<impl IntoResponse> {
|
||||
let stream = file_repository
|
||||
let stream = service
|
||||
.file_repository
|
||||
.download_file(file_id as i64, ctx.user_id())
|
||||
.await?;
|
||||
Ok(Body::from_stream(stream))
|
||||
}
|
||||
|
||||
/// Download a file thumbnail
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/files/{id}/thumbnail",
|
||||
tag = "files",
|
||||
security(("cookie_auth" = [])),
|
||||
params(("id" = u64, Path, description = "File id")),
|
||||
responses(
|
||||
(status = 200, description = "File bytes", content_type = "application/octet-stream"),
|
||||
(status = 404, description = "File not found"),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 500, description = "Internal server error")
|
||||
)
|
||||
)]
|
||||
async fn thumbnail(
|
||||
State(service): State<FileState>,
|
||||
ctx: Ctx,
|
||||
Path(file_id): Path<u64>,
|
||||
) -> Result<impl IntoResponse> {
|
||||
let stream = service
|
||||
.file_repository
|
||||
.thumbnail(file_id as i64, ctx.user_id())
|
||||
.await?;
|
||||
Ok(Body::from_stream(stream))
|
||||
}
|
||||
|
||||
/// Stream a byte range of a file (HTTP 206)
|
||||
#[utoipa::path(
|
||||
get,
|
||||
@@ -146,24 +203,26 @@ async fn download_file(
|
||||
)
|
||||
)]
|
||||
async fn stream_part(
|
||||
State(file_repository): State<FileRepository>,
|
||||
State(service): State<FileState>,
|
||||
ctx: Ctx,
|
||||
headers: HeaderMap,
|
||||
Path(file_id): Path<u64>,
|
||||
) -> Result<impl IntoResponse> {
|
||||
let file_record = file_repository
|
||||
let record = service
|
||||
.file_repository
|
||||
.get_file(file_id as i64, ctx.user_id())
|
||||
.await?;
|
||||
let file_size = file_record.size as u64;
|
||||
let file_size = record.size as u64;
|
||||
let (start, end): (u64, u64) = parse_range(&headers, file_size)?;
|
||||
|
||||
let stream = file_repository
|
||||
let stream = service
|
||||
.file_repository
|
||||
.stream_part(file_id as i64, ctx.user_id(), start, end + 1)
|
||||
.await?;
|
||||
|
||||
let respones = Response::builder()
|
||||
.status(StatusCode::PARTIAL_CONTENT)
|
||||
.header(header::CONTENT_TYPE, file_record.file_type)
|
||||
.header(header::CONTENT_TYPE, record.file_type)
|
||||
.header(header::CONTENT_LENGTH, end - start + 1)
|
||||
.header(
|
||||
header::CONTENT_RANGE,
|
||||
@@ -207,11 +266,12 @@ fn parse_range(headers: &HeaderMap, file_size: u64) -> Result<(u64, u64)> {
|
||||
)
|
||||
)]
|
||||
async fn delete_file(
|
||||
State(file_repository): State<FileRepository>,
|
||||
State(service): State<FileState>,
|
||||
ctx: Ctx,
|
||||
Path(file_id): Path<u64>,
|
||||
) -> Result<Json<FileRecord>> {
|
||||
let file = file_repository
|
||||
let file = service
|
||||
.file_repository
|
||||
.delete_file(file_id as i64, ctx.user_id())
|
||||
.await?;
|
||||
Ok(Json(file))
|
||||
@@ -229,11 +289,8 @@ async fn delete_file(
|
||||
(status = 500, description = "Internal server error")
|
||||
)
|
||||
)]
|
||||
async fn list_files(
|
||||
State(file_repository): State<FileRepository>,
|
||||
ctx: Ctx,
|
||||
) -> Result<Json<Vec<FileRecord>>> {
|
||||
let files = file_repository.list_files(ctx.user_id()).await?;
|
||||
async fn list_files(State(service): State<FileState>, ctx: Ctx) -> Result<Json<Vec<FileRecord>>> {
|
||||
let files = service.file_repository.list_files(ctx.user_id()).await?;
|
||||
Ok(Json(files))
|
||||
}
|
||||
|
||||
@@ -251,13 +308,14 @@ mod tests {
|
||||
use rand::RngExt;
|
||||
use serde_json::json;
|
||||
use sqlx::PgPool;
|
||||
use tokio::sync::mpsc;
|
||||
use tower_cookies::CookieManagerLayer;
|
||||
|
||||
use crate::{
|
||||
model::{FileRepository, UserRepository},
|
||||
web::{
|
||||
mw_auth::{mw_ctx_resolver, mw_require_auth},
|
||||
routes_file::routes_file,
|
||||
routes_file::{FileState, routes_file},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -282,8 +340,12 @@ mod tests {
|
||||
async fn test_server() -> TestServer {
|
||||
let user_repository = user_repository().await;
|
||||
let file_repository = file_repository().await;
|
||||
|
||||
let (tx, _) = mpsc::channel(100);
|
||||
let file_service = FileState::new(file_repository.clone(), tx);
|
||||
|
||||
let routes_file =
|
||||
routes_file(file_repository.clone()).route_layer(middleware::from_fn(mw_require_auth));
|
||||
routes_file(file_service).route_layer(middleware::from_fn(mw_require_auth));
|
||||
let app = Router::new()
|
||||
.nest("/api", routes_file)
|
||||
.layer(middleware::from_fn_with_state(
|
||||
|
||||
Reference in New Issue
Block a user