diff --git a/.editorconfig b/.editorconfig index 462a3da63..e80002aa8 100644 --- a/.editorconfig +++ b/.editorconfig @@ -64,7 +64,7 @@ indent_style = space # Prisma # https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-schema/data-model#formatting [*.prisma] -indent_size = 4 +indent_size = 2 indent_style = space # YAML diff --git a/.vscode/launch.json b/.vscode/launch.json index 411d7f5a1..6ae503123 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -12,7 +12,8 @@ "args": [ "build", "--manifest-path=./apps/desktop/src-tauri/Cargo.toml", - "--no-default-features" + "--no-default-features", + "--features=ai-models" ], "problemMatcher": "$rustc" }, diff --git a/.vscode/tasks.json b/.vscode/tasks.json index a81b441ce..2a7510da5 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -56,7 +56,8 @@ "command": "run", "args": [ "--manifest-path=./apps/desktop/src-tauri/Cargo.toml", - "--no-default-features" + "--no-default-features", + "--features=ai-models" ], "env": { "RUST_BACKTRACE": "short" diff --git a/Cargo.lock b/Cargo.lock index 50a8083d7..a8ddd15cc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -151,30 +151,30 @@ checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" [[package]] name = "anstyle-parse" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "317b9a89c1868f5ea6ff1d9539a69f45dffc21ce321ac1fd1160dfa48c8e2140" +checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +checksum = "a3a318f1f38d2418400f8209655bfd825785afd25aa30bb7ba6cc792e4596748" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.1" +version = "3.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628" +checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" dependencies = [ "anstyle", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -1031,9 +1031,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.4.10" +version = "4.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fffed7514f420abec6d183b1d3acfd9099c79c3a10a06ade4f8203f1411272" +checksum = "bfaff671f6b22ca62406885ece523383b9b64022e341e53e009a62ebc47a45f2" dependencies = [ "clap_builder", "clap_derive", @@ -1041,9 +1041,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.4.9" +version = "4.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63361bae7eef3771745f02d8d892bec2fee5f6e34af316ba556e7f97a7069ff1" +checksum = "a216b506622bb1d316cd51328dce24e07bdff4a6128a47c7e7fad11878d5adbb" dependencies = [ "anstream", "anstyle", @@ -1996,12 +1996,12 @@ dependencies = [ [[package]] name = "exr" -version = "1.71.0" +version = "1.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "832a761f35ab3e6664babfbdc6cef35a4860e816ec3916dcfd0882954e98a8a8" +checksum = "279d3efcc55e19917fff7ab3ddd6c14afb6a90881a0078465196fe2f99d08c56" dependencies = [ "bit_field", - "flume 0.11.0", + "flume", "half", "lebe", "miniz_oxide", @@ -2100,14 +2100,14 @@ dependencies = [ [[package]] name = "filetime" -version = "0.2.22" +version = "0.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4029edd3e734da6fe05b6cd7bd2960760a616bd2ddd0d59a0124746d6272af0" +checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.3.5", - "windows-sys 0.48.0", + "redox_syscall 0.4.1", + "windows-sys 0.52.0", ] [[package]] @@ -2145,15 +2145,6 @@ dependencies = [ "spin 0.9.8", ] -[[package]] -name = "flume" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" -dependencies = [ - "spin 0.9.8", -] - [[package]] name = "fnv" version = "1.0.7" @@ -2760,11 +2751,13 @@ dependencies = [ [[package]] name = "half" -version = "2.2.1" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b4af3693f1b705df946e9fe5631932443781d0aabb423b62fcd4d73f6d2fd0" +checksum = "bc52e53916c08643f1b56ec082790d1e86a32e58dc5268f897f313fbae7b4872" dependencies = [ + "cfg-if", "crunchy", + "num-traits", ] [[package]] @@ -3387,9 +3380,9 @@ dependencies = [ [[package]] name = "itertools" -version = "0.11.0" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +checksum = "25db6b064527c5d482d0423354fcd07a89a2dfe07b67892e62411946db7f07b0" dependencies = [ "either", ] @@ -4202,6 +4195,16 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" +[[package]] +name = "matrixmultiply" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7574c1cf36da4798ab73da5b215bbf444f50718207754cb522201d78d1cd0ff2" +dependencies = [ + "autocfg", + "rawpointer", +] + [[package]] name = "maybe-owned" version = "0.3.4" @@ -4220,7 +4223,7 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c97183f9949c1f97921e4dda6bc059dfc30a6da5e4460a4f5218847dbe2ae1f" dependencies = [ - "flume 0.10.14", + "flume", "if-addrs 0.10.2", "log", "polling", @@ -4402,9 +4405,9 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.9" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0" +checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" dependencies = [ "libc", "log", @@ -4538,6 +4541,19 @@ dependencies = [ "socket2 0.4.10", ] +[[package]] +name = "ndarray" +version = "0.15.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb12d4e967ec485a5f71c6311fe28158e9d6f4bc4a447b474184d0f91a8fa32" +dependencies = [ + "matrixmultiply", + "num-complex", + "num-integer", + "num-traits", + "rawpointer", +] + [[package]] name = "ndk" version = "0.6.0" @@ -4827,6 +4843,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -4919,9 +4936,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "opaque-debug" @@ -4954,9 +4971,9 @@ dependencies = [ [[package]] name = "openssl" -version = "0.10.57" +version = "0.10.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bac25ee399abb46215765b1cb35bc0212377e58a061560d8b29b024fd0430e7c" +checksum = "6b8419dc8cc6d866deb801274bba2e6f8f6108c1bb7fcc10ee5ab864931dbb45" dependencies = [ "bitflags 2.4.1", "cfg-if", @@ -4995,9 +5012,9 @@ dependencies = [ [[package]] name = "openssl-sys" -version = "0.9.93" +version = "0.9.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db4d56a4c0478783083cfafcc42493dd4a981d41669da64b4572a2a089b51b1d" +checksum = "c3eaad34cdd97d81de97964fc7f29e2d104f483840d906ef56daa1912338460b" dependencies = [ "cc", "libc", @@ -5049,6 +5066,33 @@ version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a86ed3f5f244b372d6b1a00b72ef7f8876d0bc6a78a4c9985c53614041512063" +[[package]] +name = "ort" +version = "2.0.0-alpha.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a094a17bfe4f9eb561bfdf4454b8d0f6f89deaf9a5a572a1ef29c29ce708627" +dependencies = [ + "half", + "libloading 0.8.1", + "ndarray", + "once_cell", + "ort-sys", + "thiserror", + "tracing", +] + +[[package]] +name = "ort-sys" +version = "2.0.0-alpha.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dee05e7997a1cd9c0dc63fb9ddf79543efe646d9398089bf5b0b95e7e9441984" +dependencies = [ + "flate2", + "sha2 0.10.8", + "tar", + "ureq", +] + [[package]] name = "os_info" version = "3.7.0" @@ -6245,6 +6289,12 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9" +[[package]] +name = "rawpointer" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" + [[package]] name = "rayon" version = "1.8.0" @@ -6301,15 +6351,6 @@ dependencies = [ "bitflags 1.3.2", ] -[[package]] -name = "redox_syscall" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" -dependencies = [ - "bitflags 1.3.2", -] - [[package]] name = "redox_syscall" version = "0.4.1" @@ -6524,9 +6565,9 @@ dependencies = [ [[package]] name = "ring" -version = "0.17.6" +version = "0.17.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "684d5e6e18f669ccebf64a92236bb7db9a34f07be010e3627368182027180866" +checksum = "688c63d65483050968b2a8937f7995f443e27041a0f7700aa59b0822aedebb74" dependencies = [ "cc", "getrandom 0.2.11", @@ -6692,7 +6733,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "629648aced5775d558af50b2b4c7b02983a04b312126d45eeead26e7caa498b9" dependencies = [ "log", - "ring 0.17.6", + "ring 0.17.7", "rustls-webpki", "sct", ] @@ -6703,7 +6744,7 @@ version = "0.101.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" dependencies = [ - "ring 0.17.6", + "ring 0.17.7", "untrusted 0.9.0", ] @@ -6849,10 +6890,38 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" dependencies = [ - "ring 0.17.6", + "ring 0.17.7", "untrusted 0.9.0", ] +[[package]] +name = "sd-ai" +version = "0.1.0" +dependencies = [ + "async-channel", + "chrono", + "futures", + "futures-concurrency", + "half", + "image", + "ndarray", + "once_cell", + "ort", + "prisma-client-rust", + "reqwest", + "rmp-serde", + "sd-file-path-helper", + "sd-prisma", + "sd-utils", + "serde", + "thiserror", + "tokio", + "tokio-stream", + "tracing", + "url", + "uuid", +] + [[package]] name = "sd-cache" version = "0.0.0" @@ -6912,7 +6981,7 @@ dependencies = [ "http-range", "image", "int-enum", - "itertools 0.11.0", + "itertools 0.12.0", "mini-moka", "normpath", "notify", @@ -6927,12 +6996,14 @@ dependencies = [ "rmp-serde", "rmpv", "rspc", + "sd-ai", "sd-cache", "sd-cloud-api", "sd-core-sync", "sd-crypto", "sd-ffmpeg", "sd-file-ext", + "sd-file-path-helper", "sd-images", "sd-media-metadata", "sd-p2p", @@ -6962,7 +7033,6 @@ dependencies = [ "tracing-test", "uuid", "webp", - "winapi-util", ] [[package]] @@ -7091,9 +7161,26 @@ dependencies = [ "serde_json", "specta", "strum", + "strum_macros", "tokio", ] +[[package]] +name = "sd-file-path-helper" +version = "0.1.0" +dependencies = [ + "chrono", + "prisma-client-rust", + "regex", + "sd-prisma", + "sd-utils", + "serde", + "thiserror", + "tokio", + "tracing", + "winapi-util", +] + [[package]] name = "sd-images" version = "0.0.0" @@ -7164,7 +7251,7 @@ version = "0.1.0" dependencies = [ "base64 0.21.5", "ed25519-dalek", - "flume 0.10.14", + "flume", "futures-core", "if-watch", "libp2p", @@ -7235,6 +7322,10 @@ dependencies = [ name = "sd-utils" version = "0.1.0" dependencies = [ + "prisma-client-rust", + "rspc", + "sd-prisma", + "thiserror", "uuid", ] @@ -7839,11 +7930,11 @@ dependencies = [ [[package]] name = "sqlformat" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b7b278788e7be4d0d29c0f39497a0eef3fba6bbc8e70d8bf7fde46edeaa9e85" +checksum = "ce81b7bd7c4493975347ef60d8c7e8b742d4694f4c49f93e0a12ea263938176c" dependencies = [ - "itertools 0.11.0", + "itertools 0.12.0", "nom", "unicode_categories", ] @@ -8967,9 +9058,9 @@ dependencies = [ [[package]] name = "try-lock" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "ttf-parser" @@ -9057,9 +9148,9 @@ dependencies = [ [[package]] name = "unicode-bidi" -version = "0.3.13" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" +checksum = "6f2528f27a9eb2b21e69c95319b30bd0efd85d09c379741b0f78ea1d86be2416" [[package]] name = "unicode-bidi-mirroring" @@ -9165,6 +9256,21 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +[[package]] +name = "ureq" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cdd25c339e200129fe4de81451814e5228c9b771d57378817d6117cc2b3f97" +dependencies = [ + "base64 0.21.5", + "log", + "once_cell", + "rustls", + "rustls-webpki", + "url", + "webpki-roots", +] + [[package]] name = "url" version = "2.5.0" @@ -9554,6 +9660,12 @@ dependencies = [ "libwebp-sys", ] +[[package]] +name = "webpki-roots" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1778a42e8b3b90bff8d0f5032bf22250792889a5cdc752aa0020c84abe3aaf10" + [[package]] name = "webview2-com" version = "0.19.1" @@ -10018,9 +10130,9 @@ checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" [[package]] name = "winnow" -version = "0.5.19" +version = "0.5.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "829846f3e3db426d4cee4510841b71a8e58aa2a76b1132579487ae430ccd9c7b" +checksum = "b67b5f0a4e7a27a64c651977932b9dc5667ca7fc31ac44b03ed37a0cf42fdfff" dependencies = [ "memchr", ] diff --git a/Cargo.toml b/Cargo.toml index 9e6a9a48f..a0ffbc95e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ edition = "2021" repository = "https://github.com/spacedriveapp/spacedrive" [workspace.dependencies] +# First party dependencies prisma-client-rust = { git = "https://github.com/spacedriveapp/prisma-client-rust", rev = "9f8ac122e8f2b2e4957b71f48a37e06565adba40", features = [ "rspc", "sqlite-create-many", @@ -47,10 +48,40 @@ tauri-specta = { version = "=2.0.0-rc.4" } swift-rs = { version = "1.0.6" } -tokio = { version = "1.34.0" } -uuid = { version = "1.5.0", features = ["v4", "serde"] } -serde = { version = "1.0" } -serde_json = { version = "1.0" } + +# Third party dependencies used by one or more of our crates +anyhow = "1.0.75" +async-channel = "2.0.0" +axum = "0.6.20" +base64 = "0.21.5" +blake3 = "1.5.0" +chrono = "0.4.31" +clap = "4.4.7" +futures = "0.3.29" +futures-concurrency = "7.4.3" +hex = "0.4.3" +http = "0.2.9" +image = "0.24.7" +normpath = "1.1.1" +once_cell = "1.18.0" +pin-project-lite = "0.2.13" +rand = "0.8.5" +rand_chacha = "0.3.1" +regex = "1.10.2" +reqwest = "0.11.22" +rmp-serde = "1.1.2" +serde = "1.0" +serde_json = "1.0" +strum = "0.25" +strum_macros = "0.25" +tempfile = "3.8.1" +thiserror = "1.0.50" +tokio = "1.34.0" +tokio-stream = "0.1.14" +tokio-util = "0.7.10" +uhlc = "=0.5.2" +uuid = "1.5.0" +webp = "0.2.6" [patch.crates-io] # Proper IOS Support diff --git a/apps/cli/Cargo.toml b/apps/cli/Cargo.toml index 15bb1cddb..58596b805 100644 --- a/apps/cli/Cargo.toml +++ b/apps/cli/Cargo.toml @@ -6,9 +6,11 @@ repository = { workspace = true } edition = { workspace = true } [dependencies] -indoc = "2.0.4" -clap = { version = "4.4.7", features = ["derive"] } -anyhow = "1.0.75" -hex = "0.4.3" sd-crypto = { path = "../../crates/crypto" } + +anyhow = { workspace = true } +clap = { workspace = true, features = ["derive"] } +hex = { workspace = true } tokio = { workspace = true, features = ["io-util", "rt-multi-thread"] } + +indoc = "2.0.4" diff --git a/apps/desktop/crates/linux/Cargo.toml b/apps/desktop/crates/linux/Cargo.toml index c8c7d77d9..19a19cba1 100644 --- a/apps/desktop/crates/linux/Cargo.toml +++ b/apps/desktop/crates/linux/Cargo.toml @@ -6,8 +6,8 @@ repository = { workspace = true } edition = { workspace = true } [dependencies] -libc = "0.2" tokio = { workspace = true, features = ["fs"] } +libc = "0.2" [target.'cfg(target_os = "linux")'.dependencies] # WARNING: gtk should follow the same version used by tauri diff --git a/apps/desktop/crates/windows/Cargo.toml b/apps/desktop/crates/windows/Cargo.toml index d98293462..7a9d717b3 100644 --- a/apps/desktop/crates/windows/Cargo.toml +++ b/apps/desktop/crates/windows/Cargo.toml @@ -6,8 +6,9 @@ repository = { workspace = true } edition = { workspace = true } [dependencies] -thiserror = "1.0.50" -normpath = "1.1.1" +normpath = { workspace = true } +thiserror = { workspace = true } + libc = "0.2" [target.'cfg(target_os = "windows")'.dependencies.windows] diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml index 998195927..ded582286 100644 --- a/apps/desktop/src-tauri/Cargo.toml +++ b/apps/desktop/src-tauri/Cargo.toml @@ -9,6 +9,28 @@ repository = { workspace = true } edition = { workspace = true } [dependencies] +sd-core = { path = "../../../core", features = [ + "ffmpeg", + "location-watcher", + "heif", +] } +sd-fda = { path = "../../../crates/fda" } +sd-prisma = { path = "../../../crates/prisma" } + +axum = { workspace = true, features = ["headers", "query"] } +futures = { workspace = true } +http = { workspace = true } +prisma-client-rust = { workspace = true } +rand = { workspace = true } +rspc = { workspace = true, features = ["tauri"] } +serde = { workspace = true } +specta = { workspace = true } +tokio = { workspace = true, features = ["sync"] } +tracing = { workspace = true } +tauri-specta = { workspace = true, features = ["typescript"] } +uuid = { workspace = true, features = ["serde"] } + +opener = { version = "0.6.1", features = ["reveal"] } tauri = { version = "1.5.3", features = [ "macos-private-api", "path-all", @@ -22,30 +44,9 @@ tauri = { version = "1.5.3", features = [ "native-tls-vendored", "tracing", ] } - -rspc = { workspace = true, features = ["tauri"] } -sd-core = { path = "../../../core", features = [ - "ffmpeg", - "location-watcher", - "heif", -] } -sd-fda = { path = "../../../crates/fda" } -tokio = { workspace = true, features = ["sync"] } -tracing = { workspace = true } -serde = "1.0.190" -http = "0.2.9" -opener = { version = "0.6.1", features = ["reveal"] } -specta = { workspace = true } -tauri-specta = { workspace = true, features = ["typescript"] } -uuid = { version = "1.5.0", features = ["serde"] } -futures = "0.3" -axum = { version = "0.6.20", features = ["headers", "query"] } -rand = "0.8.5" - -prisma-client-rust = { workspace = true } -sd-prisma = { path = "../../../crates/prisma" } tauri-plugin-window-state = "0.1.0" + [target.'cfg(target_os = "linux")'.dependencies] sd-desktop-linux = { path = "../crates/linux" } webkit2gtk = { version = "0.18.2", features = ["v2_2"] } @@ -61,5 +62,6 @@ webview2-com = "0.19.1" tauri-build = "1.5.0" [features] -default = ["custom-protocol"] +default = ["ai-models", "custom-protocol"] +ai-models = ["sd-core/ai"] custom-protocol = ["tauri/custom-protocol"] diff --git a/apps/desktop/src-tauri/build.rs b/apps/desktop/src-tauri/build.rs index 106b71239..b410d8f86 100644 --- a/apps/desktop/src-tauri/build.rs +++ b/apps/desktop/src-tauri/build.rs @@ -1,3 +1,12 @@ fn main() { + #[cfg(all(not(target_os = "windows"), feature = "ai-models"))] + // This is required because libonnxruntime.so is incorrectly built with the Initial Executable (IE) thread-Local storage access model by zig + // https://docs.oracle.com/cd/E23824_01/html/819-0690/chapter8-20.html + // https://github.com/ziglang/zig/issues/16152 + // https://github.com/ziglang/zig/pull/17702 + // Due to this, the linker will fail to dlopen libonnxruntime.so because it runs out of the static TLS space reserved after initial load + // To workaround this problem libonnxruntime.so is added as a dependency to the binaries, which makes the linker allocate its TLS space during initial load + println!("cargo:rustc-link-lib=onnxruntime"); + tauri_build::build(); } diff --git a/apps/desktop/src-tauri/src/file.rs b/apps/desktop/src-tauri/src/file.rs index 828b635ad..4a2f3525d 100644 --- a/apps/desktop/src-tauri/src/file.rs +++ b/apps/desktop/src-tauri/src/file.rs @@ -1,3 +1,6 @@ +use sd_core::Node; +use sd_prisma::prisma::{file_path, location}; + use std::{ collections::{BTreeSet, HashMap, HashSet}, hash::{Hash, Hasher}, @@ -6,10 +9,6 @@ use std::{ }; use futures::future::join_all; -use sd_core::{ - prisma::{file_path, location}, - Node, -}; use serde::Serialize; use specta::Type; use tauri::async_runtime::spawn_blocking; diff --git a/apps/desktop/src-tauri/tauri.conf.json b/apps/desktop/src-tauri/tauri.conf.json index 1aaed648c..0efc30047 100644 --- a/apps/desktop/src-tauri/tauri.conf.json +++ b/apps/desktop/src-tauri/tauri.conf.json @@ -11,9 +11,6 @@ "tauri": { "macOSPrivateApi": true, "bundle": { - "appimage": { - "bundleMediaFramework": true - }, "active": true, "targets": ["deb", "msi", "dmg", "updater"], "identifier": "com.spacedrive.desktop", @@ -24,27 +21,32 @@ "icons/icon.icns", "icons/icon.ico" ], - "resources": [], + "resources": {}, "externalBin": [], "copyright": "Spacedrive Technology Inc.", "shortDescription": "File explorer from the future.", "longDescription": "Cross-platform universal file explorer, powered by an open-source virtual distributed filesystem.", "deb": { + "files": { + "/usr/share/models/yolov8s.onnx": "../../.deps/models/yolov8s.onnx" + }, "depends": ["libc6"] }, "macOS": { - "frameworks": ["../../.deps/Spacedrive.framework"], "minimumSystemVersion": "10.15", - "exceptionDomain": "", - "entitlements": null + "exceptionDomain": null, + "entitlements": null, + "frameworks": ["../../.deps/Spacedrive.framework"] }, "windows": { "certificateThumbprint": null, + "webviewInstallMode": { "type": "embedBootstrapper", "silent": true }, "digestAlgorithm": "sha256", "timestampUrl": "", "wix": { - "bannerPath": "icons/WindowsBanner.bmp", - "dialogImagePath": "icons/WindowsDialogImage.bmp" + "enableElevatedUpdateTask": true, + "dialogImagePath": "icons/WindowsDialogImage.bmp", + "bannerPath": "icons/WindowsBanner.bmp" } } }, diff --git a/apps/mobile/modules/sd-core/core/Cargo.toml b/apps/mobile/modules/sd-core/core/Cargo.toml index 62a0ead34..a32031d2f 100644 --- a/apps/mobile/modules/sd-core/core/Cargo.toml +++ b/apps/mobile/modules/sd-core/core/Cargo.toml @@ -7,14 +7,16 @@ repository = { workspace = true } edition = { workspace = true } [dependencies] -once_cell = "1.18.0" sd-core = { path = "../../../../../core", features = [ "mobile", ], default-features = false } + +futures = { workspace = true } +once_cell = { workspace = true } rspc = { workspace = true } serde_json = { workspace = true } tokio = { workspace = true } -futures = "0.3.29" tracing = { workspace = true } + futures-channel = "0.3.29" futures-locks = "0.7.1" diff --git a/apps/mobile/src/App.tsx b/apps/mobile/src/App.tsx index 1872b14e5..5462654d4 100644 --- a/apps/mobile/src/App.tsx +++ b/apps/mobile/src/App.tsx @@ -23,7 +23,6 @@ import { createCache, initPlausible, LibraryContextProvider, - NotificationContextProvider, P2PContextProvider, RspcProvider, useBridgeQuery, @@ -138,9 +137,7 @@ function AppContainer() { - - - + diff --git a/apps/mobile/src/components/modal/tag/CreateTagModal.tsx b/apps/mobile/src/components/modal/tag/CreateTagModal.tsx index 020d46295..2f68d60cd 100644 --- a/apps/mobile/src/components/modal/tag/CreateTagModal.tsx +++ b/apps/mobile/src/components/modal/tag/CreateTagModal.tsx @@ -2,7 +2,7 @@ import { useQueryClient } from '@tanstack/react-query'; import { forwardRef, useEffect, useState } from 'react'; import { Pressable, Text, View } from 'react-native'; import ColorPicker from 'react-native-wheel-color-picker'; -import { useLibraryMutation, usePlausibleEvent } from '@sd/client'; +import { ToastDefautlColor, useLibraryMutation, usePlausibleEvent } from '@sd/client'; import { FadeInAnimation } from '~/components/animation/layout'; import { ModalInput } from '~/components/form/Input'; import { Modal, ModalRef } from '~/components/layout/Modal'; @@ -16,7 +16,7 @@ const CreateTagModal = forwardRef((_, ref) => { const modalRef = useForwardedRef(ref); const [tagName, setTagName] = useState(''); - const [tagColor, setTagColor] = useState('#A717D9'); + const [tagColor, setTagColor] = useState(ToastDefautlColor); const [showPicker, setShowPicker] = useState(false); // TODO: Use react-hook-form? @@ -30,7 +30,7 @@ const CreateTagModal = forwardRef((_, ref) => { onSuccess: () => { // Reset form setTagName(''); - setTagColor('#A717D9'); + setTagColor(ToastDefautlColor); setShowPicker(false); queryClient.invalidateQueries(['tags.list']); @@ -61,7 +61,7 @@ const CreateTagModal = forwardRef((_, ref) => { onDismiss={() => { // Resets form onDismiss setTagName(''); - setTagColor('#A717D9'); + setTagColor(ToastDefautlColor); setShowPicker(false); }} showCloseButton diff --git a/apps/server/Cargo.toml b/apps/server/Cargo.toml index 5c1b49ebe..df0153ab8 100644 --- a/apps/server/Cargo.toml +++ b/apps/server/Cargo.toml @@ -7,6 +7,7 @@ edition = { workspace = true } [features] assets = [] +ai-models = ["sd-core/ai"] [dependencies] sd-core = { path = "../../core", features = [ @@ -14,10 +15,12 @@ sd-core = { path = "../../core", features = [ "location-watcher", "heif", ] } + +axum = { workspace = true } +http = { workspace = true } rspc = { workspace = true, features = ["axum"] } -axum = "0.6.20" tokio = { workspace = true, features = ["sync", "rt-multi-thread", "signal"] } tracing = { workspace = true } -http = "0.2.9" + include_dir = "0.7.3" mime_guess = "2.0.4" diff --git a/apps/server/build.rs b/apps/server/build.rs new file mode 100644 index 000000000..94e67c9f0 --- /dev/null +++ b/apps/server/build.rs @@ -0,0 +1,10 @@ +fn main() { + #[cfg(all(not(target_os = "windows"), feature = "ai-models"))] + // This is required because libonnxruntime.so is incorrectly built with the Initial Executable (IE) thread-Local storage access model by zig + // https://docs.oracle.com/cd/E23824_01/html/819-0690/chapter8-20.html + // https://github.com/ziglang/zig/issues/16152 + // https://github.com/ziglang/zig/pull/17702 + // Due to this, the linker will fail to dlopen libonnxruntime.so because it runs out of the static TLS space reserved after initial load + // To workaround this problem libonnxruntime.so is added as a dependency to the binaries, which makes the linker allocate its TLS space during initial load + println!("cargo:rustc-link-lib=onnxruntime"); +} diff --git a/core/Cargo.toml b/core/Cargo.toml index c2233aade..644c16ad8 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -16,31 +16,52 @@ mobile = [] ffmpeg = ["dep:sd-ffmpeg"] location-watcher = ["dep:notify"] heif = ["sd-images/heif"] +ai = ["dep:sd-ai"] [dependencies] -sd-media-metadata = { path = "../crates/media-metadata" } -sd-prisma = { path = "../crates/prisma" } -sd-ffmpeg = { path = "../crates/ffmpeg", optional = true } +# Sub-crates +sd-cache = { path = "../crates/cache" } +sd-core-sync = { path = "./crates/sync" } +# sd-cloud-api = { path = "../crates/cloud-api" } sd-crypto = { path = "../crates/crypto", features = [ "rspc", "specta", "serde", "keymanager", ] } -sd-cache = { path = "../crates/cache" } +sd-file-path-helper = { path = "../crates/file-path-helper" } +sd-ffmpeg = { path = "../crates/ffmpeg", optional = true } +sd-file-ext = { path = "../crates/file-ext" } sd-images = { path = "../crates/images", features = [ "rspc", "serde", "specta", ] } -sd-file-ext = { path = "../crates/file-ext" } -sd-sync = { path = "../crates/sync" } +sd-media-metadata = { path = "../crates/media-metadata" } sd-p2p = { path = "../crates/p2p", features = ["specta", "serde"] } +sd-prisma = { path = "../crates/prisma" } +sd-ai = { path = "../crates/ai", optional = true } +sd-sync = { path = "../crates/sync" } sd-utils = { path = "../crates/utils" } -# sd-cloud-api = { path = "../crates/cloud-api" } +sd-cloud-api = { version = "0.1.0", path = "../crates/cloud-api" } -sd-core-sync = { path = "./crates/sync" } +# Workspace dependencies +async-channel = { workspace = true } +axum = { workspace = true } +base64 = { workspace = true } +blake3 = { workspace = true } +chrono = { workspace = true, features = ["serde"] } +futures = { workspace = true } +futures-concurrency = { workspace = true } +image = { workspace = true } +normpath = { workspace = true, features = ["localization"] } +once_cell = { workspace = true } +pin-project-lite = { workspace = true } +prisma-client-rust = { workspace = true } +regex = { workspace = true } +reqwest = { workspace = true, features = ["json", "native-tls-vendored"] } +rmp-serde = { workspace = true } rspc = { workspace = true, features = [ "uuid", "chrono", @@ -48,8 +69,13 @@ rspc = { workspace = true, features = [ "alpha", "unstable", ] } -prisma-client-rust = { workspace = true } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } specta = { workspace = true } +strum = { workspace = true, features = ["derive"] } +strum_macros = { workspace = true } +tempfile = { workspace = true } +thiserror = { workspace = true } tokio = { workspace = true, features = [ "sync", "rt-multi-thread", @@ -58,74 +84,54 @@ tokio = { workspace = true, features = [ "time", "process", ] } -serde = { version = "1.0", features = ["derive"] } -chrono = { version = "0.4.31", features = ["serde"] } -serde_json = { workspace = true } -serde_repr = "0.1" -futures = "0.3" -rmp-serde = "^1.1.2" -rmpv = "^1.0.1" -blake3 = "1.5.0" -hostname = "0.3.1" -uuid = { workspace = true } -sysinfo = "0.29.10" -thiserror = "1.0.50" -async-trait = "^0.1.74" -image = "0.24.7" -webp = "0.2.6" +tokio-stream = { workspace = true, features = ["fs"] } +tokio-util = { workspace = true, features = ["io"] } tracing = { workspace = true } -tracing-subscriber = { workspace = true, features = ["env-filter"] } tracing-appender = { workspace = true } +tracing-subscriber = { workspace = true, features = ["env-filter"] } +uuid = { workspace = true, features = ["v4", "serde"] } +webp = { workspace = true } + + +# Specific Core dependencies +async-recursion = "1.0.5" async-stream = "0.3.5" -once_cell = "1.18.0" +async-trait = "^0.1.74" +bytes = "1.5.0" ctor = "0.2.5" +directories = "5.0.1" +flate2 = "1.0.28" globset = { version = "^0.4.13", features = ["serde1"] } -itertools = "^0.11.0" +hostname = "0.3.1" +http-body = "0.4.5" http-range = "0.1.5" +int-enum = "0.5.0" +itertools = "0.12.0" mini-moka = "0.10.2" -serde_with = "3.4.0" notify = { version = "=5.2.0", default-features = false, features = [ "macos_fsevent", ], optional = true } -static_assertions = "1.1.0" +rmpv = "^1.0.1" serde-hashkey = "0.4.5" -normpath = { version = "1.1.1", features = ["localization"] } -strum = { version = "0.25", features = ["derive"] } -strum_macros = "0.25" -regex = "1.10.2" -int-enum = "0.5.0" -tokio-stream = { version = "0.1.14", features = ["fs"] } -futures-concurrency = "7.4.3" -async-channel = "2.0.0" -tokio-util = { version = "0.7.10", features = ["io"] } +serde_repr = "0.1" +serde_with = "3.4.0" slotmap = "1.0.6" -flate2 = "1.0.28" +static_assertions = "1.1.0" +sysinfo = "0.29.10" tar = "0.4.40" -tempfile = "^3.8.1" -axum = "0.6.20" -http-body = "0.4.5" -pin-project-lite = "0.2.13" -bytes = "1.5.0" -reqwest = { version = "0.11.22", features = ["json", "native-tls-vendored"] } -directories = "5.0.1" -async-recursion = "1.0.5" -base64 = "0.21.5" -sd-cloud-api = { version = "0.1.0", path = "../crates/cloud-api" } # Override features of transitive dependencies [dependencies.openssl] -version = "=0.10.57" +version = "=0.10.61" features = ["vendored"] [dependencies.openssl-sys] -version = "=0.9.93" +version = "=0.9.97" features = ["vendored"] +# Platform-specific dependencies [target.'cfg(target_os = "macos")'.dependencies] plist = "1" -[target.'cfg(windows)'.dependencies.winapi-util] -version = "0.1.6" - [dev-dependencies] tracing-test = "^0.2.4" aovec = "1.1.0" diff --git a/core/crates/sync/Cargo.toml b/core/crates/sync/Cargo.toml index 703812a95..c564aa56c 100644 --- a/core/crates/sync/Cargo.toml +++ b/core/crates/sync/Cargo.toml @@ -17,4 +17,4 @@ serde_json = { workspace = true } tokio = { workspace = true } uuid = { workspace = true } tracing = { workspace = true } -uhlc = "=0.5.2" +uhlc = { workspace = true } diff --git a/core/crates/sync/src/manager.rs b/core/crates/sync/src/manager.rs index 4be17fde9..4730c6eab 100644 --- a/core/crates/sync/src/manager.rs +++ b/core/crates/sync/src/manager.rs @@ -1,12 +1,14 @@ use crate::{ db_operation::*, ingest, relation_op_db, shared_op_db, SharedState, SyncMessage, NTP64, }; + use sd_prisma::prisma::{ cloud_relation_operation, cloud_shared_operation, instance, relation_operation, shared_operation, PrismaClient, SortOrder, }; use sd_sync::{CRDTOperation, CRDTOperationType, OperationFactory}; use sd_utils::uuid_to_bytes; + use std::{ cmp::Ordering, collections::HashMap, @@ -16,6 +18,7 @@ use std::{ Arc, }, }; + use tokio::sync::{broadcast, RwLock}; use uhlc::{HLCBuilder, HLC}; use uuid::Uuid; diff --git a/core/prisma/migrations/20231204174640_update_tags_and_labels/migration.sql b/core/prisma/migrations/20231204174640_update_tags_and_labels/migration.sql new file mode 100644 index 000000000..545148522 --- /dev/null +++ b/core/prisma/migrations/20231204174640_update_tags_and_labels/migration.sql @@ -0,0 +1,39 @@ +/* + Warnings: + + - You are about to drop the column `redundancy_goal` on the `tag` table. All the data in the column will be lost. + - Made the column `name` on table `label` required. This step will fail if there are existing NULL values in that column. + +*/ +-- AlterTable +ALTER TABLE "saved_search" ADD COLUMN "search" TEXT; + +-- RedefineTables +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_label" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "pub_id" BLOB NOT NULL, + "name" TEXT NOT NULL, + "date_created" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "date_modified" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); +INSERT INTO "new_label" ("date_created", "date_modified", "id", "name", "pub_id") SELECT "date_created", "date_modified", "id", "name", "pub_id" FROM "label"; +DROP TABLE "label"; +ALTER TABLE "new_label" RENAME TO "label"; +CREATE UNIQUE INDEX "label_pub_id_key" ON "label"("pub_id"); +CREATE UNIQUE INDEX "label_name_key" ON "label"("name"); +CREATE TABLE "new_tag" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "pub_id" BLOB NOT NULL, + "name" TEXT, + "color" TEXT, + "is_hidden" BOOLEAN, + "date_created" DATETIME, + "date_modified" DATETIME +); +INSERT INTO "new_tag" ("color", "date_created", "date_modified", "id", "name", "pub_id") SELECT "color", "date_created", "date_modified", "id", "name", "pub_id" FROM "tag"; +DROP TABLE "tag"; +ALTER TABLE "new_tag" RENAME TO "tag"; +CREATE UNIQUE INDEX "tag_pub_id_key" ON "tag"("pub_id"); +PRAGMA foreign_key_check; +PRAGMA foreign_keys=ON; diff --git a/core/prisma/schema.prisma b/core/prisma/schema.prisma index 500aefbd4..9c501068f 100644 --- a/core/prisma/schema.prisma +++ b/core/prisma/schema.prisma @@ -337,8 +337,7 @@ model Tag { name String? color String? - // Enum: ?? - redundancy_goal Int? + is_hidden Boolean? // user hidden entire tag date_created DateTime? date_modified DateTime? @@ -367,7 +366,7 @@ model TagOnObject { model Label { id Int @id @default(autoincrement()) pub_id Bytes @unique - name String? + name String @unique date_created DateTime @default(now()) date_modified DateTime @default(now()) @@ -379,11 +378,11 @@ model Label { model LabelOnObject { date_created DateTime @default(now()) - label_id Int - label Label @relation(fields: [label_id], references: [id], onDelete: Restrict) + label_id Int + label Label @relation(fields: [label_id], references: [id], onDelete: Restrict) - object_id Int - object Object @relation(fields: [object_id], references: [id], onDelete: Restrict) + object_id Int + object Object @relation(fields: [object_id], references: [id], onDelete: Restrict) @@id([label_id, object_id]) @@map("label_on_object") diff --git a/core/src/api/auth.rs b/core/src/api/auth.rs index 187264a09..0b023345a 100644 --- a/core/src/api/auth.rs +++ b/core/src/api/auth.rs @@ -1,3 +1,5 @@ +use crate::util::http::ensure_response; + use std::time::Duration; use reqwest::{Response, StatusCode}; @@ -5,8 +7,6 @@ use rspc::alpha::AlphaRouter; use serde::{de::DeserializeOwned, Deserialize, Serialize}; use specta::Type; -use crate::util::http::ensure_response; - use super::{Ctx, R}; async fn parse_json_body(response: Response) -> Result { diff --git a/core/src/api/backups.rs b/core/src/api/backups.rs index 412d257c5..aa3b69956 100644 --- a/core/src/api/backups.rs +++ b/core/src/api/backups.rs @@ -1,3 +1,11 @@ +use crate::{ + invalidate_query, + library::{Library, LibraryManagerError}, + Node, +}; + +use sd_utils::error::FileIOError; + use std::{ cmp, path::{Path, PathBuf}, @@ -25,13 +33,6 @@ use tokio::{ use tracing::{error, info}; use uuid::Uuid; -use crate::{ - invalidate_query, - library::{Library, LibraryManagerError}, - util::error::FileIOError, - Node, -}; - use super::{utils::library, Ctx, R}; pub(crate) fn mount() -> AlphaRouter { diff --git a/core/src/api/cloud.rs b/core/src/api/cloud.rs index d942c0701..204c4fb62 100644 --- a/core/src/api/cloud.rs +++ b/core/src/api/cloud.rs @@ -1,13 +1,29 @@ -use super::{utils::library, Ctx, R}; use crate::{api::libraries::LibraryConfigWrapped, invalidate_query, library::LibraryName}; + +use reqwest::Response; use rspc::alpha::AlphaRouter; +use serde::de::DeserializeOwned; + use uuid::Uuid; +use super::{utils::library, Ctx, R}; + +#[allow(unused)] +async fn parse_json_body(response: Response) -> Result { + response.json().await.map_err(|_| { + rspc::Error::new( + rspc::ErrorCode::InternalServerError, + "JSON conversion failed".to_string(), + ) + }) +} + pub(crate) fn mount() -> AlphaRouter { R.router().merge("library.", library::mount()) } mod library { + use super::*; pub fn mount() -> AlphaRouter { diff --git a/core/src/api/ephemeral_files.rs b/core/src/api/ephemeral_files.rs index 504f24a3f..b79c761e5 100644 --- a/core/src/api/ephemeral_files.rs +++ b/core/src/api/ephemeral_files.rs @@ -2,18 +2,18 @@ use crate::{ api::utils::library, invalidate_query, library::Library, - location::file_path_helper::IsolatedFilePathData, object::{ fs::{error::FileSystemJobsError, find_available_filename_for_duplicate}, media::media_data_extractor::{ can_extract_media_data_for_image, extract_media_data, MediaDataError, }, }, - util::error::FileIOError, }; use sd_file_ext::extensions::ImageExtension; +use sd_file_path_helper::IsolatedFilePathData; use sd_media_metadata::MediaMetadata; +use sd_utils::error::FileIOError; use std::{ffi::OsStr, path::PathBuf, str::FromStr}; diff --git a/core/src/api/files.rs b/core/src/api/files.rs index fbe75fde8..642d9ea6c 100644 --- a/core/src/api/files.rs +++ b/core/src/api/files.rs @@ -3,12 +3,7 @@ use crate::{ invalidate_query, job::Job, library::Library, - location::{ - file_path_helper::{ - file_path_to_isolate, file_path_to_isolate_with_id, FilePathError, IsolatedFilePathData, - }, - get_location_path_from_location_id, LocationError, - }, + location::{get_location_path_from_location_id, LocationError}, object::{ fs::{ copy::FileCopierJobInit, cut::FileCutterJobInit, delete::FileDeleterJobInit, @@ -17,14 +12,17 @@ use crate::{ }, media::media_data_image_from_prisma_data, }, - prisma::{file_path, location, object}, - util::{db::maybe_missing, error::FileIOError}, }; use sd_cache::{CacheNode, Model, NormalisedResult, Reference}; use sd_file_ext::kind::ObjectKind; +use sd_file_path_helper::{ + file_path_to_isolate, file_path_to_isolate_with_id, FilePathError, IsolatedFilePathData, +}; use sd_images::ConvertableExtension; use sd_media_metadata::MediaMetadata; +use sd_prisma::prisma::{file_path, location, object}; +use sd_utils::{db::maybe_missing, error::FileIOError}; use std::{ ffi::OsString, diff --git a/core/src/api/jobs.rs b/core/src/api/jobs.rs index ff5270efd..9f0db5dae 100644 --- a/core/src/api/jobs.rs +++ b/core/src/api/jobs.rs @@ -6,9 +6,10 @@ use crate::{ file_identifier::file_identifier_job::FileIdentifierJobInit, media::MediaProcessorJobInit, validation::validator_job::ObjectValidatorJobInit, }, - prisma::{job, location, SortOrder}, }; +use sd_prisma::prisma::{job, location, SortOrder}; + use std::{ collections::{hash_map::Entry, BTreeMap, HashMap, VecDeque}, path::PathBuf, @@ -246,6 +247,39 @@ pub(crate) fn mount() -> AlphaRouter { location, sub_path: Some(path), regenerate_thumbnails: regenerate, + regenerate_labels: false, + }) + .spawn(&node, &library) + .await + .map_err(Into::into) + }, + ) + }) + .procedure("generateLabelsForLocation", { + #[derive(Type, Deserialize)] + pub struct GenerateLabelsForLocationArgs { + pub id: location::id::Type, + pub path: PathBuf, + #[serde(default)] + pub regenerate: bool, + } + + R.with2(library()).mutation( + |(node, library), + GenerateLabelsForLocationArgs { + id, + path, + regenerate, + }: GenerateLabelsForLocationArgs| async move { + let Some(location) = find_location(&library, id).exec().await? else { + return Err(LocationError::IdNotFound(id).into()); + }; + + Job::new(MediaProcessorJobInit { + location, + sub_path: Some(path), + regenerate_thumbnails: false, + regenerate_labels: regenerate, }) .spawn(&node, &library) .await diff --git a/core/src/api/labels.rs b/core/src/api/labels.rs new file mode 100644 index 000000000..fb31fd5a0 --- /dev/null +++ b/core/src/api/labels.rs @@ -0,0 +1,85 @@ +use crate::{invalidate_query, library::Library}; + +use sd_prisma::prisma::{label, label_on_object, object}; + +use std::collections::BTreeMap; + +use rspc::alpha::AlphaRouter; + +use super::{utils::library, Ctx, R}; + +pub(crate) fn mount() -> AlphaRouter { + R.router() + .procedure("list", { + R.with2(library()).query(|(_, library), _: ()| async move { + Ok(library.db.label().find_many(vec![]).exec().await?) + }) + }) + .procedure("getForObject", { + R.with2(library()) + .query(|(_, library), object_id: i32| async move { + Ok(library + .db + .label() + .find_many(vec![label::label_objects::some(vec![ + label_on_object::object_id::equals(object_id), + ])]) + .exec() + .await?) + }) + }) + .procedure("getWithObjects", { + R.with2(library()).query( + |(_, library), object_ids: Vec| async move { + let Library { db, .. } = library.as_ref(); + let labels_with_objects = db + .label() + .find_many(vec![label::label_objects::some(vec![ + label_on_object::object_id::in_vec(object_ids.clone()), + ])]) + .select(label::select!({ + id + label_objects(vec![label_on_object::object_id::in_vec(object_ids.clone())]): select { + date_created + object: select { + id + } + } + })) + .exec() + .await?; + Ok(labels_with_objects + .into_iter() + .map(|label| (label.id, label.label_objects)) + .collect::>()) + }, + ) + }) + .procedure("get", { + R.with2(library()) + .query(|(_, library), label_id: i32| async move { + Ok(library + .db + .label() + .find_unique(label::id::equals(label_id)) + .exec() + .await?) + }) + }) + .procedure( + "delete", + R.with2(library()) + .mutation(|(_, library), label_id: i32| async move { + library + .db + .label() + .delete(label::id::equals(label_id)) + .exec() + .await?; + + invalidate_query!(library, "labels.list"); + + Ok(()) + }), + ) +} diff --git a/core/src/api/locations.rs b/core/src/api/locations.rs index 9385cb277..14acbb0ea 100644 --- a/core/src/api/locations.rs +++ b/core/src/api/locations.rs @@ -11,19 +11,22 @@ use crate::{ }, object::file_identifier::file_identifier_job::FileIdentifierJobInit, p2p::PeerMetadata, - prisma::{file_path, indexer_rule, indexer_rules_in_location, location, object, SortOrder}, util::AbortOnDrop, }; +use sd_cache::{CacheNode, Model, Normalise, NormalisedResult, NormalisedResults, Reference}; +use sd_prisma::prisma::{ + file_path, indexer_rule, indexer_rules_in_location, location, object, SortOrder, +}; + use std::path::{Path, PathBuf}; use chrono::{DateTime, FixedOffset, Utc}; use directories::UserDirs; use rspc::{self, alpha::AlphaRouter, ErrorCode}; -use sd_cache::{CacheNode, Model, Normalise, NormalisedResult, NormalisedResults, Reference}; use serde::{Deserialize, Serialize}; use specta::Type; -use tracing::error; +use tracing::{debug, error}; use super::{utils::library, Ctx, R}; @@ -371,7 +374,7 @@ pub(crate) fn mount() -> AlphaRouter { reidentify_objects, }| async move { if reidentify_objects { - library + let count = library .db .file_path() .update_many( @@ -388,6 +391,8 @@ pub(crate) fn mount() -> AlphaRouter { .exec() .await?; + debug!("Disconnected {count} file paths from objects"); + library.orphan_remover.invoke().await; } diff --git a/core/src/api/mod.rs b/core/src/api/mod.rs index 3536cad10..ed255c19b 100644 --- a/core/src/api/mod.rs +++ b/core/src/api/mod.rs @@ -1,14 +1,15 @@ -use std::sync::{atomic::Ordering, Arc}; - use crate::{ invalidate_query, job::JobProgressEvent, node::config::{NodeConfig, NodePreferences}, Node, }; + use sd_cache::patch_typedef; use sd_p2p::P2PStatus; +use std::sync::{atomic::Ordering, Arc}; + use itertools::Itertools; use rspc::{alpha::Rspc, Config, ErrorCode}; use serde::{Deserialize, Serialize}; @@ -23,8 +24,10 @@ mod ephemeral_files; mod files; mod jobs; mod keys; +mod labels; mod libraries; pub mod locations; +mod models; mod nodes; pub mod notifications; mod p2p; @@ -92,6 +95,7 @@ pub struct SanitisedNodeConfig { pub p2p_port: Option, pub features: Vec, pub preferences: NodePreferences, + pub image_labeler_version: Option, } impl From for SanitisedNodeConfig { @@ -103,6 +107,7 @@ impl From for SanitisedNodeConfig { p2p_port: value.p2p.port, features: value.features, preferences: value.preferences, + image_labeler_version: value.image_labeler_version, } } } @@ -194,6 +199,7 @@ pub(crate) fn mount() -> Arc { .merge("library.", libraries::mount()) .merge("volumes.", volumes::mount()) .merge("tags.", tags::mount()) + .merge("labels.", labels::mount()) // .merge("categories.", categories::mount()) // .merge("keys.", keys::mount()) .merge("locations.", locations::mount()) @@ -201,6 +207,7 @@ pub(crate) fn mount() -> Arc { .merge("files.", files::mount()) .merge("jobs.", jobs::mount()) .merge("p2p.", p2p::mount()) + .merge("models.", models::mount()) .merge("nodes.", nodes::mount()) .merge("sync.", sync::mount()) .merge("preferences.", preferences::mount()) diff --git a/core/src/api/models.rs b/core/src/api/models.rs new file mode 100644 index 000000000..3671d529d --- /dev/null +++ b/core/src/api/models.rs @@ -0,0 +1,23 @@ +use rspc::alpha::AlphaRouter; + +use super::{Ctx, R}; + +pub(crate) fn mount() -> AlphaRouter { + R.router().procedure("image_detection.list", { + R.query( + |_, _: ()| -> std::result::Result, rspc::Error> { + #[cfg(not(feature = "ai"))] + return Err(rspc::Error::new( + rspc::ErrorCode::MethodNotSupported, + "AI feature is not aviailable".to_string(), + )); + + #[cfg(feature = "ai")] + { + use sd_ai::image_labeler::{Model, YoloV8}; + Ok(YoloV8::versions()) + } + }, + ) + }) +} diff --git a/core/src/api/nodes.rs b/core/src/api/nodes.rs index 67fc42a16..1736d4caf 100644 --- a/core/src/api/nodes.rs +++ b/core/src/api/nodes.rs @@ -1,6 +1,6 @@ -use crate::{invalidate_query, prisma::location, util::MaybeUndefined}; +use crate::{invalidate_query, util::MaybeUndefined}; -use sd_prisma::prisma::instance; +use sd_prisma::prisma::{instance, location}; use rspc::{alpha::AlphaRouter, ErrorCode}; use serde::Deserialize; @@ -16,8 +16,9 @@ pub(crate) fn mount() -> AlphaRouter { #[derive(Deserialize, Type)] pub struct ChangeNodeNameArgs { pub name: Option, - pub p2p_enabled: Option, pub p2p_port: MaybeUndefined, + pub p2p_enabled: Option, + pub image_labeler_version: Option, } R.mutation(|node, args: ChangeNodeNameArgs| async move { if let Some(name) = &args.name { @@ -32,6 +33,9 @@ pub(crate) fn mount() -> AlphaRouter { let does_p2p_need_refresh = args.p2p_enabled.is_some() || args.p2p_port.is_defined(); + #[cfg(feature = "ai")] + let mut new_model = None; + node.config .write(|config| { if let Some(name) = args.name { @@ -43,6 +47,28 @@ pub(crate) fn mount() -> AlphaRouter { if let Some(v) = args.p2p_port.into() { config.p2p.port = v; } + + #[cfg(feature = "ai")] + if let Some(version) = args.image_labeler_version { + if config + .image_labeler_version + .as_ref() + .map(|node_version| version != *node_version) + .unwrap_or(true) + { + new_model = sd_ai::image_labeler::YoloV8::model(Some(&version)) + .map_err(|e| { + error!( + "Failed to crate image_detection model: '{}'; Error: {e:#?}", + &version, + ); + }) + .ok(); + if new_model.is_some() { + config.image_labeler_version = Some(version); + } + } + } }) .await .map_err(|err| { @@ -63,6 +89,34 @@ pub(crate) fn mount() -> AlphaRouter { invalidate_query!(node; node, "nodeState"); + #[cfg(feature = "ai")] + { + use super::notifications::{NotificationData, NotificationKind}; + + if let Some(model) = new_model { + let version = model.version().to_string(); + tokio::spawn(async move { + let notification = if let Err(e) = + node.image_labeller.change_model(model).await + { + NotificationData { + title: String::from("Failed to change image detection model"), + content: format!("Error: {e}"), + kind: NotificationKind::Error, + } + } else { + NotificationData { + title: String::from("Model download completed"), + content: format!("Sucessfuly loaded model: {version}"), + kind: NotificationKind::Success, + } + }; + + node.emit_notification(notification, None).await; + }); + } + } + Ok(()) }) }) diff --git a/core/src/api/notifications.rs b/core/src/api/notifications.rs index ec8fa8a13..f05854779 100644 --- a/core/src/api/notifications.rs +++ b/core/src/api/notifications.rs @@ -1,14 +1,14 @@ +use sd_prisma::prisma::notification; + +use crate::api::{Ctx, R}; use async_stream::stream; use chrono::{DateTime, Utc}; use futures::future::join_all; use rspc::{alpha::AlphaRouter, ErrorCode}; -use sd_prisma::prisma::notification; use serde::{Deserialize, Serialize}; use specta::Type; use uuid::Uuid; -use crate::api::{Ctx, R}; - use super::utils::library; /// Represents a single notification. @@ -27,13 +27,22 @@ pub enum NotificationId { Library(Uuid, u32), Node(u32), } +#[derive(Debug, Clone, Serialize, Deserialize, Type)] +#[serde(rename_all = "camelCase")] +pub enum NotificationKind { + Info, + Success, + Error, + Warning, +} /// Represents the data of a single notification. /// This data is used by the frontend to properly display the notification. #[derive(Debug, Clone, Serialize, Deserialize, Type)] -pub enum NotificationData { - PairingRequest { id: Uuid, pairing_id: u16 }, - Test, +pub struct NotificationData { + pub title: String, + pub content: String, + pub kind: NotificationKind, } pub(crate) fn mount() -> AlphaRouter { @@ -159,21 +168,4 @@ pub(crate) fn mount() -> AlphaRouter { } }) }) - .procedure("test", { - R.mutation(|node, _: ()| async move { - node.emit_notification(NotificationData::Test, None).await; - - Ok(()) - }) - }) - .procedure("testLibrary", { - R.with2(library()) - .mutation(|(_, library), _: ()| async move { - library - .emit_notification(NotificationData::Test, None) - .await; - - Ok(()) - }) - }) } diff --git a/core/src/api/p2p.rs b/core/src/api/p2p.rs index 8f0cf0774..8a9fba17d 100644 --- a/core/src/api/p2p.rs +++ b/core/src/api/p2p.rs @@ -1,12 +1,13 @@ -use rspc::{alpha::AlphaRouter, ErrorCode}; +use crate::p2p::{operations, P2PEvent, PairingDecision}; + use sd_p2p::spacetunnel::RemoteIdentity; + +use rspc::{alpha::AlphaRouter, ErrorCode}; use serde::Deserialize; use specta::Type; use std::path::PathBuf; use uuid::Uuid; -use crate::p2p::{operations, P2PEvent, PairingDecision}; - use super::{Ctx, R}; pub(crate) fn mount() -> AlphaRouter { diff --git a/core/src/api/preferences.rs b/core/src/api/preferences.rs index ac455604c..2722fecf3 100644 --- a/core/src/api/preferences.rs +++ b/core/src/api/preferences.rs @@ -1,7 +1,8 @@ +use crate::preferences::LibraryPreferences; + use rspc::alpha::AlphaRouter; use super::{utils::library, Ctx, R}; -use crate::preferences::LibraryPreferences; pub(crate) fn mount() -> AlphaRouter { R.router() diff --git a/core/src/api/search/file_path.rs b/core/src/api/search/file_path.rs index e73f7820c..bfb682ccc 100644 --- a/core/src/api/search/file_path.rs +++ b/core/src/api/search/file_path.rs @@ -1,18 +1,19 @@ +use crate::location::LocationError; + +use sd_file_path_helper::{check_file_path_exists, IsolatedFilePathData}; +use sd_prisma::prisma::{self, file_path}; + use chrono::{DateTime, FixedOffset, Utc}; use prisma_client_rust::{OrderByQuery, PaginatedQuery, WhereQuery}; use rspc::ErrorCode; -use sd_prisma::prisma::{self, file_path}; use serde::{Deserialize, Serialize}; use specta::Type; -use crate::location::{ - file_path_helper::{check_file_path_exists, IsolatedFilePathData}, - LocationError, +use super::{ + object::*, + utils::{self, *}, }; -use super::object::*; -use super::utils::{self, *}; - #[derive(Serialize, Deserialize, Type, Debug, Clone)] #[serde(rename_all = "camelCase", tag = "field", content = "value")] pub enum FilePathOrder { diff --git a/core/src/api/search/media_data.rs b/core/src/api/search/media_data.rs index 5391e019f..52a84b0d4 100644 --- a/core/src/api/search/media_data.rs +++ b/core/src/api/search/media_data.rs @@ -1,4 +1,5 @@ use sd_prisma::prisma::{self, media_data}; + use serde::{Deserialize, Serialize}; use specta::Type; diff --git a/core/src/api/search/mod.rs b/core/src/api/search/mod.rs index 79cbbeaee..865868fba 100644 --- a/core/src/api/search/mod.rs +++ b/core/src/api/search/mod.rs @@ -1,11 +1,3 @@ -pub mod file_path; -pub mod media_data; -pub mod object; -pub mod saved; -mod utils; - -pub use self::{file_path::*, object::*, utils::*}; - use crate::{ api::{ locations::{file_path_with_object, object_with_file_paths, ExplorerItem}, @@ -16,14 +8,23 @@ use crate::{ object::media::thumbnail::get_indexed_thumb_key, }; +use sd_cache::{CacheNode, Model, Normalise, Reference}; +use sd_prisma::prisma::{self, PrismaClient}; + use std::path::PathBuf; use rspc::{alpha::AlphaRouter, ErrorCode}; -use sd_cache::{CacheNode, Model, Normalise, Reference}; -use sd_prisma::prisma::{self, PrismaClient}; use serde::{Deserialize, Serialize}; use specta::Type; +pub mod file_path; +pub mod media_data; +pub mod object; +pub mod saved; +mod utils; + +pub use self::{file_path::*, object::*, utils::*}; + use super::{Ctx, R}; const MAX_TAKE: u8 = 100; diff --git a/core/src/api/search/object.rs b/core/src/api/search/object.rs index f1748b9aa..bf7b68720 100644 --- a/core/src/api/search/object.rs +++ b/core/src/api/search/object.rs @@ -1,14 +1,16 @@ -use chrono::{DateTime, FixedOffset}; -use prisma_client_rust::not; -use prisma_client_rust::{or, OrderByQuery, PaginatedQuery, WhereQuery}; +// use crate::library::Category; + use sd_prisma::prisma::{self, object, tag_on_object}; + +use chrono::{DateTime, FixedOffset}; +use prisma_client_rust::{not, or, OrderByQuery, PaginatedQuery, WhereQuery}; use serde::{Deserialize, Serialize}; use specta::Type; -// use crate::library::Category; - -use super::media_data::*; -use super::utils::{self, *}; +use super::{ + media_data::*, + utils::{self, *}, +}; #[derive(Deserialize, Type, Debug)] #[serde(rename_all = "camelCase")] @@ -28,25 +30,25 @@ impl ObjectCursor { query.add_where(or![ match item.order { - SortOrder::Asc => prisma::object::$field::gt(data), - SortOrder::Desc => prisma::object::$field::lt(data), + SortOrder::Asc => object::$field::gt(data), + SortOrder::Desc => object::$field::lt(data), }, prisma_client_rust::and![ - prisma::object::$field::equals(Some(item.data)), + object::$field::equals(Some(item.data)), match item.order { - SortOrder::Asc => prisma::object::id::gt(id), - SortOrder::Desc => prisma::object::id::lt(id), + SortOrder::Asc => object::id::gt(id), + SortOrder::Desc => object::id::lt(id), } ] ]); - query.add_order_by(prisma::object::$field::order(item.order.into())); + query.add_order_by(object::$field::order(item.order.into())); }}; } match self { Self::None => { - query.add_where(prisma::object::id::gt(id)); + query.add_where(object::id::gt(id)); } Self::Kind(item) => arm!(kind, item), Self::DateAccessed(item) => arm!(date_accessed, item), @@ -146,7 +148,7 @@ impl ObjectFilterArgs { } pub type OrderAndPagination = - utils::OrderAndPagination; + utils::OrderAndPagination; impl OrderAndPagination { pub fn apply(self, query: &mut object::FindManyQuery) { @@ -164,7 +166,7 @@ impl OrderAndPagination { Self::Cursor { id, cursor } => { cursor.apply(query, id); - query.add_order_by(prisma::object::pub_id::order(prisma::SortOrder::Asc)) + query.add_order_by(object::pub_id::order(prisma::SortOrder::Asc)) } } } diff --git a/core/src/api/search/saved.rs b/core/src/api/search/saved.rs index 5af6babdf..d858d5287 100644 --- a/core/src/api/search/saved.rs +++ b/core/src/api/search/saved.rs @@ -1,5 +1,6 @@ -use crate::{api::utils::library, invalidate_query, prisma::saved_search}; +use crate::{api::utils::library, invalidate_query}; +use sd_prisma::prisma::saved_search; use sd_utils::chain_optional_iter; use chrono::{DateTime, FixedOffset, Utc}; diff --git a/core/src/api/search/utils.rs b/core/src/api/search/utils.rs index ceeb8892b..1206f398c 100644 --- a/core/src/api/search/utils.rs +++ b/core/src/api/search/utils.rs @@ -1,4 +1,5 @@ use sd_prisma::prisma; + use serde::{Deserialize, Serialize}; use specta::Type; diff --git a/core/src/api/sync.rs b/core/src/api/sync.rs index 142670f5b..d91ad085d 100644 --- a/core/src/api/sync.rs +++ b/core/src/api/sync.rs @@ -1,6 +1,7 @@ -use rspc::alpha::AlphaRouter; use sd_core_sync::GetOpsArgs; +use rspc::alpha::AlphaRouter; + use super::{utils::library, Ctx, R}; pub(crate) fn mount() -> AlphaRouter { diff --git a/core/src/api/tags.rs b/core/src/api/tags.rs index 2e4cb5066..2530562a1 100644 --- a/core/src/api/tags.rs +++ b/core/src/api/tags.rs @@ -1,26 +1,24 @@ +use crate::{invalidate_query, library::Library, object::tag::TagCreateArgs}; + +use sd_cache::{CacheNode, Normalise, NormalisedResult, NormalisedResults, Reference}; +use sd_file_ext::kind::ObjectKind; +use sd_prisma::{ + prisma::{file_path, object, tag, tag_on_object}, + prisma_sync, +}; +use sd_sync::OperationFactory; +use sd_utils::uuid_to_bytes; + use std::collections::BTreeMap; use chrono::{DateTime, Utc}; use itertools::{Either, Itertools}; use rspc::{alpha::AlphaRouter, ErrorCode}; -use sd_cache::{CacheNode, Normalise, NormalisedResult, NormalisedResults, Reference}; -use sd_file_ext::kind::ObjectKind; -use sd_prisma::{prisma, prisma_sync}; -use sd_sync::OperationFactory; -use sd_utils::uuid_to_bytes; use serde::{Deserialize, Serialize}; -use specta::Type; - use serde_json::json; +use specta::Type; use uuid::Uuid; -use crate::{ - invalidate_query, - library::Library, - object::tag::TagCreateArgs, - prisma::{file_path, object, tag, tag_on_object}, -}; - use super::{utils::library, Ctx, R}; pub(crate) fn mount() -> AlphaRouter { @@ -119,8 +117,8 @@ pub(crate) fn mount() -> AlphaRouter { #[derive(Debug, Type, Deserialize)] #[specta(inline)] enum Target { - Object(prisma::object::id::Type), - FilePath(prisma::file_path::id::Type), + Object(object::id::Type), + FilePath(file_path::id::Type), } #[derive(Debug, Type, Deserialize)] @@ -276,8 +274,14 @@ pub(crate) fn mount() -> AlphaRouter { }, ); - sync.write_ops(db, (sync_ops, db.tag_on_object().create_many(db_creates))) - .await?; + sync.write_ops( + db, + ( + sync_ops, + db.tag_on_object().create_many(db_creates).skip_duplicates(), + ), + ) + .await?; } invalidate_query!(library, "tags.getForObject"); diff --git a/core/src/api/volumes.rs b/core/src/api/volumes.rs index 550bc3f01..1e31258c8 100644 --- a/core/src/api/volumes.rs +++ b/core/src/api/volumes.rs @@ -1,7 +1,8 @@ -use rspc::alpha::AlphaRouter; +use crate::volume::get_volumes; + use sd_cache::{Normalise, NormalisedResults}; -use crate::volume::get_volumes; +use rspc::alpha::AlphaRouter; use super::{Ctx, R}; diff --git a/core/src/api/web_api.rs b/core/src/api/web_api.rs index ebfd3e817..674484957 100644 --- a/core/src/api/web_api.rs +++ b/core/src/api/web_api.rs @@ -1,9 +1,9 @@ +use crate::util::http::ensure_response; + use rspc::alpha::AlphaRouter; use serde::{Deserialize, Serialize}; use specta::Type; -use crate::util::http::ensure_response; - use super::{Ctx, R}; pub(crate) fn mount() -> AlphaRouter { diff --git a/core/src/cloud/sync/ingest.rs b/core/src/cloud/sync/ingest.rs index 49ed997ad..10eef8531 100644 --- a/core/src/cloud/sync/ingest.rs +++ b/core/src/cloud/sync/ingest.rs @@ -1,9 +1,11 @@ use crate::cloud::sync::err_return; -use super::Library; use std::sync::Arc; + use tokio::sync::Notify; +use super::Library; + pub async fn run_actor((library, notify): (Arc, Arc)) { let Library { sync, .. } = library.as_ref(); diff --git a/core/src/cloud/sync/mod.rs b/core/src/cloud/sync/mod.rs index d14ed8b3f..f26cceba6 100644 --- a/core/src/cloud/sync/mod.rs +++ b/core/src/cloud/sync/mod.rs @@ -1,7 +1,7 @@ -use std::sync::{atomic, Arc}; - use crate::{library::Library, Node}; +use std::sync::{atomic, Arc}; + mod ingest; mod receive; mod send; diff --git a/core/src/cloud/sync/receive.rs b/core/src/cloud/sync/receive.rs index f2339f7be..caa78006a 100644 --- a/core/src/cloud/sync/receive.rs +++ b/core/src/cloud/sync/receive.rs @@ -3,22 +3,25 @@ use crate::{ library::Library, Node, }; -use base64::prelude::*; -use chrono::Utc; -use itertools::{Either, Itertools}; + use sd_core_sync::NTP64; use sd_prisma::prisma::{ cloud_relation_operation, cloud_shared_operation, instance, PrismaClient, SortOrder, }; use sd_sync::*; use sd_utils::{from_bytes_to_uuid, uuid_to_bytes}; -use serde::Deserialize; -use serde_json::{json, to_vec}; + use std::{ collections::{hash_map::Entry, HashMap}, sync::Arc, time::Duration, }; + +use base64::prelude::*; +use chrono::Utc; +use itertools::{Either, Itertools}; +use serde::Deserialize; +use serde_json::{json, to_vec}; use tokio::{sync::Notify, time::sleep}; use uuid::Uuid; diff --git a/core/src/cloud/sync/send.rs b/core/src/cloud/sync/send.rs index cdc32d608..d1b64885c 100644 --- a/core/src/cloud/sync/send.rs +++ b/core/src/cloud/sync/send.rs @@ -1,14 +1,18 @@ -use super::Library; use crate::{cloud::sync::err_break, Node}; + use sd_core_sync::{GetOpsArgs, SyncMessage, NTP64}; use sd_prisma::prisma::instance; use sd_utils::from_bytes_to_uuid; + +use std::{sync::Arc, time::Duration}; + use serde::Deserialize; use serde_json::json; -use std::{sync::Arc, time::Duration}; use tokio::time::sleep; use uuid::Uuid; +use super::Library; + pub async fn run_actor((library, node): (Arc, Arc)) { let db = &library.db; let api_url = &library.env.api_url; diff --git a/core/src/custom_uri/mod.rs b/core/src/custom_uri/mod.rs index 0db17d673..ee8e01783 100644 --- a/core/src/custom_uri/mod.rs +++ b/core/src/custom_uri/mod.rs @@ -1,20 +1,23 @@ use crate::{ api::{utils::InvalidateOperationEvent, CoreEvent}, library::Library, - location::file_path_helper::{file_path_to_handle_custom_uri, IsolatedFilePathData}, object::media::thumbnail::WEBP_EXTENSION, p2p::{operations, IdentityOrRemoteIdentity}, - prisma::{file_path, location}, - util::{db::*, InfallibleResponse}, + util::InfallibleResponse, Node, }; +use sd_file_ext::text::is_text; +use sd_file_path_helper::{file_path_to_handle_custom_uri, IsolatedFilePathData}; +use sd_p2p::{spaceblock::Range, spacetunnel::RemoteIdentity}; +use sd_prisma::prisma::{file_path, location}; +use sd_utils::db::maybe_missing; + use std::{ cmp::min, ffi::OsStr, fmt::Debug, fs::Metadata, - io::{self, SeekFrom}, path::{Path, PathBuf}, str::FromStr, sync::{atomic::Ordering, Arc}, @@ -30,13 +33,10 @@ use axum::{ Router, }; use bytes::Bytes; - use mini_moka::sync::Cache; -use sd_file_ext::text::is_text; -use sd_p2p::{spaceblock::Range, spacetunnel::RemoteIdentity}; use tokio::{ fs::{self, File}, - io::{AsyncReadExt, AsyncSeekExt}, + io::{self, AsyncReadExt, AsyncSeekExt, SeekFrom}, }; use tokio_util::sync::PollSender; use tracing::error; diff --git a/core/src/custom_uri/mpsc_to_async_write.rs b/core/src/custom_uri/mpsc_to_async_write.rs index 889ba185e..e05364577 100644 --- a/core/src/custom_uri/mpsc_to_async_write.rs +++ b/core/src/custom_uri/mpsc_to_async_write.rs @@ -1,11 +1,10 @@ use std::{ - io, pin::Pin, task::{Context, Poll}, }; use bytes::Bytes; -use tokio::io::AsyncWrite; +use tokio::io::{self, AsyncWrite}; use tokio_util::sync::PollSender; /// Allowing wrapping an `mpsc::Sender` into an `AsyncWrite` diff --git a/core/src/custom_uri/serve_file.rs b/core/src/custom_uri/serve_file.rs index 0b86a5f20..6c5456a8c 100644 --- a/core/src/custom_uri/serve_file.rs +++ b/core/src/custom_uri/serve_file.rs @@ -1,17 +1,16 @@ use crate::util::InfallibleResponse; -use std::{ - fs::Metadata, - io::{self, SeekFrom}, - time::UNIX_EPOCH, -}; +use std::{fs::Metadata, time::UNIX_EPOCH}; use axum::{ body::{self, BoxBody, Full, StreamBody}, http::{header, request, HeaderValue, Method, Response, StatusCode}, }; use http_range::HttpRange; -use tokio::{fs::File, io::AsyncSeekExt}; +use tokio::{ + fs::File, + io::{self, AsyncSeekExt, SeekFrom}, +}; use tokio_util::io::ReaderStream; use tracing::error; diff --git a/core/src/custom_uri/utils.rs b/core/src/custom_uri/utils.rs index 0b73719d5..030173d93 100644 --- a/core/src/custom_uri/utils.rs +++ b/core/src/custom_uri/utils.rs @@ -1,3 +1,5 @@ +use crate::util::InfallibleResponse; + use std::{fmt::Debug, panic::Location}; use axum::{ @@ -8,8 +10,6 @@ use axum::{ use http_body::Full; use tracing::debug; -use crate::util::InfallibleResponse; - #[track_caller] pub(crate) fn bad_request(err: impl Debug) -> http::Response { debug!("400: Bad Request at {}: {err:?}", Location::caller()); diff --git a/core/src/job/error.rs b/core/src/job/error.rs index 01012fb91..9eb09ef84 100644 --- a/core/src/job/error.rs +++ b/core/src/job/error.rs @@ -4,10 +4,10 @@ use crate::{ file_identifier::FileIdentifierJobError, fs::error::FileSystemJobsError, media::media_processor::MediaProcessorError, validation::ValidatorError, }, - util::{db::MissingFieldError, error::FileIOError}, }; use sd_crypto::Error as CryptoError; +use sd_utils::{db::MissingFieldError, error::FileIOError}; use std::time::Duration; diff --git a/core/src/job/manager.rs b/core/src/job/manager.rs index 63bc8c2b3..ffb6286a2 100644 --- a/core/src/job/manager.rs +++ b/core/src/job/manager.rs @@ -11,10 +11,11 @@ use crate::{ media::media_processor::MediaProcessorJobInit, validation::validator_job::ObjectValidatorJobInit, }, - prisma::job, Node, }; +use sd_prisma::prisma::job; + use std::{ collections::{HashMap, HashSet, VecDeque}, sync::Arc, diff --git a/core/src/job/mod.rs b/core/src/job/mod.rs index 2b94cb72d..039f4b626 100644 --- a/core/src/job/mod.rs +++ b/core/src/job/mod.rs @@ -1,5 +1,7 @@ use crate::{library::Library, Node}; +use sd_prisma::prisma::location; + use std::{ collections::{hash_map::DefaultHasher, VecDeque}, fmt, @@ -10,8 +12,6 @@ use std::{ time::Instant, }; -use sd_prisma::prisma::location; - use async_channel as chan; use futures::stream::{self, StreamExt}; use futures_concurrency::stream::Merge; diff --git a/core/src/job/report.rs b/core/src/job/report.rs index b261a17b8..904d77e60 100644 --- a/core/src/job/report.rs +++ b/core/src/job/report.rs @@ -1,8 +1,7 @@ -use crate::{ - library::Library, - prisma::job, - util::db::{maybe_missing, MissingFieldError}, -}; +use crate::library::Library; + +use sd_prisma::prisma::job; +use sd_utils::db::{maybe_missing, MissingFieldError}; use std::{ collections::HashMap, diff --git a/core/src/lib.rs b/core/src/lib.rs index b53dddb79..af65c522c 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -6,12 +6,14 @@ use crate::{ object::media::thumbnail::actor::Thumbnailer, }; +#[cfg(feature = "ai")] +use sd_ai::image_labeler::{DownloadModelError, ImageLabeler, YoloV8}; + use api::notifications::{Notification, NotificationData, NotificationId}; use chrono::{DateTime, Utc}; use node::config; use notifications::Notifications; use reqwest::{RequestBuilder, Response}; -pub use sd_prisma::*; use std::{ fmt, @@ -64,6 +66,8 @@ pub struct Node { pub cloud_sync_flag: Arc, pub env: Arc, pub http: reqwest::Client, + #[cfg(feature = "ai")] + pub image_labeller: ImageLabeler, } impl fmt::Debug for Node { @@ -96,6 +100,11 @@ impl Node { .await .map_err(NodeError::FailedToInitializeConfig)?; + #[cfg(feature = "ai")] + sd_ai::init()?; + #[cfg(feature = "ai")] + let image_labeler_version = config.get().await.image_labeler_version; + let (locations, locations_actor) = location::Locations::new(); let (jobs, jobs_actor) = job::Jobs::new(); let libraries = library::Libraries::new(data_dir.join("libraries")).await?; @@ -107,7 +116,7 @@ impl Node { notifications: notifications::Notifications::new(), p2p, thumbnailer: Thumbnailer::new( - data_dir.to_path_buf(), + data_dir, libraries.clone(), event_bus.0.clone(), config.preferences_watcher(), @@ -120,6 +129,10 @@ impl Node { cloud_sync_flag: Arc::new(AtomicBool::new(false)), http: reqwest::Client::new(), env, + #[cfg(feature = "ai")] + image_labeller: ImageLabeler::new(YoloV8::model(image_labeler_version)?, data_dir) + .await + .map_err(sd_ai::Error::from)?, }); // Restore backend feature flags @@ -165,7 +178,7 @@ impl Node { std::env::set_var( "RUST_LOG", - format!("info,sd_core={level},sd_core::location::manager=info"), + format!("info,sd_core={level},sd_core::location::manager=info,sd_ai={level}"), ); } @@ -207,6 +220,8 @@ impl Node { self.thumbnailer.shutdown().await; self.jobs.shutdown().await; self.p2p.shutdown().await; + #[cfg(feature = "ai")] + self.image_labeller.shutdown().await; info!("Spacedrive Core shutdown successful!"); } @@ -300,4 +315,10 @@ pub enum NodeError { InitConfig(#[from] util::debug_initializer::InitConfigError), #[error("logger error: {0}")] Logger(#[from] FromEnvError), + #[cfg(feature = "ai")] + #[error("ai error: {0}")] + AI(#[from] sd_ai::Error), + #[cfg(feature = "ai")] + #[error("Failed to download model: {0}")] + DownloadModel(#[from] DownloadModelError), } diff --git a/core/src/library/cat.rs b/core/src/library/cat.rs index 59aa64be3..7c573589e 100644 --- a/core/src/library/cat.rs +++ b/core/src/library/cat.rs @@ -1,6 +1,7 @@ -use crate::prisma::object; -use prisma_client_rust::not; use sd_file_ext::kind::ObjectKind; +use sd_prisma::prisma::object; + +use prisma_client_rust::not; use serde::{Deserialize, Serialize}; use specta::Type; use std::vec; diff --git a/core/src/library/config.rs b/core/src/library/config.rs index ff75d8691..43cf6c0b1 100644 --- a/core/src/library/config.rs +++ b/core/src/library/config.rs @@ -1,16 +1,12 @@ use crate::{ node::{config::NodeConfig, Platform}, p2p::IdentityOrRemoteIdentity, - prisma::{file_path, indexer_rule, PrismaClient}, - util::{ - db::maybe_missing, - error::FileIOError, - version_manager::{Kind, ManagedVersion, VersionManager, VersionManagerError}, - }, + util::version_manager::{Kind, ManagedVersion, VersionManager, VersionManagerError}, }; use sd_p2p::spacetunnel::Identity; -use sd_prisma::prisma::{instance, location, node}; +use sd_prisma::prisma::{file_path, indexer_rule, instance, location, node, PrismaClient}; +use sd_utils::{db::maybe_missing, error::FileIOError}; use std::path::Path; diff --git a/core/src/library/library.rs b/core/src/library/library.rs index 00bea3fd5..6bf8d2c3d 100644 --- a/core/src/library/library.rs +++ b/core/src/library/library.rs @@ -1,19 +1,13 @@ use crate::{ - api::{ - notifications::{Notification, NotificationData, NotificationId}, - CoreEvent, - }, - location::file_path_helper::{file_path_to_full_path, IsolatedFilePathData}, - notifications, + api::CoreEvent, object::{media::thumbnail::get_indexed_thumbnail_path, orphan_remover::OrphanRemoverActor}, - prisma::{file_path, location, PrismaClient}, - sync, - util::{db::maybe_missing, error::FileIOError}, - Node, + sync, Node, }; +use sd_file_path_helper::{file_path_to_full_path, IsolatedFilePathData}; use sd_p2p::spacetunnel::Identity; -use sd_prisma::prisma::notification; +use sd_prisma::prisma::{file_path, location, PrismaClient}; +use sd_utils::{db::maybe_missing, error::FileIOError}; use std::{ collections::HashMap, @@ -22,7 +16,6 @@ use std::{ sync::Arc, }; -use chrono::{DateTime, Utc}; use tokio::{fs, io, sync::broadcast, sync::RwLock}; use tracing::warn; use uuid::Uuid; @@ -54,7 +47,6 @@ pub struct Library { // The UUID which matches `config.instance_id`'s primary key. pub instance_uuid: Uuid, - notifications: notifications::Notifications, pub env: Arc, // Look, I think this shouldn't be here but our current invalidation system needs it. @@ -95,7 +87,6 @@ impl Library { // key_manager, identity, orphan_remover: OrphanRemoverActor::spawn(db), - notifications: node.notifications.clone(), instance_uuid, env: node.env.clone(), event_bus_tx: node.event_bus.0.clone(), @@ -180,45 +171,4 @@ impl Library { Ok(out) } - - /// Create a new notification which will be stored into the DB and emitted to the UI. - pub async fn emit_notification(&self, data: NotificationData, expires: Option>) { - let result = match self - .db - .notification() - .create( - match rmp_serde::to_vec(&data).map_err(|err| err.to_string()) { - Ok(data) => data, - Err(err) => { - warn!( - "Failed to serialize notification data for library '{}': {}", - self.id, err - ); - return; - } - }, - expires - .map(|e| vec![notification::expires_at::set(Some(e.fixed_offset()))]) - .unwrap_or_default(), - ) - .exec() - .await - { - Ok(result) => result, - Err(err) => { - warn!( - "Failed to create notification in library '{}': {}", - self.id, err - ); - return; - } - }; - - self.notifications._internal_send(Notification { - id: NotificationId::Library(self.id, result.id as u32), - data, - read: false, - expires, - }); - } } diff --git a/core/src/library/manager/error.rs b/core/src/library/manager/error.rs index 93940e447..f6be136d1 100644 --- a/core/src/library/manager/error.rs +++ b/core/src/library/manager/error.rs @@ -2,10 +2,11 @@ use crate::{ library::LibraryConfigError, location::{indexer, LocationManagerError}, p2p::IdentityOrRemoteIdentityErr, - util::{ - db::{self, MissingFieldError}, - error::{FileIOError, NonUtf8PathError}, - }, +}; + +use sd_utils::{ + db::{self, MissingFieldError}, + error::{FileIOError, NonUtf8PathError}, }; use thiserror::Error; diff --git a/core/src/library/manager/mod.rs b/core/src/library/manager/mod.rs index 68640fc75..c3dc6bce9 100644 --- a/core/src/library/manager/mod.rs +++ b/core/src/library/manager/mod.rs @@ -8,21 +8,19 @@ use crate::{ node::Platform, object::tag, p2p::{self, IdentityOrRemoteIdentity}, - prisma::location, sync, - util::{ - db, - error::{FileIOError, NonUtf8PathError}, - mpscrr, MaybeUndefined, - }, - volume::watcher::spawn_volume_watcher, + util::{mpscrr, MaybeUndefined}, Node, }; use sd_core_sync::SyncMessage; use sd_p2p::spacetunnel::Identity; -use sd_prisma::prisma::{instance, shared_operation}; -use sd_utils::from_bytes_to_uuid; +use sd_prisma::prisma::{instance, location, shared_operation}; +use sd_utils::{ + db, + error::{FileIOError, NonUtf8PathError}, + from_bytes_to_uuid, +}; use std::{ collections::HashMap, @@ -131,11 +129,17 @@ impl Libraries { Err(e) => return Err(FileIOError::from((db_path, e)).into()), } - let library_arc = self + let _library_arc = self .load(library_id, &db_path, config_path, None, true, node) .await?; - spawn_volume_watcher(library_arc.clone()); + // This is compleaty breaking on linux now, no ideia why, but it will be irrelevant in a short while + // So let's leave it disable for now + #[cfg(not(target_os = "linux"))] + { + use crate::volume::watcher::spawn_volume_watcher; + spawn_volume_watcher(_library_arc.clone()); + } } } diff --git a/core/src/library/name.rs b/core/src/library/name.rs index e09d1ffec..3e4350236 100644 --- a/core/src/library/name.rs +++ b/core/src/library/name.rs @@ -1,10 +1,9 @@ use std::ops::Deref; +use serde::{Deserialize, Serialize}; use specta::Type; use thiserror::Error; -use serde::{Deserialize, Serialize}; - #[derive(Debug, Serialize, Clone, Type)] pub struct LibraryName(String); diff --git a/core/src/location/error.rs b/core/src/location/error.rs index cc84e92f4..c043d30cd 100644 --- a/core/src/location/error.rs +++ b/core/src/location/error.rs @@ -1,9 +1,8 @@ -use crate::{ - prisma::location, - util::{ - db::MissingFieldError, - error::{FileIOError, NonUtf8PathError}, - }, +use sd_file_path_helper::FilePathError; +use sd_prisma::prisma::location; +use sd_utils::{ + db::MissingFieldError, + error::{FileIOError, NonUtf8PathError}, }; use std::path::Path; @@ -12,9 +11,7 @@ use rspc::{self, ErrorCode}; use thiserror::Error; use uuid::Uuid; -use super::{ - file_path_helper::FilePathError, manager::LocationManagerError, metadata::LocationMetadataError, -}; +use super::{manager::LocationManagerError, metadata::LocationMetadataError}; /// Error type for location related errors #[derive(Error, Debug)] diff --git a/core/src/location/indexer/indexer_job.rs b/core/src/location/indexer/indexer_job.rs index 5975146e8..ac82625fc 100644 --- a/core/src/location/indexer/indexer_job.rs +++ b/core/src/location/indexer/indexer_job.rs @@ -5,21 +5,20 @@ use crate::{ JobStepOutput, StatefulJob, WorkerContext, }, library::Library, - location::{ - file_path_helper::{ - ensure_file_path_exists, ensure_sub_path_is_directory, ensure_sub_path_is_in_location, - IsolatedFilePathData, - }, - location_with_indexer_rules, update_location_size, - }, - prisma::{file_path, location}, + location::{location_with_indexer_rules, update_location_size}, to_remove_db_fetcher_fn, - util::db::maybe_missing, }; -use sd_prisma::prisma_sync; +use sd_file_path_helper::{ + ensure_file_path_exists, ensure_sub_path_is_directory, ensure_sub_path_is_in_location, + IsolatedFilePathData, +}; +use sd_prisma::{ + prisma::{file_path, location}, + prisma_sync, +}; use sd_sync::*; -use sd_utils::from_bytes_to_uuid; +use sd_utils::{db::maybe_missing, from_bytes_to_uuid}; use std::{ collections::HashMap, diff --git a/core/src/location/indexer/mod.rs b/core/src/location/indexer/mod.rs index d6c894b46..365019be7 100644 --- a/core/src/location/indexer/mod.rs +++ b/core/src/location/indexer/mod.rs @@ -1,14 +1,14 @@ -use crate::{ - library::Library, - util::{db::inode_to_db, error::FileIOError}, -}; +use crate::library::Library; +use sd_file_path_helper::{ + file_path_pub_and_cas_ids, FilePathError, IsolatedFilePathData, IsolatedFilePathDataParts, +}; use sd_prisma::{ prisma::{file_path, location, object as prisma_object, PrismaClient}, prisma_sync, }; use sd_sync::*; -use sd_utils::from_bytes_to_uuid; +use sd_utils::{db::inode_to_db, error::FileIOError, from_bytes_to_uuid}; use std::{collections::HashMap, path::Path}; @@ -22,10 +22,7 @@ use serde_json::json; use thiserror::Error; use tracing::{trace, warn}; -use super::{ - file_path_helper::{file_path_pub_and_cas_ids, FilePathError, IsolatedFilePathData}, - location_with_indexer_rules, -}; +use super::location_with_indexer_rules; pub mod indexer_job; pub mod rules; @@ -97,13 +94,13 @@ async fn execute_indexer_save_step( .walked .iter() .map(|entry| { - let IsolatedFilePathData { + let IsolatedFilePathDataParts { materialized_path, is_dir, name, extension, .. - } = &entry.iso_file_path; + } = &entry.iso_file_path.to_parts(); use file_path::*; @@ -197,7 +194,7 @@ async fn execute_indexer_update_step( .to_update .iter() .map(|entry| async move { - let IsolatedFilePathData { is_dir, .. } = &entry.iso_file_path; + let IsolatedFilePathDataParts { is_dir, .. } = &entry.iso_file_path.to_parts(); let pub_id = sd_utils::uuid_to_bytes(entry.pub_id); @@ -338,7 +335,7 @@ macro_rules! file_paths_db_fetcher_fn { .find_many(vec![::prisma_client_rust::operator::or( founds.collect::>(), )]) - .select($crate::location::file_path_helper::file_path_walker::select()) + .select(::sd_file_path_helper::file_path_walker::select()) }) .collect::>(); @@ -358,13 +355,13 @@ macro_rules! file_paths_db_fetcher_fn { macro_rules! to_remove_db_fetcher_fn { ($location_id:expr, $db:expr) => {{ |parent_iso_file_path, unique_location_id_materialized_path_name_extension_params| async { - let location_id: $crate::prisma::location::id::Type = $location_id; - let db: &$crate::prisma::PrismaClient = $db; - let parent_iso_file_path: $crate::location::file_path_helper::IsolatedFilePathData< + let location_id: ::sd_prisma::prisma::location::id::Type = $location_id; + let db: &::sd_prisma::prisma::PrismaClient = $db; + let parent_iso_file_path: ::sd_file_path_helper::IsolatedFilePathData< 'static, > = parent_iso_file_path; let unique_location_id_materialized_path_name_extension_params: ::std::vec::Vec< - $crate::prisma::file_path::WhereParam, + ::sd_prisma::prisma::file_path::WhereParam, > = unique_location_id_materialized_path_name_extension_params; // FIXME: Can't pass this chunks variable direct to _batch because of lifetime issues @@ -377,7 +374,7 @@ macro_rules! to_remove_db_fetcher_fn { .find_many(vec![::prisma_client_rust::operator::or( unique_params.collect(), )]) - .select($crate::prisma::file_path::select!({ id })) + .select(::sd_prisma::prisma::file_path::select!({ id })) }) .collect::<::std::vec::Vec<_>>(); @@ -398,17 +395,17 @@ macro_rules! to_remove_db_fetcher_fn { loop { let found = $db.file_path() .find_many(vec![ - $crate::prisma::file_path::location_id::equals(Some(location_id)), - $crate::prisma::file_path::materialized_path::equals(Some( + ::sd_prisma::prisma::file_path::location_id::equals(Some(location_id)), + ::sd_prisma::prisma::file_path::materialized_path::equals(Some( parent_iso_file_path .materialized_path_for_children() .expect("the received isolated file path must be from a directory"), )), ]) - .order_by($crate::prisma::file_path::id::order($crate::prisma::SortOrder::Asc)) + .order_by(::sd_prisma::prisma::file_path::id::order(::sd_prisma::prisma::SortOrder::Asc)) .take(BATCH_SIZE) - .cursor($crate::prisma::file_path::id::equals(cursor)) - .select($crate::prisma::file_path::select!({ id pub_id cas_id })) + .cursor(::sd_prisma::prisma::file_path::id::equals(cursor)) + .select(::sd_prisma::prisma::file_path::select!({ id pub_id cas_id })) .exec() .await?; @@ -424,7 +421,7 @@ macro_rules! to_remove_db_fetcher_fn { found .into_iter() .filter(|file_path| !founds_ids.contains(&file_path.id)) - .map(|file_path| $crate::location::file_path_helper::file_path_pub_and_cas_ids::Data { + .map(|file_path| ::sd_file_path_helper::file_path_pub_and_cas_ids::Data { pub_id: file_path.pub_id, cas_id: file_path.cas_id, }), diff --git a/core/src/location/indexer/rules/mod.rs b/core/src/location/indexer/rules/mod.rs index 4e8b79280..2343397f8 100644 --- a/core/src/location/indexer/rules/mod.rs +++ b/core/src/location/indexer/rules/mod.rs @@ -1,12 +1,9 @@ -pub mod seed; +use crate::library::Library; -use crate::{ - library::Library, - prisma::indexer_rule, - util::{ - db::{maybe_missing, MissingFieldError}, - error::{FileIOError, NonUtf8PathError}, - }, +use sd_prisma::prisma::indexer_rule; +use sd_utils::{ + db::{maybe_missing, MissingFieldError}, + error::{FileIOError, NonUtf8PathError}, }; use std::{ @@ -27,6 +24,8 @@ use tokio::fs; use tracing::debug; use uuid::Uuid; +pub mod seed; + #[derive(Error, Debug)] pub enum IndexerRuleError { // User errors diff --git a/core/src/location/indexer/rules/seed.rs b/core/src/location/indexer/rules/seed.rs index 117401b35..9fff82c1e 100644 --- a/core/src/location/indexer/rules/seed.rs +++ b/core/src/location/indexer/rules/seed.rs @@ -2,8 +2,10 @@ use crate::{ library::Library, location::indexer::rules::{IndexerRule, IndexerRuleError, RulePerKind}, }; -use chrono::Utc; + use sd_prisma::prisma::indexer_rule; + +use chrono::Utc; use thiserror::Error; use uuid::Uuid; diff --git a/core/src/location/indexer/shallow.rs b/core/src/location/indexer/shallow.rs index 07ae1ec25..d7cc3dfcc 100644 --- a/core/src/location/indexer/shallow.rs +++ b/core/src/location/indexer/shallow.rs @@ -3,20 +3,20 @@ use crate::{ job::JobError, library::Library, location::{ - file_path_helper::{ - check_file_path_exists, ensure_sub_path_is_directory, ensure_sub_path_is_in_location, - IsolatedFilePathData, - }, indexer::{ execute_indexer_update_step, reverse_update_directories_sizes, IndexerJobUpdateStep, }, scan_location_sub_path, update_location_size, }, - to_remove_db_fetcher_fn, - util::db::maybe_missing, - Node, + to_remove_db_fetcher_fn, Node, }; +use sd_file_path_helper::{ + check_file_path_exists, ensure_sub_path_is_directory, ensure_sub_path_is_in_location, + IsolatedFilePathData, +}; +use sd_utils::db::maybe_missing; + use std::{ collections::HashSet, path::{Path, PathBuf}, diff --git a/core/src/location/indexer/walk.rs b/core/src/location/indexer/walk.rs index 89a479753..3a27aa111 100644 --- a/core/src/location/indexer/walk.rs +++ b/core/src/location/indexer/walk.rs @@ -1,10 +1,8 @@ -use crate::{ - location::file_path_helper::{ - file_path_pub_and_cas_ids, file_path_walker, FilePathMetadata, IsolatedFilePathData, - }, - prisma::file_path, - util::{db::inode_from_db, error::FileIOError}, +use sd_file_path_helper::{ + file_path_pub_and_cas_ids, file_path_walker, FilePathMetadata, IsolatedFilePathData, }; +use sd_prisma::prisma::file_path; +use sd_utils::{db::inode_from_db, error::FileIOError}; use std::{ collections::{HashMap, HashSet, VecDeque}, @@ -388,7 +386,7 @@ where // We ignore the size of directories because it is not reliable, we need to // calculate it ourselves later && !( - entry.iso_file_path.is_dir + entry.iso_file_path.to_parts().is_dir && metadata.size_in_bytes != file_path .size_in_bytes_bytes diff --git a/core/src/location/manager/helpers.rs b/core/src/location/manager/helpers.rs index e402ec7e6..095d99768 100644 --- a/core/src/location/manager/helpers.rs +++ b/core/src/location/manager/helpers.rs @@ -1,10 +1,11 @@ use crate::{ library::{Library, LibraryId}, - prisma::location, - util::db::maybe_missing, Node, }; +use sd_prisma::prisma::location; +use sd_utils::db::maybe_missing; + use std::{ collections::{HashMap, HashSet}, path::{Path, PathBuf}, diff --git a/core/src/location/manager/mod.rs b/core/src/location/manager/mod.rs index 3c6af051e..dfbda6199 100644 --- a/core/src/location/manager/mod.rs +++ b/core/src/location/manager/mod.rs @@ -1,11 +1,13 @@ use crate::{ job::JobManagerError, library::{Library, LibraryManagerEvent}, - prisma::location, - util::{db::MissingFieldError, error::FileIOError}, Node, }; +use sd_file_path_helper::FilePathError; +use sd_prisma::prisma::location; +use sd_utils::{db::MissingFieldError, error::FileIOError}; + use std::{ collections::BTreeSet, path::{Path, PathBuf}, @@ -24,8 +26,6 @@ use tracing::error; use tokio::sync::mpsc; use uuid::Uuid; -use super::file_path_helper::FilePathError; - #[cfg(feature = "location-watcher")] mod watcher; diff --git a/core/src/location/manager/watcher/linux.rs b/core/src/location/manager/watcher/linux.rs index cada73058..ca6142c7c 100644 --- a/core/src/location/manager/watcher/linux.rs +++ b/core/src/location/manager/watcher/linux.rs @@ -6,10 +6,10 @@ //! Aside from that, when a directory is moved to our watched location from the outside, we receive //! a Create Dir event, this one is actually ok at least. -use crate::{ - invalidate_query, library::Library, location::manager::LocationManagerError, prisma::location, - util::error::FileIOError, Node, -}; +use crate::{invalidate_query, library::Library, location::manager::LocationManagerError, Node}; + +use sd_prisma::prisma::location; +use sd_utils::error::FileIOError; use std::{ collections::{BTreeMap, HashMap}, diff --git a/core/src/location/manager/watcher/macos.rs b/core/src/location/manager/watcher/macos.rs index 0c696d8b2..8eccb17ed 100644 --- a/core/src/location/manager/watcher/macos.rs +++ b/core/src/location/manager/watcher/macos.rs @@ -9,19 +9,11 @@ //! current location from anywhere else, we just receive the new path rename event, which means a //! creation. -use crate::{ - invalidate_query, - library::Library, - location::{ - file_path_helper::{ - check_file_path_exists, get_inode, FilePathError, IsolatedFilePathData, - }, - manager::LocationManagerError, - }, - prisma::location, - util::error::FileIOError, - Node, -}; +use crate::{invalidate_query, library::Library, location::manager::LocationManagerError, Node}; + +use sd_file_path_helper::{check_file_path_exists, get_inode, FilePathError, IsolatedFilePathData}; +use sd_prisma::prisma::location; +use sd_utils::error::FileIOError; use std::{ collections::HashMap, diff --git a/core/src/location/manager/watcher/mod.rs b/core/src/location/manager/watcher/mod.rs index 734e11bbf..59810f216 100644 --- a/core/src/location/manager/watcher/mod.rs +++ b/core/src/location/manager/watcher/mod.rs @@ -1,4 +1,7 @@ -use crate::{library::Library, prisma::location, util::db::maybe_missing, Node}; +use crate::{library::Library, Node}; + +use sd_prisma::prisma::location; +use sd_utils::db::maybe_missing; use std::{ collections::HashSet, diff --git a/core/src/location/manager/watcher/utils.rs b/core/src/location/manager/watcher/utils.rs index 04fa2290f..0b556f10a 100644 --- a/core/src/location/manager/watcher/utils.rs +++ b/core/src/location/manager/watcher/utils.rs @@ -2,19 +2,9 @@ use crate::{ invalidate_query, library::Library, location::{ - delete_directory, - file_path_helper::{ - check_file_path_exists, create_file_path, file_path_with_object, - filter_existing_file_path_params, - isolated_file_path_data::extract_normalized_materialized_path_str, - loose_find_existing_file_path_params, path_is_hidden, FilePathError, FilePathMetadata, - IsolatedFilePathData, MetadataExt, - }, - find_location, - indexer::reverse_update_directories_sizes, - location_with_indexer_rules, - manager::LocationManagerError, - scan_location_sub_path, update_location_size, + create_file_path, delete_directory, find_location, + indexer::reverse_update_directories_sizes, location_with_indexer_rules, + manager::LocationManagerError, scan_location_sub_path, update_location_size, }, object::{ file_identifier::FileMetadata, @@ -25,19 +15,32 @@ use crate::{ }, validation::hash::file_checksum, }, - prisma::{file_path, location, object}, - util::{ - db::{inode_from_db, inode_to_db, maybe_missing}, - error::FileIOError, - }, Node, }; +use sd_file_ext::{extensions::ImageExtension, kind::ObjectKind}; +use sd_file_path_helper::{ + check_file_path_exists, file_path_with_object, filter_existing_file_path_params, + isolated_file_path_data::extract_normalized_materialized_path_str, + loose_find_existing_file_path_params, path_is_hidden, FilePathError, FilePathMetadata, + IsolatedFilePathData, MetadataExt, +}; +use sd_prisma::{ + prisma::{file_path, location, media_data, object}, + prisma_sync, +}; +use sd_sync::OperationFactory; +use sd_utils::{ + db::{inode_from_db, inode_to_db, maybe_missing}, + error::FileIOError, + uuid_to_bytes, +}; + #[cfg(target_family = "unix")] -use crate::location::file_path_helper::get_inode; +use sd_file_path_helper::get_inode; #[cfg(target_family = "windows")] -use crate::location::file_path_helper::get_inode_from_path; +use sd_file_path_helper::get_inode_from_path; use std::{ collections::{HashMap, HashSet}, @@ -48,14 +51,9 @@ use std::{ sync::Arc, }; -use sd_file_ext::{extensions::ImageExtension, kind::ObjectKind}; - use chrono::{DateTime, FixedOffset, Local, Utc}; use notify::Event; use prisma_client_rust::{raw, PrismaValue}; -use sd_prisma::{prisma::media_data, prisma_sync}; -use sd_sync::OperationFactory; -use sd_utils::uuid_to_bytes; use serde_json::json; use tokio::{ fs, @@ -122,7 +120,7 @@ pub(super) async fn create_dir( create_file_path( library, - iso_file_path, + iso_file_path.to_parts(), None, FilePathMetadata::from_path(&path, metadata).await?, ) @@ -178,7 +176,8 @@ async fn inner_create_file( ); let iso_file_path = IsolatedFilePathData::new(location_id, location_path, path, false)?; - let extension = iso_file_path.extension.to_string(); + let iso_file_path_parts = iso_file_path.to_parts(); + let extension = iso_file_path_parts.extension.to_string(); let metadata = FilePathMetadata::from_path(&path, metadata).await?; @@ -202,9 +201,9 @@ async fn inner_create_file( .file_path() .find_unique(file_path::location_id_materialized_path_name_extension( location_id, - iso_file_path.materialized_path.to_string(), - iso_file_path.name.to_string(), - iso_file_path.extension.to_string(), + iso_file_path_parts.materialized_path.to_string(), + iso_file_path_parts.name.to_string(), + iso_file_path_parts.extension.to_string(), )) .include(file_path_with_object::include()) .exec() @@ -242,7 +241,8 @@ async fn inner_create_file( debug!("Creating path: {}", iso_file_path); - let created_file = create_file_path(library, iso_file_path, cas_id.clone(), metadata).await?; + let created_file = + create_file_path(library, iso_file_path_parts, cas_id.clone(), metadata).await?; object::select!(object_ids { id pub_id }); @@ -776,10 +776,12 @@ pub(super) async fn rename( let is_dir = maybe_missing(file_path.is_dir, "file_path.is_dir")?; let new = IsolatedFilePathData::new(location_id, &location_path, new_path, is_dir)?; + let new_parts = new.to_parts(); // If the renamed path is a directory, we have to update every successor if is_dir { let old = IsolatedFilePathData::new(location_id, &location_path, old_path, is_dir)?; + let old_parts = old.to_parts(); // TODO: Fetch all file_paths that will be updated and dispatch sync events let updated = library @@ -788,8 +790,14 @@ pub(super) async fn rename( "UPDATE file_path \ SET materialized_path = REPLACE(materialized_path, {}, {}) \ WHERE location_id = {}", - PrismaValue::String(format!("{}/{}/", old.materialized_path, old.name)), - PrismaValue::String(format!("{}/{}/", new.materialized_path, new.name)), + PrismaValue::String(format!( + "{}/{}/", + old_parts.materialized_path, old_parts.name + )), + PrismaValue::String(format!( + "{}/{}/", + new_parts.materialized_path, new_parts.name + )), PrismaValue::Int(location_id as i64) )) .exec() @@ -806,8 +814,8 @@ pub(super) async fn rename( file_path::pub_id::equals(file_path.pub_id), vec![ file_path::materialized_path::set(Some(new_path_materialized_str)), - file_path::name::set(Some(new.name.to_string())), - file_path::extension::set(Some(new.extension.to_string())), + file_path::name::set(Some(new_parts.name.to_string())), + file_path::extension::set(Some(new_parts.extension.to_string())), file_path::date_modified::set(Some( DateTime::::from(new_path_metadata.modified_or_now()).into(), )), diff --git a/core/src/location/manager/watcher/windows.rs b/core/src/location/manager/watcher/windows.rs index 0875af4c5..1f60b729f 100644 --- a/core/src/location/manager/watcher/windows.rs +++ b/core/src/location/manager/watcher/windows.rs @@ -7,17 +7,11 @@ //! a remove event to see if a create event is emitted. If it is, we just update the `file_path` //! in the database. If not, we remove the file from the database. -use crate::{ - invalidate_query, - library::Library, - location::{ - file_path_helper::{get_inode_from_path, FilePathError}, - manager::LocationManagerError, - }, - prisma::location, - util::error::FileIOError, - Node, -}; +use crate::{invalidate_query, library::Library, location::manager::LocationManagerError, Node}; + +use sd_file_path_helper::{get_inode_from_path, FilePathError}; +use sd_prisma::prisma::location; +use sd_utils::error::FileIOError; use std::{ collections::{BTreeMap, HashMap}, diff --git a/core/src/location/mod.rs b/core/src/location/mod.rs index 1bbb30a87..b47d074a3 100644 --- a/core/src/location/mod.rs +++ b/core/src/location/mod.rs @@ -2,19 +2,28 @@ use crate::{ invalidate_query, job::{JobBuilder, JobError, JobManagerError}, library::Library, - location::file_path_helper::filter_existing_file_path_params, object::{ file_identifier::{self, file_identifier_job::FileIdentifierJobInit}, media::{media_processor, MediaProcessorJobInit}, }, - prisma::{file_path, indexer_rules_in_location, location, PrismaClient}, - util::{ - db::{maybe_missing, MissingFieldError}, - error::{FileIOError, NonUtf8PathError}, - }, Node, }; +use sd_file_path_helper::{filter_existing_file_path_params, IsolatedFilePathData}; +use sd_prisma::{ + prisma::{file_path, indexer_rules_in_location, location, PrismaClient}, + prisma_sync, +}; +use sd_sync::*; +use sd_utils::{ + db::{maybe_missing, MissingFieldError}, + error::{FileIOError, NonUtf8PathError}, + uuid_to_bytes, +}; + +#[cfg(feature = "location-watcher")] +use sd_file_path_helper::IsolatedFilePathDataParts; + use std::{ collections::HashSet, path::{Component, Path, PathBuf}, @@ -25,9 +34,6 @@ use chrono::Utc; use futures::future::TryFutureExt; use normpath::PathExt; use prisma_client_rust::{operator::and, or, QueryError}; -use sd_prisma::prisma_sync; -use sd_sync::*; -use sd_utils::uuid_to_bytes; use serde::Deserialize; use serde_json::json; use specta::Type; @@ -36,7 +42,6 @@ use tracing::{debug, info, warn}; use uuid::Uuid; mod error; -pub mod file_path_helper; pub mod indexer; mod manager; pub mod metadata; @@ -47,8 +52,6 @@ use indexer::IndexerJobInit; pub use manager::{LocationManagerError, Locations}; use metadata::SpacedriveLocationMetadataFile; -use file_path_helper::IsolatedFilePathData; - pub type LocationPubId = Uuid; // Location includes! @@ -464,6 +467,7 @@ pub async fn scan_location( location: location_base_data, sub_path: None, regenerate_thumbnails: false, + regenerate_labels: false, }) .spawn(node, library) .await @@ -503,6 +507,7 @@ pub async fn scan_location_sub_path( location: location_base_data, sub_path: Some(sub_path), regenerate_thumbnails: false, + regenerate_labels: false, }) .spawn(node, library) .await @@ -526,7 +531,15 @@ pub async fn light_scan_location( indexer::shallow(&location, &sub_path, &node, &library).await?; file_identifier::shallow(&location_base_data, &sub_path, &library).await?; - media_processor::shallow(&location_base_data, &sub_path, &library, &node).await?; + media_processor::shallow( + &location_base_data, + &sub_path, + &library, + #[cfg(feature = "ai")] + false, + &node, + ) + .await?; Ok(()) } @@ -1017,3 +1030,97 @@ pub async fn get_location_path_from_location_id( }) }) } + +#[cfg(feature = "location-watcher")] +pub async fn create_file_path( + crate::location::Library { db, sync, .. }: &crate::location::Library, + IsolatedFilePathDataParts { + materialized_path, + is_dir, + location_id, + name, + extension, + .. + }: IsolatedFilePathDataParts<'_>, + cas_id: Option, + metadata: sd_file_path_helper::FilePathMetadata, +) -> Result { + use sd_utils::db::inode_to_db; + + use sd_prisma::prisma; + + let indexed_at = Utc::now(); + + let location = db + .location() + .find_unique(location::id::equals(location_id)) + .select(location::select!({ id pub_id })) + .exec() + .await? + .ok_or(sd_file_path_helper::FilePathError::LocationNotFound( + location_id, + ))?; + + let params = { + use file_path::*; + + vec![ + ( + location::NAME, + json!(prisma_sync::location::SyncId { + pub_id: location.pub_id + }), + ), + (cas_id::NAME, json!(cas_id)), + (materialized_path::NAME, json!(materialized_path)), + (name::NAME, json!(name)), + (extension::NAME, json!(extension)), + ( + size_in_bytes_bytes::NAME, + json!(metadata.size_in_bytes.to_be_bytes().to_vec()), + ), + (inode::NAME, json!(metadata.inode.to_le_bytes())), + (is_dir::NAME, json!(is_dir)), + (date_created::NAME, json!(metadata.created_at)), + (date_modified::NAME, json!(metadata.modified_at)), + (date_indexed::NAME, json!(indexed_at)), + ] + }; + + let pub_id = sd_utils::uuid_to_bytes(Uuid::new_v4()); + + let created_path = sync + .write_ops( + db, + ( + sync.shared_create( + prisma_sync::file_path::SyncId { + pub_id: pub_id.clone(), + }, + params, + ), + db.file_path().create(pub_id, { + use file_path::*; + vec![ + location::connect(prisma::location::id::equals(location.id)), + materialized_path::set(Some(materialized_path.into())), + name::set(Some(name.into())), + extension::set(Some(extension.into())), + inode::set(Some(inode_to_db(metadata.inode))), + cas_id::set(cas_id), + is_dir::set(Some(is_dir)), + size_in_bytes_bytes::set(Some( + metadata.size_in_bytes.to_be_bytes().to_vec(), + )), + date_created::set(Some(metadata.created_at.into())), + date_modified::set(Some(metadata.modified_at.into())), + date_indexed::set(Some(indexed_at.into())), + hidden::set(Some(metadata.hidden)), + ] + }), + ), + ) + .await?; + + Ok(created_path) +} diff --git a/core/src/location/non_indexed.rs b/core/src/location/non_indexed.rs index 140e11144..c6718bb41 100644 --- a/core/src/location/non_indexed.rs +++ b/core/src/location/non_indexed.rs @@ -5,22 +5,22 @@ use crate::{ cas::generate_cas_id, media::thumbnail::{get_ephemeral_thumb_key, BatchToProcess, GenerateThumbnailArgs}, }, - prisma::location, - util::error::FileIOError, Node, }; +use sd_file_ext::{extensions::Extension, kind::ObjectKind}; +use sd_file_path_helper::{path_is_hidden, MetadataExt}; +use sd_prisma::prisma::location; +use sd_utils::{chain_optional_iter, error::FileIOError}; + use std::{ collections::HashMap, path::{Path, PathBuf}, sync::Arc, }; -use sd_file_ext::{extensions::Extension, kind::ObjectKind}; - use chrono::{DateTime, Utc}; use rspc::ErrorCode; -use sd_utils::chain_optional_iter; use serde::Serialize; use specta::Type; use thiserror::Error; @@ -28,7 +28,6 @@ use tokio::{fs, io}; use tracing::{error, warn}; use super::{ - file_path_helper::{path_is_hidden, MetadataExt}, indexer::rules::{ seed::{no_hidden, no_os_protected}, IndexerRule, RuleKind, diff --git a/core/src/node/config.rs b/core/src/node/config.rs index 913fa4daf..6d43caeb2 100644 --- a/core/src/node/config.rs +++ b/core/src/node/config.rs @@ -1,13 +1,11 @@ use crate::{ api::{notifications::Notification, BackendFeature}, object::media::thumbnail::preferences::ThumbnailerPreferences, - util::{ - error::FileIOError, - version_manager::{Kind, ManagedVersion, VersionManager, VersionManagerError}, - }, + util::version_manager::{Kind, ManagedVersion, VersionManager, VersionManagerError}, }; use sd_p2p::{Keypair, ManagerConfig}; +use sd_utils::error::FileIOError; use std::{ path::{Path, PathBuf}, @@ -51,9 +49,10 @@ pub struct NodeConfig { pub features: Vec, /// Authentication for Spacedrive Accounts pub auth_token: Option, - /// The aggreagation of many different preferences for the node pub preferences: NodePreferences, + // Model version for the image labeler + pub image_labeler_version: Option, version: NodeConfigVersion, } @@ -89,6 +88,11 @@ impl ManagedVersion for NodeConfig { }; name.truncate(250); + #[cfg(feature = "ai")] + let image_labeler_version = Some(sd_ai::image_labeler::DEFAULT_MODEL_VERSION.to_string()); + #[cfg(not(feature = "ai"))] + let image_labeler_version = None; + Some(Self { id: Uuid::new_v4(), name, @@ -99,6 +103,7 @@ impl ManagedVersion for NodeConfig { notifications: vec![], auth_token: None, preferences: NodePreferences::default(), + image_labeler_version, }) } } @@ -205,7 +210,18 @@ impl Manager { let data_directory_path = data_directory_path.as_ref().to_path_buf(); let config_file_path = data_directory_path.join(NODE_STATE_CONFIG_NAME); - let config = NodeConfig::load(&config_file_path).await?; + let mut config = NodeConfig::load(&config_file_path).await?; + + #[cfg(feature = "ai")] + if config.image_labeler_version.is_none() { + config.image_labeler_version = + Some(sd_ai::image_labeler::DEFAULT_MODEL_VERSION.to_string()); + } + + #[cfg(not(feature = "ai"))] + { + config.image_labeler_version = None; + } let (preferences_watcher_tx, _preferences_watcher_rx) = watch::channel(config.preferences.clone()); diff --git a/core/src/node/platform.rs b/core/src/node/platform.rs index 8c6d679d5..0835237ed 100644 --- a/core/src/node/platform.rs +++ b/core/src/node/platform.rs @@ -1,4 +1,5 @@ use crate::NodeError; + use serde::{Deserialize, Serialize}; use specta::Type; diff --git a/core/src/notifications.rs b/core/src/notifications.rs index 3c8802ab3..666b317be 100644 --- a/core/src/notifications.rs +++ b/core/src/notifications.rs @@ -1,9 +1,9 @@ +use crate::api::notifications::Notification; + use std::sync::{atomic::AtomicU32, Arc}; use tokio::sync::broadcast; -use crate::api::notifications::Notification; - #[derive(Clone)] pub struct Notifications( // Keep this private and use `Node::emit_notification` or `Library::emit_notification` instead. diff --git a/core/src/object/file_identifier/file_identifier_job.rs b/core/src/object/file_identifier/file_identifier_job.rs index eb57fe200..d44e3c10a 100644 --- a/core/src/object/file_identifier/file_identifier_job.rs +++ b/core/src/object/file_identifier/file_identifier_job.rs @@ -4,14 +4,15 @@ use crate::{ JobStepOutput, StatefulJob, WorkerContext, }, library::Library, - location::file_path_helper::{ - ensure_file_path_exists, ensure_sub_path_is_directory, ensure_sub_path_is_in_location, - file_path_for_file_identifier, IsolatedFilePathData, - }, - prisma::{file_path, location, PrismaClient, SortOrder}, - util::db::maybe_missing, }; +use sd_file_path_helper::{ + ensure_file_path_exists, ensure_sub_path_is_directory, ensure_sub_path_is_in_location, + file_path_for_file_identifier, IsolatedFilePathData, +}; +use sd_prisma::prisma::{file_path, location, PrismaClient, SortOrder}; +use sd_utils::db::maybe_missing; + use std::{ hash::{Hash, Hasher}, path::{Path, PathBuf}, diff --git a/core/src/object/file_identifier/mod.rs b/core/src/object/file_identifier/mod.rs index d1f03af86..2a0a8d387 100644 --- a/core/src/object/file_identifier/mod.rs +++ b/core/src/object/file_identifier/mod.rs @@ -1,19 +1,17 @@ use crate::{ job::JobError, library::Library, - location::file_path_helper::{ - file_path_for_file_identifier, FilePathError, IsolatedFilePathData, - }, object::{cas::generate_cas_id, object_for_file_identifier}, - prisma::{file_path, location, object, PrismaClient}, - util::{db::maybe_missing, error::FileIOError}, }; use sd_file_ext::{extensions::Extension, kind::ObjectKind}; - -use sd_prisma::prisma_sync; +use sd_file_path_helper::{file_path_for_file_identifier, FilePathError, IsolatedFilePathData}; +use sd_prisma::{ + prisma::{file_path, location, object, PrismaClient}, + prisma_sync, +}; use sd_sync::{CRDTOperation, OperationFactory}; -use sd_utils::uuid_to_bytes; +use sd_utils::{db::maybe_missing, error::FileIOError, uuid_to_bytes}; use std::{ collections::{HashMap, HashSet}, diff --git a/core/src/object/file_identifier/shallow.rs b/core/src/object/file_identifier/shallow.rs index 3b0d901e6..4f28a8216 100644 --- a/core/src/object/file_identifier/shallow.rs +++ b/core/src/object/file_identifier/shallow.rs @@ -1,14 +1,11 @@ -use crate::{ - invalidate_query, - job::JobError, - library::Library, - location::file_path_helper::{ - ensure_file_path_exists, ensure_sub_path_is_directory, ensure_sub_path_is_in_location, - file_path_for_file_identifier, IsolatedFilePathData, - }, - prisma::{file_path, location, PrismaClient, SortOrder}, - util::db::maybe_missing, +use crate::{invalidate_query, job::JobError, library::Library}; + +use sd_file_path_helper::{ + ensure_file_path_exists, ensure_sub_path_is_directory, ensure_sub_path_is_in_location, + file_path_for_file_identifier, IsolatedFilePathData, }; +use sd_prisma::prisma::{file_path, location, PrismaClient, SortOrder}; +use sd_utils::db::maybe_missing; use std::path::{Path, PathBuf}; diff --git a/core/src/object/fs/copy.rs b/core/src/object/fs/copy.rs index 7f970c5d7..c3eec54cc 100644 --- a/core/src/object/fs/copy.rs +++ b/core/src/object/fs/copy.rs @@ -5,11 +5,12 @@ use crate::{ WorkerContext, }, library::Library, - location::file_path_helper::{join_location_relative_path, IsolatedFilePathData}, - prisma::{file_path, location}, - util::{db::maybe_missing, error::FileIOError}, }; +use sd_file_path_helper::{join_location_relative_path, IsolatedFilePathData}; +use sd_prisma::prisma::{file_path, location}; +use sd_utils::{db::maybe_missing, error::FileIOError}; + use std::{hash::Hash, path::PathBuf}; use futures_concurrency::future::TryJoin; diff --git a/core/src/object/fs/cut.rs b/core/src/object/fs/cut.rs index 30221ecf2..9ce4098ad 100644 --- a/core/src/object/fs/cut.rs +++ b/core/src/object/fs/cut.rs @@ -5,12 +5,13 @@ use crate::{ WorkerContext, }, library::Library, - location::file_path_helper::push_location_relative_path, object::fs::{construct_target_filename, error::FileSystemJobsError}, - prisma::{file_path, location}, - util::error::FileIOError, }; +use sd_file_path_helper::push_location_relative_path; +use sd_prisma::prisma::{file_path, location}; +use sd_utils::error::FileIOError; + use std::{hash::Hash, path::PathBuf}; use serde::{Deserialize, Serialize}; diff --git a/core/src/object/fs/delete.rs b/core/src/object/fs/delete.rs index ea2d880ae..9bafa93fd 100644 --- a/core/src/object/fs/delete.rs +++ b/core/src/object/fs/delete.rs @@ -5,10 +5,11 @@ use crate::{ }, library::Library, location::get_location_path_from_location_id, - prisma::{file_path, location}, - util::{db::maybe_missing, error::FileIOError}, }; +use sd_prisma::prisma::{file_path, location}; +use sd_utils::{db::maybe_missing, error::FileIOError}; + use std::hash::Hash; use serde::{Deserialize, Serialize}; diff --git a/core/src/object/fs/erase.rs b/core/src/object/fs/erase.rs index 5e186b11a..a9a19ae21 100644 --- a/core/src/object/fs/erase.rs +++ b/core/src/object/fs/erase.rs @@ -5,11 +5,13 @@ use crate::{ StatefulJob, WorkerContext, }, library::Library, - location::{file_path_helper::IsolatedFilePathData, get_location_path_from_location_id}, - prisma::{file_path, location}, - util::{db::maybe_missing, error::FileIOError}, + location::get_location_path_from_location_id, }; +use sd_file_path_helper::IsolatedFilePathData; +use sd_prisma::prisma::{file_path, location}; +use sd_utils::{db::maybe_missing, error::FileIOError}; + use std::{hash::Hash, path::PathBuf}; use futures::future::try_join_all; diff --git a/core/src/object/fs/error.rs b/core/src/object/fs/error.rs index c39975b34..d221d7169 100644 --- a/core/src/object/fs/error.rs +++ b/core/src/object/fs/error.rs @@ -1,10 +1,10 @@ -use crate::{ - location::{file_path_helper::FilePathError, LocationError}, - prisma::file_path, - util::{ - db::MissingFieldError, - error::{FileIOError, NonUtf8PathError}, - }, +use crate::location::LocationError; + +use sd_file_path_helper::FilePathError; +use sd_prisma::prisma::file_path; +use sd_utils::{ + db::MissingFieldError, + error::{FileIOError, NonUtf8PathError}, }; use std::path::Path; diff --git a/core/src/object/fs/mod.rs b/core/src/object/fs/mod.rs index 5491bdd73..244bd5c2e 100644 --- a/core/src/object/fs/mod.rs +++ b/core/src/object/fs/mod.rs @@ -1,13 +1,10 @@ -use crate::{ - location::{ - file_path_helper::{file_path_with_object, IsolatedFilePathData}, - LocationError, - }, - prisma::{file_path, location, PrismaClient}, - util::{ - db::maybe_missing, - error::{FileIOError, NonUtf8PathError}, - }, +use crate::location::LocationError; + +use sd_file_path_helper::{file_path_with_object, IsolatedFilePathData}; +use sd_prisma::prisma::{file_path, location, PrismaClient}; +use sd_utils::{ + db::maybe_missing, + error::{FileIOError, NonUtf8PathError}, }; use std::{ diff --git a/core/src/object/media/media_data_extractor.rs b/core/src/object/media/media_data_extractor.rs index 47b19071b..9c8a306dd 100644 --- a/core/src/object/media/media_data_extractor.rs +++ b/core/src/object/media/media_data_extractor.rs @@ -1,12 +1,10 @@ -use crate::{ - job::JobRunErrors, - location::file_path_helper::{file_path_for_media_processor, IsolatedFilePathData}, - prisma::{location, media_data, PrismaClient}, - util::error::FileIOError, -}; +use crate::job::JobRunErrors; use sd_file_ext::extensions::{Extension, ImageExtension, ALL_IMAGE_EXTENSIONS}; +use sd_file_path_helper::{file_path_for_media_processor, IsolatedFilePathData}; use sd_media_metadata::ImageMetadata; +use sd_prisma::prisma::{location, media_data, PrismaClient}; +use sd_utils::error::FileIOError; use std::{collections::HashSet, path::Path}; diff --git a/core/src/object/media/media_processor/job.rs b/core/src/object/media/media_processor/job.rs index f7fffd244..8941b1150 100644 --- a/core/src/object/media/media_processor/job.rs +++ b/core/src/object/media/media_processor/job.rs @@ -5,16 +5,25 @@ use crate::{ StatefulJob, WorkerContext, }, library::Library, - location::file_path_helper::{ - ensure_file_path_exists, ensure_sub_path_is_directory, ensure_sub_path_is_in_location, - file_path_for_media_processor, IsolatedFilePathData, - }, - prisma::{location, PrismaClient}, - util::db::maybe_missing, Node, }; +#[cfg(feature = "ai")] +use crate::job::JobRunErrors; + use sd_file_ext::extensions::Extension; +use sd_file_path_helper::{ + ensure_file_path_exists, ensure_sub_path_is_directory, ensure_sub_path_is_in_location, + file_path_for_media_processor, IsolatedFilePathData, +}; +use sd_prisma::prisma::{location, PrismaClient}; +use sd_utils::db::maybe_missing; + +#[cfg(feature = "ai")] +use sd_ai::image_labeler::{BatchToken as ImageLabelerBatchToken, LabelerOutput}; + +#[cfg(feature = "ai")] +use std::sync::Arc; use std::{ hash::Hash, @@ -45,6 +54,7 @@ pub struct MediaProcessorJobInit { pub location: location::Data, pub sub_path: Option, pub regenerate_thumbnails: bool, + pub regenerate_labels: bool, } impl Hash for MediaProcessorJobInit { @@ -62,12 +72,19 @@ pub struct MediaProcessorJobData { to_process_path: PathBuf, #[serde(skip, default)] maybe_thumbnailer_progress_rx: Option>, + #[cfg(feature = "ai")] + labeler_batch_token: ImageLabelerBatchToken, + #[cfg(feature = "ai")] + #[serde(skip, default)] + maybe_labels_rx: Option>, } #[derive(Debug, Serialize, Deserialize)] pub enum MediaProcessorJobStep { ExtractMediaData(Vec), WaitThumbnails(usize), + #[cfg(feature = "ai")] + WaitLabels(usize), } #[async_trait::async_trait] @@ -134,7 +151,7 @@ impl StatefulJob for MediaProcessorJobInit { &iso_file_path, &ctx.library, &ctx.node, - false, + self.regenerate_thumbnails, ) .await?; @@ -153,23 +170,58 @@ impl StatefulJob for MediaProcessorJobInit { let file_paths = get_files_for_media_data_extraction(db, &iso_file_path).await?; + #[cfg(feature = "ai")] + let file_paths_for_labeling = + get_files_for_labeling(db, &iso_file_path, self.regenerate_labels).await?; + + #[cfg(feature = "ai")] + let total_files_for_labeling = file_paths_for_labeling.len(); + + #[cfg(feature = "ai")] + let (labeler_batch_token, labels_rx) = ctx + .node + .image_labeller + .new_resumable_batch( + location_id, + location_path.clone(), + file_paths_for_labeling, + Arc::clone(db), + ) + .await; + let total_files = file_paths.len(); - let chunked_files = - file_paths + let chunked_files = file_paths + .into_iter() + .chunks(BATCH_SIZE) + .into_iter() + .map(|chunk| chunk.collect::>()) + .map(MediaProcessorJobStep::ExtractMediaData) + .chain( + [ + (thumbs_to_process_count > 0).then_some(MediaProcessorJobStep::WaitThumbnails( + thumbs_to_process_count as usize, + )), + ] .into_iter() - .chunks(BATCH_SIZE) + .flatten(), + ) + .chain( + [ + #[cfg(feature = "ai")] + { + (total_files_for_labeling > 0) + .then_some(MediaProcessorJobStep::WaitLabels(total_files_for_labeling)) + }, + #[cfg(not(feature = "ai"))] + { + None + }, + ] .into_iter() - .map(|chunk| chunk.collect::>()) - .map(MediaProcessorJobStep::ExtractMediaData) - .chain( - [(thumbs_to_process_count > 0).then_some( - MediaProcessorJobStep::WaitThumbnails(thumbs_to_process_count as usize), - )] - .into_iter() - .flatten(), - ) - .collect::>(); + .flatten(), + ) + .collect::>(); ctx.progress(vec![ JobReportUpdate::TaskCount(total_files), @@ -184,6 +236,10 @@ impl StatefulJob for MediaProcessorJobInit { location_path, to_process_path, maybe_thumbnailer_progress_rx, + #[cfg(feature = "ai")] + labeler_batch_token, + #[cfg(feature = "ai")] + maybe_labels_rx: Some(labels_rx), }); Ok(( @@ -218,6 +274,7 @@ impl StatefulJob for MediaProcessorJobInit { .await .map(Into::into) .map_err(Into::into), + MediaProcessorJobStep::WaitThumbnails(total_thumbs) => { ctx.progress(vec![ JobReportUpdate::TaskCount(*total_thumbs), @@ -262,6 +319,66 @@ impl StatefulJob for MediaProcessorJobInit { Ok(None.into()) } + + #[cfg(feature = "ai")] + MediaProcessorJobStep::WaitLabels(total_labels) => { + ctx.progress(vec![ + JobReportUpdate::TaskCount(*total_labels), + JobReportUpdate::Phase("labels".to_string()), + JobReportUpdate::Message( + format!("Extracting labels for {total_labels} files",), + ), + ]); + + let mut labels_rx = pin!(if let Some(labels_rx) = data.maybe_labels_rx.clone() { + labels_rx + } else { + match ctx + .node + .image_labeller + .resume_batch(data.labeler_batch_token, Arc::clone(&ctx.library.db)) + .await + { + Ok(labels_rx) => labels_rx, + Err(e) => return Ok(JobRunErrors(vec![e.to_string()]).into()), + } + }); + + let mut total_labeled = 0; + + let mut errors = Vec::new(); + + while let Some(LabelerOutput { + file_path_id, + has_new_labels, + result, + }) = labels_rx.next().await + { + total_labeled += 1; + ctx.progress(vec![JobReportUpdate::CompletedTaskCount(total_labeled)]); + + if let Err(e) = result { + error!( + "Failed to generate labels : {e:#?}", + file_path_id + ); + + errors.push(e.to_string()); + } else if has_new_labels { + invalidate_query!(&ctx.library, "labels.count"); + } + } + + invalidate_query!(&ctx.library, "labels.list"); + invalidate_query!(&ctx.library, "labels.getForObject"); + invalidate_query!(&ctx.library, "labels.getWithObjects"); + + if !errors.is_empty() { + Ok(JobRunErrors(errors).into()) + } else { + Ok(None.into()) + } + } } } @@ -377,6 +494,51 @@ async fn get_files_for_media_data_extraction( .map_err(Into::into) } +#[cfg(feature = "ai")] +async fn get_files_for_labeling( + db: &PrismaClient, + parent_iso_file_path: &IsolatedFilePathData<'_>, + regenerate_labels: bool, +) -> Result, MediaProcessorError> { + // FIXME: Had to use format! macro because PCR doesn't support IN with Vec for SQLite + // We have no data coming from the user, so this is sql injection safe + db._query_raw(raw!( + &format!( + "SELECT id, materialized_path, is_dir, name, extension, cas_id, object_id + FROM file_path f + WHERE + location_id={{}} + AND cas_id IS NOT NULL + AND LOWER(extension) IN ({}) + AND materialized_path LIKE {{}} + {} + ORDER BY materialized_path ASC", + // Orderind by materialized_path so we can prioritize processing the first files + // in the above part of the directories tree + &media_data_extractor::FILTERED_IMAGE_EXTENSIONS + .iter() + .map(|ext| format!("LOWER('{ext}')")) + .collect::>() + .join(","), + if !regenerate_labels { + "AND NOT EXISTS (SELECT 1 FROM label_on_object WHERE object_id = f.object_id)" + } else { + "" + } + ), + PrismaValue::Int(parent_iso_file_path.location_id() as i64), + PrismaValue::String(format!( + "{}%", + parent_iso_file_path + .materialized_path_for_children() + .expect("sub path iso_file_path must be a directory") + )) + )) + .exec() + .await + .map_err(Into::into) +} + async fn get_all_children_files_by_extensions( db: &PrismaClient, parent_iso_file_path: &IsolatedFilePathData<'_>, diff --git a/core/src/object/media/media_processor/mod.rs b/core/src/object/media/media_processor/mod.rs index ce5ee2a2d..10cf52c51 100644 --- a/core/src/object/media/media_processor/mod.rs +++ b/core/src/object/media/media_processor/mod.rs @@ -1,8 +1,6 @@ -use crate::{ - job::{JobRunErrors, JobRunMetadata}, - location::file_path_helper::{file_path_for_media_processor, FilePathError}, -}; +use crate::job::{JobRunErrors, JobRunMetadata}; +use sd_file_path_helper::{file_path_for_media_processor, FilePathError}; use sd_prisma::prisma::{location, PrismaClient}; use std::path::Path; @@ -42,6 +40,7 @@ pub enum MediaProcessorError { pub struct MediaProcessorMetadata { media_data: MediaDataExtractorMetadata, thumbs_processed: u32, + labels_extracted: u32, } impl From for MediaProcessorMetadata { @@ -49,6 +48,7 @@ impl From for MediaProcessorMetadata { Self { media_data, thumbs_processed: 0, + labels_extracted: 0, } } } @@ -58,6 +58,7 @@ impl JobRunMetadata for MediaProcessorMetadata { self.media_data.extracted += new_data.media_data.extracted; self.media_data.skipped += new_data.media_data.skipped; self.thumbs_processed += new_data.thumbs_processed; + self.labels_extracted += new_data.labels_extracted; } } diff --git a/core/src/object/media/media_processor/shallow.rs b/core/src/object/media/media_processor/shallow.rs index c31346eab..60c5604e8 100644 --- a/core/src/object/media/media_processor/shallow.rs +++ b/core/src/object/media/media_processor/shallow.rs @@ -2,23 +2,33 @@ use crate::{ invalidate_query, job::{JobError, JobRunMetadata}, library::Library, - location::file_path_helper::{ - ensure_file_path_exists, ensure_sub_path_is_directory, ensure_sub_path_is_in_location, - file_path_for_media_processor, IsolatedFilePathData, - }, object::media::thumbnail::GenerateThumbnailArgs, - prisma::{location, PrismaClient}, - util::db::maybe_missing, Node, }; +use sd_file_ext::extensions::Extension; +use sd_file_path_helper::{ + ensure_file_path_exists, ensure_sub_path_is_directory, ensure_sub_path_is_in_location, + file_path_for_media_processor, IsolatedFilePathData, +}; +use sd_prisma::prisma::{location, PrismaClient}; +use sd_utils::db::maybe_missing; + +#[cfg(feature = "ai")] +use sd_ai::image_labeler::LabelerOutput; + use std::path::{Path, PathBuf}; +#[cfg(feature = "ai")] +use std::sync::Arc; + use itertools::Itertools; use prisma_client_rust::{raw, PrismaValue}; -use sd_file_ext::extensions::Extension; use tracing::{debug, error}; +#[cfg(feature = "ai")] +use futures::StreamExt; + use super::{ media_data_extractor::{self, process}, thumbnail::{self, BatchToProcess}, @@ -30,11 +40,10 @@ const BATCH_SIZE: usize = 10; pub async fn shallow( location: &location::Data, sub_path: &PathBuf, - library: &Library, + library @ Library { db, .. }: &Library, + #[cfg(feature = "ai")] regenerate_labels: bool, node: &Node, ) -> Result<(), JobError> { - let Library { db, .. } = library; - let location_id = location.id; let location_path = maybe_missing(&location.path, "location.path").map(PathBuf::from)?; @@ -64,7 +73,7 @@ pub async fn shallow( .map_err(MediaProcessorError::from)? }; - debug!("Searching for images in location {location_id} at path {iso_file_path}"); + debug!("Searching for media in location {location_id} at path {iso_file_path}"); dispatch_thumbnails_for_processing( location.id, @@ -78,6 +87,13 @@ pub async fn shallow( let file_paths = get_files_for_media_data_extraction(db, &iso_file_path).await?; + #[cfg(feature = "ai")] + let file_paths_for_labelling = + get_files_for_labeling(db, &iso_file_path, regenerate_labels).await?; + + #[cfg(feature = "ai")] + let has_labels = !file_paths_for_labelling.is_empty(); + let total_files = file_paths.len(); let chunked_files = file_paths @@ -92,6 +108,17 @@ pub async fn shallow( chunked_files.len() ); + #[cfg(feature = "ai")] + let labels_rx = node + .image_labeller + .new_batch( + location_id, + location_path.clone(), + file_paths_for_labelling, + Arc::clone(db), + ) + .await; + let mut run_metadata = MediaProcessorMetadata::default(); for files in chunked_files { @@ -113,6 +140,33 @@ pub async fn shallow( invalidate_query!(library, "search.objects"); } + #[cfg(feature = "ai")] + { + if has_labels { + labels_rx + .for_each( + |LabelerOutput { + file_path_id, + has_new_labels, + result, + }| async move { + if let Err(e) = result { + error!( + "Failed to generate labels : {e:#?}" + ); + } else if has_new_labels { + invalidate_query!(library, "labels.count"); + } + }, + ) + .await; + + invalidate_query!(library, "labels.list"); + invalidate_query!(library, "labels.getForObject"); + invalidate_query!(library, "labels.getWithObjects"); + } + } + Ok(()) } @@ -129,6 +183,47 @@ async fn get_files_for_media_data_extraction( .map_err(Into::into) } +#[cfg(feature = "ai")] +async fn get_files_for_labeling( + db: &PrismaClient, + parent_iso_file_path: &IsolatedFilePathData<'_>, + regenerate_labels: bool, +) -> Result, MediaProcessorError> { + // FIXME: Had to use format! macro because PCR doesn't support IN with Vec for SQLite + // We have no data coming from the user, so this is sql injection safe + db._query_raw(raw!( + &format!( + "SELECT id, materialized_path, is_dir, name, extension, cas_id, object_id + FROM file_path f + WHERE + location_id={{}} + AND cas_id IS NOT NULL + AND LOWER(extension) IN ({}) + AND materialized_path = {{}} + {}", + &media_data_extractor::FILTERED_IMAGE_EXTENSIONS + .iter() + .map(|ext| format!("LOWER('{ext}')")) + .collect::>() + .join(","), + if !regenerate_labels { + "AND NOT EXISTS (SELECT 1 FROM label_on_object WHERE object_id = f.object_id)" + } else { + "" + } + ), + PrismaValue::Int(parent_iso_file_path.location_id() as i64), + PrismaValue::String( + parent_iso_file_path + .materialized_path_for_children() + .expect("sub path iso_file_path must be a directory") + ) + )) + .exec() + .await + .map_err(Into::into) +} + async fn dispatch_thumbnails_for_processing( location_id: location::id::Type, location_path: impl AsRef, diff --git a/core/src/object/media/thumbnail/actor.rs b/core/src/object/media/thumbnail/actor.rs index 6824e10a2..81e59af25 100644 --- a/core/src/object/media/thumbnail/actor.rs +++ b/core/src/object/media/thumbnail/actor.rs @@ -2,10 +2,10 @@ use crate::{ api::CoreEvent, library::{Libraries, LibraryId, LibraryManagerEvent}, node::config::NodePreferences, - util::error::{FileIOError, NonUtf8PathError}, }; use sd_prisma::prisma::{location, PrismaClient}; +use sd_utils::error::{FileIOError, NonUtf8PathError}; use std::{ path::{Path, PathBuf}, @@ -72,19 +72,18 @@ pub struct Thumbnailer { impl Thumbnailer { pub async fn new( - data_dir: PathBuf, + data_dir: impl AsRef, libraries_manager: Arc, reporter: broadcast::Sender, node_preferences_rx: watch::Receiver, ) -> Self { + let data_dir = data_dir.as_ref(); let thumbnails_directory = Arc::new( - init_thumbnail_dir(&data_dir, Arc::clone(&libraries_manager)) + init_thumbnail_dir(data_dir, Arc::clone(&libraries_manager)) .await .unwrap_or_else(|e| { error!("Failed to initialize thumbnail directory: {e:#?}"); - let mut thumbnails_directory = data_dir; - thumbnails_directory.push(THUMBNAIL_CACHE_DIR_NAME); - thumbnails_directory + data_dir.join(THUMBNAIL_CACHE_DIR_NAME) }), ); diff --git a/core/src/object/media/thumbnail/clean_up.rs b/core/src/object/media/thumbnail/clean_up.rs index f63924b8a..70a4c7f4d 100644 --- a/core/src/object/media/thumbnail/clean_up.rs +++ b/core/src/object/media/thumbnail/clean_up.rs @@ -1,6 +1,7 @@ -use crate::{library::LibraryId, util::error::FileIOError}; +use crate::library::LibraryId; use sd_prisma::prisma::{file_path, PrismaClient}; +use sd_utils::error::FileIOError; use std::{collections::HashSet, ffi::OsString, path::PathBuf, sync::Arc}; diff --git a/core/src/object/media/thumbnail/directory.rs b/core/src/object/media/thumbnail/directory.rs index 6a770c2f9..f434852e9 100644 --- a/core/src/object/media/thumbnail/directory.rs +++ b/core/src/object/media/thumbnail/directory.rs @@ -1,13 +1,11 @@ use crate::{ library::{Libraries, LibraryId}, object::media::thumbnail::ONE_SEC, - util::{ - error::FileIOError, - version_manager::{Kind, ManagedVersion, VersionManager, VersionManagerError}, - }, + util::version_manager::{Kind, ManagedVersion, VersionManager, VersionManagerError}, }; use sd_prisma::prisma::{file_path, PrismaClient}; +use sd_utils::error::FileIOError; use serde_repr::{Deserialize_repr, Serialize_repr}; use std::{ diff --git a/core/src/object/media/thumbnail/mod.rs b/core/src/object/media/thumbnail/mod.rs index 67f8daca2..9d461351c 100644 --- a/core/src/object/media/thumbnail/mod.rs +++ b/core/src/object/media/thumbnail/mod.rs @@ -1,12 +1,9 @@ -use crate::{ - library::LibraryId, - util::{error::FileIOError, version_manager::VersionManagerError}, - Node, -}; +use crate::{library::LibraryId, util::version_manager::VersionManagerError, Node}; use sd_file_ext::extensions::{ DocumentExtension, Extension, ImageExtension, ALL_DOCUMENT_EXTENSIONS, ALL_IMAGE_EXTENSIONS, }; +use sd_utils::error::FileIOError; #[cfg(feature = "ffmpeg")] use sd_file_ext::extensions::{VideoExtension, ALL_VIDEO_EXTENSIONS}; diff --git a/core/src/object/media/thumbnail/process.rs b/core/src/object/media/thumbnail/process.rs index f478b3c49..981ac93e7 100644 --- a/core/src/object/media/thumbnail/process.rs +++ b/core/src/object/media/thumbnail/process.rs @@ -1,9 +1,10 @@ -use crate::{api::CoreEvent, util::error::FileIOError}; +use crate::api::CoreEvent; use sd_file_ext::extensions::{DocumentExtension, ImageExtension}; use sd_images::{format_image, scale_dimensions, ConvertableExtension}; use sd_media_metadata::image::Orientation; use sd_prisma::prisma::location; +use sd_utils::error::FileIOError; use std::{ collections::VecDeque, diff --git a/core/src/object/media/thumbnail/state.rs b/core/src/object/media/thumbnail/state.rs index 400302725..e24bf692f 100644 --- a/core/src/object/media/thumbnail/state.rs +++ b/core/src/object/media/thumbnail/state.rs @@ -1,4 +1,7 @@ -use crate::{library::LibraryId, util::error::FileIOError}; +use crate::library::LibraryId; + +use sd_prisma::prisma::location; +use sd_utils::error::FileIOError; use std::{ collections::{hash_map::Entry, HashMap, HashSet, VecDeque}, @@ -8,7 +11,6 @@ use std::{ use async_channel as chan; use futures_concurrency::future::TryJoin; -use sd_prisma::prisma::location; use serde::{Deserialize, Serialize}; use tokio::{fs, io}; use tracing::{error, info, trace}; diff --git a/core/src/object/media/thumbnail/worker.rs b/core/src/object/media/thumbnail/worker.rs index 18873d632..b64d627bc 100644 --- a/core/src/object/media/thumbnail/worker.rs +++ b/core/src/object/media/thumbnail/worker.rs @@ -1,9 +1,9 @@ use crate::{api::CoreEvent, node::config::NodePreferences}; -use std::{collections::HashMap, ffi::OsString, path::PathBuf, pin::pin, sync::Arc}; - use sd_prisma::prisma::location; +use std::{collections::HashMap, ffi::OsString, path::PathBuf, pin::pin, sync::Arc}; + use async_channel as chan; use futures_concurrency::stream::Merge; use tokio::{ diff --git a/core/src/object/mod.rs b/core/src/object/mod.rs index c77eaf987..9dbbae18b 100644 --- a/core/src/object/mod.rs +++ b/core/src/object/mod.rs @@ -1,4 +1,4 @@ -use crate::prisma::{file_path, object}; +use sd_prisma::prisma::{file_path, object}; use serde::{Deserialize, Serialize}; use specta::Type; diff --git a/core/src/object/orphan_remover.rs b/core/src/object/orphan_remover.rs index 3fe1a5cb4..cd7faaefc 100644 --- a/core/src/object/orphan_remover.rs +++ b/core/src/object/orphan_remover.rs @@ -1,4 +1,4 @@ -use crate::prisma::{object, tag_on_object, PrismaClient}; +use sd_prisma::prisma::{object, tag_on_object, PrismaClient}; use std::{sync::Arc, time::Duration}; diff --git a/core/src/object/tag/mod.rs b/core/src/object/tag/mod.rs index ae51cd5b7..9e0aa485f 100644 --- a/core/src/object/tag/mod.rs +++ b/core/src/object/tag/mod.rs @@ -1,15 +1,16 @@ -pub mod seed; +use crate::library::Library; + +use sd_prisma::{prisma::tag, prisma_sync}; +use sd_sync::*; use chrono::{DateTime, FixedOffset, Utc}; -use sd_prisma::prisma_sync; -use sd_sync::*; + use serde::Deserialize; use serde_json::json; use specta::Type; - use uuid::Uuid; -use crate::{library::Library, prisma::tag}; +pub mod seed; #[derive(Type, Deserialize, Clone)] pub struct TagCreateArgs { @@ -35,6 +36,7 @@ impl TagCreateArgs { [ (tag::name::NAME, json!(&self.name)), (tag::color::NAME, json!(&self.color)), + (tag::is_hidden::NAME, json!(false)), (tag::date_created::NAME, json!(&date_created.to_rfc3339())), ], ), @@ -43,6 +45,7 @@ impl TagCreateArgs { vec![ tag::name::set(Some(self.name)), tag::color::set(Some(self.color)), + tag::is_hidden::set(Some(false)), tag::date_created::set(Some(date_created)), ], ), diff --git a/core/src/object/tag/seed.rs b/core/src/object/tag/seed.rs index c04124e71..e385fe9a1 100644 --- a/core/src/object/tag/seed.rs +++ b/core/src/object/tag/seed.rs @@ -1,6 +1,7 @@ -use super::TagCreateArgs; use crate::library::Library; +use super::TagCreateArgs; + /// Seeds tags in a new library. /// Shouldn't be called more than once! pub async fn new_library(library: &Library) -> prisma_client_rust::Result<()> { diff --git a/core/src/object/validation/hash.rs b/core/src/object/validation/hash.rs index 614a7b68f..9b8b4b8cd 100644 --- a/core/src/object/validation/hash.rs +++ b/core/src/object/validation/hash.rs @@ -1,5 +1,6 @@ -use blake3::Hasher; use std::path::Path; + +use blake3::Hasher; use tokio::{ fs::File, io::{self, AsyncReadExt}, diff --git a/core/src/object/validation/mod.rs b/core/src/object/validation/mod.rs index 6064c92ef..eff8b7ba7 100644 --- a/core/src/object/validation/mod.rs +++ b/core/src/object/validation/mod.rs @@ -1,4 +1,5 @@ -use crate::{location::file_path_helper::FilePathError, util::error::FileIOError}; +use sd_file_path_helper::FilePathError; +use sd_utils::error::FileIOError; use std::path::Path; diff --git a/core/src/object/validation/validator_job.rs b/core/src/object/validation/validator_job.rs index 2e692c909..0ce2eafad 100644 --- a/core/src/object/validation/validator_job.rs +++ b/core/src/object/validation/validator_job.rs @@ -3,21 +3,24 @@ use crate::{ CurrentStep, JobError, JobInitOutput, JobResult, JobStepOutput, StatefulJob, WorkerContext, }, library::Library, - location::file_path_helper::{ - ensure_file_path_exists, ensure_sub_path_is_directory, ensure_sub_path_is_in_location, - file_path_for_object_validator, IsolatedFilePathData, - }, - prisma::{file_path, location}, - util::{db::maybe_missing, error::FileIOError}, }; +use sd_file_path_helper::{ + ensure_file_path_exists, ensure_sub_path_is_directory, ensure_sub_path_is_in_location, + file_path_for_object_validator, IsolatedFilePathData, +}; +use sd_prisma::{ + prisma::{file_path, location}, + prisma_sync, +}; +use sd_sync::OperationFactory; +use sd_utils::{db::maybe_missing, error::FileIOError}; + use std::{ hash::{Hash, Hasher}, path::{Path, PathBuf}, }; -use sd_prisma::prisma_sync; -use sd_sync::OperationFactory; use serde::{Deserialize, Serialize}; use serde_json::json; use tracing::info; diff --git a/core/src/p2p/identity_or_remote_identity.rs b/core/src/p2p/identity_or_remote_identity.rs index aa679f3e4..5007fe588 100644 --- a/core/src/p2p/identity_or_remote_identity.rs +++ b/core/src/p2p/identity_or_remote_identity.rs @@ -1,4 +1,5 @@ use sd_p2p::spacetunnel::{Identity, IdentityErr, RemoteIdentity}; + use thiserror::Error; #[derive(Debug, Error)] diff --git a/core/src/p2p/libraries.rs b/core/src/p2p/libraries.rs index 57d30b10e..f4deb289e 100644 --- a/core/src/p2p/libraries.rs +++ b/core/src/p2p/libraries.rs @@ -1,16 +1,17 @@ +use crate::library::{Libraries, Library, LibraryManagerEvent}; + +use sd_p2p::Service; + use std::{ collections::HashMap, fmt, sync::{Arc, PoisonError, RwLock}, }; -use sd_p2p::Service; use tokio::sync::mpsc; use tracing::{error, warn}; use uuid::Uuid; -use crate::library::{Libraries, Library, LibraryManagerEvent}; - use super::{IdentityOrRemoteIdentity, LibraryMetadata, P2PManager}; pub struct LibraryServices { diff --git a/core/src/p2p/library_metadata.rs b/core/src/p2p/library_metadata.rs index e8d0f766b..0a6e2b198 100644 --- a/core/src/p2p/library_metadata.rs +++ b/core/src/p2p/library_metadata.rs @@ -1,6 +1,7 @@ +use sd_p2p::Metadata; + use std::collections::HashMap; -use sd_p2p::Metadata; use serde::{Deserialize, Serialize}; use specta::Type; diff --git a/core/src/p2p/operations/ping.rs b/core/src/p2p/operations/ping.rs index 4786f22a5..58a2936c6 100644 --- a/core/src/p2p/operations/ping.rs +++ b/core/src/p2p/operations/ping.rs @@ -1,9 +1,10 @@ -use std::sync::Arc; +use crate::p2p::P2PManager; use sd_p2p::PeerMessageEvent; -use tracing::debug; -use crate::p2p::P2PManager; +use std::sync::Arc; + +use tracing::debug; /// Send a ping to all peers we are connected to #[allow(unused)] diff --git a/core/src/p2p/operations/request_file.rs b/core/src/p2p/operations/request_file.rs index 52456d553..81546bc01 100644 --- a/core/src/p2p/operations/request_file.rs +++ b/core/src/p2p/operations/request_file.rs @@ -1,3 +1,17 @@ +use crate::{ + library::Library, + p2p::{Header, HeaderFile}, + Node, +}; + +use sd_file_path_helper::{file_path_to_handle_p2p_serve_file, IsolatedFilePathData}; +use sd_p2p::{ + spaceblock::{BlockSize, Range, SpaceblockRequest, SpaceblockRequests, Transfer}, + spacetime::UnicastStream, + PeerMessageEvent, +}; +use sd_prisma::prisma::file_path; + use std::{ path::Path, sync::{ @@ -6,12 +20,6 @@ use std::{ }, }; -use sd_p2p::{ - spaceblock::{BlockSize, Range, SpaceblockRequest, SpaceblockRequests, Transfer}, - spacetime::UnicastStream, - PeerMessageEvent, -}; -use sd_prisma::prisma::file_path; use tokio::{ fs::File, io::{AsyncReadExt, AsyncWrite, AsyncWriteExt, BufReader}, @@ -19,13 +27,6 @@ use tokio::{ use tracing::{debug, warn}; use uuid::Uuid; -use crate::{ - library::Library, - location::file_path_helper::{file_path_to_handle_p2p_serve_file, IsolatedFilePathData}, - p2p::{Header, HeaderFile}, - Node, -}; - /// Request a file from the remote machine over P2P. This is used for preview media and quick preview. /// /// DO NOT USE THIS WITHOUT `node.files_over_p2p_flag == true` diff --git a/core/src/p2p/operations/spacedrop.rs b/core/src/p2p/operations/spacedrop.rs index c779dae3e..a3adaabb7 100644 --- a/core/src/p2p/operations/spacedrop.rs +++ b/core/src/p2p/operations/spacedrop.rs @@ -1,3 +1,11 @@ +use crate::p2p::{Header, P2PEvent, P2PManager}; + +use sd_p2p::{ + spaceblock::{BlockSize, Range, SpaceblockRequest, SpaceblockRequests, Transfer}, + spacetunnel::RemoteIdentity, + PeerMessageEvent, +}; + use std::{ borrow::Cow, path::PathBuf, @@ -9,11 +17,6 @@ use std::{ }; use futures::future::join_all; -use sd_p2p::{ - spaceblock::{BlockSize, Range, SpaceblockRequest, SpaceblockRequests, Transfer}, - spacetunnel::RemoteIdentity, - PeerMessageEvent, -}; use tokio::{ fs::{create_dir_all, File}, io::{AsyncReadExt, AsyncWriteExt, BufReader, BufWriter}, @@ -23,8 +26,6 @@ use tokio::{ use tracing::{debug, error, info, warn}; use uuid::Uuid; -use crate::p2p::{Header, P2PEvent, P2PManager}; - /// The amount of time to wait for a Spacedrop request to be accepted or rejected before it's automatically rejected pub(crate) const SPACEDROP_TIMEOUT: Duration = Duration::from_secs(60); diff --git a/core/src/p2p/p2p_events.rs b/core/src/p2p/p2p_events.rs index 6ee88cc5d..f9bf18510 100644 --- a/core/src/p2p/p2p_events.rs +++ b/core/src/p2p/p2p_events.rs @@ -1,4 +1,5 @@ use sd_p2p::spacetunnel::RemoteIdentity; + use serde::Serialize; use specta::Type; use uuid::Uuid; diff --git a/core/src/p2p/p2p_manager.rs b/core/src/p2p/p2p_manager.rs index e10e4e3e3..be1ab9ef6 100644 --- a/core/src/p2p/p2p_manager.rs +++ b/core/src/p2p/p2p_manager.rs @@ -1,23 +1,24 @@ +use crate::{ + node::config, + p2p::{OperatingSystem, SPACEDRIVE_APP_ID}, +}; + +use sd_p2p::{ + spacetunnel::RemoteIdentity, Manager, ManagerConfig, ManagerError, PeerStatus, Service, +}; + use std::{ collections::{HashMap, HashSet}, net::SocketAddr, sync::{atomic::AtomicBool, Arc}, }; -use sd_p2p::{ - spacetunnel::RemoteIdentity, Manager, ManagerConfig, ManagerError, PeerStatus, Service, -}; use serde::Serialize; use specta::Type; use tokio::sync::{broadcast, mpsc, oneshot, Mutex}; use tracing::info; use uuid::Uuid; -use crate::{ - node::config, - p2p::{OperatingSystem, SPACEDRIVE_APP_ID}, -}; - use super::{ LibraryMetadata, LibraryServices, P2PEvent, P2PManagerActor, PairingManager, PeerMetadata, }; diff --git a/core/src/p2p/p2p_manager_actor.rs b/core/src/p2p/p2p_manager_actor.rs index b5124d0a6..3fc20b254 100644 --- a/core/src/p2p/p2p_manager_actor.rs +++ b/core/src/p2p/p2p_manager_actor.rs @@ -1,12 +1,13 @@ +use crate::Node; + +use sd_p2p::{spacetunnel::Tunnel, Event, ManagerStream, Service, ServiceEvent}; + use std::sync::Arc; use futures::StreamExt; -use sd_p2p::{spacetunnel::Tunnel, Event, ManagerStream, Service, ServiceEvent}; use tokio::sync::mpsc; use tracing::error; -use crate::Node; - use super::{operations, sync::SyncMessage, Header, LibraryMetadata, P2PEvent, P2PManager}; pub struct P2PManagerActor { diff --git a/core/src/p2p/pairing/mod.rs b/core/src/p2p/pairing/mod.rs index ff10b00ab..2a6c0f506 100644 --- a/core/src/p2p/pairing/mod.rs +++ b/core/src/p2p/pairing/mod.rs @@ -1,5 +1,18 @@ #![allow(clippy::panic, clippy::unwrap_used)] // TODO: Finish this +use crate::{ + library::{Libraries, LibraryName}, + node::Platform, + p2p::{Header, IdentityOrRemoteIdentity}, + Node, +}; + +use sd_p2p::{ + spacetunnel::{Identity, RemoteIdentity}, + Manager, +}; +use sd_prisma::prisma::instance; + use std::{ collections::HashMap, sync::{ @@ -10,12 +23,6 @@ use std::{ use chrono::Utc; use futures::channel::oneshot; -use sd_p2p::{ - spacetunnel::{Identity, RemoteIdentity}, - Manager, -}; - -use sd_prisma::prisma::instance; use serde::{Deserialize, Serialize}; use specta::Type; use tokio::{ @@ -29,13 +36,6 @@ mod proto; use proto::*; -use crate::{ - library::{Libraries, LibraryName}, - node::Platform, - p2p::{Header, IdentityOrRemoteIdentity}, - Node, -}; - use super::P2PEvent; pub struct PairingManager { diff --git a/core/src/p2p/pairing/proto.rs b/core/src/p2p/pairing/proto.rs index 518cb0230..821eccf9d 100644 --- a/core/src/p2p/pairing/proto.rs +++ b/core/src/p2p/pairing/proto.rs @@ -1,15 +1,16 @@ -use std::str::FromStr; +use crate::node::Platform; -use chrono::{DateTime, Utc}; use sd_p2p::{ proto::{decode, encode}, spacetunnel::RemoteIdentity, }; + +use std::str::FromStr; + +use chrono::{DateTime, Utc}; use tokio::io::{AsyncRead, AsyncReadExt}; use uuid::Uuid; -use crate::node::Platform; - /// Terminology: /// Instance - DB model which represents a single `.db` file. /// Originator - begins the pairing process and is asking to join a library that will be selected by the responder. diff --git a/core/src/p2p/peer_metadata.rs b/core/src/p2p/peer_metadata.rs index 6cd3a1483..51885f469 100644 --- a/core/src/p2p/peer_metadata.rs +++ b/core/src/p2p/peer_metadata.rs @@ -1,11 +1,12 @@ -use std::{collections::HashMap, env, str::FromStr}; +use crate::node::Platform; use sd_p2p::Metadata; + +use std::{collections::HashMap, env, str::FromStr}; + use serde::{Deserialize, Serialize}; use specta::Type; -use crate::node::Platform; - #[derive(Debug, Clone, Type, Serialize, Deserialize)] pub struct PeerMetadata { pub name: String, diff --git a/core/src/p2p/protocol.rs b/core/src/p2p/protocol.rs index 7376a548a..2d8a46f72 100644 --- a/core/src/p2p/protocol.rs +++ b/core/src/p2p/protocol.rs @@ -1,12 +1,12 @@ -use thiserror::Error; -use tokio::io::{AsyncRead, AsyncReadExt}; -use uuid::Uuid; - use sd_p2p::{ proto::{decode, encode}, spaceblock::{Range, SpaceblockRequests, SpaceblockRequestsError}, }; +use thiserror::Error; +use tokio::io::{AsyncRead, AsyncReadExt}; +use uuid::Uuid; + #[derive(Debug, PartialEq, Eq)] pub struct HeaderFile { // Request ID diff --git a/core/src/p2p/sync/mod.rs b/core/src/p2p/sync/mod.rs index 8650528da..a47d8261a 100644 --- a/core/src/p2p/sync/mod.rs +++ b/core/src/p2p/sync/mod.rs @@ -1,20 +1,22 @@ #![allow(clippy::panic, clippy::unwrap_used)] // TODO: Finish this -use std::sync::Arc; +use crate::{ + library::Library, + sync::{self, GetOpsArgs}, +}; use sd_p2p::{ proto::{decode, encode}, spacetunnel::Tunnel, }; use sd_sync::CRDTOperation; -use sync::GetOpsArgs; + +use std::sync::Arc; use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt}; use tracing::*; use uuid::Uuid; -use crate::{library::Library, sync}; - use super::{Header, P2PManager}; mod proto; diff --git a/core/src/preferences/kv.rs b/core/src/preferences/kv.rs index 7d8dece6c..bdfc25a73 100644 --- a/core/src/preferences/kv.rs +++ b/core/src/preferences/kv.rs @@ -1,6 +1,7 @@ +use sd_prisma::prisma::{preference, PrismaClient}; + use std::collections::BTreeMap; -use crate::prisma::{preference, PrismaClient}; use itertools::Itertools; use rmpv::Value; use serde::{de::DeserializeOwned, Serialize}; diff --git a/core/src/preferences/library.rs b/core/src/preferences/library.rs index d75a85bfc..ca2d22cce 100644 --- a/core/src/preferences/library.rs +++ b/core/src/preferences/library.rs @@ -1,9 +1,11 @@ use crate::api::search; -use crate::prisma::PrismaClient; + +use sd_prisma::prisma::PrismaClient; + +use std::collections::{BTreeMap, HashMap}; + use serde::{Deserialize, Serialize}; use specta::Type; -use std::collections::BTreeMap; -use std::collections::HashMap; use tracing::error; use uuid::Uuid; diff --git a/core/src/preferences/mod.rs b/core/src/preferences/mod.rs index 4209fde3d..269b520c2 100644 --- a/core/src/preferences/mod.rs +++ b/core/src/preferences/mod.rs @@ -1,14 +1,15 @@ +use std::collections::HashMap; + +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use specta::Type; +use tracing::error; +use uuid::Uuid; + mod kv; mod library; pub use kv::*; pub use library::*; -use serde::{de::DeserializeOwned, Deserialize, Serialize}; -use specta::Type; - -use std::collections::HashMap; -use tracing::error; -use uuid::Uuid; #[derive(Clone, Serialize, Deserialize, Type, Debug)] #[specta(inline)] diff --git a/core/src/util/debug_initializer.rs b/core/src/util/debug_initializer.rs index 06af9e558..56a7150d5 100644 --- a/core/src/util/debug_initializer.rs +++ b/core/src/util/debug_initializer.rs @@ -7,11 +7,13 @@ use crate::{ location::{ delete_location, scan_location, LocationCreateArgs, LocationError, LocationManagerError, }, - prisma::location, util::AbortOnDrop, Node, }; +use sd_prisma::prisma::location; +use sd_utils::error::FileIOError; + use std::{ io, path::{Path, PathBuf}, @@ -29,8 +31,6 @@ use tokio::{ use tracing::{info, warn}; use uuid::Uuid; -use super::error::FileIOError; - #[derive(Deserialize)] #[serde(rename_all = "camelCase")] pub struct LocationInitConfig { diff --git a/core/src/util/mod.rs b/core/src/util/mod.rs index b537809b1..926e6e51a 100644 --- a/core/src/util/mod.rs +++ b/core/src/util/mod.rs @@ -1,8 +1,6 @@ mod abort_on_drop; -pub mod db; #[cfg(debug_assertions)] pub mod debug_initializer; -pub mod error; pub mod http; mod infallible_request; mod maybe_undefined; diff --git a/core/src/util/version_manager.rs b/core/src/util/version_manager.rs index 929ea0e5a..da4cd37bd 100644 --- a/core/src/util/version_manager.rs +++ b/core/src/util/version_manager.rs @@ -1,3 +1,5 @@ +use sd_utils::error::FileIOError; + use std::{ any::type_name, fmt::Display, future::Future, num::ParseIntError, path::Path, str::FromStr, }; @@ -10,8 +12,6 @@ use thiserror::Error; use tokio::{fs, io}; use tracing::{debug, info, warn}; -use super::error::FileIOError; - #[derive(Error, Debug)] pub enum VersionManagerError> { #[error("version file does not exist")] diff --git a/core/src/volume/mod.rs b/core/src/volume/mod.rs index 4dc31dbc5..08522b0e4 100644 --- a/core/src/volume/mod.rs +++ b/core/src/volume/mod.rs @@ -1,5 +1,7 @@ // Adapted from: https://github.com/kimlimjustin/xplorer/blob/f4f3590d06783d64949766cc2975205a3b689a56/src-tauri/src/drives.rs +use sd_cache::Model; + use std::{ fmt::Display, hash::{Hash, Hasher}, @@ -7,7 +9,6 @@ use std::{ sync::OnceLock, }; -use sd_cache::Model; use serde::{Deserialize, Serialize}; use serde_with::{serde_as, DisplayFromStr}; use specta::Type; @@ -318,11 +319,13 @@ pub async fn get_volumes() -> Vec { } #[cfg(windows)] + #[allow(clippy::needless_late_init)] let mut total_capacity; #[cfg(not(windows))] + #[allow(clippy::needless_late_init)] let total_capacity; - total_capacity = disk.total_space(); + let available_capacity = disk.available_space(); let is_root_filesystem = mount_point.is_absolute() && mount_point.parent().is_none(); diff --git a/core/src/volume/watcher.rs b/core/src/volume/watcher.rs index 3074f22a6..cd311e068 100644 --- a/core/src/volume/watcher.rs +++ b/core/src/volume/watcher.rs @@ -1,15 +1,17 @@ +#[cfg(not(target_os = "linux"))] use crate::{invalidate_query, library::Library}; +#[cfg(not(target_os = "linux"))] use std::{collections::HashSet, sync::Arc}; -use tokio::{ - spawn, - time::{interval, Duration}, -}; - -use super::get_volumes; - +#[cfg(not(target_os = "linux"))] pub fn spawn_volume_watcher(library: Arc) { + use tokio::{ + spawn, + time::{interval, Duration}, + }; + + use super::get_volumes; spawn(async move { let mut interval = interval(Duration::from_secs(1)); let mut existing_volumes = get_volumes().await.into_iter().collect::>(); diff --git a/crates/ai/Cargo.toml b/crates/ai/Cargo.toml new file mode 100644 index 000000000..38aa90d6c --- /dev/null +++ b/crates/ai/Cargo.toml @@ -0,0 +1,72 @@ +[package] +name = "sd-ai" +version = "0.1.0" +authors = ["Ericson Soares "] +readme = "README.md" +description = "A simple library to generate video thumbnails using ffmpeg with the webp format" +rust-version = "1.73.0" +license = { workspace = true } +repository = { workspace = true } +edition = { workspace = true } + +[dependencies] +sd-prisma = { path = "../prisma" } +sd-utils = { path = "../utils" } +sd-file-path-helper = { path = "../file-path-helper" } + +async-channel = { workspace = true } +chrono = { workspace = true, features = ["serde"] } +futures = { workspace = true } +futures-concurrency = { workspace = true } +image = { workspace = true } +once_cell = { workspace = true } +prisma-client-rust = { workspace = true } +reqwest = { workspace = true, features = ["stream", "native-tls-vendored"] } +rmp-serde = { workspace = true } +serde = { workspace = true, features = ["derive"] } +thiserror = { workspace = true } +tokio = { workspace = true, features = ["fs"] } +tokio-stream = { workspace = true } +tracing = { workspace = true } +url = '2.5.0' +uuid = { workspace = true, features = ["v4", "serde"] } + +# Note: Keep same version as used by ort +ndarray = { version = "0.15.6" } +half = { version = "2.1", features = ['num-traits'] } + +# Microsoft does not provide a release for osx-gpu. See: https://github.com/microsoft/onnxruntime/releases +# "gpu" means CUDA or TensorRT EP. Thus, the ort crate cannot download them at build time. +# Ref: https://github.com/pykeio/ort/blob/d7defd1862969b4b44f7f3f4b9c72263690bd67b/build.rs#L148 +[target.'cfg(target_os = "windows")'.dependencies] +ort = { version = "2.0.0-alpha.2", default-features = false, features = [ + "ndarray", + "half", + "load-dynamic", + "directml", +] } +[target.'cfg(target_os = "linux")'.dependencies] +ort = { version = "2.0.0-alpha.2", default-features = false, features = [ + "ndarray", + "half", + "load-dynamic", + "xnnpack", +] } +# [target.'cfg(target_os = "android")'.dependencies] +# ort = { version = "2.0.0-alpha.2", default-features = false, features = [ +# "half", +# "load-dynamic", +# "qnn", +# "nnapi", +# "xnnpack", +# "acl", +# "armnn", +# ] } +[target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies] +ort = { version = "2.0.0-alpha.2", features = [ + "ndarray", + "half", + "load-dynamic", + "coreml", + "xnnpack", +] } diff --git a/crates/ai/README.md b/crates/ai/README.md new file mode 100644 index 000000000..514c3627d --- /dev/null +++ b/crates/ai/README.md @@ -0,0 +1,3 @@ +# Spacedrive AI + +A collection of AI baked features for Spacedrive. diff --git a/crates/ai/src/image_labeler/actor.rs b/crates/ai/src/image_labeler/actor.rs new file mode 100644 index 000000000..e67a616a4 --- /dev/null +++ b/crates/ai/src/image_labeler/actor.rs @@ -0,0 +1,569 @@ +use sd_file_path_helper::file_path_for_media_processor; +use sd_prisma::prisma::{location, PrismaClient}; +use sd_utils::error::FileIOError; + +use std::{ + cell::RefCell, + collections::{HashMap, VecDeque}, + ops::Deref, + path::{Path, PathBuf}, + pin::pin, + sync::Arc, + time::Duration, +}; + +use async_channel as chan; +use futures::stream::StreamExt; +use futures_concurrency::stream::Merge; +use serde::{Deserialize, Serialize}; +use tokio::{ + fs, io, spawn, + sync::{oneshot, RwLock}, + task::JoinHandle, + time::timeout, +}; +use tracing::{debug, error, info}; +use uuid::Uuid; + +use super::{ + model::{Model, ModelAndSession}, + process::{spawned_processing, FinishStatus}, + BatchToken, ImageLabelerError, LabelerOutput, +}; + +const ONE_SEC: Duration = Duration::from_secs(1); +const PENDING_BATCHES_FILE: &str = "pending_image_labeler_batches.bin"; + +type ResumeBatchRequest = ( + BatchToken, + Arc, + oneshot::Sender, ImageLabelerError>>, +); + +type UpdateModelRequest = ( + Box, + oneshot::Sender>, +); + +pub(super) struct Batch { + pub(super) token: BatchToken, + pub(super) location_id: location::id::Type, + pub(super) location_path: PathBuf, + pub(super) file_paths: Vec, + pub(super) output_tx: chan::Sender, + pub(super) is_resumable: bool, + pub(super) db: Arc, +} + +#[derive(Serialize, Deserialize, Debug)] +struct ResumableBatch { + location_id: location::id::Type, + location_path: PathBuf, + file_paths: Vec, +} + +pub struct ImageLabeler { + to_resume_batches_file_path: PathBuf, + new_batches_tx: chan::Sender, + resume_batch_tx: chan::Sender, + update_model_tx: chan::Sender, + shutdown_tx: chan::Sender>, + to_resume_batches: Arc>>, + handle: RefCell>>, +} + +impl ImageLabeler { + pub async fn new( + model: Box, + data_directory: impl AsRef, + ) -> Result { + let to_resume_batches_file_path = data_directory.as_ref().join(PENDING_BATCHES_FILE); + + let model_and_session = Arc::new(RwLock::new( + ModelAndSession::new(model, data_directory.as_ref().join("models")).await?, + )); + + let to_resume_batches = Arc::new(RwLock::new( + match fs::read(&to_resume_batches_file_path).await { + Ok(bytes) => { + let pending_batches = + rmp_serde::from_slice::>(&bytes)?; + info!( + "Image labeler had {} pending batches to be resumed", + pending_batches.len() + ); + + if let Err(e) = fs::remove_file(&to_resume_batches_file_path).await { + error!( + "{:#?}", + ImageLabelerError::from(FileIOError::from(( + &to_resume_batches_file_path, + e, + "Failed to remove to resume batches file", + ))) + ); + } + + pending_batches + } + Err(e) if e.kind() == io::ErrorKind::NotFound => { + // If the file doesn't exist, we just start with an empty list + HashMap::new() + } + Err(e) => { + return Err(ImageLabelerError::FileIO(FileIOError::from(( + &to_resume_batches_file_path, + e, + "Failed to read to resume batches file", + )))) + } + }, + )); + + let (new_batches_tx, new_batches_rx) = chan::unbounded(); + let (resume_batch_tx, resume_batch_rx) = chan::bounded(4); + let (update_model_tx, update_model_rx) = chan::bounded(1); + let (shutdown_tx, shutdown_rx) = chan::bounded(1); + + let batch_supervisor_handle = tokio::spawn({ + let to_resume_batches = Arc::clone(&to_resume_batches); + async move { + loop { + let handle = tokio::spawn(actor_loop( + Arc::clone(&model_and_session), + new_batches_rx.clone(), + resume_batch_rx.clone(), + update_model_rx.clone(), + shutdown_rx.clone(), + Arc::clone(&to_resume_batches), + )); + + if let Err(e) = handle.await { + error!("Batch processor panicked: {e:#?}; restarting..."); + } else { + // process_batches exited normally, so we can exit as well + break; + } + } + } + }); + + Ok(Self { + to_resume_batches_file_path, + new_batches_tx, + resume_batch_tx, + update_model_tx, + shutdown_tx, + to_resume_batches, + handle: RefCell::new(Some(batch_supervisor_handle)), + }) + } + + async fn new_batch_inner( + &self, + location_id: location::id::Type, + location_path: PathBuf, + file_paths: Vec, + db: Arc, + is_resumable: bool, + ) -> (BatchToken, chan::Receiver) { + let (tx, rx) = chan::bounded(usize::max(file_paths.len(), 1)); + let token = Uuid::new_v4(); + if !file_paths.is_empty() { + if self + .new_batches_tx + .send(Batch { + token, + location_id, + location_path, + file_paths, + output_tx: tx, + is_resumable, + db, + }) + .await + .is_err() + { + error!("Failed to send batch to image labeller"); + } + } else { + // If there are no files to process, we close the channel immediately so the receiver + // side will never wait for a message + tx.close(); + } + + (token, rx) + } + + pub async fn new_batch( + &self, + location_id: location::id::Type, + location_path: PathBuf, + file_paths: Vec, + db: Arc, + ) -> chan::Receiver { + self.new_batch_inner(location_id, location_path, file_paths, db, false) + .await + .1 + } + + /// Resumable batches have lower priority than normal batches + pub async fn new_resumable_batch( + &self, + location_id: location::id::Type, + location_path: PathBuf, + file_paths: Vec, + db: Arc, + ) -> (BatchToken, chan::Receiver) { + self.new_batch_inner(location_id, location_path, file_paths, db, true) + .await + } + + pub async fn change_model(&self, model: Box) -> Result<(), ImageLabelerError> { + let (tx, rx) = oneshot::channel(); + + if self.update_model_tx.send((model, tx)).await.is_err() { + error!("Failed to send model update to image labeller"); + } + + rx.await + .expect("model update result channel unexpectedly closed") + } + + pub async fn shutdown(&self) { + debug!("Shutting down image labeller"); + + let (tx, rx) = oneshot::channel(); + + self.new_batches_tx.close(); + self.resume_batch_tx.close(); + self.update_model_tx.close(); + + if self.shutdown_tx.send(tx).await.is_err() { + error!("Failed to send stop signal to image labeller model executor"); + } + + self.shutdown_tx.close(); + + rx.await + .expect("critical error: image labeller shutdown result channel unexpectedly closed"); + + if let Some(handle) = self + .handle + .try_borrow_mut() + .ok() + .and_then(|mut maybe_handle| maybe_handle.take()) + { + if let Err(e) = handle.await { + error!("Failed to join image labeller supervisors: {e:#?}"); + } + } + + let to_resume_batches = self.to_resume_batches.read().await; + + if !to_resume_batches.is_empty() { + if let Ok(pending_batches) = rmp_serde::to_vec_named(to_resume_batches.deref()) + .map_err(|e| error!("{:#?}", ImageLabelerError::from(e))) + { + if let Err(e) = fs::write(&self.to_resume_batches_file_path, &pending_batches).await + { + error!( + "{:#?}", + ImageLabelerError::from(FileIOError::from(( + &self.to_resume_batches_file_path, + e, + "Failed to write to resume batches file", + ))) + ); + } + } + } + } + + pub async fn resume_batch( + &self, + token: BatchToken, + db: Arc, + ) -> Result, ImageLabelerError> { + let (tx, rx) = oneshot::channel(); + + self.resume_batch_tx + .send((token, db, tx)) + .await + .expect("critical error: image labeler communication channel unexpectedly closed"); + + rx.await + .expect("critical error: image labeler resume batch result channel unexpectedly closed") + } +} + +/// SAFETY: Due to usage of refcell we lost `Sync` impl, but we only use it to have a shutdown method +/// receiving `&self` which is called once, and we also use `try_borrow_mut` so we never panic +unsafe impl Sync for ImageLabeler {} + +async fn actor_loop( + model_and_session: Arc>, + new_batches_rx: chan::Receiver, + resume_batch_rx: chan::Receiver, + update_model_rx: chan::Receiver, + shutdown_rx: chan::Receiver>, + to_resume_batches: Arc>>, +) { + let (done_tx, done_rx) = chan::bounded(1); + let (stop_tx, stop_rx) = chan::bounded(1); + + let new_batches_rx_for_shutdown = new_batches_rx.clone(); + + // TODO: Make this configurable! + let available_parallelism = std::thread::available_parallelism().map_or_else( + |e| { + error!("Failed to get available parallelism: {e:#?}"); + 1 + }, + // Using 25% of available parallelism + |non_zero| usize::max(non_zero.get() / 4, 1), + ); + + info!( + "Image labeler available parallelism: {} cores", + available_parallelism + ); + + enum StreamMessage { + NewBatch(Batch), + ResumeBatch( + BatchToken, + Arc, + oneshot::Sender, ImageLabelerError>>, + ), + UpdateModel( + Box, + oneshot::Sender>, + ), + BatchDone(FinishStatus), + Shutdown(oneshot::Sender<()>), + } + + let mut queue = VecDeque::with_capacity(16); + + let mut currently_processing = None; + + let mut msg_stream = pin!(( + new_batches_rx.map(StreamMessage::NewBatch), + resume_batch_rx.map(|(token, db, done_tx)| StreamMessage::ResumeBatch(token, db, done_tx)), + update_model_rx.map(|(model, done_tx)| StreamMessage::UpdateModel(model, done_tx)), + done_rx.clone().map(StreamMessage::BatchDone), + shutdown_rx.map(StreamMessage::Shutdown) + ) + .merge()); + + while let Some(msg) = msg_stream.next().await { + match msg { + StreamMessage::NewBatch(batch @ Batch { is_resumable, .. }) => { + if currently_processing.is_none() { + currently_processing = Some(spawn(spawned_processing( + Arc::clone(&model_and_session), + batch, + available_parallelism, + stop_rx.clone(), + done_tx.clone(), + ))); + } else if !is_resumable { + // TODO: Maybe we should cancel the current batch and start this one instead? + queue.push_front(batch) + } else { + queue.push_back(batch) + } + } + + StreamMessage::ResumeBatch(token, db, resume_done_tx) => { + let resume_result = if let Some((batch, output_rx)) = + to_resume_batches.write().await.remove(&token).map( + |ResumableBatch { + location_id, + location_path, + file_paths, + }| { + let (output_tx, output_rx) = + chan::bounded(usize::max(file_paths.len(), 1)); + ( + Batch { + token, + db, + output_tx, + location_id, + location_path, + file_paths, + is_resumable: true, + }, + output_rx, + ) + }, + ) { + if currently_processing.is_none() { + currently_processing = Some(spawn(spawned_processing( + Arc::clone(&model_and_session), + batch, + available_parallelism, + stop_rx.clone(), + done_tx.clone(), + ))); + } else { + queue.push_back(batch) + } + + Ok(output_rx) + } else { + Err(ImageLabelerError::TokenNotFound(token)) + }; + + if resume_done_tx.send(resume_result).is_err() { + error!("Failed to send batch resume result from image labeller"); + } + } + + StreamMessage::UpdateModel(new_model, update_done_tx) => { + if currently_processing.is_some() { + let (tx, rx) = oneshot::channel(); + + stop_tx.send(tx).await.expect("stop_tx unexpectedly closed"); + + if timeout(ONE_SEC, rx).await.is_err() { + error!("Failed to stop image labeller batch processor"); + if stop_rx.is_full() { + stop_rx.recv().await.ok(); + } + } + } + + if update_done_tx + .send( + model_and_session + .write() + .await + .update_model(new_model) + .await, + ) + .is_err() + { + error!("Failed to send model update result from image labeller"); + } + } + + StreamMessage::BatchDone(FinishStatus::Interrupted(batch)) => { + if currently_processing.is_none() { + currently_processing = Some(spawn(spawned_processing( + Arc::clone(&model_and_session), + batch, + 1, + stop_rx.clone(), + done_tx.clone(), + ))); + } else { + queue.push_front(batch); + } + } + + StreamMessage::BatchDone(FinishStatus::Done(token, output_tx)) => { + debug!("Batch done"); + + if let Some(handle) = currently_processing.take() { + if let Err(e) = handle.await { + error!("Failed to join image labeller batch processor: {e:#?}"); + } + } + + output_tx.close(); // So our listener can exit + + if let Some(next_batch) = queue.pop_front() { + currently_processing = Some(spawn(spawned_processing( + Arc::clone(&model_and_session), + next_batch, + 4, + stop_rx.clone(), + done_tx.clone(), + ))); + } + } + + StreamMessage::Shutdown(shutdown_done_tx) => { + debug!("Shutting down image labeller batch processor"); + + if let Some(handle) = currently_processing.take() { + let (tx, rx) = oneshot::channel(); + + stop_tx.send(tx).await.expect("stop_tx unexpectedly closed"); + + if timeout(ONE_SEC * 5, rx).await.is_err() { + error!("Failed to stop image labeller batch processor"); + if stop_rx.is_full() { + stop_rx.recv().await.ok(); + } + } + + if let Err(e) = handle.await { + error!("Failed to join image labeller batch processor: {e:#?}"); + } + + if let Ok(FinishStatus::Interrupted(batch)) = done_rx.recv().await { + queue.push_front(batch); + } + } + + let pending_batches = new_batches_rx_for_shutdown + .filter_map( + |Batch { + token, + location_id, + location_path, + file_paths, + is_resumable, + .. + }| async move { + is_resumable.then_some(( + token, + ResumableBatch { + location_id, + location_path, + file_paths, + }, + )) + }, + ) + .collect::>() + .await; + + to_resume_batches.write().await.extend( + queue + .into_iter() + .filter_map( + |Batch { + token, + location_id, + location_path, + file_paths, + is_resumable, + .. + }| { + is_resumable.then_some(( + token, + ResumableBatch { + location_id, + location_path, + file_paths, + }, + )) + }, + ) + .chain(pending_batches.into_iter()), + ); + + shutdown_done_tx + .send(()) + .expect("shutdown_done_tx unexpectedly closed"); + + break; + } + } + } +} diff --git a/crates/ai/src/image_labeler/mod.rs b/crates/ai/src/image_labeler/mod.rs new file mode 100644 index 000000000..c5c2a3529 --- /dev/null +++ b/crates/ai/src/image_labeler/mod.rs @@ -0,0 +1,54 @@ +use sd_prisma::prisma::file_path; +use sd_utils::{db::MissingFieldError, error::FileIOError}; + +use std::path::Path; + +use thiserror::Error; +use tracing::error; +use uuid::Uuid; + +mod actor; +mod model; +mod process; + +pub use actor::ImageLabeler; +pub use model::{DownloadModelError, Model, YoloV8, DEFAULT_MODEL_VERSION}; + +pub type BatchToken = Uuid; + +#[derive(Debug)] +pub struct LabelerOutput { + pub file_path_id: file_path::id::Type, + pub has_new_labels: bool, + pub result: Result<(), ImageLabelerError>, +} + +#[derive(Debug, Error)] +pub enum ImageLabelerError { + #[error("model executor failed: {0}")] + ModelExecutorFailed(#[from] ort::Error), + #[error("image load failed : {0}", .1.display())] + ImageLoadFailed(image::ImageError, Box), + #[error("failed to get isolated file path data: {0}")] + IsolateFilePathData(#[from] MissingFieldError), + #[error("file_path with unsupported extension: ")] + UnsupportedExtension(file_path::id::Type, String), + #[error("file_path too big: ")] + FileTooBig(file_path::id::Type, usize), + #[error("model file not found: {}", .0.display())] + ModelFileNotFound(Box), + #[error("no model available for inference")] + NoModelAvailable, + #[error("failed to decode pending batches: {0}")] + Decode(#[from] rmp_serde::decode::Error), + #[error("failed to encode pending batches: {0}")] + Encode(#[from] rmp_serde::encode::Error), + #[error("database error: {0}")] + Database(#[from] prisma_client_rust::QueryError), + #[error("resume token not found: {0}")] + TokenNotFound(BatchToken), + #[error(transparent)] + DownloadModel(#[from] DownloadModelError), + #[error(transparent)] + FileIO(#[from] FileIOError), +} diff --git a/crates/ai/src/image_labeler/model/mod.rs b/crates/ai/src/image_labeler/model/mod.rs new file mode 100644 index 000000000..3cb93b750 --- /dev/null +++ b/crates/ai/src/image_labeler/model/mod.rs @@ -0,0 +1,262 @@ +use sd_utils::error::FileIOError; + +use std::{ + collections::HashSet, + path::{Path, PathBuf}, +}; + +use futures::prelude::stream::StreamExt; +use image::ImageFormat; +use ort::{Session, SessionBuilder, SessionInputs, SessionOutputs}; +use thiserror::Error; +use tokio::{ + fs, + io::{self, AsyncWriteExt}, +}; +use tracing::{error, info, trace}; +use url::Url; + +use super::ImageLabelerError; + +mod yolov8; + +pub use yolov8::YoloV8; +pub use yolov8::DEFAULT_MODEL_VERSION; + +pub enum ModelSource { + Url(Url), + Path(PathBuf), +} + +pub trait Model: Send + Sync + 'static { + fn name(&self) -> &'static str { + std::any::type_name::() + } + + fn origin(&self) -> &ModelSource; + + fn version(&self) -> &str; + + fn versions() -> Vec<&'static str> + where + Self: Sized; + + fn prepare_input<'image>( + &self, + image_path: &Path, + image: &'image [u8], + format: ImageFormat, + ) -> Result, ImageLabelerError>; + + fn process_output( + &self, + output: SessionOutputs<'_>, + ) -> Result, ImageLabelerError>; +} + +pub(super) struct ModelAndSession { + maybe_model: Option>, + maybe_session: Option, + model_data_dir: PathBuf, +} + +impl ModelAndSession { + pub async fn new( + model: Box, + data_dir: impl AsRef, + ) -> Result { + let data_dir = data_dir.as_ref().join(model.name()); + let model_path = download_model(model.origin(), &data_dir).await?; + + info!( + "Loading mode: {} from {}", + model.name(), + model_path.display() + ); + + let maybe_session = check_model_file(&model_path) + .await + .map_err(|e| error!("Failed to check model file before passing to Ort: {e:#?}")) + .ok() + .and_then(|()| { + load_model(&model_path) + .map(|session| { + info!("Loaded model: {}", model.name()); + trace!("{session:#?}"); + session + }) + .map_err(|e| error!("Failed to load model: {e:#?}")) + .ok() + }); + + Ok(Self { + maybe_model: maybe_session.is_some().then_some(model), + maybe_session, + model_data_dir: data_dir, + }) + } + + pub fn can_process(&self) -> bool { + self.maybe_session.is_some() && self.maybe_model.is_some() + } + + pub async fn update_model( + &mut self, + new_model: Box, + ) -> Result<(), ImageLabelerError> { + info!("Attempting to change image labeler models..."); + + let model_path = download_model(new_model.origin(), &self.model_data_dir).await?; + + info!( + "Change mode: {} to {}", + new_model.name(), + model_path.display() + ); + + check_model_file(&model_path).await.and_then(|()| { + load_model(&model_path) + .map(|session| { + info!( + "Changing models: {} -> {}", + self.maybe_model + .as_ref() + .map(|old_model| old_model.name()) + .unwrap_or("None"), + new_model.name() + ); + + self.maybe_model = Some(new_model); + self.maybe_session = Some(session); + }) + .map_err(|e| { + self.maybe_model = None; + self.maybe_session = None; + + e + }) + }) + } + + pub fn process_single_image( + &self, + image_path: &Path, + image: Vec, + format: ImageFormat, + ) -> Result, ImageLabelerError> { + if let (Some(session), Some(model)) = (&self.maybe_session, self.maybe_model.as_deref()) { + let inputs = model.prepare_input(image_path, &image, format)?; + let outputs = session.run(inputs)?; + model.process_output(outputs) + } else { + error!("Tried to process image without a loaded model"); + Err(ImageLabelerError::NoModelAvailable) + } + } +} + +#[derive(Error, Debug)] +pub enum DownloadModelError { + #[error("Failed to download due to request error: {0}")] + RequestError(#[from] reqwest::Error), + #[error("Failed to download due to status code: {0}")] + HttpStatusError(reqwest::StatusCode), + #[error("Invalid file name for url: {0}")] + InvalidUrlFileName(Url), + #[error("Unknown model version to download: {0}")] + UnknownModelVersion(String), + + #[error(transparent)] + FileIO(#[from] FileIOError), +} + +fn load_model(model_path: impl AsRef) -> Result { + SessionBuilder::new()? + .with_parallel_execution(true)? + .with_memory_pattern(true)? + .with_model_from_file(model_path) + .map_err(Into::into) +} + +async fn download_model( + model_origin: &ModelSource, + data_dir: impl AsRef, +) -> Result { + let data_dir = data_dir.as_ref(); + + match model_origin { + ModelSource::Url(url) => { + let Some(file_name) = url.path_segments().and_then(|segments| segments.last()) else { + return Err(DownloadModelError::InvalidUrlFileName(url.to_owned())); + }; + + fs::create_dir_all(data_dir) + .await + .map_err(|e| FileIOError::from((data_dir, e, "Failed to create data directory")))?; + + let file_path = data_dir.join(file_name); + match fs::metadata(&file_path).await { + Ok(_) => return Ok(file_path), + Err(e) if e.kind() != io::ErrorKind::NotFound => { + return Err(DownloadModelError::FileIO(FileIOError::from(( + file_path, + e, + "Failed to get metadata for model file", + )))) + } + _ => { + info!("Dowloading model from: {} to {}", url, file_path.display()); + let response = reqwest::get(url.as_str()).await?; + // Ensure the request was successful (status code 2xx) + if !response.status().is_success() { + return Err(DownloadModelError::HttpStatusError(response.status())); + } + + // Create or open a file at the specified path + let mut file = fs::File::create(&file_path).await.map_err(|e| { + FileIOError::from(( + &file_path, + e, + "Failed to create the model file on disk", + )) + })?; + // Stream the response body to the file + let mut body = response.bytes_stream(); + while let Some(chunk) = body.next().await { + let chunk = chunk?; + file.write_all(&chunk).await.map_err(|e| { + FileIOError::from(( + &file_path, + e, + "Failed to write chunk of data to the model file on disk", + )) + })?; + } + } + } + + Ok(file_path) + } + ModelSource::Path(file_path) => Ok(file_path.to_owned()), + } +} + +async fn check_model_file(model_path: impl AsRef) -> Result<(), ImageLabelerError> { + let model_path = model_path.as_ref(); + + match fs::metadata(model_path).await { + Ok(_) => Ok(()), + Err(e) if e.kind() == io::ErrorKind::NotFound => { + error!( + "Model file not found: '{}'. Image labeler will be disabled!", + model_path.display() + ); + Ok(()) + } + Err(e) => Err(ImageLabelerError::FileIO(FileIOError::from(( + model_path, + e, + "Failed to get metadata for model file", + )))), + } +} diff --git a/crates/ai/src/image_labeler/model/yolov8.rs b/crates/ai/src/image_labeler/model/yolov8.rs new file mode 100644 index 000000000..8afe856e3 --- /dev/null +++ b/crates/ai/src/image_labeler/model/yolov8.rs @@ -0,0 +1,164 @@ +use crate::utils::get_path_relative_to_exe; + +use std::{ + collections::{HashMap, HashSet}, + fmt::Display, + path::Path, +}; + +use half::f16; +use image::{imageops::FilterType, load_from_memory_with_format, GenericImageView, ImageFormat}; +use ndarray::{s, Array, Axis}; +use once_cell::sync::Lazy; +use ort::{inputs, SessionInputs, SessionOutputs}; +use url::Url; + +use super::{DownloadModelError, ImageLabelerError, Model, ModelSource}; + +pub struct YoloV8 { + model_origin: &'static ModelSource, + model_version: String, +} + +// This path must be relative to the running binary +#[cfg(windows)] +const MODEL_LOCATION: &str = "./models"; +#[cfg(unix)] +const MODEL_LOCATION: &str = if cfg!(target_os = "macos") { + "../Frameworks/Spacedrive.framework/Resources/Models" +} else { + "../share/spacedrive/models" +}; + +pub static DEFAULT_MODEL_VERSION: &str = "Yolo Small"; + +static MODEL_VERSIONS: Lazy> = Lazy::new(|| { + HashMap::from([ + ("Yolo Nano", ModelSource::Url(Url::parse("https://github.com/spacedriveapp/native-deps/releases/download/yolo-2023-12-05/yolov8n.onnx").expect("Must be a valid URL"))), + (DEFAULT_MODEL_VERSION, ModelSource::Path(get_path_relative_to_exe(Path::new(MODEL_LOCATION).join("yolov8s.onnx")))), + ("Yolo Medium", ModelSource::Url(Url::parse("https://github.com/spacedriveapp/native-deps/releases/download/yolo-2023-12-05/yolov8m.onnx").expect("Must be a valid URL"))), + ("Yolo Large", ModelSource::Url(Url::parse("https://github.com/spacedriveapp/native-deps/releases/download/yolo-2023-12-05/yolov8l.onnx").expect("Must be a valid URL"))), + ("Yolo Extra", ModelSource::Url(Url::parse("https://github.com/spacedriveapp/native-deps/releases/download/yolo-2023-12-05/yolov8x.onnx").expect("Must be a valid URL"))), + ]) +}); + +impl YoloV8 { + pub fn model(version: Option) -> Result, DownloadModelError> + where + T: AsRef + Display, + { + let (model_version, model_origin) = match version { + Some(version) => ( + version.to_string(), + MODEL_VERSIONS + .get(version.as_ref()) + .ok_or_else(|| DownloadModelError::UnknownModelVersion(version.to_string()))?, + ), + None => { + let version = DEFAULT_MODEL_VERSION; + ( + version.to_string(), + MODEL_VERSIONS + .get(version) + .expect("Default model version must be valid"), + ) + } + }; + + Ok(Box::new(Self { + model_origin, + model_version, + })) + } +} + +impl Model for YoloV8 { + fn origin(&self) -> &'static ModelSource { + self.model_origin + } + + fn version(&self) -> &str { + self.model_version.as_str() + } + + fn versions() -> Vec<&'static str> { + MODEL_VERSIONS.keys().copied().collect() + } + + fn prepare_input<'image>( + &self, + path: &Path, + image: &'image [u8], + format: ImageFormat, + ) -> Result, ImageLabelerError> { + let original_img = load_from_memory_with_format(image, format) + .map_err(|e| ImageLabelerError::ImageLoadFailed(e, path.into()))?; + + let img = original_img.resize_exact(640, 640, FilterType::CatmullRom); + let mut input = Array::::zeros((1, 3, 640, 640)); + for pixel in img.pixels() { + let x = pixel.0 as _; + let y = pixel.1 as _; + let [r, g, b, _] = pixel.2 .0; + input[[0, 0, y, x]] = f16::from_f32((r as f32) / 255.); + input[[0, 1, y, x]] = f16::from_f32((g as f32) / 255.); + input[[0, 2, y, x]] = f16::from_f32((b as f32) / 255.); + } + + inputs!["images" => input.view()] + .map(Into::into) + .map_err(Into::into) + } + + fn process_output( + &self, + output: SessionOutputs<'_>, + ) -> Result, ImageLabelerError> { + #[rustfmt::skip] + const YOLOV8_CLASS_LABELS: [&str; 80] = [ + "person", "bicycle", "car", "motorcycle", "airplane", "bus", "train", "truck", + "boat", "traffic light", "fire hydrant", "stop sign", "parking meter", "bench", + "bird", "cat", "dog", "horse", "sheep", "cow", "elephant", "bear", "zebra", + "giraffe", "backpack", "umbrella", "handbag", "tie", "suitcase", "frisbee", + "skis", "snowboard", "sports ball", "kite", "baseball bat", "baseball glove", + "skateboard", "surfboard", "tennis racket", "bottle", "wine glass", "cup", + "fork", "knife", "spoon", "bowl", "banana", "apple", "sandwich", "orange", + "broccoli", "carrot", "hot dog", "pizza", "donut", "cake", "chair", "couch", + "potted plant", "bed", "dining table", "toilet", "tv", "laptop", "mouse", + "remote", "keyboard", "cell phone", "microwave", "oven", "toaster", "sink", + "refrigerator", "book", "clock", "vase", "scissors", "teddy bear", + "hair drier", "toothbrush" + ]; + + let output0 = &output["output0"]; + + let output_tensor = output0.extract_tensor::()?; + + let output_view = output_tensor.view(); + + let output_tensor_transposed = output_view.t(); + + let output = output_tensor_transposed.slice(s![.., .., 0]); + + Ok(output + .axis_iter(Axis(0)) + .map(|row| { + row.iter() + // skip bounding box coordinates + .skip(4) + .enumerate() + .map(|(class_id, probability)| (class_id, *probability)) + .reduce(|accum, row| if row.1 > accum.1 { row } else { accum }) + .expect("not empty output") + }) + .filter(|(_, probability)| probability.to_f32() > 0.6) + .map(|(class_id, _)| YOLOV8_CLASS_LABELS[class_id]) + .fold(HashSet::default(), |mut set, label| { + if !set.contains(label) { + set.insert(label.to_string()); + } + + set + })) + } +} diff --git a/crates/ai/src/image_labeler/process.rs b/crates/ai/src/image_labeler/process.rs new file mode 100644 index 000000000..b625bc621 --- /dev/null +++ b/crates/ai/src/image_labeler/process.rs @@ -0,0 +1,451 @@ +use sd_file_path_helper::{file_path_for_media_processor, IsolatedFilePathData}; +use sd_prisma::prisma::{file_path, label, label_on_object, object, PrismaClient}; +use sd_utils::{db::MissingFieldError, error::FileIOError}; + +use std::{ + collections::{HashMap, HashSet, VecDeque}, + path::{Path, PathBuf}, + sync::Arc, +}; + +use async_channel as chan; +use chrono::{DateTime, FixedOffset, Utc}; +use futures_concurrency::future::{Join, Race}; +use image::ImageFormat; +use tokio::{ + fs, spawn, + sync::{oneshot, OwnedRwLockReadGuard, OwnedSemaphorePermit, RwLock, Semaphore}, +}; +use tracing::{error, warn}; +use uuid::Uuid; + +use super::{actor::Batch, model::ModelAndSession, BatchToken, ImageLabelerError, LabelerOutput}; + +const MAX_FILE_SIZE: u64 = 100 * 1024 * 1024; // 100 MB + +async fn reject_all_no_model( + file_paths: Vec, + output_tx: &chan::Sender, +) { + file_paths + .into_iter() + .map( + |file_path_for_media_processor::Data { id, .. }| async move { + if output_tx + .send(LabelerOutput { + file_path_id: id, + has_new_labels: false, + result: Err(ImageLabelerError::NoModelAvailable), + }) + .await + .is_err() + { + error!( + "Failed to send batch output with no model error, " + ); + } + }, + ) + .collect::>() + .join() + .await; +} + +pub(super) enum FinishStatus { + Interrupted(Batch), + Done(BatchToken, chan::Sender), +} + +pub(super) async fn spawned_processing( + model_and_session: Arc>, + Batch { + token, + location_id, + location_path, + file_paths, + output_tx, + db, + is_resumable, + }: Batch, + available_parallelism: usize, + stop_rx: chan::Receiver>, + done_tx: chan::Sender, +) { + let mut errors = Vec::new(); + + // We're already discarding failed ones, so we don't need to keep track of them + let mut queue = file_paths + .into_iter() + .filter_map(|file_path| { + if file_path.object_id.is_none() { + errors.push(( + file_path.id, + ImageLabelerError::IsolateFilePathData(MissingFieldError::new( + "file_path.object_id", + )), + )); + + return None; + } + + let file_path_id = file_path.id; + let Ok(iso_file_path) = IsolatedFilePathData::try_from((location_id, &file_path)) + .map_err(|e| { + errors.push((file_path_id, e.into())); + }) + else { + return None; + }; + + match ImageFormat::from_extension(iso_file_path.extension()) { + Some(format) => { + let path = location_path.join(&iso_file_path); + Some((file_path, path, format)) + } + None => { + errors.push(( + file_path_id, + ImageLabelerError::UnsupportedExtension( + file_path_id, + iso_file_path.extension().to_owned(), + ), + )); + + None + } + } + }) + .collect::>(); + + errors + .into_iter() + .map(|(file_path_id, error)| { + let output_tx = &output_tx; + async move { + if output_tx + .send(LabelerOutput { + file_path_id, + has_new_labels: false, + result: Err(error), + }) + .await + .is_err() + { + error!( + "Failed to send batch output with errors, " + ); + } + } + }) + .collect::>() + .join() + .await; + + if queue.is_empty() { + done_tx + .send(FinishStatus::Done(token, output_tx)) + .await + .expect("done_tx unexpectedly closed"); + return; + } + + let semaphore = Arc::new(Semaphore::new(available_parallelism)); + + // From this point ownwards, we lock the model in read mode + let model_and_session = Arc::new(model_and_session.read_owned().await); + + if !model_and_session.can_process() { + reject_all_no_model( + queue + .into_iter() + .map(|(file_path, _, _)| file_path) + .collect(), + &output_tx, + ) + .await; + done_tx + .send(FinishStatus::Done(token, output_tx)) + .await + .expect("done_tx unexpectedly closed"); + return; + } + + enum RaceOutput { + Done, + Stop(oneshot::Sender<()>), + } + + let mut handles = Vec::with_capacity(queue.len()); + + let mut on_flight = HashMap::with_capacity(queue.len()); + + let (completed_tx, completed_rx) = chan::bounded(queue.len()); + + let (finish_status, maybe_interrupted_tx) = if let RaceOutput::Stop(tx) = ( + async { + while !queue.is_empty() { + let (file_path, path, format) = queue.pop_front().expect("queue is not empty"); + + let permit = Arc::clone(&semaphore) + .acquire_owned() + .await + .expect("semaphore unexpectedly closed"); + + let ids = ( + file_path.id, + file_path.object_id.expect("alredy checked above"), + ); + + if output_tx.is_closed() { + warn!("Image labeler output channel was closed, dropping current batch..."); + queue.clear(); + on_flight.clear(); + + break; + } + + on_flight.insert(file_path.id, file_path); + + handles.push(spawn(spawned_process_single_file( + Arc::clone(&model_and_session), + ids, + path, + format, + (output_tx.clone(), completed_tx.clone()), + Arc::clone(&db), + permit, + ))); + } + + RaceOutput::Done + }, + async { RaceOutput::Stop(stop_rx.recv().await.expect("stop_rx unexpectedly closed")) }, + ) + .race() + .await + { + for handle in &handles { + handle.abort(); + } + + completed_tx.close(); + + while let Ok(file_path_id) = completed_rx.recv().await { + on_flight.remove(&file_path_id); + } + + let status = if queue.is_empty() && on_flight.is_empty() { + FinishStatus::Done(token, output_tx) + } else { + FinishStatus::Interrupted(Batch { + token, + location_id, + location_path, + file_paths: on_flight + .into_values() + .chain(queue.into_iter().map(|(file_path, _, _)| file_path)) + .collect(), + output_tx, + db, + is_resumable, + }) + }; + + (status, Some(tx)) + } else { + (FinishStatus::Done(token, output_tx), None) + }; + + if let Some(tx) = maybe_interrupted_tx { + if let Err(e) = tx.send(()) { + error!("Failed to send stop signal to image labeller batch processor: {e:#?}"); + } + } else { + handles + .into_iter() + .map(|handle| async move { + if let Err(e) = handle.await { + error!("Failed to join image labeller batch processor: {e:#?}"); + } + }) + .collect::>() + .join() + .await; + } + + done_tx + .send(finish_status) + .await + .expect("critical error: image labeller batch processor unexpectedly closed"); +} + +async fn spawned_process_single_file( + model_and_session: Arc>, + (file_path_id, object_id): (file_path::id::Type, object::id::Type), + path: PathBuf, + format: ImageFormat, + (output_tx, completed_tx): ( + chan::Sender, + chan::Sender, + ), + db: Arc, + _permit: OwnedSemaphorePermit, +) { + let image = + match extract_file_data(file_path_id, &path).await { + Ok(image) => image, + Err(e) => { + if output_tx + .send(LabelerOutput { + file_path_id, + has_new_labels: false, + result: Err(e), + }) + .await + .is_err() + { + error!("Failed to send batch output with I/O errors, "); + } + + if completed_tx.send(file_path_id).await.is_err() { + warn!("Failed to send completed file path id, ") + } + + return; + } + }; + + let labels = match model_and_session.process_single_image(path.as_path(), image, format) { + Ok(labels) => labels, + Err(e) => { + if output_tx + .send(LabelerOutput { + file_path_id, + has_new_labels: false, + result: Err(e), + }) + .await + .is_err() + { + error!("Failed to send batch output with model processing errors, "); + } + + if completed_tx.send(file_path_id).await.is_err() { + warn!("Failed to send completed file path id, ") + } + + return; + } + }; + + let (has_new_labels, result) = match assign_labels(object_id, labels, &db).await { + Ok(has_new_labels) => (has_new_labels, Ok(())), + Err(e) => (false, Err(e)), + }; + + if output_tx + .send(LabelerOutput { + file_path_id, + has_new_labels, + result, + }) + .await + .is_err() + { + error!("Failed to send batch output with database assign label results, "); + } + + if completed_tx.send(file_path_id).await.is_err() { + warn!("Failed to send completed file path id, ") + } +} + +async fn extract_file_data( + file_path_id: file_path::id::Type, + path: impl AsRef, +) -> Result, ImageLabelerError> { + let path = path.as_ref(); + + let metadata = fs::metadata(path).await.map_err(|e| { + FileIOError::from((path, e, "Failed to get metadata for file to get labels")) + })?; + + if metadata.len() > MAX_FILE_SIZE { + return Err(ImageLabelerError::FileTooBig( + file_path_id, + metadata.len() as usize, + )); + } + + fs::read(path) + .await + .map_err(|e| FileIOError::from((path, e, "Failed to read file to get labels")).into()) +} + +pub async fn assign_labels( + object_id: object::id::Type, + mut labels: HashSet, + db: &PrismaClient, +) -> Result { + let mut has_new_labels = false; + + let mut labels_ids = db + .label() + .find_many(vec![label::name::in_vec(labels.iter().cloned().collect())]) + .select(label::select!({ id name })) + .exec() + .await? + .into_iter() + .map(|label| { + labels.remove(&label.name); + + label.id + }) + .collect::>(); + + labels_ids.reserve(labels.len()); + + let date_created: DateTime = Utc::now().into(); + + if !labels.is_empty() { + labels_ids.extend( + db._batch( + labels + .into_iter() + .map(|name| { + db.label() + .create( + Uuid::new_v4().as_bytes().to_vec(), + name, + vec![label::date_created::set(date_created)], + ) + .select(label::select!({ id })) + }) + .collect::>(), + ) + .await? + .into_iter() + .map(|label| label.id), + ); + has_new_labels = true; + } + + db.label_on_object() + .create_many( + labels_ids + .into_iter() + .map(|label_id| { + label_on_object::create_unchecked( + label_id, + object_id, + vec![label_on_object::date_created::set(date_created)], + ) + }) + .collect(), + ) + .skip_duplicates() + .exec() + .await?; + + Ok(has_new_labels) +} diff --git a/crates/ai/src/lib.rs b/crates/ai/src/lib.rs new file mode 100644 index 000000000..81f09de57 --- /dev/null +++ b/crates/ai/src/lib.rs @@ -0,0 +1,94 @@ +use std::path::Path; + +use ort::{EnvironmentBuilder, LoggingLevel}; +use thiserror::Error; +use tracing::{debug, error}; + +pub mod image_labeler; +mod utils; + +// This path must be relative to the running binary +#[cfg(windows)] +const BINDING_LOCATION: &str = "."; +#[cfg(unix)] +const BINDING_LOCATION: &str = if cfg!(target_os = "macos") { + "../Frameworks/Spacedrive.framework/Libraries" +} else { + "../lib/spacedrive" +}; + +#[cfg(target_os = "windows")] +const LIB_NAME: &str = "onnxruntime.dll"; + +#[cfg(any(target_os = "macos", target_os = "ios"))] +const LIB_NAME: &str = "libonnxruntime.dylib"; + +#[cfg(any(target_os = "linux", target_os = "android"))] +const LIB_NAME: &str = "libonnxruntime.so"; + +pub fn init() -> Result<(), Error> { + let path = utils::get_path_relative_to_exe(Path::new(BINDING_LOCATION).join(LIB_NAME)); + + std::env::set_var("ORT_DYLIB_PATH", path); + + // Initialize AI stuff + EnvironmentBuilder::default() + .with_name("spacedrive") + .with_log_level(if cfg!(debug_assertions) { + LoggingLevel::Verbose + } else { + LoggingLevel::Info + }) + .with_execution_providers({ + #[cfg(any(target_os = "macos", target_os = "ios"))] + { + use ort::{CoreMLExecutionProvider, XNNPACKExecutionProvider}; + + [ + CoreMLExecutionProvider::default().build(), + XNNPACKExecutionProvider::default().build(), + ] + } + + #[cfg(target_os = "windows")] + { + use ort::DirectMLExecutionProvider; + + [DirectMLExecutionProvider::default().build()] + } + + #[cfg(target_os = "linux")] + { + use ort::XNNPACKExecutionProvider; + + [XNNPACKExecutionProvider::default().build()] + } + + // #[cfg(target_os = "android")] + // { + // use ort::{ + // ACLExecutionProvider, ArmNNExecutionProvider, NNAPIExecutionProvider, + // QNNExecutionProvider, XNNPACKExecutionProvider, + // }; + // [ + // QNNExecutionProvider::default().build(), + // NNAPIExecutionProvider::default().build(), + // XNNPACKExecutionProvider::default().build(), + // ACLExecutionProvider::default().build(), + // ArmNNExecutionProvider::default().build(), + // ] + // } + }) + .commit()?; + debug!("Initialized AI environment"); + + Ok(()) +} + +#[derive(Error, Debug)] +pub enum Error { + #[error("failed to initialize AI environment: {0}")] + Init(#[from] ort::Error), + #[error(transparent)] + ImageLabeler(#[from] image_labeler::ImageLabelerError), +} diff --git a/crates/ai/src/utils/mod.rs b/crates/ai/src/utils/mod.rs new file mode 100644 index 000000000..83b5c06d8 --- /dev/null +++ b/crates/ai/src/utils/mod.rs @@ -0,0 +1,25 @@ +use std::{ + env::{args_os, current_exe}, + path::{Path, PathBuf}, +}; +use tracing::error; + +pub(crate) fn get_path_relative_to_exe(path: impl AsRef) -> PathBuf { + current_exe() + .unwrap_or_else(|e| { + error!("Failed to get current exe path: {e:#?}"); + args_os() + .next() + .expect("there is always the first arg") + .into() + }) + .parent() + .and_then(|parent_path| { + parent_path + .join(path.as_ref()) + .canonicalize() + .map_err(|e| error!("{e:#?}")) + .ok() + }) + .unwrap_or_else(|| path.as_ref().to_path_buf()) +} diff --git a/crates/cache/Cargo.toml b/crates/cache/Cargo.toml index 279c194ef..c29269854 100644 --- a/crates/cache/Cargo.toml +++ b/crates/cache/Cargo.toml @@ -1,11 +1,11 @@ [package] name = "sd-cache" version = "0.0.0" -license.workspace = true -edition.workspace = true -repository.workspace = true +license = { workspace = true } +edition = { workspace = true } +repository = { workspace = true } [dependencies] -serde.workspace = true -serde_json.workspace = true -specta.workspace = true +serde = { workspace = true } +serde_json = { workspace = true } +specta = { workspace = true } diff --git a/crates/crypto/Cargo.toml b/crates/crypto/Cargo.toml index b98b5a808..b0d9cc14a 100644 --- a/crates/crypto/Cargo.toml +++ b/crates/crypto/Cargo.toml @@ -18,13 +18,13 @@ os-keyrings = ["dep:secret-service", "dep:security-framework"] [dependencies] # rng -rand = "0.8.5" -rand_chacha = "0.3.1" +rand = { workspace = true } +rand_chacha = { workspace = true } # hashing argon2 = "0.5.0" balloon-hash = "0.4.0" -blake3 = { version = "1.3.3", features = ["traits-preview"] } +blake3 = { workspace = true, features = ["traits-preview"] } # aeads aes-gcm = "0.10.1" @@ -35,15 +35,15 @@ aead = { version = "0.5.1", features = ["stream"] } zeroize = "1.5.7" # error handling -thiserror = "1.0.37" +thiserror = { workspace = true } # metadata de/serialization -serde = { version = "1.0", features = ["derive"], optional = true } -serde_json = { version = "1.0", optional = true } +serde = { workspace = true, features = ["derive"], optional = true } +serde_json = { workspace = true, optional = true } serde-big-array = { version = "0.5.1", optional = true } # for storedkey organisation and handling -uuid = { version = "1.1.2", features = ["v4"] } +uuid = { workspace = true, features = ["v4"] } # better concurrency for the keymanager dashmap = { version = "5.4.0", optional = true } @@ -55,7 +55,7 @@ specta = { workspace = true, features = ["uuid"], optional = true } # for asynchronous crypto tokio = { workspace = true, features = ["io-util", "rt-multi-thread", "sync"] } -hex = "0.4.3" +hex = { workspace = true } # linux OS keyring - using `v2.0.2` as newer versions broke a lot due to async/lifetimes [target.'cfg(target_os = "linux")'.dependencies] diff --git a/crates/deps-generator/Cargo.toml b/crates/deps-generator/Cargo.toml index 78dd27459..68e2627e0 100644 --- a/crates/deps-generator/Cargo.toml +++ b/crates/deps-generator/Cargo.toml @@ -8,9 +8,10 @@ repository = { workspace = true } edition = { workspace = true } [dependencies] -reqwest = { version = "0.11.22", features = ["blocking"] } -clap = { version = "4.4.7", features = ["derive"] } -anyhow = "1.0.75" -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" +anyhow = { workspace = true } +clap = { workspace = true, features = ["derive"] } +reqwest = { workspace = true, features = ["blocking"] } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } + cargo_metadata = "0.18.1" diff --git a/crates/fda/Cargo.toml b/crates/fda/Cargo.toml index 2c0f82581..24f1a86e1 100644 --- a/crates/fda/Cargo.toml +++ b/crates/fda/Cargo.toml @@ -7,4 +7,4 @@ repository = { workspace = true } edition = { workspace = true } [dependencies] -thiserror = "1.0.50" +thiserror = { workspace = true } diff --git a/crates/ffmpeg/Cargo.toml b/crates/ffmpeg/Cargo.toml index ba9943d1f..04c346be1 100644 --- a/crates/ffmpeg/Cargo.toml +++ b/crates/ffmpeg/Cargo.toml @@ -10,13 +10,14 @@ repository = { workspace = true } edition = { workspace = true } [dependencies] -ffmpeg-sys-next = "6.0.1" -tracing = { workspace = true } - -thiserror = "1.0.50" -webp = "0.2.6" +thiserror = { workspace = true } tokio = { workspace = true, features = ["fs", "rt"] } +tracing = { workspace = true } +webp = { workspace = true } + +ffmpeg-sys-next = "6.0.1" + [dev-dependencies] -tempfile = "3.8.1" +tempfile = { workspace = true } tokio = { workspace = true, features = ["fs", "rt", "macros"] } diff --git a/crates/file-ext/Cargo.toml b/crates/file-ext/Cargo.toml index 506efb48b..0ba69efe6 100644 --- a/crates/file-ext/Cargo.toml +++ b/crates/file-ext/Cargo.toml @@ -10,11 +10,12 @@ repository = { workspace = true } edition = { workspace = true } [dependencies] -serde = { version = "1.0.190", features = ["derive"] } +serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } -strum = { version = "0.25", features = ["derive"] } -tokio = { workspace = true, features = ["fs", "rt", "io-util"] } specta = { workspace = true } +strum = { workspace = true, features = ["derive"] } +strum_macros = { workspace = true } +tokio = { workspace = true, features = ["fs", "rt", "io-util"] } [dev-dependencies] tokio = { workspace = true, features = ["fs", "rt", "macros"] } diff --git a/crates/file-path-helper/Cargo.toml b/crates/file-path-helper/Cargo.toml new file mode 100644 index 000000000..5f25281c4 --- /dev/null +++ b/crates/file-path-helper/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "sd-file-path-helper" +version = "0.1.0" +authors = ["Ericson Soares "] +readme = "README.md" +rust-version = "1.73.0" +license = { workspace = true } +repository = { workspace = true } +edition = { workspace = true } + +[dependencies] +sd-prisma = { path = "../prisma" } +sd-utils = { path = "../utils" } + +chrono = { workspace = true, features = ["serde"] } +prisma-client-rust = { workspace = true } +regex = { workspace = true } +serde = { workspace = true, features = ["derive"] } +thiserror = { workspace = true } +tokio = { workspace = true, features = ["fs"] } +tracing = { workspace = true } + +[target.'cfg(windows)'.dependencies.winapi-util] +version = "0.1.6" diff --git a/crates/file-path-helper/README.md b/crates/file-path-helper/README.md new file mode 100644 index 000000000..46e7d51ab --- /dev/null +++ b/crates/file-path-helper/README.md @@ -0,0 +1,3 @@ +# Spacedrive FilePathHelper + +A bunch of file_path related abstractions. diff --git a/core/src/location/file_path_helper/isolated_file_path_data.rs b/crates/file-path-helper/src/isolated_file_path_data.rs similarity index 90% rename from core/src/location/file_path_helper/isolated_file_path_data.rs rename to crates/file-path-helper/src/isolated_file_path_data.rs index 4c02a32c5..c915bccd2 100644 --- a/core/src/location/file_path_helper/isolated_file_path_data.rs +++ b/crates/file-path-helper/src/isolated_file_path_data.rs @@ -1,7 +1,5 @@ -use crate::{ - prisma::{file_path, location}, - util::error::NonUtf8PathError, -}; +use sd_prisma::prisma::{file_path, location}; +use sd_utils::error::NonUtf8PathError; use std::{ borrow::Cow, @@ -22,6 +20,16 @@ use super::{ static FORBIDDEN_FILE_NAMES: OnceLock = OnceLock::new(); +#[derive(Debug)] +pub struct IsolatedFilePathDataParts<'a> { + pub location_id: location::id::Type, + pub materialized_path: &'a str, + pub is_dir: bool, + pub name: &'a str, + pub extension: &'a str, + relative_path: &'a str, +} + #[derive(Serialize, Deserialize, Debug, Hash, Eq, PartialEq)] #[non_exhaustive] pub struct IsolatedFilePathData<'a> { @@ -29,11 +37,11 @@ pub struct IsolatedFilePathData<'a> { // and are not public. They have some specific logic on them and should not be writen to directly. // If you wanna access one of them outside from location module, write yourself an accessor method // to have read only access to them. - pub(in crate::location) location_id: location::id::Type, - pub(in crate::location) materialized_path: Cow<'a, str>, - pub(in crate::location) is_dir: bool, - pub(in crate::location) name: Cow<'a, str>, - pub(in crate::location) extension: Cow<'a, str>, + pub(super) location_id: location::id::Type, + pub(super) materialized_path: Cow<'a, str>, + pub(super) is_dir: bool, + pub(super) name: Cow<'a, str>, + pub(super) extension: Cow<'a, str>, relative_path: Cow<'a, str>, } @@ -95,6 +103,17 @@ impl<'a> IsolatedFilePathData<'a> { && self.relative_path.is_empty() } + pub fn to_parts(&self) -> IsolatedFilePathDataParts<'_> { + IsolatedFilePathDataParts { + location_id: self.location_id, + materialized_path: &self.materialized_path, + is_dir: self.is_dir, + name: &self.name, + extension: &self.extension, + relative_path: &self.relative_path, + } + } + pub fn parent(&'a self) -> Self { let (parent_path_str, name, relative_path) = if self.materialized_path == "/" { ("/", "", "") @@ -342,21 +361,25 @@ impl fmt::Display for IsolatedFilePathData<'_> { } } +impl fmt::Display for IsolatedFilePathDataParts<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.relative_path) + } +} + #[macro_use] mod macros { macro_rules! impl_from_db { ($($file_path_kind:ident),+ $(,)?) => { $( impl ::std::convert::TryFrom<$file_path_kind::Data> for $crate:: - location:: - file_path_helper:: isolated_file_path_data:: IsolatedFilePathData<'static> { - type Error = $crate::util::db::MissingFieldError; + type Error = ::sd_utils::db::MissingFieldError; fn try_from(path: $file_path_kind::Data) -> Result { - use $crate::util::db::maybe_missing; + use ::sd_utils::db::maybe_missing; use ::std::borrow::Cow; Ok(Self::from_db_data( @@ -370,15 +393,13 @@ mod macros { } impl<'a> ::std::convert::TryFrom<&'a $file_path_kind::Data> for $crate:: - location:: - file_path_helper:: isolated_file_path_data:: IsolatedFilePathData<'a> { - type Error = $crate::util::db::MissingFieldError; + type Error =::sd_utils::db::MissingFieldError; fn try_from(path: &'a $file_path_kind::Data) -> Result { - use $crate::util::db::maybe_missing; + use ::sd_utils::db::maybe_missing; use ::std::borrow::Cow; Ok(Self::from_db_data( @@ -397,16 +418,14 @@ mod macros { macro_rules! impl_from_db_without_location_id { ($($file_path_kind:ident),+ $(,)?) => { $( - impl ::std::convert::TryFrom<($crate::prisma::location::id::Type, $file_path_kind::Data)> for $crate:: - location:: - file_path_helper:: + impl ::std::convert::TryFrom<(::sd_prisma::prisma::location::id::Type, $file_path_kind::Data)> for $crate:: isolated_file_path_data:: IsolatedFilePathData<'static> { - type Error = $crate::util::db::MissingFieldError; + type Error = ::sd_utils::db::MissingFieldError; - fn try_from((location_id, path): ($crate::prisma::location::id::Type, $file_path_kind::Data)) -> Result { - use $crate::util::db::maybe_missing; + fn try_from((location_id, path): (::sd_prisma::prisma::location::id::Type, $file_path_kind::Data)) -> Result { + use ::sd_utils::db::maybe_missing; use ::std::borrow::Cow; Ok(Self::from_db_data( @@ -419,16 +438,14 @@ mod macros { } } - impl<'a> ::std::convert::TryFrom<($crate::prisma::location::id::Type, &'a $file_path_kind::Data)> for $crate:: - location:: - file_path_helper:: + impl<'a> ::std::convert::TryFrom<(::sd_prisma::prisma::location::id::Type, &'a $file_path_kind::Data)> for $crate:: isolated_file_path_data:: IsolatedFilePathData<'a> { - type Error = $crate::util::db::MissingFieldError; + type Error = ::sd_utils::db::MissingFieldError; - fn try_from((location_id, path): ($crate::prisma::location::id::Type, &'a $file_path_kind::Data)) -> Result { - use $crate::util::db::maybe_missing; + fn try_from((location_id, path): (::sd_prisma::prisma::location::id::Type, &'a $file_path_kind::Data)) -> Result { + use ::sd_utils::db::maybe_missing; use ::std::borrow::Cow; Ok(Self::from_db_data( @@ -562,7 +579,6 @@ pub fn push_location_relative_path( } #[cfg(test)] -#[allow(clippy::unwrap_used)] mod tests { use super::*; diff --git a/core/src/location/file_path_helper/mod.rs b/crates/file-path-helper/src/lib.rs similarity index 81% rename from core/src/location/file_path_helper/mod.rs rename to crates/file-path-helper/src/lib.rs index e50667d78..555b54b9a 100644 --- a/core/src/location/file_path_helper/mod.rs +++ b/crates/file-path-helper/src/lib.rs @@ -1,7 +1,5 @@ -use crate::{ - prisma::{file_path, location, PrismaClient}, - util::error::{FileIOError, NonUtf8PathError}, -}; +use sd_prisma::prisma::{file_path, location, PrismaClient}; +use sd_utils::error::{FileIOError, NonUtf8PathError}; use std::{ fs::Metadata, @@ -20,6 +18,7 @@ pub mod isolated_file_path_data; pub use isolated_file_path_data::{ join_location_relative_path, push_location_relative_path, IsolatedFilePathData, + IsolatedFilePathDataParts, }; // File Path selectables! @@ -242,101 +241,6 @@ pub enum FilePathError { InvalidFilenameAndExtension(String), } -#[cfg(feature = "location-watcher")] -pub async fn create_file_path( - crate::location::Library { db, sync, .. }: &crate::location::Library, - IsolatedFilePathData { - materialized_path, - is_dir, - location_id, - name, - extension, - .. - }: IsolatedFilePathData<'_>, - cas_id: Option, - metadata: FilePathMetadata, -) -> Result { - use crate::util::db::inode_to_db; - - use sd_prisma::{prisma, prisma_sync}; - use sd_sync::OperationFactory; - use serde_json::json; - use uuid::Uuid; - - let indexed_at = Utc::now(); - - let location = db - .location() - .find_unique(location::id::equals(location_id)) - .select(location::select!({ id pub_id })) - .exec() - .await? - .ok_or(FilePathError::LocationNotFound(location_id))?; - - let params = { - use file_path::*; - - vec![ - ( - location::NAME, - json!(prisma_sync::location::SyncId { - pub_id: location.pub_id - }), - ), - (cas_id::NAME, json!(cas_id)), - (materialized_path::NAME, json!(materialized_path)), - (name::NAME, json!(name)), - (extension::NAME, json!(extension)), - ( - size_in_bytes_bytes::NAME, - json!(metadata.size_in_bytes.to_be_bytes().to_vec()), - ), - (inode::NAME, json!(metadata.inode.to_le_bytes())), - (is_dir::NAME, json!(is_dir)), - (date_created::NAME, json!(metadata.created_at)), - (date_modified::NAME, json!(metadata.modified_at)), - (date_indexed::NAME, json!(indexed_at)), - ] - }; - - let pub_id = sd_utils::uuid_to_bytes(Uuid::new_v4()); - - let created_path = sync - .write_ops( - db, - ( - sync.shared_create( - prisma_sync::file_path::SyncId { - pub_id: pub_id.clone(), - }, - params, - ), - db.file_path().create(pub_id, { - use file_path::*; - vec![ - location::connect(prisma::location::id::equals(location.id)), - materialized_path::set(Some(materialized_path.into_owned())), - name::set(Some(name.into_owned())), - extension::set(Some(extension.into_owned())), - inode::set(Some(inode_to_db(metadata.inode))), - cas_id::set(cas_id), - is_dir::set(Some(is_dir)), - size_in_bytes_bytes::set(Some( - metadata.size_in_bytes.to_be_bytes().to_vec(), - )), - date_created::set(Some(metadata.created_at.into())), - date_modified::set(Some(metadata.modified_at.into())), - date_indexed::set(Some(indexed_at.into())), - hidden::set(Some(metadata.hidden)), - ] - }), - ), - ) - .await?; - - Ok(created_path) -} - pub fn filter_existing_file_path_params( IsolatedFilePathData { materialized_path, diff --git a/crates/images/Cargo.toml b/crates/images/Cargo.toml index 500b12d72..fb8612b2d 100644 --- a/crates/images/Cargo.toml +++ b/crates/images/Cargo.toml @@ -13,18 +13,19 @@ edition = { workspace = true } heif = ["dep:libheif-rs", "dep:libheif-sys"] [dependencies] -image = "0.24.7" -thiserror = "1.0.50" -resvg = "0.36.0" +image = { workspace = true } +once_cell = { workspace = true } rspc = { workspace = true, optional = true } # error conversion specta = { workspace = true, optional = true } serde = { workspace = true, optional = true, features = ["derive"] } +thiserror = { workspace = true } +tracing = { workspace = true } + bincode = { version = "2.0.0-rc.3", features = [ "derive", "alloc", ], optional = true } -once_cell = "1.18.0" -tracing = { workspace = true } +resvg = "0.36.0" # both of these added *default* bindgen features in 0.22.0 and 2.0.0 respectively # this broke builds as we build our own liibheif, so i disabled their default features diff --git a/crates/media-metadata/Cargo.toml b/crates/media-metadata/Cargo.toml index 47017c43b..45e9b5a89 100644 --- a/crates/media-metadata/Cargo.toml +++ b/crates/media-metadata/Cargo.toml @@ -5,14 +5,15 @@ authors = ["Jake Robinson "] edition = "2021" [dependencies] -kamadak-exif = "0.5.5" -thiserror = "1.0.50" -image-rs = { package = "image", version = "0.24.7" } -serde = { version = "1.0.190", features = ["derive"] } +chrono = { workspace = true, features = ["serde"] } +image = { workspace = true } +rand = { workspace = true } +rand_chacha = { workspace = true } +serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } specta = { workspace = true, features = ["chrono"] } -chrono = { version = "0.4.31", features = ["serde"] } -rand = "0.8.5" -rand_chacha = "0.3.1" +thiserror = { workspace = true } + +kamadak-exif = "0.5.5" # symphonia crate looks great for audio metadata diff --git a/crates/media-metadata/src/image/orientation.rs b/crates/media-metadata/src/image/orientation.rs index da2a1be80..1cf119a04 100644 --- a/crates/media-metadata/src/image/orientation.rs +++ b/crates/media-metadata/src/image/orientation.rs @@ -1,6 +1,6 @@ use super::ExifReader; use exif::Tag; -use image_rs::DynamicImage; +use image::DynamicImage; use std::path::Path; #[derive( diff --git a/crates/p2p/Cargo.toml b/crates/p2p/Cargo.toml index 5254f368c..b70d6a397 100644 --- a/crates/p2p/Cargo.toml +++ b/crates/p2p/Cargo.toml @@ -14,6 +14,13 @@ serde = [] specta = [] [dependencies] +base64 = { workspace = true } +pin-project-lite = { workspace = true } +serde = { workspace = true, features = [ + "derive", +] } # TODO: Optional or remove feature +specta = { workspace = true } +thiserror = { workspace = true } tokio = { workspace = true, features = [ "macros", "sync", @@ -21,30 +28,25 @@ tokio = { workspace = true, features = [ "io-util", "fs", ] } -libp2p = { version = "0.52.4", features = ["tokio", "serde"] } -libp2p-quic = { version = "0.9.3", features = ["tokio"] } +tokio-stream = { workspace = true, features = ["sync"] } +tokio-util = { workspace = true, features = ["compat"] } +tracing = { workspace = true } +uuid = { workspace = true } + +ed25519-dalek = { version = "2.0.0", features = [] } +flume = "0.10.0" # Must match version used by `mdns-sd` +futures-core = "0.3.29" if-watch = { version = "=3.1.0", features = [ "tokio", ] } # Override the features of if-watch which is used by libp2p-quic +libp2p = { version = "0.52.4", features = ["tokio", "serde"] } +libp2p-quic = { version = "0.9.3", features = ["tokio"] } mdns-sd = "0.9.3" -thiserror = "1.0.50" -tracing = { workspace = true } -serde = { version = "1.0.190", features = [ - "derive", -] } # TODO: Optional or remove feature -specta = { workspace = true } -flume = "0.10.0" # Must match version used by `mdns-sd` -tokio-util = { version = "0.7.10", features = ["compat"] } -ed25519-dalek = { version = "2.0.0", features = [] } rand_core = { version = "0.6.4" } -uuid = "1.5.0" streamunordered = "0.5.3" -futures-core = "0.3.29" -tokio-stream = { version = "0.1.14", features = ["sync"] } -pin-project-lite = "0.2.13" -base64 = "0.21.5" + [dev-dependencies] tokio = { workspace = true, features = ["rt-multi-thread"] } tracing-subscriber = { workspace = true, features = ["env-filter"] } -uuid = { version = "1.5.0", features = ["v4"] } +uuid = { workspace = true, features = ["v4"] } diff --git a/crates/prisma-cli/Cargo.toml b/crates/prisma-cli/Cargo.toml index b1e2f99fb..c8f4be971 100644 --- a/crates/prisma-cli/Cargo.toml +++ b/crates/prisma-cli/Cargo.toml @@ -6,5 +6,6 @@ repository = { workspace = true } edition = { workspace = true } [dependencies] -prisma-client-rust-cli = { workspace = true } sd-sync-generator = { path = "../sync-generator" } + +prisma-client-rust-cli = { workspace = true } diff --git a/crates/prisma/Cargo.toml b/crates/prisma/Cargo.toml index d57dfd69d..d10047c86 100644 --- a/crates/prisma/Cargo.toml +++ b/crates/prisma/Cargo.toml @@ -4,8 +4,9 @@ version = "0.1.0" edition = "2021" [dependencies] -prisma-client-rust = { workspace = true } -serde = "1.0" -serde_json = "1.0" -sd-sync = { path = "../sync" } sd-cache = { path = "../cache" } +sd-sync = { path = "../sync" } + +prisma-client-rust = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } diff --git a/crates/sync-generator/Cargo.toml b/crates/sync-generator/Cargo.toml index 904154a2d..66c751138 100644 --- a/crates/sync-generator/Cargo.toml +++ b/crates/sync-generator/Cargo.toml @@ -6,7 +6,8 @@ repository = { workspace = true } edition = { workspace = true } [dependencies] -nom = "7.1.3" prisma-client-rust-sdk = { workspace = true } -serde = { version = "1.0.190", features = ["derive"] } -thiserror = "1.0.50" +serde = { workspace = true, features = ["derive"] } +thiserror = { workspace = true } + +nom = "7.1.3" diff --git a/crates/sync/Cargo.toml b/crates/sync/Cargo.toml index c951151af..416a49f20 100644 --- a/crates/sync/Cargo.toml +++ b/crates/sync/Cargo.toml @@ -6,9 +6,9 @@ repository = { workspace = true } edition = { workspace = true } [dependencies] -specta = { workspace = true, features = ["uuid", "uhlc"] } -serde = "1.0.190" -serde_json = { workspace = true } -uhlc = "=0.5.2" -uuid = { version = "1.5.0", features = ["serde", "v4"] } prisma-client-rust = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +specta = { workspace = true, features = ["uuid", "uhlc"] } +uhlc = { workspace = true } +uuid = { workspace = true, features = ["serde", "v4"] } diff --git a/crates/sync/example/Cargo.toml b/crates/sync/example/Cargo.toml index c83e2a47d..400d833fa 100644 --- a/crates/sync/example/Cargo.toml +++ b/crates/sync/example/Cargo.toml @@ -10,12 +10,12 @@ edition = { workspace = true } [dependencies] serde_json = "1.0.85" serde = { version = "1.0.145", features = ["derive"] } -axum = "0.6.4" +axum = { workspace = true } rspc = { workspace = true, features = ["axum"] } tokio = { workspace = true, features = ["full"] } prisma-client-rust = { workspace = true } dotenv = "0.15.0" tower-http = { version = "0.3.4", features = ["cors"] } sd-sync = { path = ".." } -uuid = { version = "1.1.2", features = ["v4"] } +uuid = { workspace = true, features = ["v4"] } http = "0.2.8" diff --git a/crates/utils/Cargo.toml b/crates/utils/Cargo.toml index 825c16497..2d941149c 100644 --- a/crates/utils/Cargo.toml +++ b/crates/utils/Cargo.toml @@ -6,4 +6,9 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +sd-prisma = { path = "../prisma" } + +prisma-client-rust = { workspace = true } +rspc = { workspace = true } +thiserror = { workspace = true } uuid = { workspace = true } diff --git a/core/src/util/db.rs b/crates/utils/src/db.rs similarity index 98% rename from core/src/util/db.rs rename to crates/utils/src/db.rs index 6e8d2554d..2ad3dd686 100644 --- a/core/src/util/db.rs +++ b/crates/utils/src/db.rs @@ -1,5 +1,5 @@ -use crate::prisma::{self, PrismaClient}; use prisma_client_rust::{migrations::*, NewClientError}; +use sd_prisma::prisma::{self, PrismaClient}; use thiserror::Error; /// MigrationError represents an error that occurring while opening a initialising and running migrations on the database. diff --git a/core/src/util/error.rs b/crates/utils/src/error.rs similarity index 100% rename from core/src/util/error.rs rename to crates/utils/src/error.rs diff --git a/crates/utils/src/lib.rs b/crates/utils/src/lib.rs index e42b71f4d..ac6476088 100644 --- a/crates/utils/src/lib.rs +++ b/crates/utils/src/lib.rs @@ -1,5 +1,8 @@ use uuid::Uuid; +pub mod db; +pub mod error; + /// Combines an iterator of `T` and an iterator of `Option`, /// removing any `None` values in the process pub fn chain_optional_iter( diff --git a/interface/app/$libraryId/Explorer/ContextMenu/FilePath/Items.tsx b/interface/app/$libraryId/Explorer/ContextMenu/FilePath/Items.tsx index 22d6c8988..6f37c5fad 100644 --- a/interface/app/$libraryId/Explorer/ContextMenu/FilePath/Items.tsx +++ b/interface/app/$libraryId/Explorer/ContextMenu/FilePath/Items.tsx @@ -1,4 +1,4 @@ -import { Image, Package, Trash, TrashSimple } from '@phosphor-icons/react'; +import { Hash, Image, Package, Trash, TrashSimple } from '@phosphor-icons/react'; import { libraryClient, useLibraryMutation } from '@sd/client'; import { ContextMenu, dialogManager, ModifierKeys, toast } from '@sd/ui'; import { Menu } from '~/components/Menu'; @@ -225,6 +225,7 @@ export const ParentFolderActions = new ConditionalItem({ const fullRescan = useLibraryMutation('locations.fullRescan'); const generateThumbnails = useLibraryMutation('jobs.generateThumbsForLocation'); + const generateLabels = useLibraryMutation('jobs.generateLabelsForLocation'); return ( <> @@ -263,6 +264,24 @@ export const ParentFolderActions = new ConditionalItem({ label="Regen Thumbnails" icon={Image} /> + { + try { + await generateLabels.mutateAsync({ + id: parent.location.id, + path: selectedFilePaths[0]?.materialized_path ?? '/', + regenerate: true + }); + } catch (error) { + toast.error({ + title: `Failed to generate labels`, + body: `Error: ${error}.` + }); + } + }} + label="Regen Labels" + icon={Hash} + /> ); } diff --git a/interface/app/$libraryId/Explorer/Inspector/index.tsx b/interface/app/$libraryId/Explorer/Inspector/index.tsx index 16fd91318..162e89c0d 100644 --- a/interface/app/$libraryId/Explorer/Inspector/index.tsx +++ b/interface/app/$libraryId/Explorer/Inspector/index.tsx @@ -216,12 +216,17 @@ export const SingleItemMetadata = ({ item }: { item: ExplorerItem }) => { locations?.filter((location) => uniqueLocationIds.includes(location.id)) || []; const readyToFetch = useIsFetchReady(item); + const tagsQuery = useLibraryQuery(['tags.getForObject', objectData?.id ?? -1], { enabled: objectData != null && readyToFetch }); useNodes(tagsQuery.data?.nodes); const tags = useCache(tagsQuery.data?.items); + const labels = useLibraryQuery(['labels.getForObject', objectData?.id ?? -1], { + enabled: objectData != null && readyToFetch + }); + const { libraryId } = useZodRouteParams(LibraryIdParamsSchema); const queriedFullPath = useLibraryQuery(['files.getPath', filePathData?.id ?? -1], { @@ -339,6 +344,12 @@ export const SingleItemMetadata = ({ item }: { item: ExplorerItem }) => { {extension && {extension}} + {labels.data?.map((label) => ( + + {label.name} + + ))} + {tags?.map((tag) => ( @@ -406,11 +417,21 @@ const MultiItemMetadata = ({ items }: { items: ExplorerItem[] }) => { useNodes(tagsQuery.data?.nodes); const tags = useCache(tagsQuery.data?.items); + const labels = useLibraryQuery(['labels.list'], { + enabled: readyToFetch && !explorerStore.isDragSelecting, + suspense: true + }); + const tagsWithObjects = useLibraryQuery( ['tags.getWithObjects', selectedObjects.map(({ id }) => id)], { enabled: readyToFetch && !explorerStore.isDragSelecting } ); + const labelsWithObjects = useLibraryQuery( + ['labels.getWithObjects', selectedObjects.map(({ id }) => id)], + { enabled: readyToFetch && !explorerStore.isDragSelecting } + ); + const getDate = useCallback((metadataDate: MetadataDate, date: Date) => { date.setHours(0, 0, 0, 0); @@ -496,14 +517,33 @@ const MultiItemMetadata = ({ items }: { items: ExplorerItem[] }) => { {`${kind} (${items.length})`} ))} + {labels.data?.map((label) => { + const objectsWithLabel = labelsWithObjects.data?.[label.id] ?? []; + + if (objectsWithLabel.length === 0) return null; + + return ( + + {label.name} ({objectsWithLabel.length}) + + ); + })} + {tags?.map((tag) => { - const objectsWithTag = tagsWithObjects.data?.[tag.id] || []; + const objectsWithTag = tagsWithObjects.data?.[tag.id] ?? []; if (objectsWithTag.length === 0) return null; return ( - + { const { parent } = useExplorerContext(); const generateThumbsForLocation = useLibraryMutation('jobs.generateThumbsForLocation'); + const generateLabelsForLocation = useLibraryMutation('jobs.generateLabelsForLocation'); const objectValidator = useLibraryMutation('jobs.objectValidator'); const rescanLocation = useLibraryMutation('locations.subPathRescan'); const copyFiles = useLibraryMutation('files.copyFiles'); @@ -220,6 +222,25 @@ export default (props: PropsWithChildren) => { icon={Image} /> + { + try { + await generateLabelsForLocation.mutateAsync({ + id: parent.location.id, + path: currentPath ?? '/', + regenerate: true + }); + } catch (error) { + toast.error({ + title: `Failed to generate labels`, + body: `Error: ${error}.` + }); + } + }} + label="Regen Labels" + icon={Hash} + /> + { try { diff --git a/interface/app/$libraryId/Layout/Sidebar/DebugPopover.tsx b/interface/app/$libraryId/Layout/Sidebar/DebugPopover.tsx index 673809a23..9a32b1443 100644 --- a/interface/app/$libraryId/Layout/Sidebar/DebugPopover.tsx +++ b/interface/app/$libraryId/Layout/Sidebar/DebugPopover.tsx @@ -145,7 +145,7 @@ export default () => { - + {/* */} @@ -214,14 +214,14 @@ function FeatureFlagSelector() { ); } -function TestNotifications() { - const coreNotif = useBridgeMutation(['notifications.test']); - const libraryNotif = useLibraryMutation(['notifications.testLibrary']); +// function TestNotifications() { +// const coreNotif = useBridgeMutation(['notifications.test']); +// const libraryNotif = useLibraryMutation(['notifications.testLibrary']); - return ( - - - - - ); -} +// return ( +// +// +// +// +// ); +// } diff --git a/interface/app/$libraryId/settings/client/general.tsx b/interface/app/$libraryId/settings/client/general.tsx index a1451e041..3aad1ae6b 100644 --- a/interface/app/$libraryId/settings/client/general.tsx +++ b/interface/app/$libraryId/settings/client/general.tsx @@ -28,6 +28,7 @@ export const Component = () => { const debugState = useDebugState(); const editNode = useBridgeMutation('nodes.edit'); const connectedPeers = useConnectedPeers(); + const image_labeler_versions = useBridgeQuery(['models.image_detection.list']); const updateThumbnailerPreferences = useBridgeMutation('nodes.updateThumbnailerPreferences'); const form = useZodForm({ @@ -37,6 +38,7 @@ export const Component = () => { p2p_enabled: z.boolean().optional(), p2p_port: u16, customOrDefault: z.enum(['Custom', 'Default']), + image_labeler_version: z.string().optional(), background_processing_percentage: z.coerce .number({ invalid_type_error: 'Must use numbers from 0 to 100' @@ -49,9 +51,10 @@ export const Component = () => { reValidateMode: 'onChange', defaultValues: { name: node.data?.name, - p2p_enabled: node.data?.p2p_enabled, p2p_port: node.data?.p2p_port || 0, + p2p_enabled: node.data?.p2p_enabled, customOrDefault: node.data?.p2p_port ? 'Custom' : 'Default', + image_labeler_version: node.data?.image_labeler_version ?? undefined, background_processing_percentage: node.data?.preferences.thumbnailer.background_processing_percentage || 50 } @@ -65,8 +68,9 @@ export const Component = () => { if (await form.trigger()) { await editNode.mutateAsync({ name: value.name || null, - p2p_enabled: value.p2p_enabled === undefined ? null : value.p2p_enabled, - p2p_port: value.customOrDefault === 'Default' ? 0 : Number(value.p2p_port) + p2p_port: value.customOrDefault === 'Default' ? 0 : Number(value.p2p_port), + p2p_enabled: value.p2p_enabled ?? null, + image_labeler_version: value.image_labeler_version ?? null }); if (value.background_processing_percentage != undefined) { @@ -218,6 +222,29 @@ export const Component = () => { /> + +
+ ( + + )} + /> +
+

Networking

diff --git a/interface/app/$libraryId/settings/library/tags/CreateDialog.tsx b/interface/app/$libraryId/settings/library/tags/CreateDialog.tsx index 46bd14181..dfd89111b 100644 --- a/interface/app/$libraryId/settings/library/tags/CreateDialog.tsx +++ b/interface/app/$libraryId/settings/library/tags/CreateDialog.tsx @@ -2,6 +2,7 @@ import { FilePath, Object, Target, + ToastDefautlColor, useLibraryMutation, usePlausibleEvent, useZodForm @@ -53,7 +54,7 @@ export default ( const form = useZodForm({ schema: schema, - defaultValues: { color: '#A717D9' } + defaultValues: { color: ToastDefautlColor } }); const createTag = useLibraryMutation('tags.create'); diff --git a/interface/index.tsx b/interface/index.tsx index b832df5d4..60525fea4 100644 --- a/interface/index.tsx +++ b/interface/index.tsx @@ -8,13 +8,12 @@ import relativeTime from 'dayjs/plugin/relativeTime'; import { PropsWithChildren, Suspense } from 'react'; import { RouterProvider, RouterProviderProps } from 'react-router-dom'; import { - CacheProvider, - NotificationContextProvider, P2PContextProvider, + useBridgeSubscription, useInvalidateQuery, useLoadBackendFeatureFlags } from '@sd/client'; -import { TooltipProvider } from '@sd/ui'; +import { toast, TooltipProvider } from '@sd/ui'; import { createRoutes } from './app'; import { P2P, useP2PErrorToast } from './app/p2p'; @@ -24,11 +23,11 @@ import ErrorFallback, { BetterErrorBoundary } from './ErrorFallback'; import { useTheme } from './hooks'; import { RoutingContext } from './RoutingContext'; -export { ErrorPage } from './ErrorFallback'; export * from './app'; -export * from './util/Platform'; -export * from './util/keybind'; +export { ErrorPage } from './ErrorFallback'; export * from './TabsContext'; +export * from './util/keybind'; +export * from './util/Platform'; dayjs.extend(advancedFormat); dayjs.extend(relativeTime); @@ -77,17 +76,22 @@ export function SpacedriveInterfaceRoot({ children }: PropsWithChildren) { useInvalidateQuery(); useTheme(); + useBridgeSubscription(['notifications.listen'], { + onData({ data: { title, content, kind }, expires }) { + console.log(expires); + toast({ title, body: content }, { type: kind }); + } + }); + return ( - - - - - {children} - + + + + {children} diff --git a/packages/client/src/color.ts b/packages/client/src/color.ts new file mode 100644 index 000000000..dc7e6cdb6 --- /dev/null +++ b/packages/client/src/color.ts @@ -0,0 +1 @@ +export const ToastDefautlColor = '#A717D9'; diff --git a/packages/client/src/core.ts b/packages/client/src/core.ts index 664928f51..9b8225a66 100644 --- a/packages/client/src/core.ts +++ b/packages/client/src/core.ts @@ -16,6 +16,10 @@ export type Procedures = { { key: "invalidation.test-invalidate", input: never, result: number } | { key: "jobs.isActive", input: LibraryArgs, result: boolean } | { key: "jobs.reports", input: LibraryArgs, result: JobGroup[] } | + { key: "labels.get", input: LibraryArgs, result: { id: number; pub_id: number[]; name: string; date_created: string; date_modified: string } | null } | + { key: "labels.getForObject", input: LibraryArgs, result: Label[] } | + { key: "labels.getWithObjects", input: LibraryArgs, result: { [key in number]: { date_created: string; object: { id: number } }[] } } | + { key: "labels.list", input: LibraryArgs, result: Label[] } | { key: "library.list", input: never, result: NormalisedResults } | { key: "library.statistics", input: LibraryArgs, result: Statistics } | { key: "locations.get", input: LibraryArgs, result: { item: Reference; nodes: CacheNode[] } | null } | @@ -25,6 +29,7 @@ export type Procedures = { { key: "locations.indexer_rules.listForLocation", input: LibraryArgs, result: NormalisedResults } | { key: "locations.list", input: LibraryArgs, result: NormalisedResults } | { key: "locations.systemLocations", input: never, result: SystemLocations } | + { key: "models.image_detection.list", input: never, result: string[] } | { key: "nodeState", input: never, result: NodeState } | { key: "nodes.listLocations", input: LibraryArgs, result: ExplorerItem[] } | { key: "notifications.dismiss", input: NotificationId, result: null } | @@ -72,11 +77,13 @@ export type Procedures = { { key: "jobs.cancel", input: LibraryArgs, result: null } | { key: "jobs.clear", input: LibraryArgs, result: null } | { key: "jobs.clearAll", input: LibraryArgs, result: null } | + { key: "jobs.generateLabelsForLocation", input: LibraryArgs, result: null } | { key: "jobs.generateThumbsForLocation", input: LibraryArgs, result: null } | { key: "jobs.identifyUniqueFiles", input: LibraryArgs, result: null } | { key: "jobs.objectValidator", input: LibraryArgs, result: null } | { key: "jobs.pause", input: LibraryArgs, result: null } | { key: "jobs.resume", input: LibraryArgs, result: null } | + { key: "labels.delete", input: LibraryArgs, result: null } | { key: "library.create", input: CreateLibraryArgs, result: NormalisedResult } | { key: "library.delete", input: string, result: null } | { key: "library.edit", input: EditLibraryArgs, result: null } | @@ -93,8 +100,6 @@ export type Procedures = { { key: "locations.update", input: LibraryArgs, result: null } | { key: "nodes.edit", input: ChangeNodeNameArgs, result: null } | { key: "nodes.updateThumbnailerPreferences", input: UpdateThumbnailerPreferences, result: null } | - { key: "notifications.test", input: never, result: null } | - { key: "notifications.testLibrary", input: LibraryArgs, result: null } | { key: "p2p.acceptSpacedrop", input: [string, string | null], result: null } | { key: "p2p.cancelSpacedrop", input: string, result: null } | { key: "p2p.pair", input: RemoteIdentity, result: number } | @@ -145,7 +150,7 @@ export type CacheNode = { __type: string; __id: string; "#node": any } export type CameraData = { device_make: string | null; device_model: string | null; color_space: string | null; color_profile: ColorProfile | null; focal_length: number | null; shutter_speed: number | null; flash: Flash | null; orientation: Orientation; lens_make: string | null; lens_model: string | null; bit_depth: number | null; red_eye: boolean | null; zoom: number | null; iso: number | null; software: string | null; serial_number: string | null; lens_serial_number: string | null; contrast: number | null; saturation: number | null; sharpness: number | null; composite: Composite | null } -export type ChangeNodeNameArgs = { name: string | null; p2p_enabled: boolean | null; p2p_port: MaybeUndefined } +export type ChangeNodeNameArgs = { name: string | null; p2p_port: MaybeUndefined; p2p_enabled: boolean | null; image_labeler_version: string | null } export type CloudInstance = { id: string; uuid: string; identity: string } @@ -295,6 +300,8 @@ export type FromPattern = { pattern: string; replace_all: boolean } export type FullRescanArgs = { location_id: number; reidentify_objects: boolean } +export type GenerateLabelsForLocationArgs = { id: number; path: string; regenerate?: boolean } + export type GenerateThumbsForLocationArgs = { id: number; path: string; regenerate?: boolean } export type GetAll = { backups: Backup[]; directory: string } @@ -331,6 +338,8 @@ export type JobStatus = "Queued" | "Running" | "Completed" | "Canceled" | "Faile export type JsonValue = null | boolean | number | string | JsonValue[] | { [key in string]: JsonValue } +export type Label = { id: number; pub_id: number[]; name: string; date_created: string; date_modified: string } + /** * Can wrap a query argument to require it to contain a `library_id` and provide helpers for working with libraries. */ @@ -412,7 +421,7 @@ id: string; /** * name is the display name of the current node. This is set by the user and is shown in the UI. // TODO: Length validation so it can fit in DNS record */ -name: string; p2p_enabled: boolean; p2p_port: number | null; features: BackendFeature[]; preferences: NodePreferences }) & { data_path: string; p2p: P2PStatus } +name: string; p2p_enabled: boolean; p2p_port: number | null; features: BackendFeature[]; preferences: NodePreferences; image_labeler_version: string | null }) & { data_path: string; p2p: P2PStatus } export type NonIndexedPathItem = { path: string; name: string; extension: string; kind: number; is_dir: boolean; date_created: string; date_modified: string; size_in_bytes_bytes: number[]; hidden: boolean } @@ -439,10 +448,12 @@ export type Notification = ({ type: "library"; id: [string, number] } | { type: * Represents the data of a single notification. * This data is used by the frontend to properly display the notification. */ -export type NotificationData = { PairingRequest: { id: string; pairing_id: number } } | "Test" +export type NotificationData = { title: string; content: string; kind: NotificationKind } export type NotificationId = { type: "library"; id: [string, number] } | { type: "node"; id: number } +export type NotificationKind = "info" | "success" | "error" | "warning" + export type Object = { id: number; pub_id: number[]; kind: number | null; key_id: number | null; hidden: boolean | null; favorite: boolean | null; important: boolean | null; note: string | null; date_created: string | null; date_accessed: string | null } export type ObjectCursor = "none" | { dateAccessed: CursorOrderItem } | { kind: CursorOrderItem } @@ -548,7 +559,7 @@ export type Statistics = { id: number; date_captured: string; total_object_count export type SystemLocations = { desktop: string | null; documents: string | null; downloads: string | null; pictures: string | null; music: string | null; videos: string | null } -export type Tag = { id: number; pub_id: number[]; name: string | null; color: string | null; redundancy_goal: number | null; date_created: string | null; date_modified: string | null } +export type Tag = { id: number; pub_id: number[]; name: string | null; color: string | null; is_hidden: boolean | null; date_created: string | null; date_modified: string | null } export type TagCreateArgs = { name: string; color: string } diff --git a/packages/client/src/hooks/index.ts b/packages/client/src/hooks/index.ts index 9904b271e..6bc293f70 100644 --- a/packages/client/src/hooks/index.ts +++ b/packages/client/src/hooks/index.ts @@ -1,6 +1,8 @@ export * from './useClientContext'; export * from './useDebugState'; +export * from './useExplorerLayoutStore'; export * from './useFeatureFlag'; +export * from './useForceUpdate'; export * from './useLibraryContext'; export * from './useLibraryStore'; export * from './useOnboardingStore'; @@ -8,7 +10,4 @@ export * from './useP2PEvents'; export * from './usePlausible'; export * from './useTelemetryState'; export * from './useThemeStore'; -export * from './useNotifications'; -export * from './useForceUpdate'; export * from './useUnitFormatStore'; -export * from './useExplorerLayoutStore'; diff --git a/packages/client/src/hooks/useNotifications.tsx b/packages/client/src/hooks/useNotifications.tsx deleted file mode 100644 index 44ce1fae8..000000000 --- a/packages/client/src/hooks/useNotifications.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { createContext, PropsWithChildren, useState } from 'react'; - -import { Notification } from '../core'; -import { useBridgeSubscription } from '../rspc'; - -type Context = { - notifications: Set; -}; - -const Context = createContext(null as any); - -export function NotificationContextProvider({ children }: PropsWithChildren) { - const [[notifications], setNotifications] = useState([new Set()]); - - useBridgeSubscription(['notifications.listen'], { - onData(data) { - setNotifications([notifications.add(data)]); - } - }); - - return ( - - {children} - - ); -} diff --git a/packages/client/src/index.ts b/packages/client/src/index.ts index 888d3e8cc..e1fb523cf 100644 --- a/packages/client/src/index.ts +++ b/packages/client/src/index.ts @@ -28,3 +28,4 @@ export * from './utils'; export * from './lib'; export * from './form'; export * from './cache'; +export * from './color'; diff --git a/packages/client/src/utils/jobs/useJobInfo.tsx b/packages/client/src/utils/jobs/useJobInfo.tsx index c0e9fc9ff..1ab0c8181 100644 --- a/packages/client/src/utils/jobs/useJobInfo.tsx +++ b/packages/client/src/utils/jobs/useJobInfo.tsx @@ -93,6 +93,18 @@ export function useJobInfo(job: JobReport, realtimeUpdate: JobProgressEvent | nu ]; } + case 'labels': { + return [ + { + text: `Labeled ${ + completedTaskCount + ? formatNumber(completedTaskCount || 0) + : formatNumber(output?.labels_extracted) + } of ${formatNumber(taskCount)} ${plural(taskCount, 'file')}` + } + ]; + } + default: { // If we don't have a phase set, then we're done diff --git a/scripts/tauri.mjs b/scripts/tauri.mjs index fba0032a7..87c3d45b4 100755 --- a/scripts/tauri.mjs +++ b/scripts/tauri.mjs @@ -10,6 +10,7 @@ import * as toml from '@iarna/toml' import { waitLockUnlock } from './utils/flock.mjs' import { patchTauri } from './utils/patchTauri.mjs' +import { symlinkSharedLibsLinux } from './utils/shared.mjs' import spawn from './utils/spawn.mjs' if (/^(msys|mingw|cygwin)$/i.test(env.OSTYPE ?? '')) { @@ -74,14 +75,18 @@ const bundles = args .flatMap(target => target.split(',')) let code = 0 + +if (process.platform === 'linux' && (args[0] === 'dev' || args[0] === 'build')) + await symlinkSharedLibsLinux(__root, nativeDeps) + try { switch (args[0]) { case 'dev': { __cleanup.push(...(await patchTauri(__root, nativeDeps, targets, bundles, args))) switch (process.platform) { - case 'darwin': case 'linux': + case 'darwin': void waitLockUnlock(path.join(__root, 'target', 'debug', '.cargo-lock')).then( () => setTimeout(1000).then(cleanUp), () => {} diff --git a/scripts/utils/patchTauri.mjs b/scripts/utils/patchTauri.mjs index fbc494ebc..d8b1d033b 100644 --- a/scripts/utils/patchTauri.mjs +++ b/scripts/utils/patchTauri.mjs @@ -7,7 +7,7 @@ import { promisify } from 'node:util' import * as semver from 'semver' -import { copyLinuxLibs, copyWindowsDLLs } from './shared.mjs' +import { linuxLibs, windowsDLLs } from './shared.mjs' const exec = promisify(_exec) const __debug = env.NODE_ENV === 'debug' @@ -62,28 +62,28 @@ export async function patchTauri(root, nativeDeps, targets, bundles, args) { throw new Error('Custom tauri build config is not supported.') } - // Location for desktop app tauri code - const tauriRoot = path.join(root, 'apps', 'desktop', 'src-tauri') - const osType = os.type() - const resources = - osType === 'Linux' - ? await copyLinuxLibs(root, nativeDeps, args[0] === 'dev') - : osType === 'Windows_NT' - ? await copyWindowsDLLs(root, nativeDeps) - : { files: [], toClean: [] } const tauriPatch = { tauri: { bundle: { - macOS: { - minimumSystemVersion: '', - }, - resources: resources.files, + macOS: { minimumSystemVersion: '' }, + resources: {}, }, updater: /** @type {{ pubkey?: string }} */ ({}), }, } + if (osType === 'Linux') { + tauriPatch.tauri.bundle.resources = await linuxLibs(nativeDeps) + } else if (osType === 'Windows_NT') { + tauriPatch.tauri.bundle.resources = { + ...(await windowsDLLs(nativeDeps)), + [path.join(nativeDeps, 'models', 'yolov8s.onnx')]: './models/yolov8s.onnx', + } + } + + // Location for desktop app tauri code + const tauriRoot = path.join(root, 'apps', 'desktop', 'src-tauri') const tauriConfig = await fs .readFile(path.join(tauriRoot, 'tauri.conf.json'), 'utf-8') .then(JSON.parse) @@ -138,5 +138,5 @@ export async function patchTauri(root, nativeDeps, targets, bundles, args) { args.splice(1, 0, '-c', tauriPatchConf) // Files to be removed - return [tauriPatchConf, ...resources.toClean] + return [tauriPatchConf] } diff --git a/scripts/utils/shared.mjs b/scripts/utils/shared.mjs index ad4ba3a88..65bf76d52 100644 --- a/scripts/utils/shared.mjs +++ b/scripts/utils/shared.mjs @@ -27,10 +27,15 @@ async function link(origin, target, rename) { export async function symlinkSharedLibsLinux(root, nativeDeps) { // rpath=${ORIGIN}/../lib/spacedrive const targetLib = path.join(root, 'target', 'lib') + const targetShare = path.join(root, 'target', 'share', 'spacedrive') const targetRPath = path.join(targetLib, 'spacedrive') - await fs.unlink(targetRPath).catch(() => {}) - await fs.mkdir(targetLib, { recursive: true }) + const targetModelShare = path.join(targetShare, 'models') + await Promise.all([ + ...[targetRPath, targetModelShare].map(path => fs.unlink(path).catch(() => {})), + ...[targetLib, targetShare].map(path => fs.mkdir(path, { recursive: true })), + ]) await link(path.join(nativeDeps, 'lib'), targetRPath) + await link(path.join(nativeDeps, 'models'), targetModelShare) } /** @@ -66,71 +71,40 @@ export async function symlinkSharedLibsMacOS(root, nativeDeps) { /** * Copy Windows DLLs for tauri build - * @param {string} root * @param {string} nativeDeps - * @returns {Promise<{files: string[], toClean: string[]}>} + * @returns {Promise>} */ -export async function copyWindowsDLLs(root, nativeDeps) { - const tauriSrc = path.join(root, 'apps', 'desktop', 'src-tauri') - const files = await Promise.all( - await fs.readdir(path.join(nativeDeps, 'bin'), { withFileTypes: true }).then(files => - files - .filter(entry => entry.isFile() && entry.name.endsWith(`.dll`)) - .map(async entry => { - await fs.copyFile( - path.join(entry.path, entry.name), - path.join(tauriSrc, entry.name) - ) - return entry.name - }) - ) +export async function windowsDLLs(nativeDeps) { + return Object.fromEntries( + await fs + .readdir(path.join(nativeDeps, 'bin'), { withFileTypes: true }) + .then(files => + files + .filter(entry => entry.isFile() && entry.name.endsWith(`.dll`)) + .map(entry => [path.join(entry.path, entry.name), '.']) + ) ) - - return { files, toClean: files.map(file => path.join(tauriSrc, file)) } } /** * Symlink shared libs paths for Linux - * @param {string} root * @param {string} nativeDeps - * @param {boolean} isDev - * @returns {Promise<{files: string[], toClean: string[]}>} + * @returns {Promise>} */ -export async function copyLinuxLibs(root, nativeDeps, isDev) { - // rpath=${ORIGIN}/../lib/spacedrive - const tauriSrc = path.join(root, 'apps', 'desktop', 'src-tauri') - const files = await fs - .readdir(path.join(nativeDeps, 'lib'), { withFileTypes: true }) - .then(files => - Promise.all( - files - .filter( - entry => - (entry.isFile() || entry.isSymbolicLink()) && - (entry.name.endsWith('.so') || entry.name.includes('.so.')) - ) - .map(async entry => { - if (entry.isSymbolicLink()) { - await fs.symlink( - await fs.readlink(path.join(entry.path, entry.name)), - path.join(tauriSrc, entry.name) - ) - } else { - const target = path.join(tauriSrc, entry.name) - await fs.copyFile(path.join(entry.path, entry.name), target) - // https://web.archive.org/web/20220731055320/https://lintian.debian.org/tags/shared-library-is-executable - await fs.chmod(target, 0o644) - } - return entry.name - }) +export async function linuxLibs(nativeDeps) { + return Object.fromEntries( + await fs + .readdir(path.join(nativeDeps, 'lib'), { withFileTypes: true }) + .then(files => + Promise.all( + files + .filter( + entry => + (entry.isFile() || entry.isSymbolicLink()) && + (entry.name.endsWith('.so') || entry.name.includes('.so.')) + ) + .map(entry => [path.join(entry.path, entry.name), '.']) + ) ) - ) - - return { - files, - toClean: [ - ...files.map(file => path.join(tauriSrc, file)), - ...files.map(file => path.join(root, 'target', isDev ? 'debug' : 'release', file)), - ], - } + ) }