diff --git a/Cargo.lock b/Cargo.lock index e859685..e6e7990 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 4 [[package]] name = "addr2line" -version = "0.25.1" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +checksum = "9698bf0769c641b18618039fe2ebd41eb3541f98433000f64e663fab7cea2c87" dependencies = [ "gimli", ] @@ -129,9 +129,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.101" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "arbitrary" @@ -675,9 +675,9 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.43" +version = "0.4.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ "iana-time-zone", "js-sys", @@ -726,9 +726,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.57" +version = "4.5.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6899ea499e3fb9305a65d5ebf6e3d2248c5fab291f300ad0a704fbe142eae31a" +checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" dependencies = [ "clap_builder", "clap_derive", @@ -736,9 +736,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.57" +version = "4.5.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b12c8b680195a62a8364d16b8447b01b6c2c8f9aaf68bee653be34d4245e238" +checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" dependencies = [ "anstream", "anstyle", @@ -760,9 +760,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.7" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" +checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" [[package]] name = "cmake" @@ -1086,46 +1086,47 @@ dependencies = [ [[package]] name = "cranelift-assembler-x64" -version = "0.128.3" +version = "0.129.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0377b13bf002a0774fcccac4f1102a10f04893d24060cf4b7350c87e4cbb647c" +checksum = "40630d663279bc855bff805d6f5e8a0b6a1867f9df95b010511ac6dc894e9395" dependencies = [ "cranelift-assembler-x64-meta", ] [[package]] name = "cranelift-assembler-x64-meta" -version = "0.128.3" +version = "0.129.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfa027979140d023b25bf7509fb7ede3a54c3d3871fb5ead4673c4b633f671a2" +checksum = "3ee6aec5ceb55e5fdbcf7ef677d7c7195531360ff181ce39b2b31df11d57305f" dependencies = [ "cranelift-srcgen", ] [[package]] name = "cranelift-bforest" -version = "0.128.3" +version = "0.129.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "618e4da87d9179a70b3c2f664451ca8898987aa6eb9f487d16988588b5d8cc40" +checksum = "9a92d78cc3f087d7e7073828f08d98c7074a3a062b6b29a1b7783ce74305685e" dependencies = [ "cranelift-entity", ] [[package]] name = "cranelift-bitset" -version = "0.128.3" +version = "0.129.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db53764b5dad233b37b8f5dc54d3caa9900c54579195e00f17ea21f03f71aaa7" +checksum = "edcc73d756f2e0d7eda6144fe64a2bc69c624de893cb1be51f1442aed77881d2" dependencies = [ "serde", "serde_derive", + "wasmtime-internal-core", ] [[package]] name = "cranelift-codegen" -version = "0.128.3" +version = "0.129.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ae927f1d8c0abddaa863acd201471d56e7fc6c3925104f4861ed4dc3e28b421" +checksum = "683d94c2cd0d73b41369b88da1129589bc3a2d99cf49979af1d14751f35b7a1b" dependencies = [ "bumpalo", "cranelift-assembler-x64", @@ -1138,6 +1139,7 @@ dependencies = [ "cranelift-isle", "gimli", "hashbrown 0.15.5", + "libm", "log", "pulley-interpreter", "regalloc2", @@ -1145,14 +1147,14 @@ dependencies = [ "serde", "smallvec", "target-lexicon 0.13.4", - "wasmtime-internal-math", + "wasmtime-internal-core", ] [[package]] name = "cranelift-codegen-meta" -version = "0.128.3" +version = "0.129.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3fcf1e3e6757834bd2584f4cbff023fcc198e9279dcb5d684b4bb27a9b19f54" +checksum = "235da0e52ee3a0052d0e944c3470ff025b1f4234f6ec4089d3109f2d2ffa6cbd" dependencies = [ "cranelift-assembler-x64-meta", "cranelift-codegen-shared", @@ -1163,35 +1165,36 @@ dependencies = [ [[package]] name = "cranelift-codegen-shared" -version = "0.128.3" +version = "0.129.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "205dcb9e6ccf9d368b7466be675ff6ee54a63e36da6fe20e72d45169cf6fd254" +checksum = "20c07c6c440bd1bf920ff7597a1e743ede1f68dcd400730bd6d389effa7662af" [[package]] name = "cranelift-control" -version = "0.128.3" +version = "0.129.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "108eca9fcfe86026054f931eceaf57b722c1b97464bf8265323a9b5877238817" +checksum = "8797c022e02521901e1aee483dea3ed3c67f2bf0a26405c9dd48e8ee7a70944b" dependencies = [ "arbitrary", ] [[package]] name = "cranelift-entity" -version = "0.128.3" +version = "0.129.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0d96496910065d3165f84ff8e1e393916f4c086f88ac8e1b407678bc78735aa" +checksum = "59d8e72637246edd2cba337939850caa8b201f6315925ec4c156fdd089999699" dependencies = [ "cranelift-bitset", "serde", "serde_derive", + "wasmtime-internal-core", ] [[package]] name = "cranelift-frontend" -version = "0.128.3" +version = "0.129.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e303983ad7e23c850f24d9c41fc3cb346e1b930f066d3966545e4c98dac5c9fb" +checksum = "4c31db0085c3dfa131e739c3b26f9f9c84d69a9459627aac1ac4ef8355e3411b" dependencies = [ "cranelift-codegen", "log", @@ -1201,15 +1204,15 @@ dependencies = [ [[package]] name = "cranelift-isle" -version = "0.128.3" +version = "0.129.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24b0cf8d867d891245836cac7abafb0a5b0ea040a019d720702b3b8bcba40bfa" +checksum = "524d804c1ebd8c542e6f64e71aa36934cec17c5da4a9ae3799796220317f5d23" [[package]] name = "cranelift-native" -version = "0.128.3" +version = "0.129.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e24b641e315443e27807b69c440fe766737d7e718c68beb665a2d69259c77bf3" +checksum = "dc9598f02540e382e1772416eba18e93c5275b746adbbf06ac1f3cf149415270" dependencies = [ "cranelift-codegen", "libc", @@ -1218,9 +1221,9 @@ dependencies = [ [[package]] name = "cranelift-srcgen" -version = "0.128.3" +version = "0.129.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4e378a54e7168a689486d67ee1f818b7e5356e54ae51a1d7a53f4f13f7f8b7a" +checksum = "d953932541249c91e3fa70a75ff1e52adc62979a2a8132145d4b9b3e6d1a9b6a" [[package]] name = "crc32fast" @@ -2600,9 +2603,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" dependencies = [ "futures-channel", "futures-core", @@ -2615,9 +2618,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", "futures-sink", @@ -2625,15 +2628,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-executor" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" dependencies = [ "futures-core", "futures-task", @@ -2642,15 +2645,15 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] name = "futures-macro" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", @@ -2659,15 +2662,15 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-timer" @@ -2677,9 +2680,9 @@ checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-channel", "futures-core", @@ -2689,7 +2692,6 @@ dependencies = [ "futures-task", "memchr", "pin-project-lite", - "pin-utils", "slab", ] @@ -2904,11 +2906,12 @@ dependencies = [ [[package]] name = "gimli" -version = "0.32.3" +version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" +checksum = "0bf7f043f89559805f8c7cacc432749b2fa0d0a0a9ee46ce47164ed5ba7f126c" dependencies = [ - "fallible-iterator 0.3.0", + "fnv", + "hashbrown 0.16.1", "indexmap", "stable_deref_trait", ] @@ -3633,9 +3636,9 @@ dependencies = [ [[package]] name = "image_hasher" -version = "3.1.0" +version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "300d892b049fb36ce62fb515b68aeade53dca784bc02093e359edc6625c479ac" +checksum = "dd266c66b0a0e2d4c6db8e710663fc163a2d33595ce997b6fbda407c8759d344" dependencies = [ "base64", "image", @@ -4119,9 +4122,9 @@ dependencies = [ [[package]] name = "lofty" -version = "0.23.1" +version = "0.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27fc77f88b239c7e0c266a5a8fae9da13f8dae9042dcd2722806a84ecae94491" +checksum = "179408be6ddda3771589a4e940b1b5718613fa9986d78f420890d20e2b6fc278" dependencies = [ "byteorder", "data-encoding", @@ -4466,9 +4469,9 @@ dependencies = [ [[package]] name = "moka" -version = "0.12.13" +version = "0.12.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ac832c50ced444ef6be0767a008b02c106a909ba79d1d830501e94b96f6b7e" +checksum = "85f8024e1c8e71c778968af91d43700ce1d11b219d127d79fb2934153b82b42b" dependencies = [ "async-lock", "crossbeam-channel", @@ -4540,17 +4543,17 @@ checksum = "13d2233c9842d08cfe13f9eac96e207ca6a2ea10b80259ebe8ad0268be27d2af" [[package]] name = "native-tls" -version = "0.2.14" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" dependencies = [ "libc", "log", "openssl", - "openssl-probe 0.1.6", + "openssl-probe", "openssl-sys", "schannel", - "security-framework 2.11.1", + "security-framework", "security-framework-sys", "tempfile", ] @@ -4947,12 +4950,6 @@ dependencies = [ "syn 2.0.114", ] -[[package]] -name = "openssl-probe" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" - [[package]] name = "openssl-probe" version = "0.2.1" @@ -5337,7 +5334,7 @@ dependencies = [ "tokio", "tokio-postgres", "tokio-util", - "toml 0.9.11+spec-1.1.0", + "toml 1.0.3+spec-1.1.0", "tracing", "urlencoding", "uuid", @@ -5357,9 +5354,9 @@ dependencies = [ "serde_json", "thiserror 2.0.18", "tokio", - "toml 0.9.11+spec-1.1.0", + "toml 1.0.3+spec-1.1.0", "uuid", - "wit-bindgen 0.52.0", + "wit-bindgen 0.53.1", ] [[package]] @@ -5386,7 +5383,7 @@ dependencies = [ "thiserror 2.0.18", "tokio", "tokio-util", - "toml 0.9.11+spec-1.1.0", + "toml 1.0.3+spec-1.1.0", "tower", "tower-http", "tower_governor", @@ -5408,7 +5405,7 @@ dependencies = [ "serde", "serde_json", "tokio", - "toml 0.9.11+spec-1.1.0", + "toml 1.0.3+spec-1.1.0", "tracing", "tracing-subscriber", "uuid", @@ -5710,9 +5707,9 @@ dependencies = [ [[package]] name = "pulldown-cmark" -version = "0.13.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e8bbe1a966bd2f362681a44f6edce3c2310ac21e4d5067a6e7ec396297a6ea0" +checksum = "83c41efbf8f90ac44de7f3a868f0867851d261b56291732d0cbf7cceaaeb55a6" dependencies = [ "bitflags 2.10.0", "getopts", @@ -5729,21 +5726,21 @@ checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae" [[package]] name = "pulley-interpreter" -version = "41.0.3" +version = "42.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01051a5b172e07f9197b85060e6583b942aec679dac08416647bf7e7dc916b65" +checksum = "bc2d61e068654529dc196437f8df0981db93687fdc67dec6a5de92363120b9da" dependencies = [ "cranelift-bitset", "log", "pulley-macros", - "wasmtime-internal-math", + "wasmtime-internal-core", ] [[package]] name = "pulley-macros" -version = "41.0.3" +version = "42.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cf194f5b1a415ef3a44ee35056f4009092cc4038a9f7e3c7c1e392f48ee7dbb" +checksum = "c3f210c61b6ecfaebbba806b6d9113a222519d4e5cc4ab2d5ecca047bb7927ae" dependencies = [ "proc-macro2", "quote", @@ -6475,10 +6472,10 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" dependencies = [ - "openssl-probe 0.2.1", + "openssl-probe", "rustls-pki-types", "schannel", - "security-framework 3.5.1", + "security-framework", ] [[package]] @@ -6506,7 +6503,7 @@ dependencies = [ "rustls-native-certs", "rustls-platform-verifier-android", "rustls-webpki", - "security-framework 3.5.1", + "security-framework", "security-framework-sys", "webpki-root-certs", "windows-sys 0.61.2", @@ -6572,19 +6569,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "security-framework" -version = "2.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" -dependencies = [ - "bitflags 2.10.0", - "core-foundation 0.9.4", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - [[package]] name = "security-framework" version = "3.5.1" @@ -7591,6 +7575,21 @@ dependencies = [ "winnow 0.7.14", ] +[[package]] +name = "toml" +version = "1.0.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7614eaf19ad818347db24addfa201729cf2a9b6fdfd9eb0ab870fcacc606c0c" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned 1.0.4", + "toml_datetime 1.0.0+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 0.7.14", +] + [[package]] name = "toml_datetime" version = "0.6.11" @@ -7609,6 +7608,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "toml_datetime" +version = "1.0.0+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e" +dependencies = [ + "serde_core", +] + [[package]] name = "toml_edit" version = "0.19.15" @@ -7659,9 +7667,9 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.0.6+spec-1.1.0" +version = "1.0.9+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" +checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" dependencies = [ "winnow 0.7.14", ] @@ -8046,12 +8054,12 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.20.0" +version = "1.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f" +checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb" dependencies = [ "atomic", - "getrandom 0.3.4", + "getrandom 0.4.1", "js-sys", "serde_core", "wasm-bindgen", @@ -8240,9 +8248,9 @@ dependencies = [ [[package]] name = "wasm-compose" -version = "0.243.0" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af801b6f36459023eaec63fdbaedad2fd5a4ab7dc74ecc110a8b5d375c5775e4" +checksum = "92cda9c76ca8dcac01a8b497860c2cb15cd6f216dc07060517df5abbe82512ac" dependencies = [ "anyhow", "heck 0.5.0", @@ -8254,21 +8262,11 @@ dependencies = [ "serde_derive", "serde_yaml", "smallvec", - "wasm-encoder 0.243.0", - "wasmparser 0.243.0", + "wasm-encoder 0.244.0", + "wasmparser 0.244.0", "wat", ] -[[package]] -name = "wasm-encoder" -version = "0.243.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c55db9c896d70bd9fa535ce83cd4e1f2ec3726b0edd2142079f594fc3be1cb35" -dependencies = [ - "leb128fmt", - "wasmparser 0.243.0", -] - [[package]] name = "wasm-encoder" version = "0.244.0" @@ -8279,6 +8277,16 @@ dependencies = [ "wasmparser 0.244.0", ] +[[package]] +name = "wasm-encoder" +version = "0.245.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9dca005e69bf015e45577e415b9af8c67e8ee3c0e38b5b0add5aa92581ed5c" +dependencies = [ + "leb128fmt", + "wasmparser 0.245.1", +] + [[package]] name = "wasm-metadata" version = "0.244.0" @@ -8291,6 +8299,18 @@ dependencies = [ "wasmparser 0.244.0", ] +[[package]] +name = "wasm-metadata" +version = "0.245.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da55e60097e8b37b475a0fa35c3420dd71d9eb7bd66109978ab55faf56a57efb" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder 0.245.1", + "wasmparser 0.245.1", +] + [[package]] name = "wasm-streams" version = "0.4.2" @@ -8306,9 +8326,9 @@ dependencies = [ [[package]] name = "wasmparser" -version = "0.243.0" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6d8db401b0528ec316dfbe579e6ab4152d61739cfe076706d2009127970159d" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ "bitflags 2.10.0", "hashbrown 0.15.5", @@ -8319,35 +8339,34 @@ dependencies = [ [[package]] name = "wasmparser" -version = "0.244.0" +version = "0.245.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +checksum = "4f08c9adee0428b7bddf3890fc27e015ac4b761cc608c822667102b8bfd6995e" dependencies = [ "bitflags 2.10.0", - "hashbrown 0.15.5", + "hashbrown 0.16.1", "indexmap", "semver", ] [[package]] name = "wasmprinter" -version = "0.243.0" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb2b6035559e146114c29a909a3232928ee488d6507a1504d8934e8607b36d7b" +checksum = "09390d7b2bd7b938e563e4bff10aa345ef2e27a3bc99135697514ef54495e68f" dependencies = [ "anyhow", "termcolor", - "wasmparser 0.243.0", + "wasmparser 0.244.0", ] [[package]] name = "wasmtime" -version = "41.0.3" +version = "42.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a19f56cece843fa95dd929f5568ff8739c7e3873b530ceea9eda2aa02a0b4142" +checksum = "39bef52be4fb4c5b47d36f847172e896bc94b35c9c6a6f07117686bd16ed89a7" dependencies = [ "addr2line", - "anyhow", "async-trait", "bitflags 2.10.0", "bumpalo", @@ -8357,8 +8376,6 @@ dependencies = [ "futures", "fxprof-processed-profile", "gimli", - "hashbrown 0.15.5", - "indexmap", "ittapi", "libc", "log", @@ -8378,18 +8395,17 @@ dependencies = [ "target-lexicon 0.13.4", "tempfile", "wasm-compose", - "wasm-encoder 0.243.0", - "wasmparser 0.243.0", + "wasm-encoder 0.244.0", + "wasmparser 0.244.0", "wasmtime-environ", "wasmtime-internal-cache", "wasmtime-internal-component-macro", "wasmtime-internal-component-util", + "wasmtime-internal-core", "wasmtime-internal-cranelift", "wasmtime-internal-fiber", "wasmtime-internal-jit-debug", "wasmtime-internal-jit-icache-coherence", - "wasmtime-internal-math", - "wasmtime-internal-slab", "wasmtime-internal-unwinder", "wasmtime-internal-versioned-export-macros", "wasmtime-internal-winch", @@ -8399,15 +8415,16 @@ dependencies = [ [[package]] name = "wasmtime-environ" -version = "41.0.3" +version = "42.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bf9dff572c950258548cbbaf39033f68f8dcd0b43b22e80def9fe12d532d3e5" +checksum = "bb637d5aa960ac391ca5a4cbf3e45807632e56beceeeb530e14dfa67fdfccc62" dependencies = [ "anyhow", "cpp_demangle", "cranelift-bitset", "cranelift-entity", "gimli", + "hashbrown 0.15.5", "indexmap", "log", "object", @@ -8418,17 +8435,18 @@ dependencies = [ "serde_derive", "smallvec", "target-lexicon 0.13.4", - "wasm-encoder 0.243.0", - "wasmparser 0.243.0", + "wasm-encoder 0.244.0", + "wasmparser 0.244.0", "wasmprinter", "wasmtime-internal-component-util", + "wasmtime-internal-core", ] [[package]] name = "wasmtime-internal-cache" -version = "41.0.3" +version = "42.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f52a985f5b5dae53147fc596f3a313c334e2c24fd1ba708634e1382f6ecd727" +checksum = "4ab6c428c610ae3e7acd25ca2681b4d23672c50d8769240d9dda99b751d4deec" dependencies = [ "base64", "directories-next", @@ -8446,9 +8464,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-component-macro" -version = "41.0.3" +version = "42.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7920dc7dcb608352f5fe93c52582e65075b7643efc5dac3fc717c1645a8d29a0" +checksum = "ca768b11d5e7de017e8c3d4d444da6b4ce3906f565bcbc253d76b4ecbb5d2869" dependencies = [ "anyhow", "proc-macro2", @@ -8456,20 +8474,30 @@ dependencies = [ "syn 2.0.114", "wasmtime-internal-component-util", "wasmtime-internal-wit-bindgen", - "wit-parser 0.243.0", + "wit-parser 0.244.0", ] [[package]] name = "wasmtime-internal-component-util" -version = "41.0.3" +version = "42.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "066f5aed35aa60580a2ac0df145c0f0d4b04319862fee1d6036693e1cca43a12" +checksum = "763f504faf96c9b409051e96a1434655eea7f56a90bed9cb1e22e22c941253fd" + +[[package]] +name = "wasmtime-internal-core" +version = "42.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03a4a3f055a804a2f3d86e816a9df78a8fa57762212a8506164959224a40cd48" +dependencies = [ + "anyhow", + "libm", +] [[package]] name = "wasmtime-internal-cranelift" -version = "41.0.3" +version = "42.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afb8002dc415b7773d7949ee360c05ee8f91627ec25a7a0b01ee03831bdfdda1" +checksum = "55154a91d22ad51f9551124ce7fb49ddddc6a82c4910813db4c790c97c9ccf32" dependencies = [ "cfg-if", "cranelift-codegen", @@ -8485,18 +8513,18 @@ dependencies = [ "smallvec", "target-lexicon 0.13.4", "thiserror 2.0.18", - "wasmparser 0.243.0", + "wasmparser 0.244.0", "wasmtime-environ", - "wasmtime-internal-math", + "wasmtime-internal-core", "wasmtime-internal-unwinder", "wasmtime-internal-versioned-export-macros", ] [[package]] name = "wasmtime-internal-fiber" -version = "41.0.3" +version = "42.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f9c562c5a272bc9f615d8f0c085a4360bafa28eef9aa5947e63d204b1129b22" +checksum = "05decfad1021ad2efcca5c1be9855acb54b6ee7158ac4467119b30b7481508e3" dependencies = [ "cc", "cfg-if", @@ -8509,9 +8537,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-jit-debug" -version = "41.0.3" +version = "42.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db673148f26e1211db3913c12c75594be9e3858a71fa297561e9162b1a49cfb0" +checksum = "924980c50427885fd4feed2049b88380178e567768aaabf29045b02eb262eaa7" dependencies = [ "cc", "object", @@ -8521,36 +8549,21 @@ dependencies = [ [[package]] name = "wasmtime-internal-jit-icache-coherence" -version = "41.0.3" +version = "42.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bada5ca1cc47df7d14100e2254e187c2486b426df813cea2dd2553a7469f7674" +checksum = "c57d24e8d1334a0e5a8b600286ffefa1fc4c3e8176b110dff6fbc1f43c4a599b" dependencies = [ - "anyhow", "cfg-if", "libc", + "wasmtime-internal-core", "windows-sys 0.61.2", ] -[[package]] -name = "wasmtime-internal-math" -version = "41.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf6f615d528eda9adc6eefb062135f831b5215c348f4c3ec3e143690c730605b" -dependencies = [ - "libm", -] - -[[package]] -name = "wasmtime-internal-slab" -version = "41.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da169d4f789b586e1b2612ba8399c653ed5763edf3e678884ba785bb151d018f" - [[package]] name = "wasmtime-internal-unwinder" -version = "41.0.3" +version = "42.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4888301f3393e4e8c75c938cce427293fade300fee3fc8fd466fdf3e54ae068e" +checksum = "3a1a144bd4393593a868ba9df09f34a6a360cb5db6e71815f20d3f649c6e6735" dependencies = [ "cfg-if", "cranelift-codegen", @@ -8561,9 +8574,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-versioned-export-macros" -version = "41.0.3" +version = "42.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63ba3124cc2cbcd362672f9f077303ccc4cd61daa908f73447b7fdaece75ff9f" +checksum = "9a6948b56bb00c62dbd205ea18a4f1ceccbe1e4b8479651fdb0bab2553790f20" dependencies = [ "proc-macro2", "quote", @@ -8572,16 +8585,16 @@ dependencies = [ [[package]] name = "wasmtime-internal-winch" -version = "41.0.3" +version = "42.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90a4182515dabba776656de4ebd62efad03399e261cf937ecccb838ce8823534" +checksum = "9130b3ab6fb01be80b27b9a2c84817af29ae8224094f2503d2afa9fea5bf9d00" dependencies = [ "cranelift-codegen", "gimli", "log", "object", "target-lexicon 0.13.4", - "wasmparser 0.243.0", + "wasmparser 0.244.0", "wasmtime-environ", "wasmtime-internal-cranelift", "winch-codegen", @@ -8589,15 +8602,15 @@ dependencies = [ [[package]] name = "wasmtime-internal-wit-bindgen" -version = "41.0.3" +version = "42.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87acbd416227cdd279565ba49e57cf7f08d112657c3b3f39b70250acdfd094fe" +checksum = "102d0d70dbfede00e4cc9c24e86df6d32c03bf6f5ad06b5d6c76b0a4a5004c4a" dependencies = [ "anyhow", "bitflags 2.10.0", "heck 0.5.0", "indexmap", - "wit-parser 0.243.0", + "wit-parser 0.244.0", ] [[package]] @@ -8950,11 +8963,10 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "winch-codegen" -version = "41.0.3" +version = "42.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4f31dcfdfaf9d6df9e1124d7c8ee6fc29af5b99b89d11ae731c138e0f5bd77b" +checksum = "1977857998e4dd70d26e2bfc0618a9684a2fb65b1eca174dc13f3b3e9c2159ca" dependencies = [ - "anyhow", "cranelift-assembler-x64", "cranelift-codegen", "gimli", @@ -8962,10 +8974,10 @@ dependencies = [ "smallvec", "target-lexicon 0.13.4", "thiserror 2.0.18", - "wasmparser 0.243.0", + "wasmparser 0.244.0", "wasmtime-environ", + "wasmtime-internal-core", "wasmtime-internal-cranelift", - "wasmtime-internal-math", ] [[package]] @@ -9396,12 +9408,12 @@ dependencies = [ [[package]] name = "wit-bindgen" -version = "0.52.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e048f41ef90f0b5dd61f1059c35f5636252e56813bf616d0803aa3739867230" +checksum = "6e915216dde3e818093168df8380a64fba25df468d626c80dd5d6a184c87e7c7" dependencies = [ "bitflags 2.10.0", - "wit-bindgen-rust-macro 0.52.0", + "wit-bindgen-rust-macro 0.53.1", ] [[package]] @@ -9417,13 +9429,13 @@ dependencies = [ [[package]] name = "wit-bindgen-core" -version = "0.52.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c15e7a56641cc9040480a26526a3229cbc4e8065adf98c9755d21c4c9b446c4c" +checksum = "3deda4b7e9f522d994906f6e6e0fc67965ea8660306940a776b76732be8f3933" dependencies = [ "anyhow", "heck 0.5.0", - "wit-parser 0.244.0", + "wit-parser 0.245.1", ] [[package]] @@ -9437,25 +9449,25 @@ dependencies = [ "indexmap", "prettyplease", "syn 2.0.114", - "wasm-metadata", + "wasm-metadata 0.244.0", "wit-bindgen-core 0.51.0", - "wit-component", + "wit-component 0.244.0", ] [[package]] name = "wit-bindgen-rust" -version = "0.52.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd81b0ae1ec492bfe91683f1da6db6492ebc682e72d4f2715619dba783b066ca" +checksum = "863a7ab3c4dfee58db196811caeb0718b88412a0aef3d1c2b02fcbae1e37c688" dependencies = [ "anyhow", "heck 0.5.0", "indexmap", "prettyplease", "syn 2.0.114", - "wasm-metadata", - "wit-bindgen-core 0.52.0", - "wit-component", + "wasm-metadata 0.245.1", + "wit-bindgen-core 0.53.1", + "wit-component 0.245.1", ] [[package]] @@ -9475,17 +9487,17 @@ dependencies = [ [[package]] name = "wit-bindgen-rust-macro" -version = "0.52.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54e6ce04c549e7149b66a70d34fc5a2a01b374bf49ca61db65d16e3ae922866e" +checksum = "d14f3a9bfa3804bb0e9ab7f66da047f210eded6a1297ae3ba5805b384d64797f" dependencies = [ "anyhow", "prettyplease", "proc-macro2", "quote", "syn 2.0.114", - "wit-bindgen-core 0.52.0", - "wit-bindgen-rust 0.52.0", + "wit-bindgen-core 0.53.1", + "wit-bindgen-rust 0.53.1", ] [[package]] @@ -9502,27 +9514,28 @@ dependencies = [ "serde_derive", "serde_json", "wasm-encoder 0.244.0", - "wasm-metadata", + "wasm-metadata 0.244.0", "wasmparser 0.244.0", "wit-parser 0.244.0", ] [[package]] -name = "wit-parser" -version = "0.243.0" +name = "wit-component" +version = "0.245.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df983a8608e513d8997f435bb74207bf0933d0e49ca97aa9d8a6157164b9b7fc" +checksum = "4894f10d2d5cbc17c77e91f86a1e48e191a788da4425293b55c98b44ba3fcac9" dependencies = [ "anyhow", - "id-arena", + "bitflags 2.10.0", "indexmap", "log", - "semver", "serde", "serde_derive", "serde_json", - "unicode-xid", - "wasmparser 0.243.0", + "wasm-encoder 0.245.1", + "wasm-metadata 0.245.1", + "wasmparser 0.245.1", + "wit-parser 0.245.1", ] [[package]] @@ -9543,6 +9556,25 @@ dependencies = [ "wasmparser 0.244.0", ] +[[package]] +name = "wit-parser" +version = "0.245.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330698718e82983499419494dd1e3d7811a457a9bf9f69734e8c5f07a2547929" +dependencies = [ + "anyhow", + "hashbrown 0.16.1", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser 0.245.1", +] + [[package]] name = "writeable" version = "0.6.2" diff --git a/Cargo.toml b/Cargo.toml index 6ac09bb..e287f8a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,20 +24,20 @@ tokio-util = { version = "0.7.18", features = ["rt"] } # Serialization serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.149" -toml = "0.9.11" +toml = "1.0.3" # CLI argument parsing -clap = { version = "4.5.57", features = ["derive", "env"] } +clap = { version = "4.5.60", features = ["derive", "env"] } # Date/time -chrono = { version = "0.4.43", features = ["serde"] } +chrono = { version = "0.4.44", features = ["serde"] } # IDs -uuid = { version = "1.20.0", features = ["v7", "serde"] } +uuid = { version = "1.21.0", features = ["v7", "serde"] } # Error handling thiserror = "2.0.18" -anyhow = "1.0.100" +anyhow = "1.0.102" # Logging tracing = "0.1.44" @@ -47,7 +47,7 @@ tracing-subscriber = { version = "0.3.22", features = ["env-filter", "json"] } blake3 = "1.8.3" # Metadata extraction -lofty = "0.23.1" +lofty = "0.23.2" lopdf = "0.39.0" epub = "2.1.5" matroska = "0.30.0" @@ -55,7 +55,7 @@ gray_matter = "0.3.2" kamadak-exif = "0.6.1" # Database - SQLite -rusqlite = { version = "0.37", features = ["bundled", "column_decltype"] } +rusqlite = { version = "=0.37.0", features = ["bundled", "column_decltype"] } # Database - PostgreSQL tokio-postgres = { version = "0.7.16", features = [ @@ -66,7 +66,7 @@ tokio-postgres = { version = "0.7.16", features = [ deadpool-postgres = "0.14.1" postgres-types = { version = "0.2.12", features = ["derive"] } postgres-native-tls = "0.5.2" -native-tls = "0.2.14" +native-tls = "0.2.18" # Migrations refinery = { version = "0.9.0", features = ["rusqlite", "tokio-postgres"] } @@ -87,7 +87,7 @@ governor = "0.10.4" tower_governor = "0.8.0" # HTTP client -reqwest = { version = "0.13.1", features = ["json", "query", "blocking"] } +reqwest = { version = "0.13.2", features = ["json", "query", "blocking"] } # TUI ratatui = "0.30.0" @@ -100,7 +100,7 @@ dioxus = { version = "0.7.3", features = ["desktop", "router"] } async-trait = "0.1.89" # Async utilities -futures = "0.3.31" +futures = "0.3.32" # Image processing (thumbnails) image = { version = "0.25.9", default-features = false, features = [ @@ -113,7 +113,7 @@ image = { version = "0.25.9", default-features = false, features = [ ] } # Markdown rendering -pulldown-cmark = "0.13.0" +pulldown-cmark = "0.13.1" ammonia = "4.1.2" # Password hashing @@ -126,15 +126,77 @@ dioxus-free-icons = { version = "0.10.0", features = ["font-awesome-solid"] } rfd = "0.17.2" gloo-timers = { version = "0.3.0", features = ["futures"] } rand = "0.10.0" -moka = { version = "0.12.13", features = ["future"] } +moka = { version = "0.12.14", features = ["future"] } urlencoding = "2.1.3" -image_hasher = "3.1.0" +image_hasher = "3.1.1" percent-encoding = "2.3.2" http = "1.4.0" # WASM runtime for plugins -wasmtime = { version = "41.0.3", features = ["component-model"] } -wit-bindgen = "0.52.0" +wasmtime = { version = "42.0.1", features = ["component-model"] } +wit-bindgen = "0.53.1" + +# See: +# +[workspace.lints.clippy] +cargo = { level = "warn", priority = -1 } +complexity = { level = "warn", priority = -1 } +nursery = { level = "warn", priority = -1 } +pedantic = { level = "warn", priority = -1 } +perf = { level = "warn", priority = -1 } +style = { level = "warn", priority = -1 } + +# The lint groups above enable some less-than-desirable rules, we should manually +# enable those to keep our sanity. +absolute_paths = "allow" +arbitrary_source_item_ordering = "allow" +clone_on_ref_ptr = "warn" +dbg_macro = "warn" +empty_drop = "warn" +empty_structs_with_brackets = "warn" +exit = "warn" +filetype_is_file = "warn" +get_unwrap = "warn" +implicit_return = "allow" +infinite_loop = "warn" +map_with_unused_argument_over_ranges = "warn" +missing_docs_in_private_items = "allow" +multiple_crate_versions = "allow" # :( +non_ascii_literal = "allow" +non_std_lazy_statics = "warn" +pathbuf_init_then_push = "warn" +pattern_type_mismatch = "allow" +question_mark_used = "allow" +rc_buffer = "warn" +rc_mutex = "warn" +rest_pat_in_fully_bound_structs = "warn" +similar_names = "allow" +single_call_fn = "allow" +std_instead_of_core = "allow" +too_long_first_doc_paragraph = "allow" +too_many_lines = "allow" +undocumented_unsafe_blocks = "warn" +unnecessary_safety_comment = "warn" +unused_result_ok = "warn" +unused_trait_names = "allow" + +# False positive: +# clippy's build script check doesn't recognize workspace-inherited metadata +# which means in our current workspace layout, we get pranked by Clippy. +cargo_common_metadata = "allow" + +# In the honor of a recent Cloudflare regression +panic = "deny" +unwrap_used = "deny" + +# Less dangerous, but we'd like to know +# Those must be opt-in, and are fine ONLY in tests and examples. +expect_used = "warn" +print_stderr = "warn" +print_stdout = "warn" +todo = "warn" +unimplemented = "warn" +unreachable = "warn" [profile.dev.package] blake3 = { opt-level = 3 } diff --git a/crates/pinakes-core/src/audit.rs b/crates/pinakes-core/src/audit.rs index d241946..043f3a3 100644 --- a/crates/pinakes-core/src/audit.rs +++ b/crates/pinakes-core/src/audit.rs @@ -6,6 +6,22 @@ use crate::{ storage::DynStorageBackend, }; +/// Records an audit action for a media item. +/// +/// # Arguments +/// +/// * `storage` - Storage backend for persistence +/// * `media_id` - Optional media item that was affected +/// * `action` - The action being performed +/// * `details` - Optional additional details +/// +/// # Returns +/// +/// `Ok(())` on success +/// +/// # Errors +/// +/// Returns errors from the storage backend pub async fn record_action( storage: &DynStorageBackend, media_id: Option, diff --git a/crates/pinakes-core/src/collections.rs b/crates/pinakes-core/src/collections.rs index 7c8758e..789c853 100644 --- a/crates/pinakes-core/src/collections.rs +++ b/crates/pinakes-core/src/collections.rs @@ -2,6 +2,19 @@ use uuid::Uuid; use crate::{error::Result, model::*, storage::DynStorageBackend}; +/// Creates a new collection. +/// +/// # Arguments +/// +/// * `storage` - Storage backend +/// * `name` - Collection name +/// * `kind` - Manual or virtual collection +/// * `description` - Optional description +/// * `filter_query` - For virtual collections, the search query +/// +/// # Returns +/// +/// The created collection pub async fn create_collection( storage: &DynStorageBackend, name: &str, @@ -14,6 +27,18 @@ pub async fn create_collection( .await } +/// Adds a media item to a collection. +/// +/// # Arguments +/// +/// * `storage` - Storage backend +/// * `collection_id` - Target collection +/// * `media_id` - Media item to add +/// * `position` - Position in the collection order +/// +/// # Returns +/// +/// `Ok(())` on success pub async fn add_member( storage: &DynStorageBackend, collection_id: Uuid, @@ -32,6 +57,17 @@ pub async fn add_member( .await } +/// Removes a media item from a collection. +/// +/// # Arguments +/// +/// * `storage` - Storage backend +/// * `collection_id` - Target collection +/// * `media_id` - Media item to remove +/// +/// # Returns +/// +/// `Ok(())` on success pub async fn remove_member( storage: &DynStorageBackend, collection_id: Uuid, @@ -49,6 +85,19 @@ pub async fn remove_member( .await } +/// Returns all media items in a collection. +/// +/// Virtual collections are evaluated dynamically using their filter query. +/// Manual collections return stored members. +/// +/// # Arguments +/// +/// * `storage` - Storage backend +/// * `collection_id` - Collection to query +/// +/// # Returns +/// +/// List of media items in the collection pub async fn get_members( storage: &DynStorageBackend, collection_id: Uuid, diff --git a/crates/pinakes-core/src/config.rs b/crates/pinakes-core/src/config.rs index 521ef6a..cff7d4c 100644 --- a/crates/pinakes-core/src/config.rs +++ b/crates/pinakes-core/src/config.rs @@ -110,6 +110,8 @@ pub struct Config { pub sync: SyncConfig, #[serde(default)] pub sharing: SharingConfig, + #[serde(default)] + pub trash: TrashConfig, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -284,8 +286,6 @@ impl std::fmt::Display for UserRole { } } -// ===== Plugin Configuration ===== - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PluginsConfig { #[serde(default)] @@ -337,8 +337,6 @@ impl Default for PluginsConfig { } } -// ===== Transcoding Configuration ===== - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TranscodingConfig { #[serde(default)] @@ -400,8 +398,6 @@ impl Default for TranscodingConfig { } } -// ===== Enrichment Configuration ===== - #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct EnrichmentConfig { #[serde(default)] @@ -432,8 +428,6 @@ pub struct EnrichmentSource { pub api_endpoint: Option, } -// ===== Cloud Configuration ===== - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CloudConfig { #[serde(default)] @@ -483,8 +477,6 @@ impl Default for CloudConfig { } } -// ===== Analytics Configuration ===== - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AnalyticsConfig { #[serde(default)] @@ -509,8 +501,6 @@ impl Default for AnalyticsConfig { } } -// ===== Photo Management Configuration ===== - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PhotoConfig { /// Generate perceptual hashes for image duplicate detection (CPU-intensive) @@ -568,8 +558,6 @@ impl Default for PhotoConfig { } } -// ===== Managed Storage Configuration ===== - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ManagedStorageConfig { /// Enable managed storage for file uploads @@ -613,23 +601,18 @@ impl Default for ManagedStorageConfig { } } -// ===== Sync Configuration ===== - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[derive( + Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default, +)] #[serde(rename_all = "snake_case")] pub enum ConflictResolution { ServerWins, ClientWins, + #[default] KeepBoth, Manual, } -impl Default for ConflictResolution { - fn default() -> Self { - Self::KeepBoth - } -} - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SyncConfig { /// Enable cross-device sync functionality @@ -697,8 +680,6 @@ impl Default for SyncConfig { } } -// ===== Sharing Configuration ===== - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SharingConfig { /// Enable sharing functionality @@ -750,7 +731,29 @@ impl Default for SharingConfig { } } -// ===== Storage Configuration ===== +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TrashConfig { + #[serde(default)] + pub enabled: bool, + #[serde(default = "default_trash_retention_days")] + pub retention_days: u64, + #[serde(default)] + pub auto_empty: bool, +} + +fn default_trash_retention_days() -> u64 { + 30 +} + +impl Default for TrashConfig { + fn default() -> Self { + Self { + enabled: false, + retention_days: default_trash_retention_days(), + auto_empty: false, + } + } +} #[derive(Debug, Clone, Serialize, Deserialize)] pub struct StorageConfig { @@ -982,19 +985,19 @@ impl Config { /// Ensure all directories needed by this config exist and are writable. pub fn ensure_dirs(&self) -> crate::error::Result<()> { - if let Some(ref sqlite) = self.storage.sqlite { - if let Some(parent) = sqlite.path.parent() { - // Skip if parent is empty string (happens with bare filenames like - // "pinakes.db") - if !parent.as_os_str().is_empty() { - std::fs::create_dir_all(parent)?; - let metadata = std::fs::metadata(parent)?; - if metadata.permissions().readonly() { - return Err(crate::error::PinakesError::Config(format!( - "directory is not writable: {}", - parent.display() - ))); - } + if let Some(ref sqlite) = self.storage.sqlite + && let Some(parent) = sqlite.path.parent() + { + // Skip if parent is empty string (happens with bare filenames like + // "pinakes.db") + if !parent.as_os_str().is_empty() { + std::fs::create_dir_all(parent)?; + let metadata = std::fs::metadata(parent)?; + if metadata.permissions().readonly() { + return Err(crate::error::PinakesError::Config(format!( + "directory is not writable: {}", + parent.display() + ))); } } } @@ -1139,6 +1142,7 @@ impl Default for Config { managed_storage: ManagedStorageConfig::default(), sync: SyncConfig::default(), sharing: SharingConfig::default(), + trash: TrashConfig::default(), } } } diff --git a/crates/pinakes-core/src/events.rs b/crates/pinakes-core/src/events.rs index c2c726e..8b896dd 100644 --- a/crates/pinakes-core/src/events.rs +++ b/crates/pinakes-core/src/events.rs @@ -78,7 +78,7 @@ pub fn detect_events( } // Sort by date_taken - items.sort_by(|a, b| a.date_taken.unwrap().cmp(&b.date_taken.unwrap())); + items.sort_by_key(|a| a.date_taken.unwrap()); let mut events: Vec = Vec::new(); let mut current_event_items: Vec = vec![items[0].id]; @@ -181,7 +181,7 @@ pub fn detect_bursts( } // Sort by date_taken - items.sort_by(|a, b| a.date_taken.unwrap().cmp(&b.date_taken.unwrap())); + items.sort_by_key(|a| a.date_taken.unwrap()); let mut bursts: Vec> = Vec::new(); let mut current_burst: Vec = vec![items[0].id]; diff --git a/crates/pinakes-core/src/hash.rs b/crates/pinakes-core/src/hash.rs index 3c64361..fc0ebdd 100644 --- a/crates/pinakes-core/src/hash.rs +++ b/crates/pinakes-core/src/hash.rs @@ -4,6 +4,19 @@ use crate::{error::Result, model::ContentHash}; const BUFFER_SIZE: usize = 65536; +/// Computes the BLAKE3 hash of a file asynchronously. +/// +/// # Arguments +/// +/// * `path` - Path to the file to hash +/// +/// # Returns +/// +/// The content hash +/// +/// # Errors +/// +/// Returns I/O errors or task execution errors pub async fn compute_file_hash(path: &Path) -> Result { let path = path.to_path_buf(); let hash = tokio::task::spawn_blocking(move || -> Result { @@ -24,6 +37,7 @@ pub async fn compute_file_hash(path: &Path) -> Result { Ok(hash) } +/// Computes the BLAKE3 hash of a byte slice synchronously. pub fn compute_hash_sync(data: &[u8]) -> ContentHash { let hash = blake3::hash(data); ContentHash::new(hash.to_hex().to_string()) diff --git a/crates/pinakes-core/src/import.rs b/crates/pinakes-core/src/import.rs index 5df4465..4a8efe1 100644 --- a/crates/pinakes-core/src/import.rs +++ b/crates/pinakes-core/src/import.rs @@ -17,6 +17,7 @@ use crate::{ thumbnail, }; +/// Result of importing a single file. pub struct ImportResult { pub media_id: MediaId, pub was_duplicate: bool, @@ -26,7 +27,7 @@ pub struct ImportResult { } /// Options for import operations -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] pub struct ImportOptions { /// Skip files that haven't changed since last scan (based on mtime) pub incremental: bool, @@ -36,16 +37,6 @@ pub struct ImportOptions { pub photo_config: crate::config::PhotoConfig, } -impl Default for ImportOptions { - fn default() -> Self { - Self { - incremental: false, - force: false, - photo_config: crate::config::PhotoConfig::default(), - } - } -} - /// Get the modification time of a file as a Unix timestamp fn get_file_mtime(path: &Path) -> Option { std::fs::metadata(path) @@ -55,9 +46,20 @@ fn get_file_mtime(path: &Path) -> Option { .map(|d| d.as_secs() as i64) } -/// Check that a canonicalized path falls under at least one configured root -/// directory. If no roots are configured, all paths are allowed (for ad-hoc -/// imports). +/// Validates that a path is within configured root directories. +/// +/// # Arguments +/// +/// * `storage` - Storage backend to query root directories +/// * `path` - Path to validate +/// +/// # Returns +/// +/// `Ok(())` if path is within roots or no roots configured +/// +/// # Errors +/// +/// Returns `InvalidOperation` if path is outside all root directories pub async fn validate_path_in_roots( storage: &DynStorageBackend, path: &Path, @@ -79,6 +81,20 @@ pub async fn validate_path_in_roots( ))) } +/// Imports a file using default options. +/// +/// # Arguments +/// +/// * `storage` - Storage backend +/// * `path` - Path to the file to import +/// +/// # Returns +/// +/// Import result with media ID and status +/// +/// # Errors +/// +/// Returns `FileNotFound` if path doesn't exist pub async fn import_file( storage: &DynStorageBackend, path: &Path, @@ -236,15 +252,15 @@ pub async fn import_file_with_options( storage.insert_media(&item).await?; // Extract and store markdown links for markdown files - if is_markdown { - if let Err(e) = extract_and_store_links(storage, media_id, &path).await { - tracing::warn!( - media_id = %media_id, - path = %path.display(), - error = %e, - "failed to extract markdown links" - ); - } + if is_markdown + && let Err(e) = extract_and_store_links(storage, media_id, &path).await + { + tracing::warn!( + media_id = %media_id, + path = %path.display(), + error = %e, + "failed to extract markdown links" + ); } // Store extracted extra metadata as custom fields @@ -419,12 +435,10 @@ async fn extract_and_store_links( media_id: MediaId, path: &Path, ) -> Result<()> { - // Read file content let content = tokio::fs::read_to_string(path).await.map_err(|e| { - PinakesError::Io(std::io::Error::new( - std::io::ErrorKind::Other, - format!("failed to read markdown file for link extraction: {e}"), - )) + PinakesError::Io(std::io::Error::other(format!( + "failed to read markdown file for link extraction: {e}" + ))) })?; // Extract links diff --git a/crates/pinakes-core/src/integrity.rs b/crates/pinakes-core/src/integrity.rs index 76e3837..6f3b856 100644 --- a/crates/pinakes-core/src/integrity.rs +++ b/crates/pinakes-core/src/integrity.rs @@ -14,6 +14,7 @@ use crate::{ storage::DynStorageBackend, }; +/// Report of orphaned, untracked, and moved files. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct OrphanReport { /// Media items whose files no longer exist on disk. @@ -24,6 +25,7 @@ pub struct OrphanReport { pub moved_files: Vec<(MediaId, PathBuf, PathBuf)>, } +/// Action to take when resolving orphans. #[derive(Debug, Clone, Copy, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum OrphanAction { @@ -31,6 +33,7 @@ pub enum OrphanAction { Ignore, } +/// Report of file integrity verification results. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct VerificationReport { pub verified: usize, @@ -39,6 +42,7 @@ pub struct VerificationReport { pub errors: Vec<(MediaId, String)>, } +/// Status of a media item's file integrity. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum IntegrityStatus { @@ -72,9 +76,15 @@ impl std::str::FromStr for IntegrityStatus { } } -/// Detect orphaned media items (files that no longer exist on disk), -/// untracked files (files on disk not in database), and moved files (same hash, -/// different path). +/// Detect orphaned, untracked, and moved files. +/// +/// # Arguments +/// +/// * `storage` - Storage backend to query +/// +/// # Returns +/// +/// Report containing orphaned items, untracked files, and moved files pub async fn detect_orphans( storage: &DynStorageBackend, ) -> Result { diff --git a/crates/pinakes-core/src/jobs.rs b/crates/pinakes-core/src/jobs.rs index 0a73c9a..98fb633 100644 --- a/crates/pinakes-core/src/jobs.rs +++ b/crates/pinakes-core/src/jobs.rs @@ -35,6 +35,7 @@ pub enum JobKind { media_ids: Vec, }, CleanupAnalytics, + TrashPurge, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -167,7 +168,7 @@ impl JobQueue { cancel, }; - // If the channel is full we still record the job — it'll stay Pending + // If the channel is full we still record the job; it will stay Pending let _ = self.tx.send(item).await; id } diff --git a/crates/pinakes-core/src/managed_storage.rs b/crates/pinakes-core/src/managed_storage.rs index 4075ac0..631732e 100644 --- a/crates/pinakes-core/src/managed_storage.rs +++ b/crates/pinakes-core/src/managed_storage.rs @@ -164,7 +164,7 @@ impl ManagedStorageService { self.verify(hash).await?; } - fs::File::open(&path).await.map_err(|e| PinakesError::Io(e)) + fs::File::open(&path).await.map_err(PinakesError::Io) } /// Read a blob entirely into memory. @@ -271,11 +271,11 @@ impl ManagedStorageService { let mut file_entries = fs::read_dir(&sub_path).await?; while let Some(file_entry) = file_entries.next_entry().await? { let file_path = file_entry.path(); - if file_path.is_file() { - if let Some(name) = file_path.file_name() { - hashes - .push(ContentHash::new(name.to_string_lossy().to_string())); - } + if file_path.is_file() + && let Some(name) = file_path.file_name() + { + hashes + .push(ContentHash::new(name.to_string_lossy().to_string())); } } } @@ -311,15 +311,15 @@ impl ManagedStorageService { let path = entry.path(); if path.is_file() { // Check if temp file is old (> 1 hour) - if let Ok(meta) = fs::metadata(&path).await { - if let Ok(modified) = meta.modified() { - let age = std::time::SystemTime::now() - .duration_since(modified) - .unwrap_or_default(); - if age.as_secs() > 3600 { - let _ = fs::remove_file(&path).await; - count += 1; - } + if let Ok(meta) = fs::metadata(&path).await + && let Ok(modified) = meta.modified() + { + let age = std::time::SystemTime::now() + .duration_since(modified) + .unwrap_or_default(); + if age.as_secs() > 3600 { + let _ = fs::remove_file(&path).await; + count += 1; } } } diff --git a/crates/pinakes-core/src/model.rs b/crates/pinakes-core/src/model.rs index 68570d6..66a6212 100644 --- a/crates/pinakes-core/src/model.rs +++ b/crates/pinakes-core/src/model.rs @@ -6,10 +6,12 @@ use uuid::Uuid; use crate::media_type::MediaType; +/// Unique identifier for a media item. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct MediaId(pub Uuid); impl MediaId { + /// Creates a new media ID using UUIDv7. pub fn new() -> Self { Self(Uuid::now_v7()) } @@ -27,10 +29,12 @@ impl Default for MediaId { } } +/// BLAKE3 content hash for deduplication. #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct ContentHash(pub String); impl ContentHash { + /// Creates a new content hash from a hex string. pub fn new(hex: String) -> Self { Self(hex) } @@ -42,8 +46,6 @@ impl fmt::Display for ContentHash { } } -// ===== Managed Storage Types ===== - /// Storage mode for media items #[derive( Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, @@ -162,12 +164,14 @@ pub struct MediaItem { pub links_extracted_at: Option>, } +/// A custom field attached to a media item. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CustomField { pub field_type: CustomFieldType, pub value: String, } +/// Type of custom field value. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum CustomFieldType { @@ -177,6 +181,7 @@ pub enum CustomFieldType { Boolean, } +/// A tag that can be applied to media items. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Tag { pub id: Uuid, @@ -185,6 +190,7 @@ pub struct Tag { pub created_at: DateTime, } +/// A collection of media items. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Collection { pub id: Uuid, @@ -196,6 +202,7 @@ pub struct Collection { pub updated_at: DateTime, } +/// Kind of collection. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum CollectionKind { @@ -203,6 +210,7 @@ pub enum CollectionKind { Virtual, } +/// A member of a collection with position tracking. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CollectionMember { pub collection_id: Uuid, @@ -211,6 +219,7 @@ pub struct CollectionMember { pub added_at: DateTime, } +/// An audit trail entry. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AuditEntry { pub id: Uuid, @@ -329,6 +338,7 @@ impl fmt::Display for AuditAction { } } +/// Pagination parameters for list queries. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Pagination { pub offset: u64, @@ -337,6 +347,7 @@ pub struct Pagination { } impl Pagination { + /// Creates a new pagination instance. pub fn new(offset: u64, limit: u64, sort: Option) -> Self { Self { offset, @@ -356,6 +367,7 @@ impl Default for Pagination { } } +/// A saved search query. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SavedSearch { pub id: Uuid, @@ -367,6 +379,7 @@ pub struct SavedSearch { // Book Management Types +/// Metadata for book-type media. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct BookMetadata { pub media_id: MediaId, @@ -385,6 +398,7 @@ pub struct BookMetadata { pub updated_at: DateTime, } +/// Information about a book author. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct AuthorInfo { pub name: String, @@ -394,6 +408,7 @@ pub struct AuthorInfo { } impl AuthorInfo { + /// Creates a new author with the given name. pub fn new(name: String) -> Self { Self { name, @@ -403,6 +418,7 @@ impl AuthorInfo { } } + /// Sets the author's role. pub fn with_role(mut self, role: String) -> Self { self.role = role; self @@ -435,6 +451,7 @@ pub struct ExtractedBookMetadata { pub identifiers: HashMap>, } +/// Reading progress for a book. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ReadingProgress { pub media_id: MediaId, @@ -446,6 +463,7 @@ pub struct ReadingProgress { } impl ReadingProgress { + /// Creates a new reading progress entry. pub fn new( media_id: MediaId, user_id: Uuid, @@ -473,6 +491,7 @@ impl ReadingProgress { } } +/// Reading status for a book. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum ReadingStatus { @@ -493,8 +512,6 @@ impl fmt::Display for ReadingStatus { } } -// ===== Markdown Links (Obsidian-style) ===== - /// Type of markdown link #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] @@ -530,7 +547,7 @@ impl std::str::FromStr for LinkType { } } -/// A markdown link extracted from a file +/// A markdown link extracted from a file. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct MarkdownLink { pub id: Uuid, @@ -549,7 +566,7 @@ pub struct MarkdownLink { pub created_at: DateTime, } -/// Information about a backlink (incoming link) +/// Information about a backlink (incoming link). #[derive(Debug, Clone, Serialize, Deserialize)] pub struct BacklinkInfo { pub link_id: Uuid, @@ -562,14 +579,14 @@ pub struct BacklinkInfo { pub link_type: LinkType, } -/// Graph data for visualization +/// Graph data for visualization. #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct GraphData { pub nodes: Vec, pub edges: Vec, } -/// A node in the graph visualization +/// A node in the graph visualization. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct GraphNode { pub id: String, @@ -582,7 +599,7 @@ pub struct GraphNode { pub backlink_count: u32, } -/// An edge (link) in the graph visualization +/// An edge (link) in the graph visualization. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct GraphEdge { pub source: String, diff --git a/crates/pinakes-core/src/plugin/runtime.rs b/crates/pinakes-core/src/plugin/runtime.rs index 0b7ac12..2564cf7 100644 --- a/crates/pinakes-core/src/plugin/runtime.rs +++ b/crates/pinakes-core/src/plugin/runtime.rs @@ -15,14 +15,9 @@ impl WasmRuntime { /// Create a new WASM runtime pub fn new() -> Result { let mut config = Config::new(); - - // Enable WASM features config.wasm_component_model(true); - config.async_support(true); - - // Set resource limits config.max_wasm_stack(1024 * 1024); // 1MB stack - config.consume_fuel(true); // Enable fuel metering for CPU limits + config.consume_fuel(true); // enable fuel metering for CPU limits let engine = Engine::new(&config)?; @@ -39,10 +34,7 @@ impl WasmRuntime { return Err(anyhow!("WASM file not found: {:?}", wasm_path)); } - // Read WASM bytes let wasm_bytes = std::fs::read(wasm_path)?; - - // Compile module let module = Module::new(&self.engine, &wasm_bytes)?; Ok(WasmPlugin { @@ -82,7 +74,6 @@ impl WasmPlugin { ) -> Result> { let engine = self.module.engine(); - // Create store with per-invocation data let store_data = PluginStoreData { context: self.context.clone(), exchange_buffer: Vec::new(), @@ -97,17 +88,14 @@ impl WasmPlugin { store.set_fuel(1_000_000_000)?; } - // Set up linker with host functions let mut linker = Linker::new(engine); HostFunctions::setup_linker(&mut linker)?; - // Instantiate the module let instance = linker.instantiate_async(&mut store, &self.module).await?; - // Get the memory export (if available) let memory = instance.get_memory(&mut store, "memory"); - // If there are params and memory is available, write them + // If there are params and memory is available, write them to the module let mut alloc_offset: i32 = 0; if !params.is_empty() && let Some(mem) = &memory @@ -136,7 +124,6 @@ impl WasmPlugin { } } - // Look up the exported function and call it let func = instance .get_func(&mut store, function_name) @@ -150,9 +137,9 @@ impl WasmPlugin { let mut results = vec![Val::I32(0); result_count]; - // Call with appropriate params based on function signature + // Call with appropriate params based on function signature; convention: + // (ptr, len) if param_count == 2 && !params.is_empty() { - // Convention: (ptr, len) func .call_async( &mut store, @@ -171,13 +158,13 @@ impl WasmPlugin { .await?; } - // Read result from exchange buffer (host functions may have written data) + // Prefer data written into the exchange buffer by host functions let exchange = std::mem::take(&mut store.data_mut().exchange_buffer); if !exchange.is_empty() { return Ok(exchange); } - // Otherwise serialize the return values + // Fall back to serialising the WASM return value if let Some(Val::I32(ret)) = results.first() { Ok(ret.to_le_bytes().to_vec()) } else { @@ -208,9 +195,10 @@ impl Default for WasmPlugin { pub struct HostFunctions; impl HostFunctions { - /// Set up host functions in a linker + /// Registers all host ABI functions (`host_log`, `host_read_file`, + /// `host_write_file`, `host_http_request`, `host_get_config`, + /// `host_get_buffer`) into the given linker. pub fn setup_linker(linker: &mut Linker) -> Result<()> { - // host_log: log a message from the plugin linker.func_wrap( "env", "host_log", @@ -240,7 +228,6 @@ impl HostFunctions { }, )?; - // host_read_file: read a file into the exchange buffer linker.func_wrap( "env", "host_read_file", @@ -300,7 +287,6 @@ impl HostFunctions { }, )?; - // host_write_file: write data to a file linker.func_wrap( "env", "host_write_file", @@ -373,7 +359,6 @@ impl HostFunctions { }, )?; - // host_http_request: make an HTTP request (blocking) linker.func_wrap( "env", "host_http_request", @@ -461,7 +446,6 @@ impl HostFunctions { }, )?; - // host_get_config: read a config key into the exchange buffer linker.func_wrap( "env", "host_get_config", @@ -500,7 +484,6 @@ impl HostFunctions { }, )?; - // host_get_buffer: copy the exchange buffer to WASM memory linker.func_wrap( "env", "host_get_buffer", diff --git a/crates/pinakes-core/src/scan.rs b/crates/pinakes-core/src/scan.rs index be62bfc..5d2dc00 100644 --- a/crates/pinakes-core/src/scan.rs +++ b/crates/pinakes-core/src/scan.rs @@ -13,6 +13,7 @@ use tracing::{info, warn}; use crate::{error::Result, import, storage::DynStorageBackend}; +/// Status of a directory scan operation. pub struct ScanStatus { pub scanning: bool, pub files_found: usize, @@ -100,6 +101,17 @@ impl Default for ScanProgress { } } +/// Scans a directory with default options. +/// +/// # Arguments +/// +/// * `storage` - Storage backend +/// * `dir` - Directory to scan +/// * `ignore_patterns` - Patterns to exclude +/// +/// # Returns +/// +/// Scan status with counts and any errors pub async fn scan_directory( storage: &DynStorageBackend, dir: &Path, @@ -115,7 +127,19 @@ pub async fn scan_directory( .await } -/// Scan a directory with incremental scanning support +/// Scans a directory with incremental scanning support. +/// +/// Skips files that haven't changed since last scan based on mtime. +/// +/// # Arguments +/// +/// * `storage` - Storage backend +/// * `dir` - Directory to scan +/// * `ignore_patterns` - Patterns to exclude +/// +/// # Returns +/// +/// Scan status with counts and any errors pub async fn scan_directory_incremental( storage: &DynStorageBackend, dir: &Path, @@ -129,6 +153,18 @@ pub async fn scan_directory_incremental( .await } +/// Scans a directory with progress reporting. +/// +/// # Arguments +/// +/// * `storage` - Storage backend +/// * `dir` - Directory to scan +/// * `ignore_patterns` - Patterns to exclude +/// * `progress` - Optional progress tracker +/// +/// # Returns +/// +/// Scan status with counts and any errors pub async fn scan_directory_with_progress( storage: &DynStorageBackend, dir: &Path, @@ -230,6 +266,16 @@ pub async fn scan_directory_with_options( Ok(status) } +/// Scans all configured root directories with default options. +/// +/// # Arguments +/// +/// * `storage` - Storage backend +/// * `ignore_patterns` - Patterns to exclude +/// +/// # Returns +/// +/// Status for each root directory pub async fn scan_all_roots( storage: &DynStorageBackend, ignore_patterns: &[String], @@ -243,7 +289,16 @@ pub async fn scan_all_roots( .await } -/// Scan all roots incrementally (skip unchanged files) +/// Scans all roots incrementally, skipping unchanged files. +/// +/// # Arguments +/// +/// * `storage` - Storage backend +/// * `ignore_patterns` - Patterns to exclude +/// +/// # Returns +/// +/// Status for each root directory pub async fn scan_all_roots_incremental( storage: &DynStorageBackend, ignore_patterns: &[String], @@ -255,6 +310,17 @@ pub async fn scan_all_roots_incremental( scan_all_roots_with_options(storage, ignore_patterns, None, &options).await } +/// Scans all root directories with progress reporting. +/// +/// # Arguments +/// +/// * `storage` - Storage backend +/// * `ignore_patterns` - Patterns to exclude +/// * `progress` - Optional progress tracker +/// +/// # Returns +/// +/// Status for each root directory pub async fn scan_all_roots_with_progress( storage: &DynStorageBackend, ignore_patterns: &[String], @@ -269,7 +335,18 @@ pub async fn scan_all_roots_with_progress( .await } -/// Scan all roots with full options including progress and incremental mode +/// Scans all roots with full options including progress and incremental mode. +/// +/// # Arguments +/// +/// * `storage` - Storage backend +/// * `ignore_patterns` - Patterns to exclude +/// * `progress` - Optional progress tracker +/// * `scan_options` - Scan configuration +/// +/// # Returns +/// +/// Status for each root directory pub async fn scan_all_roots_with_options( storage: &DynStorageBackend, ignore_patterns: &[String], @@ -306,12 +383,14 @@ pub async fn scan_all_roots_with_options( Ok(statuses) } +/// Watches directories for file changes and imports modified files. pub struct FileWatcher { _watcher: Box, rx: mpsc::Receiver, } impl FileWatcher { + /// Creates a new file watcher for the given directories. pub fn new(dirs: &[PathBuf]) -> Result { let (tx, rx) = mpsc::channel(1024); @@ -393,11 +472,13 @@ impl FileWatcher { Ok(Box::new(watcher)) } + /// Receives the next changed file path. pub async fn next_change(&mut self) -> Option { self.rx.recv().await } } +/// Watches directories and imports files on change. pub async fn watch_and_import( storage: DynStorageBackend, dirs: Vec, diff --git a/crates/pinakes-core/src/scheduler.rs b/crates/pinakes-core/src/scheduler.rs index 55bd627..afbc4ee 100644 --- a/crates/pinakes-core/src/scheduler.rs +++ b/crates/pinakes-core/src/scheduler.rs @@ -200,6 +200,22 @@ impl TaskScheduler { running: false, last_job_id: None, }, + ScheduledTask { + id: "trash_purge".to_string(), + name: "Trash Purge".to_string(), + kind: JobKind::TrashPurge, + schedule: Schedule::Weekly { + day: 0, + hour: 3, + minute: 0, + }, + enabled: false, + last_run: None, + next_run: None, + last_status: None, + running: false, + last_job_id: None, + }, ]; Self { @@ -404,6 +420,7 @@ mod tests { use chrono::TimeZone; use super::*; + use crate::config::TrashConfig; #[test] fn test_interval_next_run() { @@ -453,7 +470,7 @@ mod tests { #[test] fn test_weekly_same_day_future() { // 2025-06-15 is Sunday (day 6). Schedule is Sunday 14:00, current is 10:00 - // => today. + let from = Utc.with_ymd_and_hms(2025, 6, 15, 10, 0, 0).unwrap(); let schedule = Schedule::Weekly { day: 6, @@ -467,7 +484,7 @@ mod tests { #[test] fn test_weekly_same_day_past() { // 2025-06-15 is Sunday (day 6). Schedule is Sunday 08:00, current is 10:00 - // => next week. + let from = Utc.with_ymd_and_hms(2025, 6, 15, 10, 0, 0).unwrap(); let schedule = Schedule::Weekly { day: 6, @@ -545,4 +562,152 @@ mod tests { "Sun 14:30" ); } + + #[test] + fn test_trash_purge_job_kind_serde() { + let job = JobKind::TrashPurge; + let json = serde_json::to_string(&job).unwrap(); + assert_eq!(json, r#"{"type":"trash_purge"}"#); + + let deserialized: JobKind = serde_json::from_str(&json).unwrap(); + assert!(matches!(deserialized, JobKind::TrashPurge)); + } + + #[test] + fn test_trash_purge_scheduled_task_defaults() { + let task = ScheduledTask { + id: "trash_purge".to_string(), + name: "Trash Purge".to_string(), + kind: JobKind::TrashPurge, + schedule: Schedule::Weekly { + day: 0, + hour: 3, + minute: 0, + }, + enabled: false, + last_run: None, + next_run: None, + last_status: None, + running: false, + last_job_id: None, + }; + + assert_eq!(task.id, "trash_purge"); + assert_eq!(task.name, "Trash Purge"); + assert!(matches!(task.kind, JobKind::TrashPurge)); + assert!(!task.enabled); + assert!(!task.running); + } + + #[tokio::test] + async fn test_default_tasks_contain_trash_purge() { + let cancel = CancellationToken::new(); + let config = Arc::new(RwLock::new(Config::default())); + let job_queue = JobQueue::new(1, |_, _, _, _| tokio::spawn(async move {})); + + let scheduler = TaskScheduler::new(job_queue, cancel, config, None); + let tasks = scheduler.list_tasks().await; + + let trash_task = tasks.iter().find(|t| t.id == "trash_purge"); + assert!( + trash_task.is_some(), + "trash_purge task should be in default tasks" + ); + + let task = trash_task.unwrap(); + assert_eq!(task.id, "trash_purge"); + assert_eq!(task.name, "Trash Purge"); + assert!(matches!(task.kind, JobKind::TrashPurge)); + assert!(!task.enabled, "trash_purge should be disabled by default"); + } + + #[test] + fn test_trash_purge_serde_roundtrip() { + let task = ScheduledTask { + id: "trash_purge".to_string(), + name: "Trash Purge".to_string(), + kind: JobKind::TrashPurge, + schedule: Schedule::Weekly { + day: 0, + hour: 3, + minute: 0, + }, + enabled: true, + last_run: Some(Utc.with_ymd_and_hms(2025, 1, 15, 10, 0, 0).unwrap()), + next_run: Some(Utc.with_ymd_and_hms(2025, 1, 19, 3, 0, 0).unwrap()), + last_status: Some("completed".to_string()), + running: false, + last_job_id: Some(Uuid::now_v7()), + }; + + let json = serde_json::to_string(&task).unwrap(); + let deserialized: ScheduledTask = serde_json::from_str(&json).unwrap(); + + assert_eq!(deserialized.id, "trash_purge"); + assert_eq!(deserialized.enabled, true); + assert!(!deserialized.running); + assert!(deserialized.last_job_id.is_none()); + } + + #[test] + fn test_all_job_kinds_serde() { + let kinds: Vec = vec![ + JobKind::Scan { path: None }, + JobKind::Scan { + path: Some(PathBuf::from("/test")), + }, + JobKind::GenerateThumbnails { media_ids: vec![] }, + JobKind::VerifyIntegrity { media_ids: vec![] }, + JobKind::OrphanDetection, + JobKind::CleanupThumbnails, + JobKind::TrashPurge, + ]; + + for kind in kinds { + let json = serde_json::to_string(&kind).unwrap(); + let deserialized: JobKind = serde_json::from_str(&json).unwrap(); + assert!( + matches!(deserialized, JobKind::Scan { path: None }) + || matches!(deserialized, JobKind::Scan { path: Some(_) }) + || matches!(deserialized, JobKind::GenerateThumbnails { .. }) + || matches!(deserialized, JobKind::VerifyIntegrity { .. }) + || matches!(deserialized, JobKind::OrphanDetection) + || matches!(deserialized, JobKind::CleanupThumbnails) + || matches!(deserialized, JobKind::TrashPurge) + ); + } + } + + #[test] + fn test_task_serde_skips_runtime_fields() { + let task = ScheduledTask { + id: "test".to_string(), + name: "Test".to_string(), + kind: JobKind::TrashPurge, + schedule: Schedule::Daily { + hour: 0, + minute: 0, + }, + enabled: true, + last_run: Some(Utc::now()), + next_run: Some(Utc::now()), + last_status: Some("running".to_string()), + running: true, + last_job_id: Some(Uuid::now_v7()), + }; + + let json = serde_json::to_string(&task).unwrap(); + let deserialized: ScheduledTask = serde_json::from_str(&json).unwrap(); + + assert_eq!(deserialized.running, false); + assert!(deserialized.last_job_id.is_none()); + } + + #[test] + fn test_trash_config_defaults() { + let config = TrashConfig::default(); + assert!(!config.enabled); + assert_eq!(config.retention_days, 30); + assert!(!config.auto_empty); + } } diff --git a/crates/pinakes-core/src/search.rs b/crates/pinakes-core/src/search.rs index adc4cf9..b146d97 100644 --- a/crates/pinakes-core/src/search.rs +++ b/crates/pinakes-core/src/search.rs @@ -6,6 +6,7 @@ use winnow::{ token::{take_till, take_while}, }; +/// Represents a parsed search query. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum SearchQuery { FullText(String), @@ -39,6 +40,7 @@ pub enum SearchQuery { }, } +/// Comparison operators for range queries. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum CompareOp { GreaterThan, @@ -47,6 +49,7 @@ pub enum CompareOp { LessOrEqual, } +/// Date values for date-based queries. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum DateValue { Today, @@ -61,6 +64,7 @@ pub enum DateValue { DaysAgo(u32), } +/// Request for executing a search. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SearchRequest { pub query: SearchQuery, @@ -68,12 +72,14 @@ pub struct SearchRequest { pub pagination: crate::model::Pagination, } +/// Results of a search operation. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SearchResults { pub items: Vec, pub total_count: u64, } +/// Sorting options for search results. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] #[derive(Default)] @@ -139,19 +145,25 @@ fn parse_date_value(s: &str) -> Option { } /// Parse size strings like "10MB", "1GB", "500KB" to bytes +/// +/// Returns `None` if the input is invalid or if the value would overflow. fn parse_size_value(s: &str) -> Option { let s = s.to_uppercase(); - if let Some(num) = s.strip_suffix("GB") { - num.parse::().ok().map(|n| n * 1024 * 1024 * 1024) - } else if let Some(num) = s.strip_suffix("MB") { - num.parse::().ok().map(|n| n * 1024 * 1024) - } else if let Some(num) = s.strip_suffix("KB") { - num.parse::().ok().map(|n| n * 1024) - } else if let Some(num) = s.strip_suffix('B') { - num.parse::().ok() + let (num_str, multiplier): (&str, i64) = if let Some(n) = s.strip_suffix("GB") + { + (n, 1024 * 1024 * 1024) + } else if let Some(n) = s.strip_suffix("MB") { + (n, 1024 * 1024) + } else if let Some(n) = s.strip_suffix("KB") { + (n, 1024) + } else if let Some(n) = s.strip_suffix('B') { + (n, 1) } else { - s.parse::().ok() - } + (s.as_str(), 1) + }; + + let num: i64 = num_str.parse().ok()?; + num.checked_mul(multiplier) } fn field_match(input: &mut &str) -> ModalResult { @@ -332,6 +344,22 @@ fn or_expr(input: &mut &str) -> ModalResult { } } +/// Parses a search query string into a structured query. +/// +/// Supports full-text search, field matches, operators (AND/OR/NOT), +/// prefixes, fuzzy matching, and type/tag filters. +/// +/// # Arguments +/// +/// * `input` - Raw query string +/// +/// # Returns +/// +/// Parsed query tree +/// +/// # Errors +/// +/// Returns `SearchParse` error for invalid syntax pub fn parse_search_query(input: &str) -> crate::error::Result { let trimmed = input.trim(); if trimmed.is_empty() { diff --git a/crates/pinakes-core/src/sharing.rs b/crates/pinakes-core/src/sharing.rs index 9d75593..92c8cc9 100644 --- a/crates/pinakes-core/src/sharing.rs +++ b/crates/pinakes-core/src/sharing.rs @@ -12,13 +12,14 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use uuid::Uuid; -use crate::{model::MediaId, users::UserId}; +use crate::{error::PinakesError, model::MediaId, users::UserId}; /// Unique identifier for a share. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct ShareId(pub Uuid); impl ShareId { + /// Creates a new share ID. pub fn new() -> Self { Self(Uuid::now_v7()) } @@ -47,6 +48,7 @@ pub enum ShareTarget { } impl ShareTarget { + /// Returns the type of target being shared. pub fn target_type(&self) -> &'static str { match self { Self::Media { .. } => "media", @@ -56,6 +58,7 @@ impl ShareTarget { } } + /// Returns the ID of the target being shared. pub fn target_id(&self) -> Uuid { match self { Self::Media { media_id } => media_id.0, @@ -87,6 +90,7 @@ pub enum ShareRecipient { } impl ShareRecipient { + /// Returns the type of recipient. pub fn recipient_type(&self) -> &'static str { match self { Self::PublicLink { .. } => "public_link", @@ -117,7 +121,7 @@ pub struct SharePermissions { } impl SharePermissions { - /// View-only permissions + /// Creates a new share with view-only permissions. pub fn view_only() -> Self { Self { can_view: true, @@ -125,7 +129,7 @@ impl SharePermissions { } } - /// Download permissions (includes view) + /// Creates a new share with download permissions. pub fn download() -> Self { Self { can_view: true, @@ -134,7 +138,7 @@ impl SharePermissions { } } - /// Edit permissions (includes view and download) + /// Creates a new share with edit permissions. pub fn edit() -> Self { Self { can_view: true, @@ -145,7 +149,7 @@ impl SharePermissions { } } - /// Full permissions + /// Creates a new share with full permissions. pub fn full() -> Self { Self { can_view: true, @@ -157,7 +161,7 @@ impl SharePermissions { } } - /// Merge permissions (takes the most permissive of each) + /// Merges two permission sets, taking the most permissive values. pub fn merge(&self, other: &Self) -> Self { Self { can_view: self.can_view || other.can_view, @@ -246,17 +250,17 @@ impl Share { } } - /// Check if the share has expired. + /// Checks if the share has expired. pub fn is_expired(&self) -> bool { self.expires_at.map(|exp| exp < Utc::now()).unwrap_or(false) } - /// Check if this is a public link share. + /// Checks if this is a public link share. pub fn is_public(&self) -> bool { matches!(self.recipient, ShareRecipient::PublicLink { .. }) } - /// Get the public token if this is a public link share. + /// Returns the public token if this is a public link share. pub fn public_token(&self) -> Option<&str> { match &self.recipient { ShareRecipient::PublicLink { token, .. } => Some(token), @@ -322,6 +326,7 @@ pub struct ShareActivity { } impl ShareActivity { + /// Creates a new share activity entry. pub fn new(share_id: ShareId, action: ShareActivityAction) -> Self { Self { id: Uuid::now_v7(), @@ -334,16 +339,19 @@ impl ShareActivity { } } + /// Sets the actor who performed the activity. pub fn with_actor(mut self, actor_id: UserId) -> Self { self.actor_id = Some(actor_id); self } + /// Sets the IP address of the actor. pub fn with_ip(mut self, ip: &str) -> Self { self.actor_ip = Some(ip.to_string()); self } + /// Sets additional details about the activity. pub fn with_details(mut self, details: &str) -> Self { self.details = Some(details.to_string()); self @@ -400,6 +408,7 @@ pub struct ShareNotification { } impl ShareNotification { + /// Creates a new share notification. pub fn new( user_id: UserId, share_id: ShareId, @@ -416,20 +425,18 @@ impl ShareNotification { } } -/// Generate a random share token using UUID. +/// Generates a random share token. pub fn generate_share_token() -> String { // Use UUIDv4 for random tokens - simple string representation Uuid::new_v4().simple().to_string() } -/// Hash a share password. -pub fn hash_share_password(password: &str) -> String { - // Use BLAKE3 for password hashing (in production, use Argon2) - blake3::hash(password.as_bytes()).to_hex().to_string() +/// Hashes a share password using Argon2id. +pub fn hash_share_password(password: &str) -> Result { + crate::users::auth::hash_password(password) } -/// Verify a share password. +/// Verifies a share password against an Argon2id hash. pub fn verify_share_password(password: &str, hash: &str) -> bool { - let computed = hash_share_password(password); - computed == hash + crate::users::auth::verify_password(password, hash).unwrap_or(false) } diff --git a/crates/pinakes-core/src/storage/mod.rs b/crates/pinakes-core/src/storage/mod.rs index 04b3c29..ef8b9b6 100644 --- a/crates/pinakes-core/src/storage/mod.rs +++ b/crates/pinakes-core/src/storage/mod.rs @@ -142,29 +142,13 @@ pub trait StorageBackend: Send + Sync + 'static { ) -> Result<()>; // Batch operations (transactional where supported) - async fn batch_delete_media(&self, ids: &[MediaId]) -> Result { - let mut count = 0u64; - for id in ids { - self.delete_media(*id).await?; - count += 1; - } - Ok(count) - } + async fn batch_delete_media(&self, ids: &[MediaId]) -> Result; async fn batch_tag_media( &self, media_ids: &[MediaId], tag_ids: &[Uuid], - ) -> Result { - let mut count = 0u64; - for media_id in media_ids { - for tag_id in tag_ids { - self.tag_media(*media_id, *tag_id).await?; - count += 1; - } - } - Ok(count) - } + ) -> Result; // Integrity async fn list_media_paths( @@ -342,7 +326,6 @@ pub trait StorageBackend: Send + Sync + 'static { } } - // ===== Ratings ===== async fn rate_media( &self, user_id: UserId, @@ -358,7 +341,6 @@ pub trait StorageBackend: Send + Sync + 'static { ) -> Result>; async fn delete_rating(&self, id: Uuid) -> Result<()>; - // ===== Comments ===== async fn add_comment( &self, user_id: UserId, @@ -370,7 +352,6 @@ pub trait StorageBackend: Send + Sync + 'static { -> Result>; async fn delete_comment(&self, id: Uuid) -> Result<()>; - // ===== Favorites ===== async fn add_favorite( &self, user_id: UserId, @@ -392,7 +373,6 @@ pub trait StorageBackend: Send + Sync + 'static { media_id: MediaId, ) -> Result; - // ===== Share Links ===== async fn create_share_link( &self, media_id: MediaId, @@ -405,7 +385,6 @@ pub trait StorageBackend: Send + Sync + 'static { async fn increment_share_views(&self, token: &str) -> Result<()>; async fn delete_share_link(&self, id: Uuid) -> Result<()>; - // ===== Playlists ===== async fn create_playlist( &self, owner_id: UserId, @@ -450,7 +429,6 @@ pub trait StorageBackend: Send + Sync + 'static { new_position: i32, ) -> Result<()>; - // ===== Analytics ===== async fn record_usage_event(&self, event: &UsageEvent) -> Result<()>; async fn get_usage_events( &self, @@ -477,7 +455,6 @@ pub trait StorageBackend: Send + Sync + 'static { ) -> Result>; async fn cleanup_old_events(&self, before: DateTime) -> Result; - // ===== Subtitles ===== async fn add_subtitle(&self, subtitle: &Subtitle) -> Result<()>; async fn get_media_subtitles( &self, @@ -490,7 +467,6 @@ pub trait StorageBackend: Send + Sync + 'static { offset_ms: i64, ) -> Result<()>; - // ===== External Metadata (Enrichment) ===== async fn store_external_metadata( &self, meta: &ExternalMetadata, @@ -501,7 +477,6 @@ pub trait StorageBackend: Send + Sync + 'static { ) -> Result>; async fn delete_external_metadata(&self, id: Uuid) -> Result<()>; - // ===== Transcode Sessions ===== async fn create_transcode_session( &self, session: &TranscodeSession, @@ -522,7 +497,6 @@ pub trait StorageBackend: Send + Sync + 'static { before: DateTime, ) -> Result; - // ===== Session Management ===== /// Create a new session in the database async fn create_session(&self, session: &SessionData) -> Result<()>; @@ -623,8 +597,6 @@ pub trait StorageBackend: Send + Sync + 'static { pagination: &Pagination, ) -> Result>; - // ===== Managed Storage ===== - /// Insert a media item that uses managed storage async fn insert_managed_media(&self, item: &MediaItem) -> Result<()>; @@ -658,8 +630,6 @@ pub trait StorageBackend: Send + Sync + 'static { /// Get managed storage statistics async fn managed_storage_stats(&self) -> Result; - // ===== Sync Devices ===== - /// Register a new sync device async fn register_device( &self, @@ -695,8 +665,6 @@ pub trait StorageBackend: Send + Sync + 'static { /// Update the last_seen_at timestamp for a device async fn touch_device(&self, id: crate::sync::DeviceId) -> Result<()>; - // ===== Sync Log ===== - /// Record a change in the sync log async fn record_sync_change( &self, @@ -716,8 +684,6 @@ pub trait StorageBackend: Send + Sync + 'static { /// Clean up old sync log entries async fn cleanup_old_sync_log(&self, before: DateTime) -> Result; - // ===== Device Sync State ===== - /// Get sync state for a device and path async fn get_device_sync_state( &self, @@ -737,8 +703,6 @@ pub trait StorageBackend: Send + Sync + 'static { device_id: crate::sync::DeviceId, ) -> Result>; - // ===== Upload Sessions (Chunked Uploads) ===== - /// Create a new upload session async fn create_upload_session( &self, @@ -773,8 +737,6 @@ pub trait StorageBackend: Send + Sync + 'static { /// Clean up expired upload sessions async fn cleanup_expired_uploads(&self) -> Result; - // ===== Sync Conflicts ===== - /// Record a sync conflict async fn record_conflict( &self, @@ -794,8 +756,6 @@ pub trait StorageBackend: Send + Sync + 'static { resolution: crate::config::ConflictResolution, ) -> Result<()>; - // ===== Enhanced Sharing ===== - /// Create a new share async fn create_share( &self, @@ -872,8 +832,6 @@ pub trait StorageBackend: Send + Sync + 'static { /// Clean up expired shares async fn cleanup_expired_shares(&self) -> Result; - // ===== Share Activity ===== - /// Record share activity async fn record_share_activity( &self, @@ -887,8 +845,6 @@ pub trait StorageBackend: Send + Sync + 'static { pagination: &Pagination, ) -> Result>; - // ===== Share Notifications ===== - /// Create a share notification async fn create_share_notification( &self, @@ -907,8 +863,6 @@ pub trait StorageBackend: Send + Sync + 'static { /// Mark all notifications as read for a user async fn mark_all_notifications_read(&self, user_id: UserId) -> Result<()>; - // ===== File Management ===== - /// Rename a media item (changes file_name and updates path accordingly). /// For external storage, this actually renames the file on disk. /// For managed storage, this only updates the metadata. @@ -939,8 +893,6 @@ pub trait StorageBackend: Send + Sync + 'static { Ok(results) } - // ===== Trash / Soft Delete ===== - /// Soft delete a media item (set deleted_at timestamp). async fn soft_delete_media(&self, id: MediaId) -> Result<()>; @@ -960,8 +912,6 @@ pub trait StorageBackend: Send + Sync + 'static { /// Count items in trash. async fn count_trash(&self) -> Result; - // ===== Markdown Links (Obsidian-style) ===== - /// Save extracted markdown links for a media item. /// This replaces any existing links for the source media. async fn save_markdown_links( diff --git a/crates/pinakes-core/src/storage/postgres.rs b/crates/pinakes-core/src/storage/postgres.rs index c4b697e..9f57fb0 100644 --- a/crates/pinakes-core/src/storage/postgres.rs +++ b/crates/pinakes-core/src/storage/postgres.rs @@ -583,8 +583,7 @@ impl StorageBackend for PostgresBackend { crate::storage::migrations::run_postgres_migrations(client).await } - // ---- Root directories ---- - + // Root directories async fn add_root_dir(&self, path: PathBuf) -> Result<()> { let client = self .pool @@ -638,8 +637,7 @@ impl StorageBackend for PostgresBackend { Ok(()) } - // ---- Media CRUD ---- - + // Media CRUD async fn insert_media(&self, item: &MediaItem) -> Result<()> { let client = self .pool @@ -1032,8 +1030,7 @@ impl StorageBackend for PostgresBackend { Ok(count as u64) } - // ---- Batch Operations ---- - + // Batch Operations async fn batch_delete_media(&self, ids: &[MediaId]) -> Result { if ids.is_empty() { return Ok(0); @@ -1089,8 +1086,7 @@ impl StorageBackend for PostgresBackend { Ok(rows) } - // ---- Tags ---- - + // Tags async fn create_tag( &self, name: &str, @@ -1257,8 +1253,7 @@ impl StorageBackend for PostgresBackend { rows.iter().map(row_to_tag).collect() } - // ---- Collections ---- - + // Collections async fn create_collection( &self, name: &str, @@ -1499,8 +1494,7 @@ impl StorageBackend for PostgresBackend { Ok(items) } - // ---- Search ---- - + // Search async fn search(&self, request: &SearchRequest) -> Result { let client = self .pool @@ -1666,8 +1660,7 @@ impl StorageBackend for PostgresBackend { }) } - // ---- Audit ---- - + // Audit async fn record_audit(&self, entry: &AuditEntry) -> Result<()> { let client = self .pool @@ -1739,8 +1732,7 @@ impl StorageBackend for PostgresBackend { rows.iter().map(row_to_audit_entry).collect() } - // ---- Custom fields ---- - + // Custom fields async fn set_custom_field( &self, media_id: MediaId, @@ -1821,8 +1813,7 @@ impl StorageBackend for PostgresBackend { Ok(()) } - // ---- Duplicates ---- - + // Duplicates async fn find_duplicates(&self) -> Result>> { let client = self .pool @@ -2007,8 +1998,7 @@ impl StorageBackend for PostgresBackend { Ok(groups) } - // ---- Database management ---- - + // Database management async fn database_stats(&self) -> Result { let client = self .pool @@ -2524,7 +2514,6 @@ impl StorageBackend for PostgresBackend { Ok(()) } - // ===== Ratings ===== async fn rate_media( &self, user_id: crate::users::UserId, @@ -2635,7 +2624,6 @@ impl StorageBackend for PostgresBackend { Ok(()) } - // ===== Comments ===== async fn add_comment( &self, user_id: crate::users::UserId, @@ -2712,7 +2700,6 @@ impl StorageBackend for PostgresBackend { Ok(()) } - // ===== Favorites ===== async fn add_favorite( &self, user_id: crate::users::UserId, @@ -2838,7 +2825,6 @@ impl StorageBackend for PostgresBackend { Ok(count > 0) } - // ===== Share Links ===== async fn create_share_link( &self, media_id: MediaId, @@ -2942,7 +2928,6 @@ impl StorageBackend for PostgresBackend { Ok(()) } - // ===== Playlists ===== async fn create_playlist( &self, owner_id: crate::users::UserId, @@ -3250,7 +3235,6 @@ impl StorageBackend for PostgresBackend { Ok(()) } - // ===== Analytics ===== async fn record_usage_event( &self, event: &crate::analytics::UsageEvent, @@ -3540,7 +3524,6 @@ impl StorageBackend for PostgresBackend { Ok(affected) } - // ===== Subtitles ===== async fn add_subtitle( &self, subtitle: &crate::subtitles::Subtitle, @@ -3652,7 +3635,6 @@ impl StorageBackend for PostgresBackend { Ok(()) } - // ===== External Metadata (Enrichment) ===== async fn store_external_metadata( &self, meta: &crate::enrichment::ExternalMetadata, @@ -3742,7 +3724,6 @@ impl StorageBackend for PostgresBackend { Ok(()) } - // ===== Transcode Sessions ===== async fn create_transcode_session( &self, session: &crate::transcode::TranscodeSession, @@ -3930,8 +3911,6 @@ impl StorageBackend for PostgresBackend { Ok(affected) } - // ===== Session Management ===== - async fn create_session( &self, session: &crate::storage::SessionData, @@ -4666,10 +4645,6 @@ impl StorageBackend for PostgresBackend { items } - // ========================================================================= - // Managed Storage - // ========================================================================= - async fn insert_managed_media(&self, item: &MediaItem) -> Result<()> { let client = self.pool.get().await.map_err(|e| { PinakesError::Database(format!("failed to get connection: {e}")) @@ -4967,10 +4942,6 @@ impl StorageBackend for PostgresBackend { }) } - // ========================================================================= - // Sync Devices - // ========================================================================= - async fn register_device( &self, device: &crate::sync::SyncDevice, @@ -5188,10 +5159,6 @@ impl StorageBackend for PostgresBackend { Ok(()) } - // ========================================================================= - // Sync Log - // ========================================================================= - async fn record_sync_change( &self, change: &crate::sync::SyncLogEntry, @@ -5310,10 +5277,6 @@ impl StorageBackend for PostgresBackend { Ok(result) } - // ========================================================================= - // Device Sync State - // ========================================================================= - async fn get_device_sync_state( &self, device_id: crate::sync::DeviceId, @@ -5437,10 +5400,6 @@ impl StorageBackend for PostgresBackend { ) } - // ========================================================================= - // Upload Sessions - // ========================================================================= - async fn create_upload_session( &self, session: &crate::sync::UploadSession, @@ -5618,10 +5577,6 @@ impl StorageBackend for PostgresBackend { Ok(result) } - // ========================================================================= - // Sync Conflicts - // ========================================================================= - async fn record_conflict( &self, conflict: &crate::sync::SyncConflict, @@ -5737,10 +5692,6 @@ impl StorageBackend for PostgresBackend { Ok(()) } - // ========================================================================= - // Shares - // ========================================================================= - async fn create_share( &self, share: &crate::sharing::Share, @@ -6050,10 +6001,10 @@ impl StorageBackend for PostgresBackend { for share in shares { // Skip expired shares - if let Some(exp) = share.expires_at { - if exp < now { - continue; - } + if let Some(exp) = share.expires_at + && exp < now + { + continue; } match (&share.recipient, user_id) { @@ -6167,10 +6118,6 @@ impl StorageBackend for PostgresBackend { Ok(result) } - // ========================================================================= - // Share Activity - // ========================================================================= - async fn record_share_activity( &self, activity: &crate::sharing::ShareActivity, @@ -6244,10 +6191,6 @@ impl StorageBackend for PostgresBackend { ) } - // ========================================================================= - // Share Notifications - // ========================================================================= - async fn create_share_notification( &self, notification: &crate::sharing::ShareNotification, @@ -6349,8 +6292,6 @@ impl StorageBackend for PostgresBackend { Ok(()) } - // ===== File Management ===== - async fn rename_media(&self, id: MediaId, new_name: &str) -> Result { // Validate the new name if new_name.is_empty() || new_name.contains('/') || new_name.contains('\\') @@ -6468,8 +6409,6 @@ impl StorageBackend for PostgresBackend { Ok(old_path) } - // ===== Trash / Soft Delete ===== - async fn soft_delete_media(&self, id: MediaId) -> Result<()> { let client = self .pool @@ -6671,8 +6610,6 @@ impl StorageBackend for PostgresBackend { Ok(count as u64) } - // ===== Markdown Links (Obsidian-style) ===== - async fn save_markdown_links( &self, media_id: MediaId, diff --git a/crates/pinakes-core/src/storage/sqlite.rs b/crates/pinakes-core/src/storage/sqlite.rs index 09e2a61..6b47a87 100644 --- a/crates/pinakes-core/src/storage/sqlite.rs +++ b/crates/pinakes-core/src/storage/sqlite.rs @@ -59,10 +59,6 @@ impl SqliteBackend { } } -// --------------------------------------------------------------------------- -// Row-parsing helpers -// --------------------------------------------------------------------------- - fn parse_datetime(s: &str) -> DateTime { // Try RFC 3339 first (includes timezone), then fall back to a naive format. if let Ok(dt) = DateTime::parse_from_rfc3339(s) { @@ -92,7 +88,7 @@ fn parse_media_type(s: &str) -> MediaType { } fn media_type_to_str(mt: &MediaType) -> String { - // Produces e.g. `"mp3"` -- strip the surrounding quotes. + // Produces e.g. `"mp3"`, strip the surrounding quotes. let s = serde_json::to_string(mt).unwrap_or_else(|_| "\"plaintext\"".to_string()); s.trim_matches('"').to_string() @@ -387,10 +383,6 @@ fn load_custom_fields_batch( Ok(()) } -// --------------------------------------------------------------------------- -// Search query translation -// --------------------------------------------------------------------------- - /// Translate a `SearchQuery` into components that can be assembled into SQL. /// /// Returns `(fts_expr, like_terms, where_clauses, join_clauses, params)` where: @@ -617,8 +609,8 @@ fn sanitize_fts_token(s: &str) -> String { fn sort_order_to_sql(sort: &SortOrder) -> &'static str { match sort { - SortOrder::Relevance => "m.created_at DESC", /* FTS rank not easily - * portable; use date */ + SortOrder::Relevance => "m.created_at DESC", // FTS rank not easily + // portable; use date SortOrder::DateAsc => "m.created_at ASC", SortOrder::DateDesc => "m.created_at DESC", SortOrder::NameAsc => "m.file_name ASC", @@ -628,14 +620,9 @@ fn sort_order_to_sql(sort: &SortOrder) -> &'static str { } } -// --------------------------------------------------------------------------- -// StorageBackend implementation -// --------------------------------------------------------------------------- - #[async_trait::async_trait] impl StorageBackend for SqliteBackend { - // -- Migrations -------------------------------------------------------- - + // Migrations async fn run_migrations(&self) -> Result<()> { let conn = Arc::clone(&self.conn); tokio::task::spawn_blocking(move || { @@ -645,12 +632,11 @@ impl StorageBackend for SqliteBackend { crate::storage::migrations::run_sqlite_migrations(&mut db) }) .await - .map_err(|e| PinakesError::Database(e.to_string()))? + .map_err(|e| PinakesError::Database(format!("run_migrations: {}", e)))? } - // -- Root directories -------------------------------------------------- - async fn add_root_dir(&self, path: PathBuf) -> Result<()> { + let path_display = path.display().to_string(); let conn = Arc::clone(&self.conn); tokio::task::spawn_blocking(move || { let db = conn @@ -663,7 +649,9 @@ impl StorageBackend for SqliteBackend { Ok(()) }) .await - .map_err(|e| PinakesError::Database(e.to_string()))? + .map_err(|e| { + PinakesError::Database(format!("add_root_dir {}: {}", path_display, e)) + })? } async fn list_root_dirs(&self) -> Result> { @@ -682,11 +670,12 @@ impl StorageBackend for SqliteBackend { Ok(rows) }) .await - .map_err(|e| PinakesError::Database(e.to_string()))? + .map_err(|e| PinakesError::Database(format!("list_root_dirs: {}", e)))? } async fn remove_root_dir(&self, path: &Path) -> Result<()> { let path = path.to_path_buf(); + let path_display = path.display().to_string(); let conn = Arc::clone(&self.conn); tokio::task::spawn_blocking(move || { let db = conn @@ -698,11 +687,11 @@ impl StorageBackend for SqliteBackend { Ok(()) }) .await - .map_err(|e| PinakesError::Database(e.to_string()))? + .map_err(|e| { + PinakesError::Database(format!("remove_root_dir {}: {}", path_display, e)) + })? } - // -- Media CRUD -------------------------------------------------------- - async fn insert_media(&self, item: &MediaItem) -> Result<()> { let item = item.clone(); let conn = Arc::clone(&self.conn); @@ -997,8 +986,6 @@ impl StorageBackend for SqliteBackend { .map_err(|e| PinakesError::Database(e.to_string()))? } - // -- Tags -------------------------------------------------------------- - async fn create_tag( &self, name: &str, @@ -1166,8 +1153,6 @@ impl StorageBackend for SqliteBackend { .map_err(|e| PinakesError::Database(e.to_string()))? } - // -- Collections ------------------------------------------------------- - async fn create_collection( &self, name: &str, @@ -1354,8 +1339,6 @@ impl StorageBackend for SqliteBackend { .map_err(|e| PinakesError::Database(e.to_string()))? } - // -- Search ------------------------------------------------------------ - async fn search(&self, request: &SearchRequest) -> Result { let request = request.clone(); let conn = Arc::clone(&self.conn); @@ -1458,8 +1441,6 @@ impl StorageBackend for SqliteBackend { .map_err(|e| PinakesError::Database(e.to_string()))? } - // -- Audit ------------------------------------------------------------- - async fn record_audit(&self, entry: &AuditEntry) -> Result<()> { let entry = entry.clone(); let conn = Arc::clone(&self.conn); @@ -1535,8 +1516,6 @@ impl StorageBackend for SqliteBackend { .map_err(|e| PinakesError::Database(e.to_string()))? } - // -- Custom fields ----------------------------------------------------- - async fn set_custom_field( &self, media_id: MediaId, @@ -1691,8 +1670,6 @@ impl StorageBackend for SqliteBackend { .map_err(|e| PinakesError::Database(e.to_string()))? } - // -- Duplicates ----------------------------------------------------------- - async fn find_duplicates(&self) -> Result>> { let conn = Arc::clone(&self.conn); tokio::task::spawn_blocking(move || { @@ -1806,11 +1783,14 @@ impl StorageBackend for SqliteBackend { Ok(groups) }) .await - .map_err(|e| PinakesError::Database(e.to_string()))? + .map_err(|e| { + PinakesError::Database(format!( + "find_perceptual_duplicates (threshold={}): {}", + threshold, e + )) + })? } - // -- Database management ----------------------------------------------- - async fn database_stats(&self) -> Result { let conn = Arc::clone(&self.conn); tokio::task::spawn_blocking(move || { @@ -1840,7 +1820,7 @@ impl StorageBackend for SqliteBackend { }) }) .await - .map_err(|e| PinakesError::Database(e.to_string()))? + .map_err(|e| PinakesError::Database(format!("database_stats: {}", e)))? } async fn vacuum(&self) -> Result<()> { @@ -1853,7 +1833,7 @@ impl StorageBackend for SqliteBackend { Ok(()) }) .await - .map_err(|e| PinakesError::Database(e.to_string()))? + .map_err(|e| PinakesError::Database(format!("vacuum: {}", e)))? } async fn clear_all_data(&self) -> Result<()> { @@ -1874,7 +1854,7 @@ impl StorageBackend for SqliteBackend { Ok(()) }) .await - .map_err(|e| PinakesError::Database(e.to_string()))? + .map_err(|e| PinakesError::Database(format!("clear_all_data: {}", e)))? } async fn list_media_paths( @@ -1905,7 +1885,7 @@ impl StorageBackend for SqliteBackend { Ok(results) }) .await - .map_err(|e| PinakesError::Database(e.to_string()))? + .map_err(|e| PinakesError::Database(format!("list_media_paths: {}", e)))? } async fn save_search( @@ -1933,7 +1913,7 @@ impl StorageBackend for SqliteBackend { Ok(()) }) .await - .map_err(|e| PinakesError::Database(e.to_string()))? + .map_err(|e| PinakesError::Database(format!("save_search {}: {}", id, e)))? } async fn list_saved_searches( @@ -1970,7 +1950,9 @@ impl StorageBackend for SqliteBackend { Ok(results) }) .await - .map_err(|e| PinakesError::Database(e.to_string()))? + .map_err(|e| { + PinakesError::Database(format!("list_saved_searches: {}", e)) + })? } async fn delete_saved_search(&self, id: Uuid) -> Result<()> { @@ -1984,7 +1966,9 @@ impl StorageBackend for SqliteBackend { Ok(()) }) .await - .map_err(|e| PinakesError::Database(e.to_string()))? + .map_err(|e| { + PinakesError::Database(format!("delete_saved_search {}: {}", id, e)) + })? } async fn list_media_ids_for_thumbnails( &self, @@ -2012,7 +1996,12 @@ impl StorageBackend for SqliteBackend { Ok(ids) }) .await - .map_err(|e| PinakesError::Database(e.to_string()))? + .map_err(|e| { + PinakesError::Database(format!( + "list_media_ids_for_thumbnails (only_missing={}): {}", + only_missing, e + )) + })? } async fn library_statistics(&self) -> Result { @@ -2121,7 +2110,9 @@ impl StorageBackend for SqliteBackend { .map_err(|_| { PinakesError::Database("library_statistics query timed out".to_string()) })? - .map_err(|e| PinakesError::Database(e.to_string()))? + .map_err(|e| { + PinakesError::Database(format!("library_statistics: {}", e)) + })? } async fn list_users(&self) -> Result> { @@ -2169,7 +2160,7 @@ impl StorageBackend for SqliteBackend { PinakesError::Database("list_users query timed out".to_string()) })? .map_err(|e: tokio::task::JoinError| { - PinakesError::Database(e.to_string()) + PinakesError::Database(format!("list_users: {}", e)) })? } @@ -2227,7 +2218,7 @@ impl StorageBackend for SqliteBackend { )) })? .map_err(|e: tokio::task::JoinError| { - PinakesError::Database(e.to_string()) + PinakesError::Database(format!("get_user {}: {}", id_str_for_err, e)) })? } @@ -2287,7 +2278,10 @@ impl StorageBackend for SqliteBackend { )) })? .map_err(|e: tokio::task::JoinError| { - PinakesError::Database(e.to_string()) + PinakesError::Database(format!( + "get_user_by_username {}: {}", + username_for_err, e + )) })? } @@ -2380,7 +2374,7 @@ impl StorageBackend for SqliteBackend { PinakesError::Database("create_user query timed out".to_string()) })? .map_err(|e: tokio::task::JoinError| { - PinakesError::Database(e.to_string()) + PinakesError::Database(format!("create_user: {}", e)) })? } @@ -2496,7 +2490,7 @@ impl StorageBackend for SqliteBackend { PinakesError::Database("update_user query timed out".to_string()) })? .map_err(|e: tokio::task::JoinError| { - PinakesError::Database(e.to_string()) + PinakesError::Database(format!("update_user: {}", e)) })? } @@ -2532,7 +2526,7 @@ impl StorageBackend for SqliteBackend { PinakesError::Database("delete_user query timed out".to_string()) })? .map_err(|e: tokio::task::JoinError| { - PinakesError::Database(e.to_string()) + PinakesError::Database(format!("delete_user: {}", e)) })? } @@ -2622,7 +2616,7 @@ impl StorageBackend for SqliteBackend { ) })? .map_err(|e: tokio::task::JoinError| { - PinakesError::Database(e.to_string()) + PinakesError::Database(format!("grant_library_access: {}", e)) })? } @@ -2655,11 +2649,10 @@ impl StorageBackend for SqliteBackend { ) })? .map_err(|e: tokio::task::JoinError| { - PinakesError::Database(e.to_string()) + PinakesError::Database(format!("revoke_library_access: {}", e)) })? } - // ===== Ratings ===== async fn rate_media( &self, user_id: crate::users::UserId, @@ -2717,7 +2710,7 @@ impl StorageBackend for SqliteBackend { .await .map_err(|_| PinakesError::Database("rate_media timed out".into()))? .map_err(|e: tokio::task::JoinError| { - PinakesError::Database(e.to_string()) + PinakesError::Database(format!("rate_media: {}", e)) })? } @@ -2763,7 +2756,7 @@ impl StorageBackend for SqliteBackend { PinakesError::Database("get_media_ratings timed out".into()) })? .map_err(|e: tokio::task::JoinError| { - PinakesError::Database(e.to_string()) + PinakesError::Database(format!("get_media_ratings: {}", e)) })? } @@ -2809,7 +2802,7 @@ impl StorageBackend for SqliteBackend { .await .map_err(|_| PinakesError::Database("get_user_rating timed out".into()))? .map_err(|e: tokio::task::JoinError| { - PinakesError::Database(e.to_string()) + PinakesError::Database(format!("get_user_rating: {}", e)) })? } @@ -2830,11 +2823,10 @@ impl StorageBackend for SqliteBackend { .await .map_err(|_| PinakesError::Database("delete_rating timed out".into()))? .map_err(|e: tokio::task::JoinError| { - PinakesError::Database(e.to_string()) + PinakesError::Database(format!("delete_rating: {}", e)) })? } - // ===== Comments ===== async fn add_comment( &self, user_id: crate::users::UserId, @@ -2882,7 +2874,7 @@ impl StorageBackend for SqliteBackend { .await .map_err(|_| PinakesError::Database("add_comment timed out".into()))? .map_err(|e: tokio::task::JoinError| { - PinakesError::Database(e.to_string()) + PinakesError::Database(format!("add_comment: {}", e)) })? } @@ -2930,7 +2922,7 @@ impl StorageBackend for SqliteBackend { PinakesError::Database("get_media_comments timed out".into()) })? .map_err(|e: tokio::task::JoinError| { - PinakesError::Database(e.to_string()) + PinakesError::Database(format!("get_media_comments: {}", e)) })? } @@ -2951,11 +2943,10 @@ impl StorageBackend for SqliteBackend { .await .map_err(|_| PinakesError::Database("delete_comment timed out".into()))? .map_err(|e: tokio::task::JoinError| { - PinakesError::Database(e.to_string()) + PinakesError::Database(format!("delete_comment: {}", e)) })? } - // ===== Favorites ===== async fn add_favorite( &self, user_id: crate::users::UserId, @@ -2983,7 +2974,7 @@ impl StorageBackend for SqliteBackend { .await .map_err(|_| PinakesError::Database("add_favorite timed out".into()))? .map_err(|e: tokio::task::JoinError| { - PinakesError::Database(e.to_string()) + PinakesError::Database(format!("add_favorite: {}", e)) })? } @@ -3012,7 +3003,7 @@ impl StorageBackend for SqliteBackend { .await .map_err(|_| PinakesError::Database("remove_favorite timed out".into()))? .map_err(|e: tokio::task::JoinError| { - PinakesError::Database(e.to_string()) + PinakesError::Database(format!("remove_favorite: {}", e)) })? } @@ -3053,7 +3044,7 @@ impl StorageBackend for SqliteBackend { PinakesError::Database("get_user_favorites timed out".into()) })? .map_err(|e: tokio::task::JoinError| { - PinakesError::Database(e.to_string()) + PinakesError::Database(format!("get_user_favorites: {}", e)) })? } @@ -3083,11 +3074,10 @@ impl StorageBackend for SqliteBackend { .await .map_err(|_| PinakesError::Database("is_favorite timed out".into()))? .map_err(|e: tokio::task::JoinError| { - PinakesError::Database(e.to_string()) + PinakesError::Database(format!("is_favorite: {}", e)) })? } - // ===== Share Links ===== async fn create_share_link( &self, media_id: MediaId, @@ -3143,7 +3133,7 @@ impl StorageBackend for SqliteBackend { PinakesError::Database("create_share_link timed out".into()) })? .map_err(|e: tokio::task::JoinError| { - PinakesError::Database(e.to_string()) + PinakesError::Database(format!("create_share_link: {}", e)) })? } @@ -3195,7 +3185,7 @@ impl StorageBackend for SqliteBackend { .await .map_err(|_| PinakesError::Database("get_share_link timed out".into()))? .map_err(|e: tokio::task::JoinError| { - PinakesError::Database(e.to_string()) + PinakesError::Database(format!("get_share_link: {}", e)) })? } @@ -3221,7 +3211,7 @@ impl StorageBackend for SqliteBackend { PinakesError::Database("increment_share_views timed out".into()) })? .map_err(|e: tokio::task::JoinError| { - PinakesError::Database(e.to_string()) + PinakesError::Database(format!("increment_share_views: {}", e)) })? } @@ -3244,11 +3234,10 @@ impl StorageBackend for SqliteBackend { PinakesError::Database("delete_share_link timed out".into()) })? .map_err(|e: tokio::task::JoinError| { - PinakesError::Database(e.to_string()) + PinakesError::Database(format!("delete_share_link: {}", e)) })? } - // ===== Playlists ===== async fn create_playlist( &self, owner_id: crate::users::UserId, @@ -3305,7 +3294,7 @@ impl StorageBackend for SqliteBackend { .await .map_err(|_| PinakesError::Database("create_playlist timed out".into()))? .map_err(|e: tokio::task::JoinError| { - PinakesError::Database(e.to_string()) + PinakesError::Database(format!("create_playlist: {}", e)) })? } @@ -3354,7 +3343,7 @@ impl StorageBackend for SqliteBackend { .await .map_err(|_| PinakesError::Database("get_playlist timed out".into()))? .map_err(|e: tokio::task::JoinError| { - PinakesError::Database(e.to_string()) + PinakesError::Database(format!("get_playlist: {}", e)) })? } @@ -3440,7 +3429,7 @@ impl StorageBackend for SqliteBackend { .await .map_err(|_| PinakesError::Database("list_playlists timed out".into()))? .map_err(|e: tokio::task::JoinError| { - PinakesError::Database(e.to_string()) + PinakesError::Database(format!("list_playlists: {}", e)) })? } @@ -3520,7 +3509,7 @@ impl StorageBackend for SqliteBackend { .await .map_err(|_| PinakesError::Database("update_playlist timed out".into()))? .map_err(|e: tokio::task::JoinError| { - PinakesError::Database(e.to_string()) + PinakesError::Database(format!("update_playlist: {}", e)) })? } @@ -3541,7 +3530,7 @@ impl StorageBackend for SqliteBackend { .await .map_err(|_| PinakesError::Database("delete_playlist timed out".into()))? .map_err(|e: tokio::task::JoinError| { - PinakesError::Database(e.to_string()) + PinakesError::Database(format!("delete_playlist: {}", e)) })? } @@ -3573,7 +3562,7 @@ impl StorageBackend for SqliteBackend { .await .map_err(|_| PinakesError::Database("add_to_playlist timed out".into()))? .map_err(|e: tokio::task::JoinError| { - PinakesError::Database(e.to_string()) + PinakesError::Database(format!("add_to_playlist: {}", e)) })? } @@ -3604,7 +3593,7 @@ impl StorageBackend for SqliteBackend { PinakesError::Database("remove_from_playlist timed out".into()) })? .map_err(|e: tokio::task::JoinError| { - PinakesError::Database(e.to_string()) + PinakesError::Database(format!("remove_from_playlist: {}", e)) })? } @@ -3641,7 +3630,7 @@ impl StorageBackend for SqliteBackend { PinakesError::Database("get_playlist_items timed out".into()) })? .map_err(|e: tokio::task::JoinError| { - PinakesError::Database(e.to_string()) + PinakesError::Database(format!("get_playlist_items: {}", e)) })? } @@ -3672,11 +3661,10 @@ impl StorageBackend for SqliteBackend { .await .map_err(|_| PinakesError::Database("reorder_playlist timed out".into()))? .map_err(|e: tokio::task::JoinError| { - PinakesError::Database(e.to_string()) + PinakesError::Database(format!("reorder_playlist: {}", e)) })? } - // ===== Analytics ===== async fn record_usage_event( &self, event: &crate::analytics::UsageEvent, @@ -3717,7 +3705,7 @@ impl StorageBackend for SqliteBackend { PinakesError::Database("record_usage_event timed out".into()) })? .map_err(|e: tokio::task::JoinError| { - PinakesError::Database(e.to_string()) + PinakesError::Database(format!("record_usage_event: {}", e)) })? } @@ -3790,7 +3778,7 @@ impl StorageBackend for SqliteBackend { .await .map_err(|_| PinakesError::Database("get_usage_events timed out".into()))? .map_err(|e: tokio::task::JoinError| { - PinakesError::Database(e.to_string()) + PinakesError::Database(format!("get_usage_events: {}", e)) })? } @@ -3832,7 +3820,7 @@ impl StorageBackend for SqliteBackend { .await .map_err(|_| PinakesError::Database("get_most_viewed timed out".into()))? .map_err(|e: tokio::task::JoinError| { - PinakesError::Database(e.to_string()) + PinakesError::Database(format!("get_most_viewed: {}", e)) })? } @@ -3871,7 +3859,7 @@ impl StorageBackend for SqliteBackend { PinakesError::Database("get_recently_viewed timed out".into()) })? .map_err(|e: tokio::task::JoinError| { - PinakesError::Database(e.to_string()) + PinakesError::Database(format!("get_recently_viewed: {}", e)) })? } @@ -3914,7 +3902,7 @@ impl StorageBackend for SqliteBackend { PinakesError::Database("update_watch_progress timed out".into()) })? .map_err(|e: tokio::task::JoinError| { - PinakesError::Database(e.to_string()) + PinakesError::Database(format!("update_watch_progress: {}", e)) })? } @@ -3949,7 +3937,7 @@ impl StorageBackend for SqliteBackend { PinakesError::Database("get_watch_progress timed out".into()) })? .map_err(|e: tokio::task::JoinError| { - PinakesError::Database(e.to_string()) + PinakesError::Database(format!("get_watch_progress: {}", e)) })? } @@ -3978,11 +3966,10 @@ impl StorageBackend for SqliteBackend { PinakesError::Database("cleanup_old_events timed out".into()) })? .map_err(|e: tokio::task::JoinError| { - PinakesError::Database(e.to_string()) + PinakesError::Database(format!("cleanup_old_events: {}", e)) })? } - // ===== Subtitles ===== async fn add_subtitle( &self, subtitle: &crate::subtitles::Subtitle, @@ -4029,7 +4016,7 @@ impl StorageBackend for SqliteBackend { .await .map_err(|_| PinakesError::Database("add_subtitle timed out".into()))? .map_err(|e: tokio::task::JoinError| { - PinakesError::Database(e.to_string()) + PinakesError::Database(format!("add_subtitle: {}", e)) })? } @@ -4082,7 +4069,7 @@ impl StorageBackend for SqliteBackend { PinakesError::Database("get_media_subtitles timed out".into()) })? .map_err(|e: tokio::task::JoinError| { - PinakesError::Database(e.to_string()) + PinakesError::Database(format!("get_media_subtitles: {}", e)) })? } @@ -4103,7 +4090,7 @@ impl StorageBackend for SqliteBackend { .await .map_err(|_| PinakesError::Database("delete_subtitle timed out".into()))? .map_err(|e: tokio::task::JoinError| { - PinakesError::Database(e.to_string()) + PinakesError::Database(format!("delete_subtitle: {}", e)) })? } @@ -4132,11 +4119,10 @@ impl StorageBackend for SqliteBackend { PinakesError::Database("update_subtitle_offset timed out".into()) })? .map_err(|e: tokio::task::JoinError| { - PinakesError::Database(e.to_string()) + PinakesError::Database(format!("update_subtitle_offset: {}", e)) })? } - // ===== External Metadata (Enrichment) ===== async fn store_external_metadata( &self, meta: &crate::enrichment::ExternalMetadata, @@ -4178,7 +4164,7 @@ impl StorageBackend for SqliteBackend { PinakesError::Database("store_external_metadata timed out".into()) })? .map_err(|e: tokio::task::JoinError| { - PinakesError::Database(e.to_string()) + PinakesError::Database(format!("store_external_metadata: {}", e)) })? } @@ -4227,7 +4213,7 @@ impl StorageBackend for SqliteBackend { PinakesError::Database("get_external_metadata timed out".into()) })? .map_err(|e: tokio::task::JoinError| { - PinakesError::Database(e.to_string()) + PinakesError::Database(format!("get_external_metadata: {}", e)) })? } @@ -4250,11 +4236,10 @@ impl StorageBackend for SqliteBackend { PinakesError::Database("delete_external_metadata timed out".into()) })? .map_err(|e: tokio::task::JoinError| { - PinakesError::Database(e.to_string()) + PinakesError::Database(format!("delete_external_metadata: {}", e)) })? } - // ===== Transcode Sessions ===== async fn create_transcode_session( &self, session: &crate::transcode::TranscodeSession, @@ -4302,7 +4287,7 @@ impl StorageBackend for SqliteBackend { PinakesError::Database("create_transcode_session timed out".into()) })? .map_err(|e: tokio::task::JoinError| { - PinakesError::Database(e.to_string()) + PinakesError::Database(format!("create_transcode_session: {}", e)) })? } @@ -4367,7 +4352,7 @@ impl StorageBackend for SqliteBackend { PinakesError::Database("get_transcode_session timed out".into()) })? .map_err(|e: tokio::task::JoinError| { - PinakesError::Database(e.to_string()) + PinakesError::Database(format!("get_transcode_session: {}", e)) })? } @@ -4451,7 +4436,7 @@ impl StorageBackend for SqliteBackend { PinakesError::Database("list_transcode_sessions timed out".into()) })? .map_err(|e: tokio::task::JoinError| { - PinakesError::Database(e.to_string()) + PinakesError::Database(format!("list_transcode_sessions: {}", e)) })? } @@ -4485,7 +4470,7 @@ impl StorageBackend for SqliteBackend { PinakesError::Database("update_transcode_status timed out".into()) })? .map_err(|e: tokio::task::JoinError| { - PinakesError::Database(e.to_string()) + PinakesError::Database(format!("update_transcode_status: {}", e)) })? } @@ -4515,12 +4500,10 @@ impl StorageBackend for SqliteBackend { PinakesError::Database("cleanup_expired_transcodes timed out".into()) })? .map_err(|e: tokio::task::JoinError| { - PinakesError::Database(e.to_string()) + PinakesError::Database(format!("cleanup_expired_transcodes: {}", e)) })? } - // ===== Session Management ===== - async fn create_session( &self, session: &crate::storage::SessionData, @@ -4561,7 +4544,7 @@ impl StorageBackend for SqliteBackend { .await .map_err(|_| PinakesError::Database("create_session timed out".into()))? .map_err(|e: tokio::task::JoinError| { - PinakesError::Database(e.to_string()) + PinakesError::Database(format!("create_session: {}", e)) })? } @@ -4628,7 +4611,7 @@ impl StorageBackend for SqliteBackend { .await .map_err(|_| PinakesError::Database("get_session timed out".into()))? .map_err(|e: tokio::task::JoinError| { - PinakesError::Database(e.to_string()) + PinakesError::Database(format!("get_session: {}", e)) })? } @@ -4654,7 +4637,7 @@ impl StorageBackend for SqliteBackend { .await .map_err(|_| PinakesError::Database("touch_session timed out".into()))? .map_err(|e: tokio::task::JoinError| { - PinakesError::Database(e.to_string()) + PinakesError::Database(format!("touch_session: {}", e)) })? } @@ -4676,7 +4659,7 @@ impl StorageBackend for SqliteBackend { .await .map_err(|_| PinakesError::Database("delete_session timed out".into()))? .map_err(|e: tokio::task::JoinError| { - PinakesError::Database(e.to_string()) + PinakesError::Database(format!("delete_session: {}", e)) })? } @@ -4701,7 +4684,7 @@ impl StorageBackend for SqliteBackend { PinakesError::Database("delete_user_sessions timed out".into()) })? .map_err(|e: tokio::task::JoinError| { - PinakesError::Database(e.to_string()) + PinakesError::Database(format!("delete_user_sessions: {}", e)) })? } @@ -4726,7 +4709,7 @@ impl StorageBackend for SqliteBackend { PinakesError::Database("delete_expired_sessions timed out".into()) })? .map_err(|e: tokio::task::JoinError| { - PinakesError::Database(e.to_string()) + PinakesError::Database(format!("delete_expired_sessions: {}", e)) })? } @@ -4802,7 +4785,7 @@ impl StorageBackend for SqliteBackend { PinakesError::Database("list_active_sessions timed out".into()) })? .map_err(|e: tokio::task::JoinError| { - PinakesError::Database(e.to_string()) + PinakesError::Database(format!("list_active_sessions: {}", e)) })? } @@ -4901,7 +4884,7 @@ impl StorageBackend for SqliteBackend { PinakesError::Database("upsert_book_metadata timed out".into()) })? .map_err(|e: tokio::task::JoinError| { - PinakesError::Database(e.to_string()) + PinakesError::Database(format!("upsert_book_metadata: {}", e)) })??; Ok(()) } @@ -5022,7 +5005,7 @@ impl StorageBackend for SqliteBackend { PinakesError::Database("get_book_metadata timed out".into()) })? .map_err(|e: tokio::task::JoinError| { - PinakesError::Database(e.to_string()) + PinakesError::Database(format!("get_book_metadata: {}", e)) })??, ) } @@ -5059,7 +5042,7 @@ impl StorageBackend for SqliteBackend { .await .map_err(|_| PinakesError::Database("add_book_author timed out".into()))? .map_err(|e: tokio::task::JoinError| { - PinakesError::Database(e.to_string()) + PinakesError::Database(format!("add_book_author: {}", e)) })??; Ok(()) } @@ -5097,7 +5080,7 @@ impl StorageBackend for SqliteBackend { PinakesError::Database("get_book_authors timed out".into()) })? .map_err(|e: tokio::task::JoinError| { - PinakesError::Database(e.to_string()) + PinakesError::Database(format!("get_book_authors: {}", e)) })??, ) } @@ -5134,7 +5117,7 @@ impl StorageBackend for SqliteBackend { PinakesError::Database("list_all_authors timed out".into()) })? .map_err(|e: tokio::task::JoinError| { - PinakesError::Database(e.to_string()) + PinakesError::Database(format!("list_all_authors: {}", e)) })??, ) } @@ -5162,7 +5145,7 @@ impl StorageBackend for SqliteBackend { .await .map_err(|_| PinakesError::Database("list_series timed out".into()))? .map_err(|e: tokio::task::JoinError| { - PinakesError::Database(e.to_string()) + PinakesError::Database(format!("list_series: {}", e)) })??, ) } @@ -5201,7 +5184,7 @@ impl StorageBackend for SqliteBackend { PinakesError::Database("get_series_books timed out".into()) })? .map_err(|e: tokio::task::JoinError| { - PinakesError::Database(e.to_string()) + PinakesError::Database(format!("get_series_books: {}", e)) })??, ) } @@ -5236,7 +5219,7 @@ impl StorageBackend for SqliteBackend { PinakesError::Database("update_reading_progress timed out".into()) })? .map_err(|e: tokio::task::JoinError| { - PinakesError::Database(e.to_string()) + PinakesError::Database(format!("update_reading_progress: {}", e)) })??; Ok(()) } @@ -5299,7 +5282,7 @@ impl StorageBackend for SqliteBackend { PinakesError::Database("get_reading_progress timed out".into()) })? .map_err(|e: tokio::task::JoinError| { - PinakesError::Database(e.to_string()) + PinakesError::Database(format!("get_reading_progress: {}", e)) })??, ) } @@ -5381,7 +5364,7 @@ impl StorageBackend for SqliteBackend { PinakesError::Database("get_reading_list timed out".into()) })? .map_err(|e: tokio::task::JoinError| { - PinakesError::Database(e.to_string()) + PinakesError::Database(format!("get_reading_list: {}", e)) })??, ) } @@ -5470,13 +5453,11 @@ impl StorageBackend for SqliteBackend { .await .map_err(|_| PinakesError::Database("search_books timed out".into()))? .map_err(|e: tokio::task::JoinError| { - PinakesError::Database(e.to_string()) + PinakesError::Database(format!("search_books: {}", e)) })??, ) } - // ===== Managed Storage ===== - async fn insert_managed_media(&self, item: &MediaItem) -> Result<()> { let conn = self.conn.clone(); let item = item.clone(); @@ -5520,7 +5501,9 @@ impl StorageBackend for SqliteBackend { Ok::<_, rusqlite::Error>(()) }) .await - .map_err(|e| PinakesError::Database(e.to_string()))??; + .map_err(|e| { + PinakesError::Database(format!("insert_managed_media: {}", e)) + })??; Ok(()) } @@ -5582,7 +5565,7 @@ impl StorageBackend for SqliteBackend { }) }) .await - .map_err(|e| PinakesError::Database(e.to_string()))? + .map_err(|e| PinakesError::Database(format!("get_or_create_blob: {}", e)))? } async fn get_blob(&self, hash: &ContentHash) -> Result> { @@ -5613,8 +5596,8 @@ impl StorageBackend for SqliteBackend { .optional() }) .await - .map_err(|e| PinakesError::Database(e.to_string()))? - .map_err(|e| PinakesError::Database(e.to_string())) + .map_err(|e| PinakesError::Database(format!("get_blob: {}", e)))? + .map_err(|e| PinakesError::Database(format!("get_blob query: {}", e))) } async fn increment_blob_ref(&self, hash: &ContentHash) -> Result<()> { @@ -5631,7 +5614,9 @@ impl StorageBackend for SqliteBackend { Ok::<_, rusqlite::Error>(()) }) .await - .map_err(|e| PinakesError::Database(e.to_string()))??; + .map_err(|e| { + PinakesError::Database(format!("increment_blob_ref: {}", e)) + })??; Ok(()) } @@ -5659,8 +5644,10 @@ impl StorageBackend for SqliteBackend { Ok::<_, rusqlite::Error>(count <= 0) }) .await - .map_err(|e| PinakesError::Database(e.to_string()))? - .map_err(|e| PinakesError::Database(e.to_string())) + .map_err(|e| PinakesError::Database(format!("decrement_blob_ref: {}", e)))? + .map_err(|e| { + PinakesError::Database(format!("decrement_blob_ref query: {}", e)) + }) } async fn update_blob_verified(&self, hash: &ContentHash) -> Result<()> { @@ -5677,7 +5664,9 @@ impl StorageBackend for SqliteBackend { Ok::<_, rusqlite::Error>(()) }) .await - .map_err(|e| PinakesError::Database(e.to_string()))??; + .map_err(|e| { + PinakesError::Database(format!("update_blob_verified: {}", e)) + })??; Ok(()) } @@ -5708,8 +5697,10 @@ impl StorageBackend for SqliteBackend { Ok::<_, rusqlite::Error>(blobs) }) .await - .map_err(|e| PinakesError::Database(e.to_string()))? - .map_err(|e| PinakesError::Database(e.to_string())) + .map_err(|e| PinakesError::Database(format!("list_orphaned_blobs: {}", e)))? + .map_err(|e| { + PinakesError::Database(format!("list_orphaned_blobs query: {}", e)) + }) } async fn delete_blob(&self, hash: &ContentHash) -> Result<()> { @@ -5725,7 +5716,7 @@ impl StorageBackend for SqliteBackend { Ok::<_, rusqlite::Error>(()) }) .await - .map_err(|e| PinakesError::Database(e.to_string()))??; + .map_err(|e| PinakesError::Database(format!("delete_blob: {}", e)))??; Ok(()) } @@ -5781,12 +5772,14 @@ impl StorageBackend for SqliteBackend { }) }) .await - .map_err(|e| PinakesError::Database(e.to_string()))? - .map_err(|e| PinakesError::Database(e.to_string())) + .map_err(|e| { + PinakesError::Database(format!("managed_storage_stats: {}", e)) + })? + .map_err(|e| { + PinakesError::Database(format!("managed_storage_stats query: {}", e)) + }) } - // ===== Sync Devices ===== - async fn register_device( &self, device: &crate::sync::SyncDevice, @@ -5822,8 +5815,10 @@ impl StorageBackend for SqliteBackend { Ok::<_, rusqlite::Error>(device) }) .await - .map_err(|e| PinakesError::Database(e.to_string()))? - .map_err(|e| PinakesError::Database(e.to_string())) + .map_err(|e| PinakesError::Database(format!("register_device: {}", e)))? + .map_err(|e| { + PinakesError::Database(format!("register_device query: {}", e)) + }) } async fn get_device( @@ -5868,8 +5863,8 @@ impl StorageBackend for SqliteBackend { ) }) .await - .map_err(|e| PinakesError::Database(e.to_string()))? - .map_err(|e| PinakesError::Database(e.to_string())) + .map_err(|e| PinakesError::Database(format!("get_device: {}", e)))? + .map_err(|e| PinakesError::Database(format!("get_device query: {}", e))) } async fn get_device_by_token( @@ -5917,8 +5912,10 @@ impl StorageBackend for SqliteBackend { .optional() }) .await - .map_err(|e| PinakesError::Database(e.to_string()))? - .map_err(|e| PinakesError::Database(e.to_string())) + .map_err(|e| PinakesError::Database(format!("get_device_by_token: {}", e)))? + .map_err(|e| { + PinakesError::Database(format!("get_device_by_token query: {}", e)) + }) } async fn list_user_devices( @@ -5966,8 +5963,10 @@ impl StorageBackend for SqliteBackend { Ok::<_, rusqlite::Error>(devices) }) .await - .map_err(|e| PinakesError::Database(e.to_string()))? - .map_err(|e| PinakesError::Database(e.to_string())) + .map_err(|e| PinakesError::Database(format!("list_user_devices: {}", e)))? + .map_err(|e| { + PinakesError::Database(format!("list_user_devices query: {}", e)) + }) } async fn update_device( @@ -6001,7 +6000,7 @@ impl StorageBackend for SqliteBackend { Ok::<_, rusqlite::Error>(()) }) .await - .map_err(|e| PinakesError::Database(e.to_string()))??; + .map_err(|e| PinakesError::Database(format!("update_device: {}", e)))??; Ok(()) } @@ -6016,7 +6015,7 @@ impl StorageBackend for SqliteBackend { Ok::<_, rusqlite::Error>(()) }) .await - .map_err(|e| PinakesError::Database(e.to_string()))??; + .map_err(|e| PinakesError::Database(format!("delete_device: {}", e)))??; Ok(()) } @@ -6034,12 +6033,10 @@ impl StorageBackend for SqliteBackend { Ok::<_, rusqlite::Error>(()) }) .await - .map_err(|e| PinakesError::Database(e.to_string()))??; + .map_err(|e| PinakesError::Database(format!("touch_device: {}", e)))??; Ok(()) } - // ===== Sync Log ===== - async fn record_sync_change( &self, change: &crate::sync::SyncLogEntry, @@ -6079,7 +6076,9 @@ impl StorageBackend for SqliteBackend { Ok::<_, rusqlite::Error>(()) }) .await - .map_err(|e| PinakesError::Database(e.to_string()))??; + .map_err(|e| { + PinakesError::Database(format!("record_sync_change: {}", e)) + })??; Ok(()) } @@ -6125,8 +6124,10 @@ impl StorageBackend for SqliteBackend { Ok::<_, rusqlite::Error>(entries) }) .await - .map_err(|e| PinakesError::Database(e.to_string()))? - .map_err(|e| PinakesError::Database(e.to_string())) + .map_err(|e| PinakesError::Database(format!("get_changes_since: {}", e)))? + .map_err(|e| { + PinakesError::Database(format!("get_changes_since query: {}", e)) + }) } async fn get_current_sync_cursor(&self) -> Result { @@ -6141,8 +6142,12 @@ impl StorageBackend for SqliteBackend { ) }) .await - .map_err(|e| PinakesError::Database(e.to_string()))? - .map_err(|e| PinakesError::Database(e.to_string())) + .map_err(|e| { + PinakesError::Database(format!("get_current_sync_cursor: {}", e)) + })? + .map_err(|e| { + PinakesError::Database(format!("get_current_sync_cursor query: {}", e)) + }) } async fn cleanup_old_sync_log(&self, before: DateTime) -> Result { @@ -6156,13 +6161,15 @@ impl StorageBackend for SqliteBackend { ]) }) .await - .map_err(|e| PinakesError::Database(e.to_string()))? + .map_err(|e| { + PinakesError::Database(format!("cleanup_old_sync_log: {}", e)) + })? .map(|n| n as u64) - .map_err(|e| PinakesError::Database(e.to_string())) + .map_err(|e| { + PinakesError::Database(format!("cleanup_old_sync_log query: {}", e)) + }) } - // ===== Device Sync State ===== - async fn get_device_sync_state( &self, device_id: crate::sync::DeviceId, @@ -6204,8 +6211,12 @@ impl StorageBackend for SqliteBackend { .optional() }) .await - .map_err(|e| PinakesError::Database(e.to_string()))? - .map_err(|e| PinakesError::Database(e.to_string())) + .map_err(|e| { + PinakesError::Database(format!("get_device_sync_state: {}", e)) + })? + .map_err(|e| { + PinakesError::Database(format!("get_device_sync_state query: {}", e)) + }) } async fn upsert_device_sync_state( @@ -6246,7 +6257,9 @@ impl StorageBackend for SqliteBackend { Ok::<_, rusqlite::Error>(()) }) .await - .map_err(|e| PinakesError::Database(e.to_string()))??; + .map_err(|e| { + PinakesError::Database(format!("upsert_device_sync_state: {}", e)) + })??; Ok(()) } @@ -6291,12 +6304,12 @@ impl StorageBackend for SqliteBackend { Ok::<_, rusqlite::Error>(states) }) .await - .map_err(|e| PinakesError::Database(e.to_string()))? - .map_err(|e| PinakesError::Database(e.to_string())) + .map_err(|e| PinakesError::Database(format!("list_pending_sync: {}", e)))? + .map_err(|e| { + PinakesError::Database(format!("list_pending_sync query: {}", e)) + }) } - // ===== Upload Sessions ===== - async fn create_upload_session( &self, session: &crate::sync::UploadSession, @@ -6329,7 +6342,9 @@ impl StorageBackend for SqliteBackend { Ok::<_, rusqlite::Error>(()) }) .await - .map_err(|e| PinakesError::Database(e.to_string()))??; + .map_err(|e| { + PinakesError::Database(format!("create_upload_session: {}", e)) + })??; Ok(()) } @@ -6371,8 +6386,10 @@ impl StorageBackend for SqliteBackend { ) }) .await - .map_err(|e| PinakesError::Database(e.to_string()))? - .map_err(|e| PinakesError::Database(e.to_string())) + .map_err(|e| PinakesError::Database(format!("get_upload_session: {}", e)))? + .map_err(|e| { + PinakesError::Database(format!("get_upload_session query: {}", e)) + }) } async fn update_upload_session( @@ -6396,7 +6413,9 @@ impl StorageBackend for SqliteBackend { Ok::<_, rusqlite::Error>(()) }) .await - .map_err(|e| PinakesError::Database(e.to_string()))??; + .map_err(|e| { + PinakesError::Database(format!("update_upload_session: {}", e)) + })??; Ok(()) } @@ -6429,7 +6448,7 @@ impl StorageBackend for SqliteBackend { Ok::<_, rusqlite::Error>(()) }) .await - .map_err(|e| PinakesError::Database(e.to_string()))??; + .map_err(|e| PinakesError::Database(format!("record_chunk: {}", e)))??; Ok(()) } @@ -6460,8 +6479,10 @@ impl StorageBackend for SqliteBackend { Ok::<_, rusqlite::Error>(chunks) }) .await - .map_err(|e| PinakesError::Database(e.to_string()))? - .map_err(|e| PinakesError::Database(e.to_string())) + .map_err(|e| PinakesError::Database(format!("get_upload_chunks: {}", e)))? + .map_err(|e| { + PinakesError::Database(format!("get_upload_chunks query: {}", e)) + }) } async fn cleanup_expired_uploads(&self) -> Result { @@ -6476,13 +6497,15 @@ impl StorageBackend for SqliteBackend { ) }) .await - .map_err(|e| PinakesError::Database(e.to_string()))? + .map_err(|e| { + PinakesError::Database(format!("cleanup_expired_uploads: {}", e)) + })? .map(|n| n as u64) - .map_err(|e| PinakesError::Database(e.to_string())) + .map_err(|e| { + PinakesError::Database(format!("cleanup_expired_uploads query: {}", e)) + }) } - // ===== Sync Conflicts ===== - async fn record_conflict( &self, conflict: &crate::sync::SyncConflict, @@ -6511,7 +6534,7 @@ impl StorageBackend for SqliteBackend { Ok::<_, rusqlite::Error>(()) }) .await - .map_err(|e| PinakesError::Database(e.to_string()))??; + .map_err(|e| PinakesError::Database(format!("record_conflict: {}", e)))??; Ok(()) } @@ -6567,8 +6590,12 @@ impl StorageBackend for SqliteBackend { Ok::<_, rusqlite::Error>(conflicts) }) .await - .map_err(|e| PinakesError::Database(e.to_string()))? - .map_err(|e| PinakesError::Database(e.to_string())) + .map_err(|e| { + PinakesError::Database(format!("get_unresolved_conflicts: {}", e)) + })? + .map_err(|e| { + PinakesError::Database(format!("get_unresolved_conflicts query: {}", e)) + }) } async fn resolve_conflict( @@ -6595,12 +6622,12 @@ impl StorageBackend for SqliteBackend { Ok::<_, rusqlite::Error>(()) }) .await - .map_err(|e| PinakesError::Database(e.to_string()))??; + .map_err(|e| { + PinakesError::Database(format!("resolve_conflict: {}", e)) + })??; Ok(()) } - // ===== Enhanced Sharing ===== - async fn create_share( &self, share: &crate::sharing::Share, @@ -6673,8 +6700,8 @@ impl StorageBackend for SqliteBackend { Ok::<_, rusqlite::Error>(share) }) .await - .map_err(|e| PinakesError::Database(e.to_string()))? - .map_err(|e| PinakesError::Database(e.to_string())) + .map_err(|e| PinakesError::Database(format!("create_share: {}", e)))? + .map_err(|e| PinakesError::Database(format!("create_share query: {}", e))) } async fn get_share( @@ -6696,12 +6723,12 @@ impl StorageBackend for SqliteBackend { created_at, updated_at FROM shares WHERE id = ?1", params![id.0.to_string()], - |row| row_to_share(row), + row_to_share, ) }) .await - .map_err(|e| PinakesError::Database(e.to_string()))? - .map_err(|e| PinakesError::Database(e.to_string())) + .map_err(|e| PinakesError::Database(format!("get_share: {}", e)))? + .map_err(|e| PinakesError::Database(format!("get_share query: {}", e))) } async fn get_share_by_token( @@ -6724,12 +6751,14 @@ impl StorageBackend for SqliteBackend { created_at, updated_at FROM shares WHERE public_token = ?1", params![&token], - |row| row_to_share(row), + row_to_share, ) }) .await - .map_err(|e| PinakesError::Database(e.to_string()))? - .map_err(|e| PinakesError::Database(e.to_string())) + .map_err(|e| PinakesError::Database(format!("get_share_by_token: {}", e)))? + .map_err(|e| { + PinakesError::Database(format!("get_share_by_token query: {}", e)) + }) } async fn list_shares_by_owner( @@ -6758,14 +6787,18 @@ impl StorageBackend for SqliteBackend { let shares = stmt .query_map( params![owner_id.0.to_string(), limit as i64, offset as i64], - |row| row_to_share(row), + row_to_share, )? .collect::>>()?; Ok::<_, rusqlite::Error>(shares) }) .await - .map_err(|e| PinakesError::Database(e.to_string()))? - .map_err(|e| PinakesError::Database(e.to_string())) + .map_err(|e| { + PinakesError::Database(format!("list_shares_by_owner: {}", e)) + })? + .map_err(|e| { + PinakesError::Database(format!("list_shares_by_owner query: {}", e)) + }) } async fn list_shares_for_user( @@ -6794,14 +6827,18 @@ impl StorageBackend for SqliteBackend { let shares = stmt .query_map( params![user_id.0.to_string(), limit as i64, offset as i64], - |row| row_to_share(row), + row_to_share, )? .collect::>>()?; Ok::<_, rusqlite::Error>(shares) }) .await - .map_err(|e| PinakesError::Database(e.to_string()))? - .map_err(|e| PinakesError::Database(e.to_string())) + .map_err(|e| { + PinakesError::Database(format!("list_shares_for_user: {}", e)) + })? + .map_err(|e| { + PinakesError::Database(format!("list_shares_for_user query: {}", e)) + }) } async fn list_shares_for_target( @@ -6826,13 +6863,17 @@ impl StorageBackend for SqliteBackend { FROM shares WHERE target_type = ?1 AND target_id = ?2", )?; let shares = stmt - .query_map(params![&target_type, &target_id], |row| row_to_share(row))? + .query_map(params![&target_type, &target_id], row_to_share)? .collect::>>()?; Ok::<_, rusqlite::Error>(shares) }) .await - .map_err(|e| PinakesError::Database(e.to_string()))? - .map_err(|e| PinakesError::Database(e.to_string())) + .map_err(|e| { + PinakesError::Database(format!("list_shares_for_target: {}", e)) + })? + .map_err(|e| { + PinakesError::Database(format!("list_shares_for_target query: {}", e)) + }) } async fn update_share( @@ -6869,8 +6910,8 @@ impl StorageBackend for SqliteBackend { Ok::<_, rusqlite::Error>(share) }) .await - .map_err(|e| PinakesError::Database(e.to_string()))? - .map_err(|e| PinakesError::Database(e.to_string())) + .map_err(|e| PinakesError::Database(format!("update_share: {}", e)))? + .map_err(|e| PinakesError::Database(format!("update_share query: {}", e))) } async fn delete_share(&self, id: crate::sharing::ShareId) -> Result<()> { @@ -6884,7 +6925,7 @@ impl StorageBackend for SqliteBackend { Ok::<_, rusqlite::Error>(()) }) .await - .map_err(|e| PinakesError::Database(e.to_string()))??; + .map_err(|e| PinakesError::Database(format!("delete_share: {}", e)))??; Ok(()) } @@ -6905,7 +6946,9 @@ impl StorageBackend for SqliteBackend { Ok::<_, rusqlite::Error>(()) }) .await - .map_err(|e| PinakesError::Database(e.to_string()))??; + .map_err(|e| { + PinakesError::Database(format!("record_share_access: {}", e)) + })??; Ok(()) } @@ -6919,10 +6962,10 @@ impl StorageBackend for SqliteBackend { for share in shares { // Skip expired shares - if let Some(exp) = share.expires_at { - if exp < now { - continue; - } + if let Some(exp) = share.expires_at + && exp < now + { + continue; } match (&share.recipient, user_id) { @@ -6976,8 +7019,18 @@ impl StorageBackend for SqliteBackend { Ok::<_, rusqlite::Error>(ids) }) .await - .map_err(|e| PinakesError::Database(e.to_string()))? - .map_err(|e| PinakesError::Database(e.to_string()))?; + .map_err(|e| { + PinakesError::Database(format!( + "get_effective_share_permissions (collections): {}", + e + )) + })? + .map_err(|e| { + PinakesError::Database(format!( + "get_effective_share_permissions (collections) query: {}", + e + )) + })?; for collection_id in collection_ids { let target = crate::sharing::ShareTarget::Collection { collection_id }; @@ -7004,8 +7057,18 @@ impl StorageBackend for SqliteBackend { Ok::<_, rusqlite::Error>(ids) }) .await - .map_err(|e| PinakesError::Database(e.to_string()))? - .map_err(|e| PinakesError::Database(e.to_string()))?; + .map_err(|e| { + PinakesError::Database(format!( + "get_effective_share_permissions (tags): {}", + e + )) + })? + .map_err(|e| { + PinakesError::Database(format!( + "get_effective_share_permissions (tags) query: {}", + e + )) + })?; for tag_id in tag_ids { let target = crate::sharing::ShareTarget::Tag { tag_id }; @@ -7044,9 +7107,11 @@ impl StorageBackend for SqliteBackend { conn.execute(&sql, &*params) }) .await - .map_err(|e| PinakesError::Database(e.to_string()))? + .map_err(|e| PinakesError::Database(format!("batch_delete_shares: {}", e)))? .map(|n| n as u64) - .map_err(|e| PinakesError::Database(e.to_string())) + .map_err(|e| { + PinakesError::Database(format!("batch_delete_shares query: {}", e)) + }) } async fn cleanup_expired_shares(&self) -> Result { @@ -7061,9 +7126,13 @@ impl StorageBackend for SqliteBackend { ) }) .await - .map_err(|e| PinakesError::Database(e.to_string()))? + .map_err(|e| { + PinakesError::Database(format!("cleanup_expired_shares: {}", e)) + })? .map(|n| n as u64) - .map_err(|e| PinakesError::Database(e.to_string())) + .map_err(|e| { + PinakesError::Database(format!("cleanup_expired_shares query: {}", e)) + }) } async fn record_share_activity( @@ -7092,7 +7161,9 @@ impl StorageBackend for SqliteBackend { Ok::<_, rusqlite::Error>(()) }) .await - .map_err(|e| PinakesError::Database(e.to_string()))??; + .map_err(|e| { + PinakesError::Database(format!("record_share_activity: {}", e)) + })??; Ok(()) } @@ -7138,8 +7209,10 @@ impl StorageBackend for SqliteBackend { Ok::<_, rusqlite::Error>(activities) }) .await - .map_err(|e| PinakesError::Database(e.to_string()))? - .map_err(|e| PinakesError::Database(e.to_string())) + .map_err(|e| PinakesError::Database(format!("get_share_activity: {}", e)))? + .map_err(|e| { + PinakesError::Database(format!("get_share_activity query: {}", e)) + }) } async fn create_share_notification( @@ -7167,7 +7240,9 @@ impl StorageBackend for SqliteBackend { Ok::<_, rusqlite::Error>(()) }) .await - .map_err(|e| PinakesError::Database(e.to_string()))??; + .map_err(|e| { + PinakesError::Database(format!("create_share_notification: {}", e)) + })??; Ok(()) } @@ -7206,8 +7281,12 @@ impl StorageBackend for SqliteBackend { Ok::<_, rusqlite::Error>(notifications) }) .await - .map_err(|e| PinakesError::Database(e.to_string()))? - .map_err(|e| PinakesError::Database(e.to_string())) + .map_err(|e| { + PinakesError::Database(format!("get_unread_notifications: {}", e)) + })? + .map_err(|e| { + PinakesError::Database(format!("get_unread_notifications query: {}", e)) + }) } async fn mark_notification_read(&self, id: Uuid) -> Result<()> { @@ -7222,7 +7301,9 @@ impl StorageBackend for SqliteBackend { Ok::<_, rusqlite::Error>(()) }) .await - .map_err(|e| PinakesError::Database(e.to_string()))??; + .map_err(|e| { + PinakesError::Database(format!("mark_notification_read: {}", e)) + })??; Ok(()) } @@ -7241,12 +7322,12 @@ impl StorageBackend for SqliteBackend { Ok::<_, rusqlite::Error>(()) }) .await - .map_err(|e| PinakesError::Database(e.to_string()))??; + .map_err(|e| { + PinakesError::Database(format!("mark_all_notifications_read: {}", e)) + })??; Ok(()) } - // ===== File Management ===== - async fn rename_media(&self, id: MediaId, new_name: &str) -> Result { // Validate the new name if new_name.is_empty() || new_name.contains('/') || new_name.contains('\\') @@ -7276,7 +7357,9 @@ impl StorageBackend for SqliteBackend { } }) .await - .map_err(|e| PinakesError::Database(e.to_string()))??; + .map_err(|e| { + PinakesError::Database(format!("rename_media (get info): {}", e)) + })??; let old_path_buf = std::path::PathBuf::from(&old_path); let parent = old_path_buf.parent().unwrap_or(std::path::Path::new("")); @@ -7307,7 +7390,9 @@ impl StorageBackend for SqliteBackend { Ok::<_, rusqlite::Error>(()) }) .await - .map_err(|e| PinakesError::Database(e.to_string()))??; + .map_err(|e| { + PinakesError::Database(format!("rename_media (update db): {}", e)) + })??; Ok(old_path) } @@ -7336,7 +7421,9 @@ impl StorageBackend for SqliteBackend { } }) .await - .map_err(|e| PinakesError::Database(e.to_string()))??; + .map_err(|e| { + PinakesError::Database(format!("move_media (get info): {}", e)) + })??; let old_path_buf = std::path::PathBuf::from(&old_path); let new_path = new_dir.join(&file_name); @@ -7370,13 +7457,13 @@ impl StorageBackend for SqliteBackend { Ok::<_, rusqlite::Error>(()) }) .await - .map_err(|e| PinakesError::Database(e.to_string()))??; + .map_err(|e| { + PinakesError::Database(format!("move_media (update db): {}", e)) + })??; Ok(old_path) } - // ===== Trash / Soft Delete ===== - async fn soft_delete_media(&self, id: MediaId) -> Result<()> { let conn = self.conn.clone(); let id_str = id.0.to_string(); @@ -7391,7 +7478,9 @@ impl StorageBackend for SqliteBackend { ) }) .await - .map_err(|e| PinakesError::Database(e.to_string()))??; + .map_err(|e| { + PinakesError::Database(format!("soft_delete_media: {}", e)) + })??; if rows_affected == 0 { return Err(PinakesError::NotFound(format!( @@ -7417,7 +7506,7 @@ impl StorageBackend for SqliteBackend { ) }) .await - .map_err(|e| PinakesError::Database(e.to_string()))??; + .map_err(|e| PinakesError::Database(format!("restore_media: {}", e)))??; if rows_affected == 0 { return Err(PinakesError::NotFound(format!( @@ -7463,7 +7552,7 @@ impl StorageBackend for SqliteBackend { Ok::<_, rusqlite::Error>(items) }) .await - .map_err(|e| PinakesError::Database(e.to_string()))??; + .map_err(|e| PinakesError::Database(format!("list_trash: {}", e)))??; Ok(items) } @@ -7502,7 +7591,7 @@ impl StorageBackend for SqliteBackend { Ok::<_, rusqlite::Error>(count as u64) }) .await - .map_err(|e| PinakesError::Database(e.to_string()))??; + .map_err(|e| PinakesError::Database(format!("empty_trash: {}", e)))??; Ok(count) } @@ -7550,7 +7639,7 @@ impl StorageBackend for SqliteBackend { Ok::<_, rusqlite::Error>(count as u64) }) .await - .map_err(|e| PinakesError::Database(e.to_string()))??; + .map_err(|e| PinakesError::Database(format!("purge_old_trash: {}", e)))??; Ok(count) } @@ -7568,13 +7657,11 @@ impl StorageBackend for SqliteBackend { Ok::<_, rusqlite::Error>(count as u64) }) .await - .map_err(|e| PinakesError::Database(e.to_string()))??; + .map_err(|e| PinakesError::Database(format!("count_trash: {}", e)))??; Ok(count) } - // ===== Markdown Links (Obsidian-style) ===== - async fn save_markdown_links( &self, media_id: MediaId, @@ -7624,7 +7711,9 @@ impl StorageBackend for SqliteBackend { Ok::<_, rusqlite::Error>(()) }) .await - .map_err(|e| PinakesError::Database(e.to_string()))??; + .map_err(|e| { + PinakesError::Database(format!("save_markdown_links: {}", e)) + })??; Ok(()) } @@ -7646,8 +7735,7 @@ impl StorageBackend for SqliteBackend { ORDER BY line_number", )?; - let rows = - stmt.query_map([&media_id_str], |row| row_to_markdown_link(row))?; + let rows = stmt.query_map([&media_id_str], row_to_markdown_link)?; let mut links = Vec::new(); for row in rows { @@ -7656,7 +7744,9 @@ impl StorageBackend for SqliteBackend { Ok::<_, rusqlite::Error>(links) }) .await - .map_err(|e| PinakesError::Database(e.to_string()))??; + .map_err(|e| { + PinakesError::Database(format!("get_outgoing_links: {}", e)) + })??; Ok(links) } @@ -7710,7 +7800,7 @@ impl StorageBackend for SqliteBackend { Ok::<_, rusqlite::Error>(backlinks) }) .await - .map_err(|e| PinakesError::Database(e.to_string()))??; + .map_err(|e| PinakesError::Database(format!("get_backlinks: {}", e)))??; Ok(backlinks) } @@ -7728,7 +7818,9 @@ impl StorageBackend for SqliteBackend { Ok::<_, rusqlite::Error>(()) }) .await - .map_err(|e| PinakesError::Database(e.to_string()))??; + .map_err(|e| { + PinakesError::Database(format!("clear_links_for_media: {}", e)) + })??; Ok(()) } @@ -7883,7 +7975,7 @@ impl StorageBackend for SqliteBackend { Ok::<_, rusqlite::Error>(crate::model::GraphData { nodes, edges }) }) .await - .map_err(|e| PinakesError::Database(e.to_string()))??; + .map_err(|e| PinakesError::Database(format!("get_graph_data: {}", e)))??; Ok(graph_data) } @@ -7941,7 +8033,7 @@ impl StorageBackend for SqliteBackend { Ok::<_, rusqlite::Error>((updated1 + updated2) as u64) }) .await - .map_err(|e| PinakesError::Database(e.to_string()))??; + .map_err(|e| PinakesError::Database(format!("resolve_links: {}", e)))??; Ok(count) } @@ -7960,7 +8052,9 @@ impl StorageBackend for SqliteBackend { Ok::<_, rusqlite::Error>(()) }) .await - .map_err(|e| PinakesError::Database(e.to_string()))??; + .map_err(|e| { + PinakesError::Database(format!("mark_links_extracted: {}", e)) + })??; Ok(()) } @@ -7978,7 +8072,9 @@ impl StorageBackend for SqliteBackend { Ok::<_, rusqlite::Error>(count as u64) }) .await - .map_err(|e| PinakesError::Database(e.to_string()))??; + .map_err(|e| { + PinakesError::Database(format!("count_unresolved_links: {}", e)) + })??; Ok(count) } diff --git a/crates/pinakes-core/src/sync/chunked.rs b/crates/pinakes-core/src/sync/chunked.rs index 43e569f..0e92730 100644 --- a/crates/pinakes-core/src/sync/chunked.rs +++ b/crates/pinakes-core/src/sync/chunked.rs @@ -204,17 +204,16 @@ impl ChunkedUploadManager { let mut entries = fs::read_dir(&self.temp_dir).await?; while let Some(entry) = entries.next_entry().await? { let path = entry.path(); - if path.extension().map(|e| e == "upload").unwrap_or(false) { - if let Ok(metadata) = fs::metadata(&path).await { - if let Ok(modified) = metadata.modified() { - let age = std::time::SystemTime::now() - .duration_since(modified) - .unwrap_or_default(); - if age > max_age { - let _ = fs::remove_file(&path).await; - count += 1; - } - } + if path.extension().map(|e| e == "upload").unwrap_or(false) + && let Ok(metadata) = fs::metadata(&path).await + && let Ok(modified) = metadata.modified() + { + let age = std::time::SystemTime::now() + .duration_since(modified) + .unwrap_or_default(); + if age > max_age { + let _ = fs::remove_file(&path).await; + count += 1; } } } diff --git a/crates/pinakes-core/src/sync/models.rs b/crates/pinakes-core/src/sync/models.rs index 3240d71..5b8b917 100644 --- a/crates/pinakes-core/src/sync/models.rs +++ b/crates/pinakes-core/src/sync/models.rs @@ -35,22 +35,19 @@ impl fmt::Display for DeviceId { } /// Type of sync device. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[derive( + Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default, +)] #[serde(rename_all = "lowercase")] pub enum DeviceType { Desktop, Mobile, Tablet, Server, + #[default] Other, } -impl Default for DeviceType { - fn default() -> Self { - Self::Other - } -} - impl fmt::Display for DeviceType { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { @@ -353,7 +350,7 @@ impl UploadSession { timeout_hours: u64, ) -> Self { let now = Utc::now(); - let chunk_count = (expected_size + chunk_size - 1) / chunk_size; + let chunk_count = expected_size.div_ceil(chunk_size); Self { id: Uuid::now_v7(), device_id, diff --git a/crates/pinakes-core/src/tags.rs b/crates/pinakes-core/src/tags.rs index 290a748..cab6a0d 100644 --- a/crates/pinakes-core/src/tags.rs +++ b/crates/pinakes-core/src/tags.rs @@ -6,6 +6,17 @@ use crate::{ storage::DynStorageBackend, }; +/// Creates a new tag. +/// +/// # Arguments +/// +/// * `storage` - Storage backend +/// * `name` - Tag name +/// * `parent_id` - Optional parent tag for hierarchy +/// +/// # Returns +/// +/// The created tag pub async fn create_tag( storage: &DynStorageBackend, name: &str, @@ -14,6 +25,17 @@ pub async fn create_tag( storage.create_tag(name, parent_id).await } +/// Applies a tag to a media item. +/// +/// # Arguments +/// +/// * `storage` - Storage backend +/// * `media_id` - Media item to tag +/// * `tag_id` - Tag to apply +/// +/// # Returns +/// +/// `Ok(())` on success pub async fn tag_media( storage: &DynStorageBackend, media_id: MediaId, @@ -29,6 +51,17 @@ pub async fn tag_media( .await } +/// Removes a tag from a media item. +/// +/// # Arguments +/// +/// * `storage` - Storage backend +/// * `media_id` - Media item to untag +/// * `tag_id` - Tag to remove +/// +/// # Returns +/// +/// `Ok(())` on success pub async fn untag_media( storage: &DynStorageBackend, media_id: MediaId, @@ -44,6 +77,16 @@ pub async fn untag_media( .await } +/// Returns all descendants of a tag in the hierarchy. +/// +/// # Arguments +/// +/// * `storage` - Storage backend +/// * `tag_id` - Root tag to query +/// +/// # Returns +/// +/// List of child tags pub async fn get_tag_tree( storage: &DynStorageBackend, tag_id: Uuid, diff --git a/crates/pinakes-core/src/thumbnail.rs b/crates/pinakes-core/src/thumbnail.rs index a97221b..60d4e37 100644 --- a/crates/pinakes-core/src/thumbnail.rs +++ b/crates/pinakes-core/src/thumbnail.rs @@ -261,7 +261,7 @@ fn generate_raw_thumbnail( ))); } - // The extracted preview is typically a JPEG — try loading it + // The extracted preview is typically a JPEG; try loading it if temp_ppm.exists() { let result = image::open(&temp_ppm); let _ = std::fs::remove_file(&temp_ppm); diff --git a/crates/pinakes-core/src/users.rs b/crates/pinakes-core/src/users.rs index 5f58aee..2ce35e0 100644 --- a/crates/pinakes-core/src/users.rs +++ b/crates/pinakes-core/src/users.rs @@ -16,6 +16,7 @@ use crate::{ pub struct UserId(pub Uuid); impl UserId { + /// Creates a new user ID. pub fn new() -> Self { Self(Uuid::now_v7()) } @@ -94,14 +95,17 @@ pub enum LibraryPermission { } impl LibraryPermission { + /// Checks if read permission is granted. pub fn can_read(&self) -> bool { true } + /// Checks if write permission is granted. pub fn can_write(&self) -> bool { matches!(self, Self::Write | Self::Admin) } + /// Checks if admin permission is granted. pub fn can_admin(&self) -> bool { matches!(self, Self::Admin) } diff --git a/crates/pinakes-core/tests/common/mod.rs b/crates/pinakes-core/tests/common/mod.rs index 47cf0c7..030c106 100644 --- a/crates/pinakes-core/tests/common/mod.rs +++ b/crates/pinakes-core/tests/common/mod.rs @@ -1,3 +1,8 @@ +// Common test utilities shared across integration tests +// Functions may appear unused in individual test binaries - they're used across +// the test suite +#![allow(dead_code)] + use std::{collections::HashMap, path::PathBuf, sync::Arc}; use pinakes_core::{ diff --git a/crates/pinakes-core/tests/integration.rs b/crates/pinakes-core/tests/integration.rs index ee9632a..161755f 100644 --- a/crates/pinakes-core/tests/integration.rs +++ b/crates/pinakes-core/tests/integration.rs @@ -1,9 +1,6 @@ -use std::{collections::HashMap, sync::Arc}; +use std::collections::HashMap; -use pinakes_core::{ - model::*, - storage::{StorageBackend, sqlite::SqliteBackend}, -}; +use pinakes_core::{model::*, storage::StorageBackend}; mod common; use common::{make_test_media, setup}; diff --git a/crates/pinakes-server/src/app.rs b/crates/pinakes-server/src/app.rs index 2e70ac4..58b9d3e 100644 --- a/crates/pinakes-server/src/app.rs +++ b/crates/pinakes-server/src/app.rs @@ -136,7 +136,7 @@ pub fn create_router_with_tls( ) // Webhooks (read) .route("/webhooks", get(routes::webhooks::list_webhooks)) - // Auth endpoints (self-service) — login handled separately with stricter rate limit + // Auth endpoints (self-service); login is handled separately with a stricter rate limit .route("/auth/logout", post(routes::auth::logout)) .route("/auth/me", get(routes::auth::me)) .route("/auth/revoke-all", post(routes::auth::revoke_all_sessions)) diff --git a/crates/pinakes-server/src/dto.rs b/crates/pinakes-server/src/dto.rs index 0b610d3..3cc940c 100644 --- a/crates/pinakes-server/src/dto.rs +++ b/crates/pinakes-server/src/dto.rs @@ -721,8 +721,6 @@ impl From for UserLibraryResponse { } } -// ===== Social (Ratings, Comments, Favorites, Shares) ===== - #[derive(Debug, Serialize)] pub struct RatingResponse { pub id: String, @@ -816,8 +814,6 @@ impl From for ShareLinkResponse { } } -// ===== Playlists ===== - #[derive(Debug, Serialize)] pub struct PlaylistResponse { pub id: String, @@ -875,8 +871,6 @@ pub struct ReorderPlaylistRequest { pub new_position: i32, } -// ===== Analytics ===== - #[derive(Debug, Serialize)] pub struct UsageEventResponse { pub id: String, @@ -924,8 +918,6 @@ pub struct WatchProgressResponse { pub progress_secs: f64, } -// ===== Subtitles ===== - #[derive(Debug, Serialize)] pub struct SubtitleResponse { pub id: String, @@ -968,8 +960,6 @@ pub struct UpdateSubtitleOffsetRequest { pub offset_ms: i64, } -// ===== Enrichment ===== - #[derive(Debug, Serialize)] pub struct ExternalMetadataResponse { pub id: String, @@ -1005,8 +995,6 @@ impl From } } -// ===== Transcode ===== - #[derive(Debug, Serialize)] pub struct TranscodeSessionResponse { pub id: String, @@ -1039,8 +1027,6 @@ pub struct CreateTranscodeRequest { pub profile: String, } -// ===== Managed Storage / Upload ===== - #[derive(Debug, Serialize)] pub struct UploadResponse { pub media_id: String, @@ -1081,8 +1067,6 @@ impl From } } -// ===== Sync ===== - #[derive(Debug, Deserialize)] pub struct RegisterDeviceRequest { pub name: String, @@ -1269,8 +1253,6 @@ pub struct AcknowledgeChangesRequest { pub cursor: i64, } -// ===== Enhanced Sharing ===== - #[derive(Debug, Deserialize)] pub struct CreateShareRequest { pub target_type: String, diff --git a/crates/pinakes-server/src/main.rs b/crates/pinakes-server/src/main.rs index 54808e8..2c1eb28 100644 --- a/crates/pinakes-server/src/main.rs +++ b/crates/pinakes-server/src/main.rs @@ -438,13 +438,123 @@ async fn main() -> Result<()> { } }, JobKind::Enrich { media_ids } => { - // Enrichment job placeholder + use pinakes_core::{ + enrichment::{ + MetadataEnricher, + books::BookEnricher, + lastfm::LastFmEnricher, + musicbrainz::MusicBrainzEnricher, + tmdb::TmdbEnricher, + }, + media_type::MediaCategory, + }; + + let enrich_cfg = &config.enrichment; + let mut enrichers: Vec> = Vec::new(); + + if enrich_cfg.enabled { + if enrich_cfg.sources.musicbrainz.enabled { + enrichers.push(Box::new(MusicBrainzEnricher::new())); + } + if let (true, Some(key)) = ( + enrich_cfg.sources.tmdb.enabled, + enrich_cfg.sources.tmdb.api_key.clone(), + ) { + enrichers.push(Box::new(TmdbEnricher::new(key))); + } + if let (true, Some(key)) = ( + enrich_cfg.sources.lastfm.enabled, + enrich_cfg.sources.lastfm.api_key.clone(), + ) { + enrichers.push(Box::new(LastFmEnricher::new(key))); + } + // BookEnricher handles documents/epub. No dedicated config + // key is required; the Google Books key is optional. + enrichers.push(Box::new(BookEnricher::new(None))); + } + + let total = media_ids.len(); + let mut enriched: usize = 0; + let mut errors: usize = 0; + + 'items: for media_id in media_ids { + if cancel.is_cancelled() { + break 'items; + } + let item = match storage.get_media(media_id).await { + Ok(i) => i, + Err(e) => { + tracing::warn!( + %media_id, + error = %e, + "enrich: failed to fetch media item" + ); + errors += 1; + continue; + }, + }; + + // Select enrichers appropriate for this media category. + let category = item.media_type.category(); + for enricher in &enrichers { + let source = enricher.source(); + use pinakes_core::enrichment::EnrichmentSourceType; + let applicable = match source { + EnrichmentSourceType::MusicBrainz + | EnrichmentSourceType::LastFm => { + category == MediaCategory::Audio + }, + EnrichmentSourceType::Tmdb => { + category == MediaCategory::Video + }, + EnrichmentSourceType::OpenLibrary + | EnrichmentSourceType::GoogleBooks => { + category == MediaCategory::Document + }, + }; + if !applicable { + continue; + } + + match enricher.enrich(&item).await { + Ok(Some(meta)) => { + if let Err(e) = storage.store_external_metadata(&meta).await + { + tracing::warn!( + %media_id, + %source, + error = %e, + "enrich: failed to store external metadata" + ); + errors += 1; + } else { + enriched += 1; + } + }, + Ok(None) => {}, + Err(e) => { + tracing::warn!( + %media_id, + %source, + error = %e, + "enrich: enricher returned error" + ); + errors += 1; + }, + } + } + } + JobQueue::complete( - &jobs, - job_id, - serde_json::json!({"media_ids": media_ids.len(), "status": "not_implemented"}), - ) - .await; + &jobs, + job_id, + serde_json::json!({ + "total": total, + "enriched": enriched, + "errors": errors, + }), + ) + .await; }, JobKind::CleanupAnalytics => { let before = chrono::Utc::now() - chrono::Duration::days(90); @@ -460,6 +570,27 @@ async fn main() -> Result<()> { Err(e) => JobQueue::fail(&jobs, job_id, e.to_string()).await, } }, + JobKind::TrashPurge => { + let retention_days = config.trash.retention_days; + let before = chrono::Utc::now() + - chrono::Duration::days(retention_days as i64); + + match storage.purge_old_trash(before).await { + Ok(count) => { + tracing::info!(count, "purged {} items from trash", count); + JobQueue::complete( + &jobs, + job_id, + serde_json::json!({"purged": count, "retention_days": retention_days}), + ) + .await; + }, + Err(e) => { + tracing::error!(error = %e, "failed to purge trash"); + JobQueue::fail(&jobs, job_id, e.to_string()).await; + }, + } + }, }; drop(cancel); }) diff --git a/crates/pinakes-server/src/routes/media.rs b/crates/pinakes-server/src/routes/media.rs index 9a809aa..9c652ac 100644 --- a/crates/pinakes-server/src/routes/media.rs +++ b/crates/pinakes-server/src/routes/media.rs @@ -836,8 +836,6 @@ pub async fn get_media_count( Ok(Json(MediaCountResponse { count })) } -// ===== File Management Endpoints ===== - pub async fn rename_media( State(state): State, Path(id): Path, @@ -978,8 +976,6 @@ pub async fn batch_move_media( } } -// ===== Trash Endpoints ===== - pub async fn soft_delete_media( State(state): State, Path(id): Path, diff --git a/crates/pinakes-server/src/routes/notes.rs b/crates/pinakes-server/src/routes/notes.rs index 291a208..b682db9 100644 --- a/crates/pinakes-server/src/routes/notes.rs +++ b/crates/pinakes-server/src/routes/notes.rs @@ -25,8 +25,6 @@ use uuid::Uuid; use crate::{error::ApiError, state::AppState}; -// ===== Response DTOs ===== - /// Response for backlinks query #[derive(Debug, Serialize)] pub struct BacklinksResponse { @@ -200,8 +198,6 @@ pub struct UnresolvedLinksResponse { pub count: u64, } -// ===== Handlers ===== - /// Get backlinks (incoming links) to a media item. /// /// GET /api/v1/media/{id}/backlinks diff --git a/crates/pinakes-server/src/routes/shares.rs b/crates/pinakes-server/src/routes/shares.rs index 980f53e..ded0160 100644 --- a/crates/pinakes-server/src/routes/shares.rs +++ b/crates/pinakes-server/src/routes/shares.rs @@ -93,7 +93,12 @@ pub async fn create_share( let recipient = match req.recipient_type.as_str() { "public_link" => { let token = generate_share_token(); - let password_hash = req.password.as_ref().map(|p| hash_share_password(p)); + let password_hash = req + .password + .as_ref() + .map(|p| hash_share_password(p)) + .transpose() + .map_err(ApiError)?; ShareRecipient::PublicLink { token, password_hash, @@ -409,35 +414,37 @@ pub async fn access_shared( .map_err(|e| ApiError::not_found(format!("Share not found: {}", e)))?; // Check expiration - if let Some(expires_at) = share.expires_at { - if Utc::now() > expires_at { - return Err(ApiError::not_found("Share has expired")); - } + if let Some(expires_at) = share.expires_at + && Utc::now() > expires_at + { + return Err(ApiError::not_found("Share has expired")); } // Check password if required - if let ShareRecipient::PublicLink { password_hash, .. } = &share.recipient { - if let Some(hash) = password_hash { - let provided_password = params - .password - .as_ref() - .ok_or_else(|| ApiError::unauthorized("Password required"))?; + if let ShareRecipient::PublicLink { + password_hash: Some(hash), + .. + } = &share.recipient + { + let provided_password = params + .password + .as_ref() + .ok_or_else(|| ApiError::unauthorized("Password required"))?; - if !verify_share_password(provided_password, hash) { - // Log failed attempt - let activity = ShareActivity { - id: Uuid::now_v7(), - share_id: share.id, - actor_id: None, - actor_ip: Some(addr.ip().to_string()), - action: ShareActivityAction::PasswordFailed, - details: None, - timestamp: Utc::now(), - }; - let _ = state.storage.record_share_activity(&activity).await; + if !verify_share_password(provided_password, hash) { + // Log failed attempt + let activity = ShareActivity { + id: Uuid::now_v7(), + share_id: share.id, + actor_id: None, + actor_ip: Some(addr.ip().to_string()), + action: ShareActivityAction::PasswordFailed, + details: None, + timestamp: Utc::now(), + }; + let _ = state.storage.record_share_activity(&activity).await; - return Err(ApiError::unauthorized("Invalid password")); - } + return Err(ApiError::unauthorized("Invalid password")); } } @@ -473,8 +480,6 @@ pub async fn access_shared( Ok(Json(item.into())) }, _ => { - // For collections/tags, return a placeholder - // Full implementation would return the collection contents Err(ApiError::bad_request( "Collection/tag sharing not yet fully implemented", )) diff --git a/crates/pinakes-server/src/routes/social.rs b/crates/pinakes-server/src/routes/social.rs index bf47014..ae9a66e 100644 --- a/crates/pinakes-server/src/routes/social.rs +++ b/crates/pinakes-server/src/routes/social.rs @@ -13,8 +13,6 @@ pub struct ShareLinkQuery { pub password: Option, } -// ===== Ratings ===== - pub async fn rate_media( State(state): State, Extension(username): Extension, @@ -46,8 +44,6 @@ pub async fn get_media_ratings( )) } -// ===== Comments ===== - pub async fn add_comment( State(state): State, Extension(username): Extension, @@ -80,8 +76,6 @@ pub async fn get_media_comments( )) } -// ===== Favorites ===== - pub async fn add_favorite( State(state): State, Extension(username): Extension, @@ -120,8 +114,6 @@ pub async fn list_favorites( Ok(Json(items.into_iter().map(MediaResponse::from).collect())) } -// ===== Share Links ===== - pub async fn create_share_link( State(state): State, Extension(username): Extension, diff --git a/crates/pinakes-server/src/routes/sync.rs b/crates/pinakes-server/src/routes/sync.rs index 988b32e..e824219 100644 --- a/crates/pinakes-server/src/routes/sync.rs +++ b/crates/pinakes-server/src/routes/sync.rs @@ -301,7 +301,7 @@ pub async fn report_changes( if !config.sync.enabled { return Err(ApiError::bad_request("Sync is not enabled")); } - let conflict_resolution = config.sync.default_conflict_resolution.clone(); + let conflict_resolution = config.sync.default_conflict_resolution; drop(config); let mut accepted = Vec::new(); @@ -514,7 +514,7 @@ pub async fn create_upload( .ok_or_else(|| ApiError::unauthorized("Invalid device token"))?; let chunk_size = req.chunk_size.unwrap_or(DEFAULT_CHUNK_SIZE); - let chunk_count = (req.expected_size + chunk_size - 1) / chunk_size; + let chunk_count = req.expected_size.div_ceil(chunk_size); let now = Utc::now(); let session = UploadSession { @@ -784,10 +784,10 @@ pub async fn cancel_upload( })?; // Clean up temp file if manager is available - if let Some(ref manager) = state.chunked_upload_manager { - if let Err(e) = manager.cancel(id).await { - tracing::warn!(session_id = %id, error = %e, "failed to clean up temp file"); - } + if let Some(ref manager) = state.chunked_upload_manager + && let Err(e) = manager.cancel(id).await + { + tracing::warn!(session_id = %id, error = %e, "failed to clean up temp file"); } session.status = UploadStatus::Cancelled; @@ -827,38 +827,37 @@ pub async fn download_file( let file_size = metadata.len(); // Check for Range header - if let Some(range_header) = headers.get(header::RANGE) { - if let Ok(range_str) = range_header.to_str() { - if let Some(range) = parse_range_header(range_str, file_size) { - // Partial content response - let (start, end) = range; - let length = end - start + 1; + if let Some(range_header) = headers.get(header::RANGE) + && let Ok(range_str) = range_header.to_str() + && let Some(range) = parse_range_header(range_str, file_size) + { + // Partial content response + let (start, end) = range; + let length = end - start + 1; - let file = tokio::fs::File::open(&item.path).await.map_err(|e| { - ApiError::internal(format!("Failed to reopen file: {}", e)) - })?; + let file = tokio::fs::File::open(&item.path).await.map_err(|e| { + ApiError::internal(format!("Failed to reopen file: {}", e)) + })?; - let stream = ReaderStream::new(file); - let body = Body::from_stream(stream); + let stream = ReaderStream::new(file); + let body = Body::from_stream(stream); - return Ok( + return Ok( + ( + StatusCode::PARTIAL_CONTENT, + [ + (header::CONTENT_TYPE, item.media_type.mime_type()), + (header::CONTENT_LENGTH, length.to_string()), ( - StatusCode::PARTIAL_CONTENT, - [ - (header::CONTENT_TYPE, item.media_type.mime_type()), - (header::CONTENT_LENGTH, length.to_string()), - ( - header::CONTENT_RANGE, - format!("bytes {}-{}/{}", start, end, file_size), - ), - (header::ACCEPT_RANGES, "bytes".to_string()), - ], - body, - ) - .into_response(), - ); - } - } + header::CONTENT_RANGE, + format!("bytes {}-{}/{}", start, end, file_size), + ), + (header::ACCEPT_RANGES, "bytes".to_string()), + ], + body, + ) + .into_response(), + ); } // Full content response diff --git a/crates/pinakes-server/tests/api.rs b/crates/pinakes-server/tests/api.rs index 1a548af..bd30789 100644 --- a/crates/pinakes-server/tests/api.rs +++ b/crates/pinakes-server/tests/api.rs @@ -29,6 +29,7 @@ use pinakes_core::{ ThumbnailConfig, TlsConfig, TranscodingConfig, + TrashConfig, UiConfig, UserAccount, UserRole, @@ -151,6 +152,7 @@ fn default_config() -> Config { managed_storage: ManagedStorageConfig::default(), sync: SyncConfig::default(), sharing: SharingConfig::default(), + trash: TrashConfig::default(), } } @@ -298,10 +300,6 @@ async fn response_body( serde_json::from_slice(&body).unwrap_or(serde_json::Value::Null) } -// =================================================================== -// Existing tests (no auth) -// =================================================================== - #[tokio::test] async fn test_list_media_empty() { let app = setup_app().await; @@ -515,10 +513,6 @@ async fn test_user_duplicate_username() { assert_eq!(response.status(), StatusCode::CONFLICT); } -// =================================================================== -// Authentication tests -// =================================================================== - #[tokio::test] async fn test_unauthenticated_request_rejected() { let (app, ..) = setup_app_with_auth().await; @@ -623,10 +617,6 @@ async fn test_logout() { assert_eq!(response.status(), StatusCode::UNAUTHORIZED); } -// =================================================================== -// Authorization / RBAC tests -// =================================================================== - #[tokio::test] async fn test_viewer_cannot_access_editor_routes() { let (app, _, _, viewer_token) = setup_app_with_auth().await; @@ -713,10 +703,6 @@ async fn test_admin_can_access_all() { assert_eq!(response.status(), StatusCode::OK); } -// =================================================================== -// Phase 2 feature tests: Social -// =================================================================== - #[tokio::test] async fn test_rating_invalid_stars_zero() { let (app, _, editor_token, _) = setup_app_with_auth().await; @@ -775,10 +761,6 @@ async fn test_favorites_list_empty() { assert!(body.as_array().unwrap().is_empty()); } -// =================================================================== -// Phase 2 feature tests: Playlists -// =================================================================== - #[tokio::test] async fn test_playlist_crud() { let (app, _, editor_token, _) = setup_app_with_auth().await; @@ -860,10 +842,6 @@ async fn test_playlist_empty_name() { assert_eq!(response.status(), StatusCode::BAD_REQUEST); } -// =================================================================== -// Phase 2 feature tests: Analytics -// =================================================================== - #[tokio::test] async fn test_most_viewed_empty() { let (app, _, _, viewer_token) = setup_app_with_auth().await; @@ -896,10 +874,6 @@ async fn test_record_event_and_query() { assert_eq!(body["recorded"], true); } -// =================================================================== -// Phase 2 feature tests: Streaming/Transcode -// =================================================================== - #[tokio::test] async fn test_transcode_session_not_found() { let (app, _, _, viewer_token) = setup_app_with_auth().await; @@ -951,10 +925,6 @@ async fn test_hls_segment_no_session() { ); } -// =================================================================== -// Phase 2 feature tests: Subtitles -// =================================================================== - #[tokio::test] async fn test_subtitles_list() { let (app, _, _, viewer_token) = setup_app_with_auth().await; @@ -974,10 +944,6 @@ async fn test_subtitles_list() { ); } -// =================================================================== -// Health: public access test -// =================================================================== - #[tokio::test] async fn test_health_public() { let (app, ..) = setup_app_with_auth().await; @@ -988,10 +954,6 @@ async fn test_health_public() { assert_eq!(response.status(), StatusCode::OK); } -// =================================================================== -// Input validation & edge case tests -// =================================================================== - #[tokio::test] async fn test_invalid_uuid_in_path() { let (app, _, _, viewer_token) = setup_app_with_auth().await; @@ -1026,7 +988,7 @@ async fn test_share_link_expired() { // (need real media items). Verify the expire check logic works. let app = setup_app().await; - // First import a dummy file to get a media_id — but we can't without a real + // First import a dummy file to get a media_id, but we can't without a real // file. So let's test the public share access endpoint with a nonexistent // token. let response = app diff --git a/crates/pinakes-server/tests/plugin.rs b/crates/pinakes-server/tests/plugin.rs index ef36f4f..988e3ef 100644 --- a/crates/pinakes-server/tests/plugin.rs +++ b/crates/pinakes-server/tests/plugin.rs @@ -29,6 +29,7 @@ use pinakes_core::{ ThumbnailConfig, TlsConfig, TranscodingConfig, + TrashConfig, UiConfig, WebhookConfig, }, @@ -118,6 +119,7 @@ async fn setup_app_with_plugins() managed_storage: ManagedStorageConfig::default(), sync: SyncConfig::default(), sharing: SharingConfig::default(), + trash: TrashConfig::default(), }; let job_queue = diff --git a/crates/pinakes-ui/src/components/backlinks_panel.rs b/crates/pinakes-ui/src/components/backlinks_panel.rs index b224eb2..78b9041 100644 --- a/crates/pinakes-ui/src/components/backlinks_panel.rs +++ b/crates/pinakes-ui/src/components/backlinks_panel.rs @@ -174,7 +174,7 @@ pub fn BacklinksPanel( for backlink in &data.backlinks { BacklinkItemView { backlink: backlink.clone(), - on_navigate: on_navigate.clone(), + on_navigate: on_navigate, } } } @@ -328,7 +328,7 @@ pub fn OutgoingLinksPanel( for link in &data.links { OutgoingLinkItemView { link: link.clone(), - on_navigate: on_navigate.clone(), + on_navigate: on_navigate, } } } diff --git a/crates/pinakes-ui/src/components/detail.rs b/crates/pinakes-ui/src/components/detail.rs index afc4778..a12ba7c 100644 --- a/crates/pinakes-ui/src/components/detail.rs +++ b/crates/pinakes-ui/src/components/detail.rs @@ -484,7 +484,7 @@ pub fn Detail( span { class: "detail-value mono", "{media.content_hash}" } } - // Editable fields — conditional by media category + // Editable fields, conditional by media category div { class: "detail-field", label { class: "detail-label", "Title" } input { diff --git a/crates/pinakes-ui/src/components/graph_view.rs b/crates/pinakes-ui/src/components/graph_view.rs index ddb3766..123f56b 100644 --- a/crates/pinakes-ui/src/components/graph_view.rs +++ b/crates/pinakes-ui/src/components/graph_view.rs @@ -100,7 +100,7 @@ pub fn GraphView( ForceDirectedGraph { nodes: graph.nodes.clone(), edges: graph.edges.clone(), - selected_node: selected_node.clone(), + selected_node: selected_node, on_node_click: move |id: String| { selected_node.set(Some(id.clone())); }, @@ -525,7 +525,7 @@ fn ForceDirectedGraph( }, onwheel: move |evt| { let delta = if evt.delta().strip_units().y < 0.0 { 1.1 } else { 0.9 }; - let new_zoom = (*zoom.read() * delta).max(0.1).min(5.0); + let new_zoom = (*zoom.read() * delta).clamp(0.1, 5.0); zoom.set(new_zoom); }, diff --git a/crates/pinakes-ui/src/components/markdown_viewer.rs b/crates/pinakes-ui/src/components/markdown_viewer.rs index b8d96a1..cbdf635 100644 --- a/crates/pinakes-ui/src/components/markdown_viewer.rs +++ b/crates/pinakes-ui/src/components/markdown_viewer.rs @@ -80,12 +80,11 @@ pub fn MarkdownViewer( })(); "#; - if let Ok(result) = eval(check_js).await { - if let Some(target) = result.as_str() { - if !target.is_empty() { - handler.call(target.to_string()); - } - } + if let Ok(result) = eval(check_js).await + && let Some(target) = result.as_str() + && !target.is_empty() + { + handler.call(target.to_string()); } } });