mirror of
https://github.com/spacedriveapp/spacedrive
synced 2024-07-04 12:13:27 +00:00
[ENG-381] Update context menufs actions to support multiple source (#927)
* Generalizing filesystem jobs to accept multiple files at once * Some small changes and fixing front * Enable rename route to replicate Finder's behavior * Assign tag to multiple objects at once * Remove from recents accepting multiple files * Fixing some warnings * Adding multiple files feature to Open and Open With * Conditional stuff for macos * Generating commands.ts and other minor warnings * Rust fmt * TS typecheck * Rust format and TS typecheck * Requested changes * Requested changes --------- Co-authored-by: Utku <74243531+utkubakir@users.noreply.github.com>
This commit is contained in:
parent
b5dd19c8e7
commit
e693c7a542
145
Cargo.lock
generated
145
Cargo.lock
generated
|
@ -69,7 +69,7 @@ version = "0.7.5"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e8b47f52ea9bae42228d07ec09eb676433d7c4ed1ebdf0f1d1c29ed446f1ab8"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"cfg-if",
|
||||
"cipher 0.3.0",
|
||||
"cpufeatures",
|
||||
"opaque-debug",
|
||||
|
@ -81,7 +81,7 @@ version = "0.8.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "433cfd6710c9986c576a25ca913c39d66a6474107b406f34f91d4a8923395241"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"cfg-if",
|
||||
"cipher 0.4.4",
|
||||
"cpufeatures",
|
||||
]
|
||||
|
@ -364,7 +364,7 @@ checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af"
|
|||
dependencies = [
|
||||
"async-lock",
|
||||
"autocfg",
|
||||
"cfg-if 1.0.0",
|
||||
"cfg-if",
|
||||
"concurrent-queue",
|
||||
"futures-lite",
|
||||
"log",
|
||||
|
@ -575,7 +575,7 @@ checksum = "233d376d6d185f2a3093e58f283f60f880315b6c60075b01f36b3b85154564ca"
|
|||
dependencies = [
|
||||
"addr2line",
|
||||
"cc",
|
||||
"cfg-if 1.0.0",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"miniz_oxide 0.6.2",
|
||||
"object",
|
||||
|
@ -741,7 +741,7 @@ dependencies = [
|
|||
"arrayref",
|
||||
"arrayvec 0.7.2",
|
||||
"cc",
|
||||
"cfg-if 1.0.0",
|
||||
"cfg-if",
|
||||
"constant_time_eq",
|
||||
"digest 0.10.7",
|
||||
]
|
||||
|
@ -1027,12 +1027,6 @@ dependencies = [
|
|||
"target-lexicon",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.0"
|
||||
|
@ -1045,7 +1039,7 @@ version = "0.8.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c80e5460aa66fe3b91d40bcbdab953a597b60053e34d684ac6903f863b680a6"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"cfg-if",
|
||||
"cipher 0.3.0",
|
||||
"cpufeatures",
|
||||
"zeroize",
|
||||
|
@ -1057,7 +1051,7 @@ version = "0.9.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"cfg-if",
|
||||
"cipher 0.4.4",
|
||||
"cpufeatures",
|
||||
]
|
||||
|
@ -1414,7 +1408,7 @@ version = "1.3.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1423,7 +1417,7 @@ version = "0.5.8"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"cfg-if",
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
|
@ -1433,7 +1427,7 @@ version = "0.8.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"cfg-if",
|
||||
"crossbeam-epoch",
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
@ -1445,7 +1439,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "46bd5f3f85273295a9d14aedfb86f6aadbff6d8f5295c4a9edb08e819dcf5695"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"cfg-if 1.0.0",
|
||||
"cfg-if",
|
||||
"crossbeam-utils",
|
||||
"memoffset 0.8.0",
|
||||
"scopeguard",
|
||||
|
@ -1457,7 +1451,7 @@ version = "0.8.15"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c063cd8cc95f5c377ed0d4b49a4b21f632396ff690e8470c29b3359b346984b"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1632,7 +1626,7 @@ version = "4.0.0-rc.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8d4ba9852b42210c7538b75484f9daa0655e9a3ac04f693747bb0f02cf3cfe16"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"cfg-if",
|
||||
"fiat-crypto",
|
||||
"packed_simd_2",
|
||||
"platforms",
|
||||
|
@ -1716,7 +1710,7 @@ version = "5.4.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "907076dfda823b0b36d2a1bb5f90c96660a5bbcd7729e10727f07858f22c4edc"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"cfg-if",
|
||||
"hashbrown 0.12.3",
|
||||
"lock_api",
|
||||
"once_cell",
|
||||
|
@ -1912,7 +1906,7 @@ version = "2.0.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"cfg-if",
|
||||
"dirs-sys-next",
|
||||
]
|
||||
|
||||
|
@ -2099,7 +2093,7 @@ version = "0.8.32"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -2323,7 +2317,7 @@ version = "0.2.21"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5cbc844cecaee9d4443931972e1289c8ff485cb4cc2767cb03ca139ed6885153"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"redox_syscall 0.2.16",
|
||||
"windows-sys 0.48.0",
|
||||
|
@ -2686,7 +2680,7 @@ version = "0.1.16"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"wasi 0.9.0+wasi-snapshot-preview1",
|
||||
]
|
||||
|
@ -2697,7 +2691,7 @@ version = "0.2.9"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||
|
@ -2798,7 +2792,7 @@ checksum = "10c6ae9f6fa26f4fb2ac16b528d138d971ead56141de489f8111e259b9df3c4a"
|
|||
dependencies = [
|
||||
"anyhow",
|
||||
"heck 0.4.1",
|
||||
"proc-macro-crate 1.1.3",
|
||||
"proc-macro-crate 1.3.1",
|
||||
"proc-macro-error",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
@ -2915,7 +2909,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "684c0456c086e8e7e9af73ec5b84e35938df394712054550e81558d21c44ab0d"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"proc-macro-crate 1.1.3",
|
||||
"proc-macro-crate 1.3.1",
|
||||
"proc-macro-error",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
@ -3461,27 +3455,25 @@ version = "0.1.12"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "int-enum"
|
||||
version = "0.4.0"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b1428b2b1abe959e6eedb0a17d0ab12f6ba20e1106cc29fc4874e3ba393c177"
|
||||
checksum = "cff87d3cc4b79b4559e3c75068d64247284aceb6a038bd4bb38387f3f164476d"
|
||||
dependencies = [
|
||||
"cfg-if 0.1.10",
|
||||
"int-enum-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "int-enum-impl"
|
||||
version = "0.4.0"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2c3cecaad8ca1a5020843500c696de2b9a07b63b624ddeef91f85f9bafb3671"
|
||||
checksum = "df1f2f068675add1a3fc77f5f5ab2e29290c841ee34d151abc007bce902e5d34"
|
||||
dependencies = [
|
||||
"cfg-if 0.1.10",
|
||||
"proc-macro-crate 0.1.5",
|
||||
"proc-macro-crate 1.3.1",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
|
@ -3757,7 +3749,7 @@ checksum = "6607c62aa161d23d17a9072cc5da0be67cdfc89d3afb1e8d9c842bebc2525ffe"
|
|||
dependencies = [
|
||||
"arrayvec 0.5.2",
|
||||
"bitflags",
|
||||
"cfg-if 1.0.0",
|
||||
"cfg-if",
|
||||
"ryu",
|
||||
"static_assertions",
|
||||
]
|
||||
|
@ -3795,7 +3787,7 @@ version = "0.7.4"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"cfg-if",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
|
@ -4207,7 +4199,7 @@ version = "0.5.6"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff50ecb28bb86013e935fb6683ab1f6d3a20016f123c76fd4c27470076ac30f5"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"cfg-if",
|
||||
"generator",
|
||||
"scoped-tls",
|
||||
"serde",
|
||||
|
@ -4600,11 +4592,11 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "multihash-derive"
|
||||
version = "0.8.1"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d6d4752e6230d8ef7adf7bd5d8c4b1f6561c1014c5ba9a37445ccefe18aa1db"
|
||||
checksum = "fc076939022111618a5026d3be019fd8b366e76314538ff9a1b59ffbcbf98bcd"
|
||||
dependencies = [
|
||||
"proc-macro-crate 1.1.3",
|
||||
"proc-macro-crate 1.3.1",
|
||||
"proc-macro-error",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
@ -4789,7 +4781,7 @@ checksum = "e4916f159ed8e5de0082076562152a76b7a1f64a01fd9d1e0fea002c37624faf"
|
|||
dependencies = [
|
||||
"bitflags",
|
||||
"cc",
|
||||
"cfg-if 1.0.0",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"memoffset 0.6.5",
|
||||
]
|
||||
|
@ -4801,7 +4793,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "fa52e972a9a719cecb6864fb88568781eb706bac2cd1d4f04a648542dbf78069"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cfg-if 1.0.0",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"memoffset 0.6.5",
|
||||
]
|
||||
|
@ -4813,7 +4805,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "bfdda3d196821d6af13126e40375cdf7da646a96114af134d5f417a9a1dc8e1a"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cfg-if 1.0.0",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"static_assertions",
|
||||
]
|
||||
|
@ -5013,7 +5005,7 @@ version = "0.5.11"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dcbff9bc912032c62bf65ef1d5aea88983b420f4f839db1e9b0c281a25c9c799"
|
||||
dependencies = [
|
||||
"proc-macro-crate 1.1.3",
|
||||
"proc-macro-crate 1.3.1",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
|
@ -5134,7 +5126,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "12df40a956736488b7b44fe79fe12d4f245bb5b3f5a1f6095e499760015be392"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cfg-if 1.0.0",
|
||||
"cfg-if",
|
||||
"foreign-types",
|
||||
"libc",
|
||||
"once_cell",
|
||||
|
@ -5273,7 +5265,7 @@ version = "0.3.8"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1914cd452d8fccd6f9db48147b29fd4ae05bea9dc5d9ad578509f72415de282"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"cfg-if",
|
||||
"libm 0.1.4",
|
||||
]
|
||||
|
||||
|
@ -5335,7 +5327,7 @@ version = "0.8.6"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"cfg-if",
|
||||
"instant",
|
||||
"libc",
|
||||
"redox_syscall 0.2.16",
|
||||
|
@ -5349,7 +5341,7 @@ version = "0.9.7"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"redox_syscall 0.2.16",
|
||||
"smallvec",
|
||||
|
@ -5662,7 +5654,7 @@ checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce"
|
|||
dependencies = [
|
||||
"autocfg",
|
||||
"bitflags",
|
||||
"cfg-if 1.0.0",
|
||||
"cfg-if",
|
||||
"concurrent-queue",
|
||||
"libc",
|
||||
"log",
|
||||
|
@ -5698,7 +5690,7 @@ version = "0.5.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8419d2b623c7c0896ff2d5d96e2cb4ede590fed28fcc34934f4c33c036e620a1"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"opaque-debug",
|
||||
"universal-hash 0.4.1",
|
||||
|
@ -5710,7 +5702,7 @@ version = "0.6.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ef234e08c11dfcb2e56f79fd70f6f2eb7f025c0ce2333e82f4f0518ecad30c6"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"opaque-debug",
|
||||
"universal-hash 0.5.1",
|
||||
|
@ -5739,7 +5731,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "prisma-client-rust"
|
||||
version = "0.6.8"
|
||||
source = "git+https://github.com/Brendonovich/prisma-client-rust?rev=687a00812130454613eee2c8e804bc615e755180#687a00812130454613eee2c8e804bc615e755180"
|
||||
source = "git+https://github.com/Brendonovich/prisma-client-rust?rev=1c1a7fb7b436a01ee7763e7f75cfa8f25a5c10e2#1c1a7fb7b436a01ee7763e7f75cfa8f25a5c10e2"
|
||||
dependencies = [
|
||||
"base64 0.13.1",
|
||||
"bigdecimal",
|
||||
|
@ -5772,7 +5764,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "prisma-client-rust-cli"
|
||||
version = "0.6.8"
|
||||
source = "git+https://github.com/Brendonovich/prisma-client-rust?rev=687a00812130454613eee2c8e804bc615e755180#687a00812130454613eee2c8e804bc615e755180"
|
||||
source = "git+https://github.com/Brendonovich/prisma-client-rust?rev=1c1a7fb7b436a01ee7763e7f75cfa8f25a5c10e2#1c1a7fb7b436a01ee7763e7f75cfa8f25a5c10e2"
|
||||
dependencies = [
|
||||
"directories",
|
||||
"flate2",
|
||||
|
@ -5792,7 +5784,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "prisma-client-rust-macros"
|
||||
version = "0.6.8"
|
||||
source = "git+https://github.com/Brendonovich/prisma-client-rust?rev=687a00812130454613eee2c8e804bc615e755180#687a00812130454613eee2c8e804bc615e755180"
|
||||
source = "git+https://github.com/Brendonovich/prisma-client-rust?rev=1c1a7fb7b436a01ee7763e7f75cfa8f25a5c10e2#1c1a7fb7b436a01ee7763e7f75cfa8f25a5c10e2"
|
||||
dependencies = [
|
||||
"convert_case 0.6.0",
|
||||
"proc-macro2",
|
||||
|
@ -5804,7 +5796,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "prisma-client-rust-sdk"
|
||||
version = "0.6.8"
|
||||
source = "git+https://github.com/Brendonovich/prisma-client-rust?rev=687a00812130454613eee2c8e804bc615e755180#687a00812130454613eee2c8e804bc615e755180"
|
||||
source = "git+https://github.com/Brendonovich/prisma-client-rust?rev=1c1a7fb7b436a01ee7763e7f75cfa8f25a5c10e2#1c1a7fb7b436a01ee7763e7f75cfa8f25a5c10e2"
|
||||
dependencies = [
|
||||
"convert_case 0.5.0",
|
||||
"dmmf",
|
||||
|
@ -5863,12 +5855,12 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "proc-macro-crate"
|
||||
version = "1.1.3"
|
||||
version = "1.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e17d47ce914bf4de440332250b0edd23ce48c005f59fab39d3335866b114f11a"
|
||||
checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919"
|
||||
dependencies = [
|
||||
"thiserror",
|
||||
"toml 0.5.11",
|
||||
"once_cell",
|
||||
"toml_edit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -6418,9 +6410,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.8.3"
|
||||
version = "1.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "81ca098a9821bd52d6b24fd8b10bd081f47d39c22778cafaa75a2857a62c6390"
|
||||
checksum = "d0ab3ca65655bb1e41f2a8c8cd662eb4fb035e67c3f78da1d61dffe89d07300f"
|
||||
dependencies = [
|
||||
"aho-corasick 1.0.2",
|
||||
"memchr",
|
||||
|
@ -6924,6 +6916,7 @@ dependencies = [
|
|||
"notify",
|
||||
"once_cell",
|
||||
"prisma-client-rust",
|
||||
"regex",
|
||||
"rmp",
|
||||
"rmp-serde",
|
||||
"rspc",
|
||||
|
@ -7429,7 +7422,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "99cd6713db3cf16b6c84e06321e049a9b9f699826e16096d23bbcc44d15d51a6"
|
||||
dependencies = [
|
||||
"block-buffer 0.9.0",
|
||||
"cfg-if 1.0.0",
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"digest 0.9.0",
|
||||
"opaque-debug",
|
||||
|
@ -7441,7 +7434,7 @@ version = "0.10.5"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"digest 0.10.7",
|
||||
]
|
||||
|
@ -7453,7 +7446,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800"
|
||||
dependencies = [
|
||||
"block-buffer 0.9.0",
|
||||
"cfg-if 1.0.0",
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"digest 0.9.0",
|
||||
"opaque-debug",
|
||||
|
@ -7465,7 +7458,7 @@ version = "0.10.6"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"digest 0.10.7",
|
||||
]
|
||||
|
@ -7985,7 +7978,7 @@ version = "0.28.4"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4c2f3ca6693feb29a89724516f016488e9aafc7f37264f898593ee4b942f31b"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"cfg-if",
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
"ntapi",
|
||||
|
@ -8345,7 +8338,7 @@ version = "3.5.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9fbec84f381d5795b08656e4912bec604d162bff9291d6189a78f4c8ab87998"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"cfg-if",
|
||||
"fastrand",
|
||||
"redox_syscall 0.3.5",
|
||||
"rustix",
|
||||
|
@ -8404,7 +8397,7 @@ version = "1.1.7"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
|
@ -8642,7 +8635,7 @@ version = "0.1.37"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"cfg-if",
|
||||
"log",
|
||||
"pin-project-lite",
|
||||
"tracing-attributes 0.1.24",
|
||||
|
@ -8844,7 +8837,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "4f7f83d1e4a0e4358ac54c5c3681e5d7da5efc5a7a632c90bb6d6669ddd9bc26"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"cfg-if 1.0.0",
|
||||
"cfg-if",
|
||||
"data-encoding",
|
||||
"enum-as-inner",
|
||||
"futures-channel",
|
||||
|
@ -8869,7 +8862,7 @@ version = "0.22.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aff21aa4dcefb0a1afbfac26deb0adc93888c7d295fb63ab273ef276ba2b7cfe"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"cfg-if",
|
||||
"futures-util",
|
||||
"ipconfig",
|
||||
"lazy_static",
|
||||
|
@ -9248,7 +9241,7 @@ version = "0.2.86"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5bba0e8cb82ba49ff4e229459ff22a191bbe9a1cb3a341610c9c33efc27ddf73"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"cfg-if",
|
||||
"wasm-bindgen-macro",
|
||||
]
|
||||
|
||||
|
@ -9273,7 +9266,7 @@ version = "0.4.36"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2d1985d03709c53167ce907ff394f5316aa22cb4e12761295c5dc57dacb6297e"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
|
@ -10054,7 +10047,7 @@ version = "0.11.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76a1a57ff50e9b408431e8f97d5456f2807f8eb2a2cd79b06068fc87f8ecf189"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"cfg-if",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
|
@ -10311,7 +10304,7 @@ version = "2.10.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e4ca5e22593eb4212382d60d26350065bf2a02c34b85bc850474a74b589a3de9"
|
||||
dependencies = [
|
||||
"proc-macro-crate 1.1.3",
|
||||
"proc-macro-crate 1.3.1",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
|
|
|
@ -18,19 +18,19 @@ edition = "2021"
|
|||
repository = "https://github.com/spacedriveapp/spacedrive"
|
||||
|
||||
[workspace.dependencies]
|
||||
prisma-client-rust = { git = "https://github.com/Brendonovich/prisma-client-rust", rev = "687a00812130454613eee2c8e804bc615e755180", features = [
|
||||
prisma-client-rust = { git = "https://github.com/Brendonovich/prisma-client-rust", rev = "1c1a7fb7b436a01ee7763e7f75cfa8f25a5c10e2", features = [
|
||||
"rspc",
|
||||
"sqlite-create-many",
|
||||
"migrations",
|
||||
"sqlite",
|
||||
] }
|
||||
prisma-client-rust-cli = { git = "https://github.com/Brendonovich/prisma-client-rust", rev = "687a00812130454613eee2c8e804bc615e755180", features = [
|
||||
prisma-client-rust-cli = { git = "https://github.com/Brendonovich/prisma-client-rust", rev = "1c1a7fb7b436a01ee7763e7f75cfa8f25a5c10e2", features = [
|
||||
"rspc",
|
||||
"sqlite-create-many",
|
||||
"migrations",
|
||||
"sqlite",
|
||||
] }
|
||||
prisma-client-rust-sdk = { git = "https://github.com/Brendonovich/prisma-client-rust", rev = "687a00812130454613eee2c8e804bc615e755180", features = [
|
||||
prisma-client-rust-sdk = { git = "https://github.com/Brendonovich/prisma-client-rust", rev = "1c1a7fb7b436a01ee7763e7f75cfa8f25a5c10e2", features = [
|
||||
"sqlite",
|
||||
] }
|
||||
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
[package]
|
||||
name = "cli"
|
||||
version = "0.1.0"
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
edition.workspace = true
|
||||
license = { workspace = true }
|
||||
repository = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
|
|
|
@ -157,10 +157,10 @@ fn parse_file(path: &Path) -> Option<DesktopEntry> {
|
|||
}
|
||||
}
|
||||
|
||||
impl TryFrom<PathBuf> for DesktopEntry {
|
||||
impl TryFrom<&PathBuf> for DesktopEntry {
|
||||
type Error = Error;
|
||||
fn try_from(path: PathBuf) -> Result<DesktopEntry> {
|
||||
parse_file(&path).ok_or(Error::BadEntry(path))
|
||||
fn try_from(path: &PathBuf) -> Result<DesktopEntry> {
|
||||
parse_file(path).ok_or(Error::BadEntry(path.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -41,7 +41,7 @@ impl Handler {
|
|||
}
|
||||
|
||||
pub fn get_entry(&self) -> Result<DesktopEntry> {
|
||||
DesktopEntry::try_from(self.get_path()?)
|
||||
DesktopEntry::try_from(&self.get_path()?)
|
||||
}
|
||||
|
||||
pub fn launch(&self, args: &[&str]) -> Result<()> {
|
||||
|
|
|
@ -14,11 +14,10 @@ pub struct SystemApps(pub HashMap<Mime, VecDeque<Handler>>);
|
|||
|
||||
impl SystemApps {
|
||||
pub fn get_handlers(&self, handler_type: HandlerType) -> VecDeque<Handler> {
|
||||
let mime_db = SharedMimeInfo::new();
|
||||
match handler_type {
|
||||
HandlerType::Ext(ext) => {
|
||||
let mut handlers: HashSet<Handler> = HashSet::new();
|
||||
for mime in mime_db.get_mime_types_from_file_name(ext.as_str()) {
|
||||
for mime in SharedMimeInfo::new().get_mime_types_from_file_name(ext.as_str()) {
|
||||
if let Some(mime_handlers) = self.0.get(&mime) {
|
||||
for handler in mime_handlers {
|
||||
handlers.insert(handler.clone());
|
||||
|
@ -40,12 +39,7 @@ impl SystemApps {
|
|||
.list_data_files_once("applications")
|
||||
.into_iter()
|
||||
.filter(|p| p.extension().and_then(|x| x.to_str()) == Some("desktop"))
|
||||
.filter_map(|p| {
|
||||
Some((
|
||||
p.file_name()?.to_owned(),
|
||||
DesktopEntry::try_from(p.clone()).ok()?,
|
||||
))
|
||||
}))
|
||||
.filter_map(|p| Some((p.file_name()?.to_owned(), DesktopEntry::try_from(&p).ok()?))))
|
||||
}
|
||||
|
||||
pub fn populate() -> Result<Self> {
|
||||
|
|
|
@ -1,81 +1,110 @@
|
|||
use std::sync::Arc;
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use sd_core::Node;
|
||||
use serde::Serialize;
|
||||
use specta::Type;
|
||||
use tracing::error;
|
||||
|
||||
#[derive(Serialize, Type)]
|
||||
#[serde(tag = "t", content = "c")]
|
||||
pub enum OpenFilePathResult {
|
||||
NoLibrary,
|
||||
NoFile,
|
||||
OpenError(String),
|
||||
AllGood,
|
||||
NoFile(i32),
|
||||
OpenError(i32, String),
|
||||
AllGood(i32),
|
||||
Internal(String),
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
#[specta::specta]
|
||||
pub async fn open_file_path(
|
||||
library: uuid::Uuid,
|
||||
id: i32,
|
||||
ids: Vec<i32>,
|
||||
node: tauri::State<'_, Arc<Node>>,
|
||||
) -> Result<OpenFilePathResult, ()> {
|
||||
) -> Result<Vec<OpenFilePathResult>, ()> {
|
||||
let res = if let Some(library) = node.library_manager.get_library(library).await {
|
||||
let Ok(Some(path)) = library
|
||||
.get_file_path(id)
|
||||
.await
|
||||
else {
|
||||
return Ok(OpenFilePathResult::NoFile)
|
||||
};
|
||||
|
||||
opener::open(path)
|
||||
.map(|_| OpenFilePathResult::AllGood)
|
||||
.unwrap_or_else(|e| OpenFilePathResult::OpenError(e.to_string()))
|
||||
library.get_file_paths(ids).await.map_or_else(
|
||||
|e| vec![OpenFilePathResult::Internal(e.to_string())],
|
||||
|paths| {
|
||||
paths
|
||||
.into_iter()
|
||||
.map(|(id, maybe_path)| {
|
||||
if let Some(path) = maybe_path {
|
||||
opener::open(path)
|
||||
.map(|_| OpenFilePathResult::AllGood(id))
|
||||
.unwrap_or_else(|e| {
|
||||
OpenFilePathResult::OpenError(id, e.to_string())
|
||||
})
|
||||
} else {
|
||||
OpenFilePathResult::NoFile(id)
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
},
|
||||
)
|
||||
} else {
|
||||
OpenFilePathResult::NoLibrary
|
||||
vec![OpenFilePathResult::NoLibrary]
|
||||
};
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
#[derive(Type, Debug, serde::Serialize)]
|
||||
pub struct OpenWithApplication {
|
||||
name: String,
|
||||
#[cfg(target_os = "linux")]
|
||||
url: std::path::PathBuf,
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
url: String,
|
||||
#[derive(Serialize, Type)]
|
||||
#[serde(tag = "t", content = "c")]
|
||||
#[allow(dead_code)]
|
||||
pub enum OpenWithApplication {
|
||||
File {
|
||||
id: i32,
|
||||
name: String,
|
||||
#[cfg(target_os = "linux")]
|
||||
url: std::path::PathBuf,
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
url: String,
|
||||
},
|
||||
Error(i32, String),
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
#[specta::specta]
|
||||
#[allow(unused_variables)]
|
||||
pub async fn get_file_path_open_with_apps(
|
||||
library: uuid::Uuid,
|
||||
id: i32,
|
||||
ids: Vec<i32>,
|
||||
node: tauri::State<'_, Arc<Node>>,
|
||||
) -> Result<Vec<OpenWithApplication>, ()> {
|
||||
let Some(library) = node.library_manager.get_library(library).await else {
|
||||
return Err(())
|
||||
};
|
||||
|
||||
let Ok(Some(path)) = library
|
||||
.get_file_path(id)
|
||||
.await
|
||||
else {
|
||||
return Err(())
|
||||
};
|
||||
let Ok(paths) = library.get_file_paths(ids).await.map_err(|e| {error!("{e:#?}");})
|
||||
else {
|
||||
return Err(());
|
||||
};
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
return Ok(unsafe {
|
||||
sd_desktop_macos::get_open_with_applications(&path.to_str().unwrap().into())
|
||||
}
|
||||
.as_slice()
|
||||
.iter()
|
||||
.map(|app| OpenWithApplication {
|
||||
name: app.name.to_string(),
|
||||
url: app.url.to_string(),
|
||||
})
|
||||
.collect());
|
||||
return Ok(paths
|
||||
.into_iter()
|
||||
.flat_map(|(id, path)| {
|
||||
if let Some(path) = path {
|
||||
unsafe {
|
||||
sd_desktop_macos::get_open_with_applications(&path.to_str().unwrap().into())
|
||||
}
|
||||
.as_slice()
|
||||
.iter()
|
||||
.map(|app| OpenWithApplication::File {
|
||||
id,
|
||||
name: app.name.to_string(),
|
||||
url: app.url.to_string(),
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
} else {
|
||||
vec![OpenWithApplication::Error(
|
||||
id,
|
||||
"File not found in database".into(),
|
||||
)]
|
||||
}
|
||||
})
|
||||
.collect());
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
|
@ -84,71 +113,131 @@ pub async fn get_file_path_open_with_apps(
|
|||
// TODO: cache this, and only update when the underlying XDG desktop apps changes
|
||||
let system_apps = SystemApps::populate().map_err(|_| ())?;
|
||||
|
||||
let handlers = system_apps.get_handlers(HandlerType::Ext(
|
||||
path.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.map(|name| name.to_string())
|
||||
.ok_or(
|
||||
// io::Error::new(
|
||||
// io::ErrorKind::Other,
|
||||
// "Missing file name from path",
|
||||
// )
|
||||
(),
|
||||
)?,
|
||||
));
|
||||
return Ok(paths
|
||||
.into_iter()
|
||||
.flat_map(|(id, path)| {
|
||||
if let Some(path) = path {
|
||||
let Some(name) = path.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.map(|name| name.to_string())
|
||||
else {
|
||||
return vec![OpenWithApplication::Error(
|
||||
id,
|
||||
"Failed to extract file name".into(),
|
||||
)]
|
||||
};
|
||||
|
||||
let data = handlers
|
||||
.iter()
|
||||
.map(|handler| {
|
||||
let path = handler.get_path().map_err(|_| ())?;
|
||||
let entry = DesktopEntry::try_from(path.clone()).map_err(|_| ())?;
|
||||
Ok(OpenWithApplication {
|
||||
name: entry.name,
|
||||
url: path,
|
||||
})
|
||||
system_apps
|
||||
.get_handlers(HandlerType::Ext(name))
|
||||
.iter()
|
||||
.map(|handler| {
|
||||
handler
|
||||
.get_path()
|
||||
.map(|path| {
|
||||
DesktopEntry::try_from(&path)
|
||||
.map(|entry| OpenWithApplication::File {
|
||||
id,
|
||||
name: entry.name,
|
||||
url: path,
|
||||
})
|
||||
.unwrap_or_else(|e| {
|
||||
error!("{e:#?}");
|
||||
OpenWithApplication::Error(
|
||||
id,
|
||||
"Failed to parse desktop entry".into(),
|
||||
)
|
||||
})
|
||||
})
|
||||
.unwrap_or_else(|e| {
|
||||
error!("{e:#?}");
|
||||
OpenWithApplication::Error(
|
||||
id,
|
||||
"Failed to get path from desktop entry".into(),
|
||||
)
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
} else {
|
||||
vec![OpenWithApplication::Error(
|
||||
id,
|
||||
"File not found in database".into(),
|
||||
)]
|
||||
}
|
||||
})
|
||||
.collect::<Result<Vec<OpenWithApplication>, _>>()?;
|
||||
|
||||
return Ok(data);
|
||||
.collect());
|
||||
}
|
||||
|
||||
#[allow(unreachable_code)]
|
||||
Err(())
|
||||
}
|
||||
|
||||
type FileIdAndUrl = (i32, String);
|
||||
|
||||
#[tauri::command(async)]
|
||||
#[specta::specta]
|
||||
#[allow(unused_variables)]
|
||||
pub async fn open_file_path_with(
|
||||
library: uuid::Uuid,
|
||||
id: i32,
|
||||
url: String,
|
||||
file_ids_and_urls: Vec<FileIdAndUrl>,
|
||||
node: tauri::State<'_, Arc<Node>>,
|
||||
) -> Result<(), ()> {
|
||||
let Some(library) = node.library_manager.get_library(library).await else {
|
||||
return Err(())
|
||||
};
|
||||
|
||||
let Ok(Some(path)) = library
|
||||
.get_file_path(id)
|
||||
.await
|
||||
else {
|
||||
return Err(())
|
||||
};
|
||||
let url_by_id = file_ids_and_urls.into_iter().collect::<HashMap<_, _>>();
|
||||
let ids = url_by_id.keys().copied().collect::<Vec<_>>();
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
unsafe {
|
||||
sd_desktop_macos::open_file_path_with(
|
||||
&path.to_str().ok_or(())?.into(),
|
||||
&url.as_str().into(),
|
||||
)
|
||||
};
|
||||
{
|
||||
library
|
||||
.get_file_paths(ids)
|
||||
.await
|
||||
.map(|paths| {
|
||||
paths.iter().for_each(|(id, path)| {
|
||||
if let Some(path) = path {
|
||||
unsafe {
|
||||
sd_desktop_macos::open_file_path_with(
|
||||
&path.to_str().unwrap().into(),
|
||||
&url_by_id
|
||||
.get(id)
|
||||
.expect("we just created this hashmap")
|
||||
.as_str()
|
||||
.into(),
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
.map_err(|e| {
|
||||
error!("{e:#?}");
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
sd_desktop_linux::Handler::assume_valid(url.into())
|
||||
.open(&[path.to_str().ok_or(())?])
|
||||
.map_err(|_| ())?;
|
||||
library
|
||||
.get_file_paths(ids)
|
||||
.await
|
||||
.map(|paths| {
|
||||
paths.iter().for_each(|(id, path)| {
|
||||
if let Some(path) = path.as_ref().and_then(|path| path.to_str()) {
|
||||
if let Err(e) = sd_desktop_linux::Handler::assume_valid(
|
||||
url_by_id
|
||||
.get(id)
|
||||
.expect("we just created this hashmap")
|
||||
.as_str()
|
||||
.into(),
|
||||
)
|
||||
.open(&[path])
|
||||
{
|
||||
error!("{e:#?}");
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
.map_err(|e| {
|
||||
error!("{e:#?}");
|
||||
})
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ pub enum AppThemeType {
|
|||
|
||||
#[tauri::command(async)]
|
||||
#[specta::specta]
|
||||
#[allow(unused_variables)]
|
||||
pub async fn lock_app_theme(theme_type: AppThemeType) {
|
||||
#[cfg(target_os = "macos")]
|
||||
unsafe {
|
||||
|
|
|
@ -22,22 +22,22 @@ export function openLogsDir() {
|
|||
return invoke()<null>("open_logs_dir")
|
||||
}
|
||||
|
||||
export function openFilePath(library: string, id: number) {
|
||||
return invoke()<OpenFilePathResult>("open_file_path", { library,id })
|
||||
export function openFilePath(library: string, ids: number[]) {
|
||||
return invoke()<OpenFilePathResult[]>("open_file_path", { library,ids })
|
||||
}
|
||||
|
||||
export function getFilePathOpenWithApps(library: string, id: number) {
|
||||
return invoke()<OpenWithApplication[]>("get_file_path_open_with_apps", { library,id })
|
||||
export function getFilePathOpenWithApps(library: string, ids: number[]) {
|
||||
return invoke()<OpenWithApplication[]>("get_file_path_open_with_apps", { library,ids })
|
||||
}
|
||||
|
||||
export function openFilePathWith(library: string, id: number, url: string) {
|
||||
return invoke()<null>("open_file_path_with", { library,id,url })
|
||||
export function openFilePathWith(library: string, fileIdsAndUrls: ([number, string])[]) {
|
||||
return invoke()<null>("open_file_path_with", { library,fileIdsAndUrls })
|
||||
}
|
||||
|
||||
export function lockAppTheme(themeType: AppThemeType) {
|
||||
return invoke()<null>("lock_app_theme", { themeType })
|
||||
}
|
||||
|
||||
export type OpenWithApplication = { name: string; url: string }
|
||||
export type OpenFilePathResult = { t: "NoLibrary" } | { t: "NoFile" } | { t: "OpenError"; c: string } | { t: "AllGood" }
|
||||
export type OpenWithApplication = { t: "File"; c: { id: number; name: string; url: string } } | { t: "Error"; c: [number, string] }
|
||||
export type AppThemeType = "Auto" | "Light" | "Dark"
|
||||
export type OpenFilePathResult = { t: "NoLibrary" } | { t: "NoFile"; c: number } | { t: "OpenError"; c: [number, string] } | { t: "AllGood"; c: number } | { t: "Internal"; c: string }
|
||||
|
|
|
@ -2,9 +2,9 @@
|
|||
name = "sd-mobile-android"
|
||||
version = "0.1.0"
|
||||
rust-version = "1.64.0"
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
edition.workspace = true
|
||||
license = { workspace = true }
|
||||
repository = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
[lib]
|
||||
# Android can use dynamic linking since all FFI is done via JNI
|
||||
|
|
|
@ -2,16 +2,16 @@
|
|||
name = "sd-mobile-core"
|
||||
version = "0.1.0"
|
||||
rust-version = "1.64.0"
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
edition.workspace = true
|
||||
license = { workspace = true }
|
||||
repository = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
[dependencies]
|
||||
once_cell = "1.17.2"
|
||||
sd-core = { path = "../../../../core", features = [
|
||||
"mobile",
|
||||
], default-features = false }
|
||||
rspc.workspace = true
|
||||
rspc = { workspace = true }
|
||||
serde_json = "1.0.96"
|
||||
tokio = { workspace = true }
|
||||
openssl = { version = "0.10.53", features = [
|
||||
|
|
|
@ -2,9 +2,9 @@
|
|||
name = "sd-mobile-ios"
|
||||
version = "0.1.0"
|
||||
rust-version = "1.64.0"
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
edition.workspace = true
|
||||
license = { workspace = true }
|
||||
repository = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
|
||||
[lib]
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
[package]
|
||||
name = "server"
|
||||
version = "0.1.0"
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
edition.workspace = true
|
||||
license = { workspace = true }
|
||||
repository = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
[features]
|
||||
assets = []
|
||||
|
|
|
@ -3,10 +3,10 @@ name = "sd-core"
|
|||
version = "0.1.0"
|
||||
description = "Virtual distributed filesystem engine that powers Spacedrive."
|
||||
authors = ["Spacedrive Technology Inc."]
|
||||
rust-version = "1.68.1"
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version = "1.70.0"
|
||||
license = { workspace = true }
|
||||
repository = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
@ -88,8 +88,9 @@ normpath = { version = "1.1.1", features = ["localization"] }
|
|||
tracing-appender = { git = "https://github.com/tokio-rs/tracing", rev = "29146260fb4615d271d2e899ad95a753bb42915e" } # Unreleased changes for log deletion
|
||||
strum = { version = "0.24", features = ["derive"] }
|
||||
strum_macros = "0.24"
|
||||
regex = "1.8.4"
|
||||
hex = "0.4.3"
|
||||
int-enum = "0.4.0"
|
||||
int-enum = "0.5.0"
|
||||
|
||||
[target.'cfg(windows)'.dependencies.winapi-util]
|
||||
version = "0.1.5"
|
||||
|
|
|
@ -1,20 +1,30 @@
|
|||
use crate::{
|
||||
api::utils::library,
|
||||
invalidate_query,
|
||||
location::{file_path_helper::IsolatedFilePathData, find_location, LocationError},
|
||||
library::Library,
|
||||
location::{
|
||||
file_path_helper::{
|
||||
file_path_to_isolate, file_path_to_isolate_with_id, FilePathError, IsolatedFilePathData,
|
||||
},
|
||||
find_location, LocationError,
|
||||
},
|
||||
object::fs::{
|
||||
copy::FileCopierJobInit, cut::FileCutterJobInit, delete::FileDeleterJobInit,
|
||||
erase::FileEraserJobInit,
|
||||
},
|
||||
prisma::{location, object},
|
||||
prisma::{file_path, location, object},
|
||||
};
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use chrono::Utc;
|
||||
use futures::future::try_join_all;
|
||||
use regex::Regex;
|
||||
use rspc::{alpha::AlphaRouter, ErrorCode};
|
||||
use serde::Deserialize;
|
||||
use specta::Type;
|
||||
use std::path::Path;
|
||||
use tokio::fs;
|
||||
use tracing::error;
|
||||
|
||||
use super::{Ctx, R};
|
||||
|
||||
|
@ -86,20 +96,6 @@ pub(crate) fn mount() -> AlphaRouter<Ctx> {
|
|||
Ok(())
|
||||
})
|
||||
})
|
||||
.procedure("delete", {
|
||||
R.with2(library())
|
||||
.mutation(|(_, library), id: i32| async move {
|
||||
library
|
||||
.db
|
||||
.object()
|
||||
.delete(object::id::equals(id))
|
||||
.exec()
|
||||
.await?;
|
||||
|
||||
invalidate_query!(library, "search.paths");
|
||||
Ok(())
|
||||
})
|
||||
})
|
||||
.procedure("updateAccessTime", {
|
||||
R.with2(library())
|
||||
.mutation(|(_, library), id: i32| async move {
|
||||
|
@ -119,12 +115,12 @@ pub(crate) fn mount() -> AlphaRouter<Ctx> {
|
|||
})
|
||||
.procedure("removeAccessTime", {
|
||||
R.with2(library())
|
||||
.mutation(|(_, library), id: i32| async move {
|
||||
.mutation(|(_, library), object_ids: Vec<i32>| async move {
|
||||
library
|
||||
.db
|
||||
.object()
|
||||
.update(
|
||||
object::id::equals(id),
|
||||
.update_many(
|
||||
vec![object::id::in_vec(object_ids)],
|
||||
vec![object::date_accessed::set(None)],
|
||||
)
|
||||
.exec()
|
||||
|
@ -178,52 +174,244 @@ pub(crate) fn mount() -> AlphaRouter<Ctx> {
|
|||
})
|
||||
.procedure("renameFile", {
|
||||
#[derive(Type, Deserialize)]
|
||||
pub struct RenameFileArgs {
|
||||
pub location_id: i32,
|
||||
pub file_name: String,
|
||||
pub new_file_name: String,
|
||||
pub struct FromPattern {
|
||||
pub pattern: String,
|
||||
pub replace_all: bool,
|
||||
}
|
||||
|
||||
R.with2(library()).mutation(
|
||||
|(_, library),
|
||||
RenameFileArgs {
|
||||
location_id,
|
||||
file_name,
|
||||
new_file_name,
|
||||
}: RenameFileArgs| async move {
|
||||
let location = find_location(&library, location_id)
|
||||
#[derive(Type, Deserialize)]
|
||||
pub struct RenameOne {
|
||||
pub from_file_path_id: file_path::id::Type,
|
||||
pub to: String,
|
||||
}
|
||||
|
||||
#[derive(Type, Deserialize)]
|
||||
pub struct RenameMany {
|
||||
pub from_pattern: FromPattern,
|
||||
pub to_pattern: String,
|
||||
pub from_file_path_ids: Vec<file_path::id::Type>,
|
||||
}
|
||||
|
||||
#[derive(Type, Deserialize)]
|
||||
pub enum RenameKind {
|
||||
One(RenameOne),
|
||||
Many(RenameMany),
|
||||
}
|
||||
|
||||
#[derive(Type, Deserialize)]
|
||||
pub struct RenameFileArgs {
|
||||
pub location_id: location::id::Type,
|
||||
pub kind: RenameKind,
|
||||
}
|
||||
|
||||
impl RenameFileArgs {
|
||||
pub async fn rename_one(
|
||||
RenameOne {
|
||||
from_file_path_id,
|
||||
to,
|
||||
}: RenameOne,
|
||||
location_path: impl AsRef<Path>,
|
||||
library: &Library,
|
||||
) -> Result<(), rspc::Error> {
|
||||
let location_path = location_path.as_ref();
|
||||
let iso_file_path = IsolatedFilePathData::from(
|
||||
library
|
||||
.db
|
||||
.file_path()
|
||||
.find_unique(file_path::id::equals(from_file_path_id))
|
||||
.select(file_path_to_isolate::select())
|
||||
.exec()
|
||||
.await?
|
||||
.ok_or(LocationError::FilePath(FilePathError::IdNotFound(
|
||||
from_file_path_id,
|
||||
)))?,
|
||||
);
|
||||
|
||||
if iso_file_path.full_name() == to {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let (new_file_name, new_extension) =
|
||||
IsolatedFilePathData::separate_name_and_extension_from_str(&to)
|
||||
.map_err(LocationError::FilePath)?;
|
||||
|
||||
let mut new_file_full_path = location_path.join(iso_file_path.parent());
|
||||
new_file_full_path.push(new_file_name);
|
||||
if !new_extension.is_empty() {
|
||||
new_file_full_path.set_extension(new_extension);
|
||||
}
|
||||
|
||||
match fs::metadata(&new_file_full_path).await {
|
||||
Ok(_) => {
|
||||
return Err(rspc::Error::new(
|
||||
ErrorCode::Conflict,
|
||||
"File already exists".to_string(),
|
||||
))
|
||||
}
|
||||
Err(e) => {
|
||||
if e.kind() != std::io::ErrorKind::NotFound {
|
||||
return Err(rspc::Error::with_cause(
|
||||
ErrorCode::InternalServerError,
|
||||
"Failed to check if file exists".to_string(),
|
||||
e,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fs::rename(location_path.join(&iso_file_path), new_file_full_path)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
rspc::Error::with_cause(
|
||||
ErrorCode::Conflict,
|
||||
"Failed to rename file".to_string(),
|
||||
e,
|
||||
)
|
||||
})?;
|
||||
|
||||
library
|
||||
.db
|
||||
.file_path()
|
||||
.update(
|
||||
file_path::id::equals(from_file_path_id),
|
||||
vec![
|
||||
file_path::name::set(new_file_name.to_string()),
|
||||
file_path::extension::set(new_extension.to_string()),
|
||||
],
|
||||
)
|
||||
.exec()
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn rename_many(
|
||||
RenameMany {
|
||||
from_pattern,
|
||||
to_pattern,
|
||||
from_file_path_ids,
|
||||
}: RenameMany,
|
||||
location_path: impl AsRef<Path>,
|
||||
library: &Library,
|
||||
) -> Result<(), rspc::Error> {
|
||||
let location_path = location_path.as_ref();
|
||||
|
||||
let Ok(from_regex) = Regex::new(&from_pattern.pattern) else {
|
||||
return Err(rspc::Error::new(
|
||||
rspc::ErrorCode::BadRequest,
|
||||
"Invalid `from` regex pattern".into(),
|
||||
));
|
||||
};
|
||||
|
||||
let to_update = try_join_all(
|
||||
library
|
||||
.db
|
||||
.file_path()
|
||||
.find_many(vec![file_path::id::in_vec(from_file_path_ids)])
|
||||
.select(file_path_to_isolate_with_id::select())
|
||||
.exec()
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|file_path| (file_path.id, IsolatedFilePathData::from(file_path)))
|
||||
.map(|(file_path_id, iso_file_path)| {
|
||||
let from = location_path.join(&iso_file_path);
|
||||
let mut to = location_path.join(iso_file_path.parent());
|
||||
let full_name = iso_file_path.full_name();
|
||||
let replaced_full_name = if from_pattern.replace_all {
|
||||
from_regex.replace_all(&full_name, &to_pattern)
|
||||
} else {
|
||||
from_regex.replace(&full_name, &to_pattern)
|
||||
}
|
||||
.to_string();
|
||||
|
||||
to.push(&replaced_full_name);
|
||||
|
||||
async move {
|
||||
if !IsolatedFilePathData::accept_file_name(&replaced_full_name)
|
||||
{
|
||||
Err(rspc::Error::new(
|
||||
ErrorCode::BadRequest,
|
||||
"Invalid file name".to_string(),
|
||||
))
|
||||
} else {
|
||||
fs::rename(&from, &to)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!(
|
||||
"Failed to rename file from: '{}' to: '{}'",
|
||||
from.display(),
|
||||
to.display()
|
||||
);
|
||||
rspc::Error::with_cause(
|
||||
ErrorCode::Conflict,
|
||||
"Failed to rename file".to_string(),
|
||||
e,
|
||||
)
|
||||
})
|
||||
.map(|_| {
|
||||
let (name, extension) =
|
||||
IsolatedFilePathData::separate_name_and_extension_from_str(
|
||||
&replaced_full_name,
|
||||
)
|
||||
.expect("we just built this full name and validated it");
|
||||
|
||||
(
|
||||
file_path_id,
|
||||
(name.to_string(), extension.to_string()),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// TODO: dispatch sync update events
|
||||
|
||||
library
|
||||
.db
|
||||
._batch(
|
||||
to_update
|
||||
.into_iter()
|
||||
.map(|(file_path_id, (new_name, new_extension))| {
|
||||
library.db.file_path().update(
|
||||
file_path::id::equals(file_path_id),
|
||||
vec![
|
||||
file_path::name::set(new_name),
|
||||
file_path::extension::set(new_extension),
|
||||
],
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
R.with2(library())
|
||||
.mutation(|(_, library), args: RenameFileArgs| async move {
|
||||
let location_path = find_location(&library, args.location_id)
|
||||
.select(location::select!({ path }))
|
||||
.exec()
|
||||
.await?
|
||||
.ok_or(LocationError::IdNotFound(location_id))?;
|
||||
.ok_or(LocationError::IdNotFound(args.location_id))?
|
||||
.path
|
||||
.ok_or(LocationError::MissingPath(args.location_id))?;
|
||||
|
||||
let Some(location_path) = location.path.as_ref().map(Path::new) else {
|
||||
Err(LocationError::MissingPath)?
|
||||
};
|
||||
|
||||
fs::rename(
|
||||
location_path.join(IsolatedFilePathData::from_relative_str(
|
||||
location_id,
|
||||
&file_name,
|
||||
)),
|
||||
location_path.join(IsolatedFilePathData::from_relative_str(
|
||||
location_id,
|
||||
&new_file_name,
|
||||
)),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
rspc::Error::with_cause(
|
||||
ErrorCode::Conflict,
|
||||
"Failed to rename file".to_string(),
|
||||
e,
|
||||
)
|
||||
})?;
|
||||
let res = match args.kind {
|
||||
RenameKind::One(one) => {
|
||||
RenameFileArgs::rename_one(one, location_path, &library).await
|
||||
}
|
||||
RenameKind::Many(many) => {
|
||||
RenameFileArgs::rename_many(many, location_path, &library).await
|
||||
}
|
||||
};
|
||||
|
||||
invalidate_query!(library, "search.objects");
|
||||
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
res
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
@ -6,12 +6,14 @@ use crate::{
|
|||
preview::thumbnailer_job::ThumbnailerJobInit,
|
||||
validation::validator_job::ObjectValidatorJobInit,
|
||||
},
|
||||
prisma::location,
|
||||
};
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use rspc::alpha::AlphaRouter;
|
||||
use serde::Deserialize;
|
||||
use specta::Type;
|
||||
use std::path::PathBuf;
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::{utils::library, CoreEvent, Ctx, R};
|
||||
|
@ -46,7 +48,7 @@ pub(crate) fn mount() -> AlphaRouter<Ctx> {
|
|||
.procedure("generateThumbsForLocation", {
|
||||
#[derive(Type, Deserialize)]
|
||||
pub struct GenerateThumbsForLocationArgs {
|
||||
pub id: i32,
|
||||
pub id: location::id::Type,
|
||||
pub path: PathBuf,
|
||||
}
|
||||
|
||||
|
@ -69,7 +71,7 @@ pub(crate) fn mount() -> AlphaRouter<Ctx> {
|
|||
.procedure("objectValidator", {
|
||||
#[derive(Type, Deserialize)]
|
||||
pub struct ObjectValidatorArgs {
|
||||
pub id: i32,
|
||||
pub id: location::id::Type,
|
||||
pub path: PathBuf,
|
||||
}
|
||||
|
||||
|
@ -92,7 +94,7 @@ pub(crate) fn mount() -> AlphaRouter<Ctx> {
|
|||
.procedure("identifyUniqueFiles", {
|
||||
#[derive(Type, Deserialize)]
|
||||
pub struct IdentifyUniqueFilesArgs {
|
||||
pub id: i32,
|
||||
pub id: location::id::Type,
|
||||
pub path: PathBuf,
|
||||
}
|
||||
|
||||
|
|
|
@ -9,10 +9,11 @@ use crate::{
|
|||
util::AbortOnDrop,
|
||||
};
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use rspc::{self, alpha::AlphaRouter, ErrorCode};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use super::{utils::library, Ctx, R};
|
||||
|
||||
|
@ -67,7 +68,7 @@ pub(crate) fn mount() -> AlphaRouter<Ctx> {
|
|||
})
|
||||
.procedure("get", {
|
||||
R.with2(library())
|
||||
.query(|(_, library), location_id: i32| async move {
|
||||
.query(|(_, library), location_id: location::id::Type| async move {
|
||||
Ok(library
|
||||
.db
|
||||
.location()
|
||||
|
@ -78,7 +79,7 @@ pub(crate) fn mount() -> AlphaRouter<Ctx> {
|
|||
})
|
||||
.procedure("getWithRules", {
|
||||
R.with2(library())
|
||||
.query(|(_, library), location_id: i32| async move {
|
||||
.query(|(_, library), location_id: location::id::Type| async move {
|
||||
Ok(library
|
||||
.db
|
||||
.location()
|
||||
|
@ -106,12 +107,13 @@ pub(crate) fn mount() -> AlphaRouter<Ctx> {
|
|||
})
|
||||
})
|
||||
.procedure("delete", {
|
||||
R.with2(library())
|
||||
.mutation(|(_, library), location_id: i32| async move {
|
||||
R.with2(library()).mutation(
|
||||
|(_, library), location_id: location::id::Type| async move {
|
||||
delete_location(&library, location_id).await?;
|
||||
invalidate_query!(library, "locations.list");
|
||||
Ok(())
|
||||
})
|
||||
},
|
||||
)
|
||||
})
|
||||
.procedure("relink", {
|
||||
R.with2(library())
|
||||
|
@ -132,8 +134,8 @@ pub(crate) fn mount() -> AlphaRouter<Ctx> {
|
|||
})
|
||||
})
|
||||
.procedure("fullRescan", {
|
||||
R.with2(library())
|
||||
.mutation(|(_, library), location_id: i32| async move {
|
||||
R.with2(library()).mutation(
|
||||
|(_, library), location_id: location::id::Type| async move {
|
||||
// rescan location
|
||||
scan_location(
|
||||
&library,
|
||||
|
@ -145,12 +147,13 @@ pub(crate) fn mount() -> AlphaRouter<Ctx> {
|
|||
)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
})
|
||||
},
|
||||
)
|
||||
})
|
||||
.procedure("quickRescan", {
|
||||
#[derive(Clone, Serialize, Deserialize, Type, Debug)]
|
||||
pub struct LightScanArgs {
|
||||
pub location_id: i32,
|
||||
pub location_id: location::id::Type,
|
||||
pub sub_path: String,
|
||||
}
|
||||
|
||||
|
@ -275,7 +278,7 @@ fn mount_indexer_rule_routes() -> AlphaRouter<Ctx> {
|
|||
// list indexer rules for location, returning the indexer rule
|
||||
.procedure("listForLocation", {
|
||||
R.with2(library())
|
||||
.query(|(_, library), location_id: i32| async move {
|
||||
.query(|(_, library), location_id: location::id::Type| async move {
|
||||
library
|
||||
.db
|
||||
.indexer_rule()
|
||||
|
|
|
@ -1,7 +1,18 @@
|
|||
use crate::{
|
||||
location::file_path_helper::{check_file_path_exists, IsolatedFilePathData},
|
||||
api::{
|
||||
locations::{file_path_with_object, object_with_file_paths, ExplorerItem},
|
||||
utils::library,
|
||||
},
|
||||
library::Library,
|
||||
location::{
|
||||
file_path_helper::{check_file_path_exists, IsolatedFilePathData},
|
||||
find_location, LocationError,
|
||||
},
|
||||
object::preview::get_thumb_key,
|
||||
prisma::{self, file_path, location, object, tag, tag_on_object},
|
||||
util::db::chain_optional_iter,
|
||||
};
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use chrono::{DateTime, FixedOffset, Utc};
|
||||
|
@ -10,17 +21,6 @@ use rspc::{alpha::AlphaRouter, ErrorCode};
|
|||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
|
||||
use crate::{
|
||||
api::{
|
||||
locations::{file_path_with_object, object_with_file_paths, ExplorerItem},
|
||||
utils::library,
|
||||
},
|
||||
library::Library,
|
||||
location::{find_location, LocationError},
|
||||
prisma::{self, file_path, object, tag, tag_on_object},
|
||||
util::db::chain_optional_iter,
|
||||
};
|
||||
|
||||
use super::{Ctx, R};
|
||||
|
||||
#[derive(Serialize, Type, Debug)]
|
||||
|
@ -109,7 +109,7 @@ impl<T> MaybeNot<T> {
|
|||
#[serde(rename_all = "camelCase")]
|
||||
struct FilePathFilterArgs {
|
||||
#[specta(optional)]
|
||||
location_id: Option<i32>,
|
||||
location_id: Option<location::id::Type>,
|
||||
#[serde(default)]
|
||||
search: String,
|
||||
#[specta(optional)]
|
||||
|
@ -166,11 +166,11 @@ enum ObjectHiddenFilter {
|
|||
Include,
|
||||
}
|
||||
|
||||
impl Into<Option<object::WhereParam>> for ObjectHiddenFilter {
|
||||
fn into(self) -> Option<object::WhereParam> {
|
||||
match self {
|
||||
Self::Exclude => Some(object::hidden::not(true)),
|
||||
Self::Include => None,
|
||||
impl From<ObjectHiddenFilter> for Option<object::WhereParam> {
|
||||
fn from(value: ObjectHiddenFilter) -> Self {
|
||||
match value {
|
||||
ObjectHiddenFilter::Exclude => Some(object::hidden::not(true)),
|
||||
ObjectHiddenFilter::Include => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ use uuid::Uuid;
|
|||
use crate::{
|
||||
invalidate_query,
|
||||
library::Library,
|
||||
prisma::{object, tag, tag_on_object},
|
||||
prisma::{tag, tag_on_object},
|
||||
sync,
|
||||
};
|
||||
|
||||
|
@ -45,19 +45,6 @@ pub(crate) fn mount() -> AlphaRouter<Ctx> {
|
|||
.await?)
|
||||
})
|
||||
})
|
||||
// .library_mutation("create", |t| {
|
||||
// #[derive(Type, Deserialize)]
|
||||
// pub struct TagCreateArgs {
|
||||
// pub name: String,
|
||||
// pub color: String,
|
||||
// }
|
||||
// t(|_, args: TagCreateArgs, library| async move {
|
||||
// let created_tag = Tag::new(args.name, args.color);
|
||||
// created_tag.save(&library.db).await?;
|
||||
// invalidate_query!(library, "tags.list");
|
||||
// Ok(created_tag)
|
||||
// })
|
||||
// })
|
||||
.procedure("create", {
|
||||
#[derive(Type, Deserialize)]
|
||||
pub struct TagCreateArgs {
|
||||
|
@ -101,7 +88,7 @@ pub(crate) fn mount() -> AlphaRouter<Ctx> {
|
|||
.procedure("assign", {
|
||||
#[derive(Debug, Type, Deserialize)]
|
||||
pub struct TagAssignArgs {
|
||||
pub object_id: i32,
|
||||
pub object_ids: Vec<i32>,
|
||||
pub tag_id: i32,
|
||||
pub unassign: bool,
|
||||
}
|
||||
|
@ -112,17 +99,29 @@ pub(crate) fn mount() -> AlphaRouter<Ctx> {
|
|||
library
|
||||
.db
|
||||
.tag_on_object()
|
||||
.delete(tag_on_object::tag_id_object_id(args.tag_id, args.object_id))
|
||||
.delete_many(
|
||||
args.object_ids
|
||||
.iter()
|
||||
.map(|&object_id| {
|
||||
tag_on_object::tag_id_object_id(args.tag_id, object_id)
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
.exec()
|
||||
.await?;
|
||||
} else {
|
||||
library
|
||||
.db
|
||||
.tag_on_object()
|
||||
.create(
|
||||
tag::id::equals(args.tag_id),
|
||||
object::id::equals(args.object_id),
|
||||
vec![],
|
||||
.create_many(
|
||||
args.object_ids
|
||||
.iter()
|
||||
.map(|&object_id| tag_on_object::CreateUnchecked {
|
||||
tag_id: args.tag_id,
|
||||
object_id,
|
||||
_params: vec![],
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
.exec()
|
||||
.await?;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use crate::{
|
||||
location::file_path_helper::{file_path_to_handle_custom_uri, IsolatedFilePathData},
|
||||
prisma::file_path,
|
||||
prisma::{file_path, location},
|
||||
util::error::FileIOError,
|
||||
Node,
|
||||
};
|
||||
|
@ -34,7 +34,7 @@ use uuid::Uuid;
|
|||
|
||||
// This LRU cache allows us to avoid doing a DB lookup on every request.
|
||||
// The main advantage of this LRU Cache is for video files. Video files are fetch in multiple chunks and the cache prevents a DB lookup on every chunk reducing the request time from 15-25ms to 1-10ms.
|
||||
type MetadataCacheKey = (Uuid, i32);
|
||||
type MetadataCacheKey = (Uuid, file_path::id::Type);
|
||||
type NameAndExtension = (PathBuf, String);
|
||||
static FILE_METADATA_CACHE: Lazy<Cache<MetadataCacheKey, NameAndExtension>> =
|
||||
Lazy::new(|| Cache::new(100));
|
||||
|
@ -160,14 +160,14 @@ async fn handle_file(
|
|||
|
||||
let location_id = path
|
||||
.get(2)
|
||||
.and_then(|id| id.parse::<i32>().ok())
|
||||
.and_then(|id| id.parse::<location::id::Type>().ok())
|
||||
.ok_or_else(|| {
|
||||
HandleCustomUriError::BadRequest("Invalid number of parameters. Missing location_id!")
|
||||
})?;
|
||||
|
||||
let file_path_id = path
|
||||
.get(3)
|
||||
.and_then(|id| id.parse::<i32>().ok())
|
||||
.and_then(|id| id.parse::<file_path::id::Type>().ok())
|
||||
.ok_or_else(|| {
|
||||
HandleCustomUriError::BadRequest("Invalid number of parameters. Missing file_path_id!")
|
||||
})?;
|
||||
|
|
|
@ -48,9 +48,6 @@ pub enum JobManagerError {
|
|||
|
||||
#[error("Failed to fetch job data from database: {0}")]
|
||||
Database(#[from] prisma_client_rust::QueryError),
|
||||
|
||||
#[error("Job error: {0}")]
|
||||
Job(#[from] JobError),
|
||||
}
|
||||
|
||||
impl From<JobManagerError> for rspc::Error {
|
||||
|
@ -66,11 +63,6 @@ impl From<JobManagerError> for rspc::Error {
|
|||
"Error accessing the database".to_string(),
|
||||
value,
|
||||
),
|
||||
JobManagerError::Job(_) => Self::with_cause(
|
||||
rspc::ErrorCode::InternalServerError,
|
||||
"Job error".to_string(),
|
||||
value,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -221,12 +213,12 @@ impl JobManager {
|
|||
}
|
||||
}
|
||||
|
||||
pub async fn resume_jobs(self: Arc<Self>, library: &Library) -> Result<(), JobManagerError> {
|
||||
pub async fn resume_jobs(self: Arc<Self>, library: &Library) -> Result<(), JobError> {
|
||||
library
|
||||
.db
|
||||
.job()
|
||||
.delete_many(vec![job::name::not_in_vec(
|
||||
ALL_JOB_NAMES.into_iter().map(|s| s.to_string()).collect(),
|
||||
ALL_JOB_NAMES.iter().map(|s| s.to_string()).collect(),
|
||||
)])
|
||||
.exec()
|
||||
.await?;
|
||||
|
@ -547,7 +539,7 @@ fn get_background_info_by_job_name(name: &str) -> bool {
|
|||
fn get_resumable_job(
|
||||
job_report: JobReport,
|
||||
next_jobs: VecDeque<Box<dyn DynJob>>,
|
||||
) -> Result<Box<dyn DynJob>, JobManagerError> {
|
||||
) -> Result<Box<dyn DynJob>, JobError> {
|
||||
dispatch_call_to_job_by_name!(
|
||||
job_report.name.as_str(),
|
||||
T -> Job::resume(job_report, T {}, next_jobs),
|
||||
|
@ -569,7 +561,6 @@ fn get_resumable_job(
|
|||
FileEraserJob,
|
||||
]
|
||||
)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
const ALL_JOB_NAMES: &[&str] = &[
|
||||
|
|
|
@ -1,21 +1,25 @@
|
|||
use crate::{
|
||||
library::Library,
|
||||
location::indexer::IndexerError,
|
||||
object::{file_identifier::FileIdentifierJobError, preview::ThumbnailerError},
|
||||
location::{indexer::IndexerError, LocationError},
|
||||
object::{
|
||||
file_identifier::FileIdentifierJobError, fs::error::FileSystemJobsError,
|
||||
preview::ThumbnailerError,
|
||||
},
|
||||
util::error::FileIOError,
|
||||
};
|
||||
|
||||
use sd_crypto::Error as CryptoError;
|
||||
|
||||
use std::{
|
||||
collections::{hash_map::DefaultHasher, VecDeque},
|
||||
fmt::Debug,
|
||||
hash::{Hash, Hasher},
|
||||
mem,
|
||||
path::PathBuf,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use prisma_client_rust::QueryError;
|
||||
use rmp_serde::{decode::Error as DecodeError, encode::Error as EncodeError};
|
||||
use sd_crypto::Error as CryptoError;
|
||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
use tracing::{debug, error, info, warn};
|
||||
|
@ -31,16 +35,16 @@ pub use worker::*;
|
|||
pub enum JobError {
|
||||
// General errors
|
||||
#[error("database error: {0}")]
|
||||
DatabaseError(#[from] prisma_client_rust::QueryError),
|
||||
Database(#[from] QueryError),
|
||||
#[error("Failed to join Tokio spawn blocking: {0}")]
|
||||
JoinTaskError(#[from] tokio::task::JoinError),
|
||||
#[error("Job state encode error: {0}")]
|
||||
JoinTask(#[from] tokio::task::JoinError),
|
||||
#[error("job state encode error: {0}")]
|
||||
StateEncode(#[from] EncodeError),
|
||||
#[error("Job state decode error: {0}")]
|
||||
#[error("job state decode error: {0}")]
|
||||
StateDecode(#[from] DecodeError),
|
||||
#[error("Job metadata serialization error: {0}")]
|
||||
#[error("job metadata serialization error: {0}")]
|
||||
MetadataSerialization(#[from] serde_json::Error),
|
||||
#[error("Tried to resume a job with unknown name: job <name='{1}', uuid='{0}'>")]
|
||||
#[error("tried to resume a job with unknown name: job <name='{1}', uuid='{0}'>")]
|
||||
UnknownJobName(Uuid, String),
|
||||
#[error(
|
||||
"Tried to resume a job that doesn't have saved state data: job <name='{1}', uuid='{0}'>"
|
||||
|
@ -50,28 +54,26 @@ pub enum JobError {
|
|||
MissingReport { id: Uuid, name: String },
|
||||
#[error("missing some job data: '{value}'")]
|
||||
MissingData { value: String },
|
||||
#[error("error converting/handling OS strings")]
|
||||
OsStr,
|
||||
#[error("error converting/handling paths")]
|
||||
Path,
|
||||
#[error("invalid job status integer: {0}")]
|
||||
InvalidJobStatusInt(i32),
|
||||
#[error(transparent)]
|
||||
FileIO(#[from] FileIOError),
|
||||
#[error("Location error: {0}")]
|
||||
Location(#[from] LocationError),
|
||||
|
||||
// Specific job errors
|
||||
#[error("Indexer error: {0}")]
|
||||
IndexerError(#[from] IndexerError),
|
||||
#[error("Thumbnailer error: {0}")]
|
||||
#[error(transparent)]
|
||||
Indexer(#[from] IndexerError),
|
||||
#[error(transparent)]
|
||||
ThumbnailError(#[from] ThumbnailerError),
|
||||
#[error("Identifier error: {0}")]
|
||||
#[error(transparent)]
|
||||
IdentifierError(#[from] FileIdentifierJobError),
|
||||
#[error("Crypto error: {0}")]
|
||||
#[error(transparent)]
|
||||
FileSystemJobsError(#[from] FileSystemJobsError),
|
||||
#[error(transparent)]
|
||||
CryptoError(#[from] CryptoError),
|
||||
#[error("source and destination path are the same: {}", .0.display())]
|
||||
MatchingSrcDest(PathBuf),
|
||||
#[error("action would overwrite another file: {}", .0.display())]
|
||||
WouldOverwrite(PathBuf),
|
||||
#[error("item of type '{0}' with id '{1}' is missing from the db")]
|
||||
MissingFromDb(&'static str, String),
|
||||
#[error("the cas id is not set on the path data")]
|
||||
|
@ -469,3 +471,23 @@ impl<SJob: StatefulJob> DynJob for Job<SJob> {
|
|||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! extract_job_data {
|
||||
($state:ident) => {{
|
||||
$state
|
||||
.data
|
||||
.as_ref()
|
||||
.expect("critical error: missing data on job state")
|
||||
}};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! extract_job_data_mut {
|
||||
($state:ident) => {{
|
||||
$state
|
||||
.data
|
||||
.as_mut()
|
||||
.expect("critical error: missing data on job state")
|
||||
}};
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ use crate::{
|
|||
};
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fmt::{Debug, Formatter},
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
|
@ -100,27 +101,42 @@ impl Library {
|
|||
}
|
||||
|
||||
/// Returns the full path of a file
|
||||
pub async fn get_file_path(&self, id: i32) -> Result<Option<PathBuf>, LibraryManagerError> {
|
||||
Ok(self
|
||||
.db
|
||||
.file_path()
|
||||
.find_first(vec![
|
||||
file_path::location::is(vec![location::node_id::equals(Some(self.node_local_id))]),
|
||||
file_path::id::equals(id),
|
||||
])
|
||||
.select(file_path_to_full_path::select())
|
||||
.exec()
|
||||
.await?
|
||||
.map(|record| {
|
||||
record
|
||||
.location
|
||||
.path
|
||||
.as_ref()
|
||||
.map(|p| {
|
||||
Path::new(p).join(IsolatedFilePathData::from((record.location.id, &record)))
|
||||
})
|
||||
.ok_or_else(|| LibraryManagerError::NoPath(record.location.id))
|
||||
})
|
||||
.transpose()?)
|
||||
pub async fn get_file_paths(
|
||||
&self,
|
||||
ids: Vec<file_path::id::Type>,
|
||||
) -> Result<HashMap<file_path::id::Type, Option<PathBuf>>, LibraryManagerError> {
|
||||
let mut out = ids
|
||||
.iter()
|
||||
.copied()
|
||||
.map(|id| (id, None))
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
out.extend(
|
||||
self.db
|
||||
.file_path()
|
||||
.find_many(vec![
|
||||
file_path::location::is(vec![location::node_id::equals(Some(
|
||||
self.node_local_id,
|
||||
))]),
|
||||
file_path::id::in_vec(ids),
|
||||
])
|
||||
.select(file_path_to_full_path::select())
|
||||
.exec()
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|file_path| {
|
||||
(
|
||||
file_path.id,
|
||||
file_path.location.path.as_ref().map(|location_path| {
|
||||
Path::new(&location_path).join(IsolatedFilePathData::from((
|
||||
file_path.location.id,
|
||||
&file_path,
|
||||
)))
|
||||
}),
|
||||
)
|
||||
}),
|
||||
);
|
||||
|
||||
Ok(out)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use crate::util::error::FileIOError;
|
||||
use crate::{prisma::location, util::error::FileIOError};
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
|
@ -19,7 +19,7 @@ pub enum LocationError {
|
|||
#[error("location not found <uuid='{0}'>")]
|
||||
UuidNotFound(Uuid),
|
||||
#[error("location not found <id='{0}'>")]
|
||||
IdNotFound(i32),
|
||||
IdNotFound(location::id::Type),
|
||||
|
||||
// User errors
|
||||
#[error("location not a directory <path='{}'>", .0.display())]
|
||||
|
@ -49,25 +49,25 @@ pub enum LocationError {
|
|||
|
||||
// Internal Errors
|
||||
#[error(transparent)]
|
||||
LocationMetadataError(#[from] LocationMetadataError),
|
||||
LocationMetadata(#[from] LocationMetadataError),
|
||||
#[error("failed to read location path metadata info: {0}")]
|
||||
LocationPathFilesystemMetadataAccess(FileIOError),
|
||||
#[error("missing metadata file for location <path='{}'>", .0.display())]
|
||||
MissingMetadataFile(PathBuf),
|
||||
#[error("failed to open file from local OS: {0}")]
|
||||
FileReadError(FileIOError),
|
||||
FileRead(FileIOError),
|
||||
#[error("failed to read mounted volumes from local OS: {0}")]
|
||||
VolumeReadError(String),
|
||||
#[error("database error: {0}")]
|
||||
DatabaseError(#[from] prisma_client_rust::QueryError),
|
||||
Database(#[from] prisma_client_rust::QueryError),
|
||||
#[error(transparent)]
|
||||
LocationManagerError(#[from] LocationManagerError),
|
||||
LocationManager(#[from] LocationManagerError),
|
||||
#[error(transparent)]
|
||||
FilePathError(#[from] FilePathError),
|
||||
FilePath(#[from] FilePathError),
|
||||
#[error(transparent)]
|
||||
FileIO(#[from] FileIOError),
|
||||
#[error("missing-path")]
|
||||
MissingPath,
|
||||
#[error("location missing path <id='{0}'>")]
|
||||
MissingPath(location::id::Type),
|
||||
}
|
||||
|
||||
impl From<LocationError> for rspc::Error {
|
||||
|
|
|
@ -1,19 +1,30 @@
|
|||
use crate::{location::LocationId, prisma::file_path, util::error::NonUtf8PathError};
|
||||
use crate::{
|
||||
prisma::{file_path, location},
|
||||
util::error::NonUtf8PathError,
|
||||
};
|
||||
|
||||
use std::{borrow::Cow, fmt, path::Path};
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
fmt,
|
||||
path::{Path, MAIN_SEPARATOR},
|
||||
sync::OnceLock,
|
||||
};
|
||||
|
||||
use regex::RegexSet;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::{
|
||||
file_path_for_file_identifier, file_path_for_object_validator, file_path_for_thumbnailer,
|
||||
file_path_to_full_path, file_path_to_handle_custom_uri, file_path_to_isolate,
|
||||
file_path_with_object, FilePathError,
|
||||
file_path_to_isolate_with_id, file_path_with_object, FilePathError,
|
||||
};
|
||||
|
||||
static FORBIDDEN_FILE_NAMES: OnceLock<RegexSet> = OnceLock::new();
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Hash, Eq, PartialEq)]
|
||||
#[non_exhaustive]
|
||||
pub struct IsolatedFilePathData<'a> {
|
||||
pub(in crate::location) location_id: LocationId,
|
||||
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>,
|
||||
|
@ -23,7 +34,7 @@ pub struct IsolatedFilePathData<'a> {
|
|||
|
||||
impl IsolatedFilePathData<'static> {
|
||||
pub fn new(
|
||||
location_id: LocationId,
|
||||
location_id: location::id::Type,
|
||||
location_path: impl AsRef<Path>,
|
||||
full_path: impl AsRef<Path>,
|
||||
is_dir: bool,
|
||||
|
@ -67,10 +78,18 @@ impl IsolatedFilePathData<'static> {
|
|||
}
|
||||
|
||||
impl<'a> IsolatedFilePathData<'a> {
|
||||
pub fn location_id(&self) -> LocationId {
|
||||
pub fn location_id(&self) -> location::id::Type {
|
||||
self.location_id
|
||||
}
|
||||
|
||||
pub fn name(&'a self) -> &'a str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
pub fn extension(&'a self) -> &'a str {
|
||||
&self.extension
|
||||
}
|
||||
|
||||
pub fn parent(&'a self) -> Self {
|
||||
let (parent_path_str, name, relative_path) = if self.materialized_path == "/" {
|
||||
("/", "", "")
|
||||
|
@ -97,7 +116,10 @@ impl<'a> IsolatedFilePathData<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn from_relative_str(location_id: LocationId, relative_file_path_str: &'a str) -> Self {
|
||||
pub fn from_relative_str(
|
||||
location_id: location::id::Type,
|
||||
relative_file_path_str: &'a str,
|
||||
) -> Self {
|
||||
let is_dir = relative_file_path_str.ends_with('/');
|
||||
|
||||
let (materialized_path, maybe_name, maybe_extension) =
|
||||
|
@ -113,6 +135,14 @@ impl<'a> IsolatedFilePathData<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn full_name(&self) -> String {
|
||||
if self.extension.is_empty() {
|
||||
self.name.to_string()
|
||||
} else {
|
||||
format!("{}.{}", self.name, self.extension)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn materialized_path_for_children(&self) -> Option<String> {
|
||||
if self.materialized_path == "/" && self.name.is_empty() && self.is_dir {
|
||||
// We're at the root file_path
|
||||
|
@ -123,6 +153,53 @@ impl<'a> IsolatedFilePathData<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn separate_name_and_extension_from_str(
|
||||
source: &'a str,
|
||||
) -> Result<(&'a str, &'a str), FilePathError> {
|
||||
if source.contains(MAIN_SEPARATOR) {
|
||||
return Err(FilePathError::InvalidFilenameAndExtension(
|
||||
source.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(last_dot_idx) = source.rfind('.') {
|
||||
if last_dot_idx == 0 {
|
||||
// The dot is the first character, so it's a hidden file
|
||||
Ok((source, ""))
|
||||
} else {
|
||||
Ok((&source[..last_dot_idx], &source[last_dot_idx + 1..]))
|
||||
}
|
||||
} else {
|
||||
// It's a file without extension
|
||||
Ok((source, ""))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn accept_file_name(name: &str) -> bool {
|
||||
let reg = {
|
||||
// Maybe we should enforce windows more restrictive rules on all platforms?
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
FORBIDDEN_FILE_NAMES.get_or_init(|| {
|
||||
RegexSet::new([
|
||||
r"(?i)^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(\.\w+)*$",
|
||||
r#"[<>:"/\\|?*\u0000-\u0031]"#,
|
||||
])
|
||||
.expect("this regex should always be valid")
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
FORBIDDEN_FILE_NAMES.get_or_init(|| {
|
||||
RegexSet::new([r"/|\x00"]).expect("this regex should always be valid")
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
!reg.is_match(name)
|
||||
}
|
||||
|
||||
pub fn separate_path_name_and_extension_from_str(
|
||||
source: &'a str,
|
||||
is_dir: bool,
|
||||
|
@ -178,7 +255,7 @@ impl<'a> IsolatedFilePathData<'a> {
|
|||
}
|
||||
|
||||
fn from_db_data(
|
||||
location_id: LocationId,
|
||||
location_id: location::id::Type,
|
||||
db_materialized_path: &'a str,
|
||||
db_is_dir: bool,
|
||||
db_name: &'a str,
|
||||
|
@ -313,13 +390,13 @@ mod macros {
|
|||
macro_rules! impl_from_db_without_location_id {
|
||||
($($file_path_kind:ident),+ $(,)?) => {
|
||||
$(
|
||||
impl ::std::convert::From<($crate::location::LocationId, $file_path_kind::Data)> for $crate::
|
||||
impl ::std::convert::From<($crate::prisma::location::id::Type, $file_path_kind::Data)> for $crate::
|
||||
location::
|
||||
file_path_helper::
|
||||
isolated_file_path_data::
|
||||
IsolatedFilePathData<'static>
|
||||
{
|
||||
fn from((location_id, path): ($crate::location::LocationId, $file_path_kind::Data)) -> Self {
|
||||
fn from((location_id, path): ($crate::prisma::location::id::Type, $file_path_kind::Data)) -> Self {
|
||||
Self {
|
||||
location_id,
|
||||
relative_path: Cow::Owned(
|
||||
|
@ -342,13 +419,13 @@ mod macros {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a> ::std::convert::From<($crate::location::LocationId, &'a $file_path_kind::Data)> for $crate::
|
||||
impl<'a> ::std::convert::From<($crate::prisma::location::id::Type, &'a $file_path_kind::Data)> for $crate::
|
||||
location::
|
||||
file_path_helper::
|
||||
isolated_file_path_data::
|
||||
IsolatedFilePathData<'a>
|
||||
{
|
||||
fn from((location_id, path): ($crate::location::LocationId, &'a $file_path_kind::Data)) -> Self {
|
||||
fn from((location_id, path): ($crate::prisma::location::id::Type, &'a $file_path_kind::Data)) -> Self {
|
||||
Self::from_db_data(
|
||||
location_id,
|
||||
&path.materialized_path,
|
||||
|
@ -363,7 +440,12 @@ mod macros {
|
|||
}
|
||||
}
|
||||
|
||||
impl_from_db!(file_path, file_path_to_isolate, file_path_with_object);
|
||||
impl_from_db!(
|
||||
file_path,
|
||||
file_path_to_isolate,
|
||||
file_path_to_isolate_with_id,
|
||||
file_path_with_object
|
||||
);
|
||||
|
||||
impl_from_db_without_location_id!(
|
||||
file_path_for_file_identifier,
|
||||
|
@ -374,7 +456,7 @@ impl_from_db_without_location_id!(
|
|||
);
|
||||
|
||||
fn extract_relative_path(
|
||||
location_id: LocationId,
|
||||
location_id: location::id::Type,
|
||||
location_path: impl AsRef<Path>,
|
||||
path: impl AsRef<Path>,
|
||||
) -> Result<String, FilePathError> {
|
||||
|
@ -396,7 +478,7 @@ fn extract_relative_path(
|
|||
/// This function separates a file path from a location path, and normalizes replacing '\' with '/'
|
||||
/// to be consistent between Windows and Unix like systems
|
||||
pub fn extract_normalized_materialized_path_str(
|
||||
location_id: LocationId,
|
||||
location_id: location::id::Type,
|
||||
location_path: impl AsRef<Path>,
|
||||
path: impl AsRef<Path>,
|
||||
) -> Result<String, FilePathError> {
|
||||
|
@ -439,6 +521,7 @@ fn assemble_relative_path(
|
|||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::unwrap_used)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use crate::{
|
||||
prisma::{file_path, PrismaClient},
|
||||
prisma::{file_path, location, PrismaClient},
|
||||
util::error::{FileIOError, NonUtf8PathError},
|
||||
};
|
||||
|
||||
|
@ -20,8 +20,6 @@ pub mod isolated_file_path_data;
|
|||
|
||||
pub use isolated_file_path_data::IsolatedFilePathData;
|
||||
|
||||
use super::LocationId;
|
||||
|
||||
// File Path selectables!
|
||||
file_path::select!(file_path_just_pub_id { pub_id });
|
||||
file_path::select!(file_path_just_pub_id_materialized_path {
|
||||
|
@ -63,6 +61,14 @@ file_path::select!(file_path_to_isolate {
|
|||
name
|
||||
extension
|
||||
});
|
||||
file_path::select!(file_path_to_isolate_with_id {
|
||||
id
|
||||
location_id
|
||||
materialized_path
|
||||
is_dir
|
||||
name
|
||||
extension
|
||||
});
|
||||
file_path::select!(file_path_to_handle_custom_uri {
|
||||
materialized_path
|
||||
is_dir
|
||||
|
@ -74,6 +80,7 @@ file_path::select!(file_path_to_handle_custom_uri {
|
|||
}
|
||||
});
|
||||
file_path::select!(file_path_to_full_path {
|
||||
id
|
||||
materialized_path
|
||||
is_dir
|
||||
name
|
||||
|
@ -98,10 +105,12 @@ pub struct FilePathMetadata {
|
|||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum FilePathError {
|
||||
#[error("file path not found: <id='{0}'>")]
|
||||
IdNotFound(file_path::id::Type),
|
||||
#[error("file Path not found: <path='{}'>", .0.display())]
|
||||
NotFound(Box<Path>),
|
||||
#[error("location '{0}' not found")]
|
||||
LocationNotFound(i32),
|
||||
LocationNotFound(location::id::Type),
|
||||
#[error("received an invalid sub path: <location_path='{}', sub_path='{}'>", .location_path.display(), .sub_path.display())]
|
||||
InvalidSubPath {
|
||||
location_path: Box<Path>,
|
||||
|
@ -115,12 +124,12 @@ pub enum FilePathError {
|
|||
.sub_path.display()
|
||||
)]
|
||||
SubPathParentNotInLocation {
|
||||
location_id: LocationId,
|
||||
location_id: location::id::Type,
|
||||
sub_path: Box<Path>,
|
||||
},
|
||||
#[error("unable to extract materialized path from location: <id='{}', path='{}'>", .location_id, .path.display())]
|
||||
UnableToExtractMaterializedPath {
|
||||
location_id: LocationId,
|
||||
location_id: location::id::Type,
|
||||
path: Box<Path>,
|
||||
},
|
||||
#[error("database error: {0}")]
|
||||
|
@ -130,6 +139,8 @@ pub enum FilePathError {
|
|||
FileIO(#[from] FileIOError),
|
||||
#[error(transparent)]
|
||||
NonUtf8Path(#[from] NonUtf8PathError),
|
||||
#[error("received an invalid filename and extension: <filename_and_extension='{0}'>")]
|
||||
InvalidFilenameAndExtension(String),
|
||||
}
|
||||
|
||||
#[cfg(feature = "location-watcher")]
|
||||
|
@ -146,7 +157,7 @@ pub async fn create_file_path(
|
|||
cas_id: Option<String>,
|
||||
metadata: FilePathMetadata,
|
||||
) -> Result<file_path::Data, FilePathError> {
|
||||
use crate::{prisma::location, sync, util::db::uuid_to_bytes};
|
||||
use crate::{sync, util::db::uuid_to_bytes};
|
||||
|
||||
use serde_json::json;
|
||||
use uuid::Uuid;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use crate::{
|
||||
file_paths_db_fetcher_fn,
|
||||
extract_job_data_mut, file_paths_db_fetcher_fn,
|
||||
job::{JobError, JobInitData, JobResult, JobState, StatefulJob, WorkerContext},
|
||||
location::file_path_helper::{
|
||||
ensure_file_path_exists, ensure_sub_path_is_directory, ensure_sub_path_is_in_location,
|
||||
|
@ -180,10 +180,7 @@ impl StatefulJob for IndexerJob {
|
|||
mut ctx: WorkerContext,
|
||||
state: &mut JobState<Self>,
|
||||
) -> Result<(), JobError> {
|
||||
let data = state
|
||||
.data
|
||||
.as_mut()
|
||||
.expect("critical error: missing data on job state");
|
||||
let data = extract_job_data_mut!(state);
|
||||
|
||||
match &state.steps[0] {
|
||||
IndexerJobStepInput::Save(step) => {
|
||||
|
@ -288,6 +285,6 @@ impl StatefulJob for IndexerJob {
|
|||
return Err(JobError::MissingPath)
|
||||
};
|
||||
|
||||
finalize_indexer(&location_path, state, ctx)
|
||||
finalize_indexer(location_path, state, ctx)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
use crate::{
|
||||
invalidate_query,
|
||||
extract_job_data, invalidate_query,
|
||||
job::{JobReportUpdate, JobResult, JobState, StatefulJob, WorkerContext},
|
||||
library::Library,
|
||||
prisma::{file_path, PrismaClient},
|
||||
prisma::{file_path, location, PrismaClient},
|
||||
sync,
|
||||
util::{db::uuid_to_bytes, error::FileIOError},
|
||||
};
|
||||
|
@ -21,7 +21,7 @@ use tracing::info;
|
|||
|
||||
use super::{
|
||||
file_path_helper::{file_path_just_pub_id, FilePathError, IsolatedFilePathData},
|
||||
location_with_indexer_rules, LocationId,
|
||||
location_with_indexer_rules,
|
||||
};
|
||||
|
||||
pub mod indexer_job;
|
||||
|
@ -215,10 +215,7 @@ where
|
|||
Init: Serialize + DeserializeOwned + Send + Sync + Hash,
|
||||
Step: Serialize + DeserializeOwned + Send + Sync,
|
||||
{
|
||||
let data = state
|
||||
.data
|
||||
.as_ref()
|
||||
.expect("critical error: missing data on job state");
|
||||
let data = extract_job_data!(state);
|
||||
|
||||
info!(
|
||||
"scan of {} completed in {:?}. {} new files found, \
|
||||
|
@ -250,7 +247,7 @@ fn update_notifier_fn(batch_size: usize, ctx: &mut WorkerContext) -> impl FnMut(
|
|||
}
|
||||
|
||||
fn iso_file_path_factory(
|
||||
location_id: LocationId,
|
||||
location_id: location::id::Type,
|
||||
location_path: &Path,
|
||||
) -> impl Fn(&Path, bool) -> Result<IsolatedFilePathData<'static>, IndexerError> + '_ {
|
||||
move |path, is_dir| {
|
||||
|
|
|
@ -7,6 +7,12 @@ use crate::{
|
|||
},
|
||||
};
|
||||
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
marker::PhantomData,
|
||||
path::Path,
|
||||
};
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use futures::future::try_join_all;
|
||||
use globset::{Glob, GlobSet, GlobSetBuilder};
|
||||
|
@ -14,11 +20,6 @@ use rmp_serde::{self, decode, encode};
|
|||
use rspc::ErrorCode;
|
||||
use serde::{de, ser, Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
marker::PhantomData,
|
||||
path::Path,
|
||||
};
|
||||
use thiserror::Error;
|
||||
use tokio::fs;
|
||||
use tracing::debug;
|
||||
|
@ -453,17 +454,6 @@ pub struct IndexerRule {
|
|||
}
|
||||
|
||||
impl IndexerRule {
|
||||
pub fn new(name: String, default: bool, rules: Vec<RulePerKind>) -> Self {
|
||||
Self {
|
||||
id: None,
|
||||
name,
|
||||
default,
|
||||
rules,
|
||||
date_created: Utc::now(),
|
||||
date_modified: Utc::now(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn apply(
|
||||
&self,
|
||||
source: impl AsRef<Path>,
|
||||
|
@ -777,7 +767,7 @@ mod seeder {
|
|||
]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
).unwrap(),
|
||||
).expect("this is hardcoded and should always work"),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
@ -786,7 +776,8 @@ mod seeder {
|
|||
SystemIndexerRule {
|
||||
name: "No Hidden",
|
||||
default: true,
|
||||
rules: vec![RulePerKind::new_reject_files_by_globs_str(["**/.*"]).unwrap()],
|
||||
rules: vec![RulePerKind::new_reject_files_by_globs_str(["**/.*"])
|
||||
.expect("this is hardcoded and should always work")],
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -807,7 +798,7 @@ mod seeder {
|
|||
rules: vec![RulePerKind::new_accept_files_by_globs_str([
|
||||
"*.{avif,bmp,gif,ico,jpeg,jpg,png,svg,tif,tiff,webp}",
|
||||
])
|
||||
.unwrap()],
|
||||
.expect("this is hardcoded and should always work")],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -815,11 +806,25 @@ mod seeder {
|
|||
pub use seeder::*;
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::unwrap_used)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use tempfile::tempdir;
|
||||
use tokio::fs;
|
||||
|
||||
impl IndexerRule {
|
||||
pub fn new(name: String, default: bool, rules: Vec<RulePerKind>) -> Self {
|
||||
Self {
|
||||
id: None,
|
||||
name,
|
||||
default,
|
||||
rules,
|
||||
date_created: Utc::now(),
|
||||
date_modified: Utc::now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn check_rule(indexer_rule: &IndexerRule, path: impl AsRef<Path>) -> bool {
|
||||
indexer_rule
|
||||
.apply(path)
|
||||
|
|
|
@ -2,9 +2,12 @@ use crate::{
|
|||
file_paths_db_fetcher_fn, invalidate_query,
|
||||
job::JobError,
|
||||
library::Library,
|
||||
location::file_path_helper::{
|
||||
check_file_path_exists, ensure_sub_path_is_directory, ensure_sub_path_is_in_location,
|
||||
IsolatedFilePathData,
|
||||
location::{
|
||||
file_path_helper::{
|
||||
check_file_path_exists, ensure_sub_path_is_directory, ensure_sub_path_is_in_location,
|
||||
IsolatedFilePathData,
|
||||
},
|
||||
LocationError,
|
||||
},
|
||||
to_remove_db_fetcher_fn,
|
||||
};
|
||||
|
@ -30,7 +33,7 @@ pub async fn shallow(
|
|||
) -> Result<(), JobError> {
|
||||
let location_id = location.id;
|
||||
let Some(location_path) = location.path.as_ref().map(PathBuf::from) else {
|
||||
panic!();
|
||||
return Err(JobError::Location(LocationError::MissingPath(location_id)));
|
||||
};
|
||||
|
||||
let db = library.db.clone();
|
||||
|
@ -100,10 +103,10 @@ pub async fn shallow(
|
|||
.collect::<Vec<_>>();
|
||||
|
||||
for step in steps {
|
||||
execute_indexer_save_step(&location, &step, &library).await?;
|
||||
execute_indexer_save_step(location, &step, library).await?;
|
||||
}
|
||||
|
||||
invalidate_query!(&library, "search.paths");
|
||||
invalidate_query!(library, "search.paths");
|
||||
|
||||
library.orphan_remover.invoke().await;
|
||||
|
||||
|
|
|
@ -608,6 +608,7 @@ where
|
|||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::unwrap_used, clippy::panic)]
|
||||
mod tests {
|
||||
use super::super::rules::RulePerKind;
|
||||
use super::*;
|
||||
|
|
|
@ -10,10 +10,10 @@ use tokio::{fs, io::ErrorKind, sync::oneshot, time::sleep};
|
|||
use tracing::{error, warn};
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::{watcher::LocationWatcher, LocationId, LocationManagerError};
|
||||
use super::{watcher::LocationWatcher, LocationManagerError};
|
||||
|
||||
type LibraryId = Uuid;
|
||||
type LocationAndLibraryKey = (LocationId, LibraryId);
|
||||
type LocationAndLibraryKey = (location::id::Type, LibraryId);
|
||||
|
||||
const LOCATION_CHECK_INTERVAL: Duration = Duration::from_secs(5);
|
||||
|
||||
|
@ -25,7 +25,7 @@ pub(super) async fn check_online(
|
|||
|
||||
let location_path = location.path.as_ref();
|
||||
let Some(location_path) = location_path.map(Path::new) else {
|
||||
return Err(LocationManagerError::MissingPath)
|
||||
return Err(LocationManagerError::MissingPath(location.id))
|
||||
};
|
||||
|
||||
if location.node_id == Some(library.node_local_id) {
|
||||
|
@ -51,9 +51,9 @@ pub(super) async fn check_online(
|
|||
}
|
||||
|
||||
pub(super) async fn location_check_sleep(
|
||||
location_id: LocationId,
|
||||
location_id: location::id::Type,
|
||||
library: Library,
|
||||
) -> (LocationId, Library) {
|
||||
) -> (location::id::Type, Library) {
|
||||
sleep(LOCATION_CHECK_INTERVAL).await;
|
||||
(location_id, library)
|
||||
}
|
||||
|
@ -101,7 +101,7 @@ pub(super) fn unwatch_location(
|
|||
}
|
||||
|
||||
pub(super) fn drop_location(
|
||||
location_id: LocationId,
|
||||
location_id: location::id::Type,
|
||||
library_id: LibraryId,
|
||||
message: &str,
|
||||
locations_watched: &mut HashMap<LocationAndLibraryKey, LocationWatcher>,
|
||||
|
@ -115,7 +115,10 @@ pub(super) fn drop_location(
|
|||
}
|
||||
}
|
||||
|
||||
pub(super) async fn get_location(location_id: i32, library: &Library) -> Option<location::Data> {
|
||||
pub(super) async fn get_location(
|
||||
location_id: location::id::Type,
|
||||
library: &Library,
|
||||
) -> Option<location::Data> {
|
||||
library
|
||||
.db
|
||||
.location()
|
||||
|
@ -129,7 +132,7 @@ pub(super) async fn get_location(location_id: i32, library: &Library) -> Option<
|
|||
}
|
||||
|
||||
pub(super) async fn handle_remove_location_request(
|
||||
location_id: LocationId,
|
||||
location_id: location::id::Type,
|
||||
library: Library,
|
||||
response_tx: oneshot::Sender<Result<(), LocationManagerError>>,
|
||||
forced_unwatch: &mut HashSet<LocationAndLibraryKey>,
|
||||
|
@ -169,7 +172,7 @@ pub(super) async fn handle_remove_location_request(
|
|||
}
|
||||
|
||||
pub(super) async fn handle_stop_watcher_request(
|
||||
location_id: LocationId,
|
||||
location_id: location::id::Type,
|
||||
library: Library,
|
||||
response_tx: oneshot::Sender<Result<(), LocationManagerError>>,
|
||||
forced_unwatch: &mut HashSet<LocationAndLibraryKey>,
|
||||
|
@ -177,7 +180,7 @@ pub(super) async fn handle_stop_watcher_request(
|
|||
locations_unwatched: &mut HashMap<LocationAndLibraryKey, LocationWatcher>,
|
||||
) {
|
||||
async fn inner(
|
||||
location_id: LocationId,
|
||||
location_id: location::id::Type,
|
||||
library: Library,
|
||||
forced_unwatch: &mut HashSet<LocationAndLibraryKey>,
|
||||
locations_watched: &mut HashMap<LocationAndLibraryKey, LocationWatcher>,
|
||||
|
@ -212,7 +215,7 @@ pub(super) async fn handle_stop_watcher_request(
|
|||
}
|
||||
|
||||
pub(super) async fn handle_reinit_watcher_request(
|
||||
location_id: LocationId,
|
||||
location_id: location::id::Type,
|
||||
library: Library,
|
||||
response_tx: oneshot::Sender<Result<(), LocationManagerError>>,
|
||||
forced_unwatch: &mut HashSet<LocationAndLibraryKey>,
|
||||
|
@ -220,7 +223,7 @@ pub(super) async fn handle_reinit_watcher_request(
|
|||
locations_unwatched: &mut HashMap<LocationAndLibraryKey, LocationWatcher>,
|
||||
) {
|
||||
async fn inner(
|
||||
location_id: LocationId,
|
||||
location_id: location::id::Type,
|
||||
library: Library,
|
||||
forced_unwatch: &mut HashSet<LocationAndLibraryKey>,
|
||||
locations_watched: &mut HashMap<LocationAndLibraryKey, LocationWatcher>,
|
||||
|
@ -255,7 +258,7 @@ pub(super) async fn handle_reinit_watcher_request(
|
|||
}
|
||||
|
||||
pub(super) fn handle_ignore_path_request(
|
||||
location_id: LocationId,
|
||||
location_id: location::id::Type,
|
||||
library: Library,
|
||||
path: PathBuf,
|
||||
ignore: bool,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use crate::{job::JobManagerError, library::Library, util::error::FileIOError};
|
||||
use crate::{job::JobManagerError, library::Library, prisma::location, util::error::FileIOError};
|
||||
|
||||
use std::{
|
||||
collections::BTreeSet,
|
||||
|
@ -18,7 +18,7 @@ use tracing::{debug, error};
|
|||
use tokio::sync::mpsc;
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::{file_path_helper::FilePathError, LocationId};
|
||||
use super::file_path_helper::FilePathError;
|
||||
|
||||
#[cfg(feature = "location-watcher")]
|
||||
mod watcher;
|
||||
|
@ -36,7 +36,7 @@ enum ManagementMessageAction {
|
|||
#[derive(Debug)]
|
||||
#[allow(dead_code)]
|
||||
pub struct LocationManagementMessage {
|
||||
location_id: LocationId,
|
||||
location_id: location::id::Type,
|
||||
library: Library,
|
||||
action: ManagementMessageAction,
|
||||
response_tx: oneshot::Sender<Result<(), LocationManagerError>>,
|
||||
|
@ -53,7 +53,7 @@ enum WatcherManagementMessageAction {
|
|||
#[derive(Debug)]
|
||||
#[allow(dead_code)]
|
||||
pub struct WatcherManagementMessage {
|
||||
location_id: LocationId,
|
||||
location_id: location::id::Type,
|
||||
library: Library,
|
||||
action: WatcherManagementMessageAction,
|
||||
response_tx: oneshot::Sender<Result<(), LocationManagerError>>,
|
||||
|
@ -84,10 +84,10 @@ pub enum LocationManagerError {
|
|||
FailedToStopOrReinitWatcher { reason: String },
|
||||
|
||||
#[error("Missing location from database: <id='{0}'>")]
|
||||
MissingLocation(LocationId),
|
||||
MissingLocation(location::id::Type),
|
||||
|
||||
#[error("Non local location: <id='{0}'>")]
|
||||
NonLocalLocation(LocationId),
|
||||
NonLocalLocation(location::id::Type),
|
||||
|
||||
#[error("failed to move file '{}' for reason: {reason}", .path.display())]
|
||||
MoveError { path: Box<Path>, reason: String },
|
||||
|
@ -102,8 +102,8 @@ pub enum LocationManagerError {
|
|||
CorruptedLocationPubId(#[from] uuid::Error),
|
||||
#[error("Job Manager error: (error: {0})")]
|
||||
JobManager(#[from] JobManagerError),
|
||||
#[error("missing-location-path")]
|
||||
MissingPath,
|
||||
#[error("location missing location path: <id='{0}'>")]
|
||||
MissingPath(location::id::Type),
|
||||
|
||||
#[error("invalid inode")]
|
||||
InvalidInode,
|
||||
|
@ -170,7 +170,7 @@ impl LocationManager {
|
|||
#[allow(unused_variables)]
|
||||
async fn location_management_message(
|
||||
&self,
|
||||
location_id: LocationId,
|
||||
location_id: location::id::Type,
|
||||
library: Library,
|
||||
action: ManagementMessageAction,
|
||||
) -> Result<(), LocationManagerError> {
|
||||
|
@ -198,7 +198,7 @@ impl LocationManager {
|
|||
#[allow(unused_variables)]
|
||||
async fn watcher_management_message(
|
||||
&self,
|
||||
location_id: LocationId,
|
||||
location_id: location::id::Type,
|
||||
library: Library,
|
||||
action: WatcherManagementMessageAction,
|
||||
) -> Result<(), LocationManagerError> {
|
||||
|
@ -224,7 +224,7 @@ impl LocationManager {
|
|||
|
||||
pub async fn add(
|
||||
&self,
|
||||
location_id: LocationId,
|
||||
location_id: location::id::Type,
|
||||
library: Library,
|
||||
) -> Result<(), LocationManagerError> {
|
||||
self.location_management_message(location_id, library, ManagementMessageAction::Add)
|
||||
|
@ -233,7 +233,7 @@ impl LocationManager {
|
|||
|
||||
pub async fn remove(
|
||||
&self,
|
||||
location_id: LocationId,
|
||||
location_id: location::id::Type,
|
||||
library: Library,
|
||||
) -> Result<(), LocationManagerError> {
|
||||
self.location_management_message(location_id, library, ManagementMessageAction::Remove)
|
||||
|
@ -242,7 +242,7 @@ impl LocationManager {
|
|||
|
||||
pub async fn stop_watcher(
|
||||
&self,
|
||||
location_id: LocationId,
|
||||
location_id: location::id::Type,
|
||||
library: Library,
|
||||
) -> Result<(), LocationManagerError> {
|
||||
self.watcher_management_message(location_id, library, WatcherManagementMessageAction::Stop)
|
||||
|
@ -251,7 +251,7 @@ impl LocationManager {
|
|||
|
||||
pub async fn reinit_watcher(
|
||||
&self,
|
||||
location_id: LocationId,
|
||||
location_id: location::id::Type,
|
||||
library: Library,
|
||||
) -> Result<(), LocationManagerError> {
|
||||
self.watcher_management_message(
|
||||
|
@ -264,7 +264,7 @@ impl LocationManager {
|
|||
|
||||
pub async fn temporary_stop(
|
||||
&self,
|
||||
location_id: LocationId,
|
||||
location_id: location::id::Type,
|
||||
library: Library,
|
||||
) -> Result<StopWatcherGuard, LocationManagerError> {
|
||||
self.stop_watcher(location_id, library.clone()).await?;
|
||||
|
@ -278,7 +278,7 @@ impl LocationManager {
|
|||
|
||||
pub async fn temporary_ignore_events_for_path(
|
||||
&self,
|
||||
location_id: LocationId,
|
||||
location_id: location::id::Type,
|
||||
library: Library,
|
||||
path: impl AsRef<Path>,
|
||||
) -> Result<IgnoreEventsForPathGuard, LocationManagerError> {
|
||||
|
@ -553,7 +553,7 @@ impl Drop for LocationManager {
|
|||
#[must_use = "this `StopWatcherGuard` must be held for some time, so the watcher is stopped"]
|
||||
pub struct StopWatcherGuard<'m> {
|
||||
manager: &'m LocationManager,
|
||||
location_id: LocationId,
|
||||
location_id: location::id::Type,
|
||||
library: Option<Library>,
|
||||
}
|
||||
|
||||
|
@ -575,7 +575,7 @@ impl Drop for StopWatcherGuard<'_> {
|
|||
pub struct IgnoreEventsForPathGuard<'m> {
|
||||
manager: &'m LocationManager,
|
||||
path: Option<PathBuf>,
|
||||
location_id: LocationId,
|
||||
location_id: location::id::Type,
|
||||
library: Option<Library>,
|
||||
}
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
//! a Create Dir event, this one is actually ok at least.
|
||||
|
||||
use crate::{
|
||||
invalidate_query, library::Library, location::manager::LocationManagerError,
|
||||
invalidate_query, library::Library, location::manager::LocationManagerError, prisma::location,
|
||||
util::error::FileIOError,
|
||||
};
|
||||
|
||||
|
@ -26,12 +26,12 @@ use tracing::{error, trace};
|
|||
|
||||
use super::{
|
||||
utils::{create_dir, file_creation_or_update, remove, rename},
|
||||
EventHandler, LocationId, HUNDRED_MILLIS,
|
||||
EventHandler, HUNDRED_MILLIS,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(super) struct LinuxEventHandler<'lib> {
|
||||
location_id: LocationId,
|
||||
location_id: location::id::Type,
|
||||
library: &'lib Library,
|
||||
last_check_rename: Instant,
|
||||
rename_from: HashMap<PathBuf, Instant>,
|
||||
|
@ -41,7 +41,7 @@ pub(super) struct LinuxEventHandler<'lib> {
|
|||
|
||||
#[async_trait]
|
||||
impl<'lib> EventHandler<'lib> for LinuxEventHandler<'lib> {
|
||||
fn new(location_id: LocationId, library: &'lib Library) -> Self {
|
||||
fn new(location_id: location::id::Type, library: &'lib Library) -> Self {
|
||||
Self {
|
||||
location_id,
|
||||
library,
|
||||
|
|
|
@ -15,8 +15,8 @@ use crate::{
|
|||
location::{
|
||||
file_path_helper::{check_existing_file_path, get_inode_and_device, IsolatedFilePathData},
|
||||
manager::LocationManagerError,
|
||||
LocationId,
|
||||
},
|
||||
prisma::location,
|
||||
util::error::FileIOError,
|
||||
};
|
||||
|
||||
|
@ -43,7 +43,7 @@ use super::{
|
|||
|
||||
#[derive(Debug)]
|
||||
pub(super) struct MacOsEventHandler<'lib> {
|
||||
location_id: LocationId,
|
||||
location_id: location::id::Type,
|
||||
library: &'lib Library,
|
||||
recently_created_files: BTreeMap<PathBuf, Instant>,
|
||||
last_check_created_files: Instant,
|
||||
|
@ -56,7 +56,7 @@ pub(super) struct MacOsEventHandler<'lib> {
|
|||
|
||||
#[async_trait]
|
||||
impl<'lib> EventHandler<'lib> for MacOsEventHandler<'lib> {
|
||||
fn new(location_id: LocationId, library: &'lib Library) -> Self
|
||||
fn new(location_id: location::id::Type, library: &'lib Library) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use crate::{library::Library, location::LocationId, prisma::location};
|
||||
use crate::{library::Library, prisma::location};
|
||||
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
|
@ -47,7 +47,7 @@ const HUNDRED_MILLIS: Duration = Duration::from_millis(100);
|
|||
|
||||
#[async_trait]
|
||||
trait EventHandler<'lib> {
|
||||
fn new(location_id: LocationId, library: &'lib Library) -> Self
|
||||
fn new(location_id: location::id::Type, library: &'lib Library) -> Self
|
||||
where
|
||||
Self: Sized;
|
||||
|
||||
|
@ -107,7 +107,7 @@ impl LocationWatcher {
|
|||
));
|
||||
|
||||
let Some(path) = location.path else {
|
||||
return Err(LocationManagerError::MissingPath)
|
||||
return Err(LocationManagerError::MissingPath(location.id))
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
|
@ -121,7 +121,7 @@ impl LocationWatcher {
|
|||
}
|
||||
|
||||
async fn handle_watch_events(
|
||||
location_id: LocationId,
|
||||
location_id: location::id::Type,
|
||||
location_pub_id: Uuid,
|
||||
library: Library,
|
||||
mut events_rx: mpsc::UnboundedReceiver<notify::Result<Event>>,
|
||||
|
@ -181,7 +181,7 @@ impl LocationWatcher {
|
|||
}
|
||||
|
||||
async fn handle_single_event<'lib>(
|
||||
location_id: LocationId,
|
||||
location_id: location::id::Type,
|
||||
location_pub_id: Uuid,
|
||||
event: Event,
|
||||
event_handler: &mut impl EventHandler<'lib>,
|
||||
|
@ -261,9 +261,7 @@ impl Drop for LocationWatcher {
|
|||
|
||||
// FIXME: change this Drop to async drop in the future
|
||||
if let Some(handle) = self.handle.take() {
|
||||
if let Err(e) =
|
||||
block_in_place(move || Handle::current().block_on(async move { handle.await }))
|
||||
{
|
||||
if let Err(e) = block_in_place(move || Handle::current().block_on(handle)) {
|
||||
error!("Failed to join watcher task: {e:#?}")
|
||||
}
|
||||
}
|
||||
|
@ -344,6 +342,7 @@ impl Drop for LocationWatcher {
|
|||
* *
|
||||
***************************************************************************************************/
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::unwrap_used, clippy::panic)]
|
||||
mod tests {
|
||||
use std::{
|
||||
io::ErrorKind,
|
||||
|
@ -358,7 +357,7 @@ mod tests {
|
|||
use tempfile::{tempdir, TempDir};
|
||||
use tokio::{fs, io::AsyncWriteExt, sync::mpsc, time::sleep};
|
||||
use tracing::{debug, error};
|
||||
use tracing_test::traced_test;
|
||||
// use tracing_test::traced_test;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
use notify::event::DataChange;
|
||||
|
@ -424,7 +423,7 @@ mod tests {
|
|||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[traced_test]
|
||||
// #[traced_test]
|
||||
async fn create_file_event() {
|
||||
let (root_dir, mut watcher, events_rx) = setup_watcher().await;
|
||||
|
||||
|
@ -462,7 +461,7 @@ mod tests {
|
|||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[traced_test]
|
||||
// #[traced_test]
|
||||
async fn create_dir_event() {
|
||||
let (root_dir, mut watcher, events_rx) = setup_watcher().await;
|
||||
|
||||
|
@ -492,7 +491,7 @@ mod tests {
|
|||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[traced_test]
|
||||
// #[traced_test]
|
||||
async fn update_file_event() {
|
||||
let (root_dir, mut watcher, events_rx) = setup_watcher().await;
|
||||
|
||||
|
@ -543,7 +542,7 @@ mod tests {
|
|||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[traced_test]
|
||||
// #[traced_test]
|
||||
async fn update_file_rename_event() {
|
||||
let (root_dir, mut watcher, events_rx) = setup_watcher().await;
|
||||
|
||||
|
@ -592,7 +591,7 @@ mod tests {
|
|||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[traced_test]
|
||||
// #[traced_test]
|
||||
async fn update_dir_event() {
|
||||
let (root_dir, mut watcher, events_rx) = setup_watcher().await;
|
||||
|
||||
|
@ -643,7 +642,7 @@ mod tests {
|
|||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[traced_test]
|
||||
// #[traced_test]
|
||||
async fn delete_file_event() {
|
||||
let (root_dir, mut watcher, events_rx) = setup_watcher().await;
|
||||
|
||||
|
@ -675,7 +674,7 @@ mod tests {
|
|||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[traced_test]
|
||||
// #[traced_test]
|
||||
async fn delete_dir_event() {
|
||||
let (root_dir, mut watcher, events_rx) = setup_watcher().await;
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ use crate::{
|
|||
},
|
||||
find_location, location_with_indexer_rules,
|
||||
manager::LocationManagerError,
|
||||
scan_location_sub_path, LocationId,
|
||||
scan_location_sub_path,
|
||||
},
|
||||
object::{
|
||||
file_identifier::FileMetadata,
|
||||
|
@ -62,7 +62,7 @@ pub(super) fn check_event(event: &Event, ignore_paths: &HashSet<PathBuf>) -> boo
|
|||
}
|
||||
|
||||
pub(super) async fn create_dir(
|
||||
location_id: LocationId,
|
||||
location_id: location::id::Type,
|
||||
path: impl AsRef<Path>,
|
||||
metadata: &Metadata,
|
||||
library: &Library,
|
||||
|
@ -76,7 +76,7 @@ pub(super) async fn create_dir(
|
|||
let path = path.as_ref();
|
||||
|
||||
let Some(location_path) = &location.path else {
|
||||
return Err(LocationManagerError::MissingPath)
|
||||
return Err(LocationManagerError::MissingPath(location_id))
|
||||
};
|
||||
|
||||
trace!(
|
||||
|
@ -85,7 +85,7 @@ pub(super) async fn create_dir(
|
|||
path.display()
|
||||
);
|
||||
|
||||
let materialized_path = IsolatedFilePathData::new(location.id, &location_path, path, true)?;
|
||||
let materialized_path = IsolatedFilePathData::new(location.id, location_path, path, true)?;
|
||||
|
||||
let (inode, device) = {
|
||||
#[cfg(target_family = "unix")]
|
||||
|
@ -135,7 +135,7 @@ pub(super) async fn create_dir(
|
|||
}
|
||||
|
||||
pub(super) async fn create_file(
|
||||
location_id: LocationId,
|
||||
location_id: location::id::Type,
|
||||
path: impl AsRef<Path>,
|
||||
metadata: &Metadata,
|
||||
library: &Library,
|
||||
|
@ -248,7 +248,7 @@ pub(super) async fn create_file(
|
|||
}
|
||||
|
||||
pub(super) async fn create_dir_or_file(
|
||||
location_id: LocationId,
|
||||
location_id: location::id::Type,
|
||||
path: impl AsRef<Path>,
|
||||
library: &Library,
|
||||
) -> Result<Metadata, LocationManagerError> {
|
||||
|
@ -266,7 +266,7 @@ pub(super) async fn create_dir_or_file(
|
|||
}
|
||||
|
||||
pub(super) async fn file_creation_or_update(
|
||||
location_id: LocationId,
|
||||
location_id: location::id::Type,
|
||||
full_path: impl AsRef<Path>,
|
||||
library: &Library,
|
||||
) -> Result<(), LocationManagerError> {
|
||||
|
@ -299,7 +299,7 @@ pub(super) async fn file_creation_or_update(
|
|||
}
|
||||
|
||||
pub(super) async fn update_file(
|
||||
location_id: LocationId,
|
||||
location_id: location::id::Type,
|
||||
full_path: impl AsRef<Path>,
|
||||
library: &Library,
|
||||
) -> Result<(), LocationManagerError> {
|
||||
|
@ -329,7 +329,7 @@ pub(super) async fn update_file(
|
|||
}
|
||||
|
||||
async fn inner_update_file(
|
||||
location_id: LocationId,
|
||||
location_id: location::id::Type,
|
||||
file_path: &file_path_with_object::Data,
|
||||
full_path: impl AsRef<Path>,
|
||||
library @ Library { db, sync, .. }: &Library,
|
||||
|
@ -343,7 +343,7 @@ async fn inner_update_file(
|
|||
.ok_or_else(|| LocationManagerError::MissingLocation(location_id))?;
|
||||
|
||||
let Some(location_path) = location.path.map(PathBuf::from) else {
|
||||
return Err(LocationManagerError::MissingPath)
|
||||
return Err(LocationManagerError::MissingPath(location_id))
|
||||
};
|
||||
|
||||
trace!(
|
||||
|
@ -469,7 +469,7 @@ async fn inner_update_file(
|
|||
}
|
||||
|
||||
pub(super) async fn rename(
|
||||
location_id: LocationId,
|
||||
location_id: location::id::Type,
|
||||
new_path: impl AsRef<Path>,
|
||||
old_path: impl AsRef<Path>,
|
||||
library: &Library,
|
||||
|
@ -552,7 +552,7 @@ pub(super) async fn rename(
|
|||
}
|
||||
|
||||
pub(super) async fn remove(
|
||||
location_id: LocationId,
|
||||
location_id: location::id::Type,
|
||||
full_path: impl AsRef<Path>,
|
||||
library: &Library,
|
||||
) -> Result<(), LocationManagerError> {
|
||||
|
@ -574,7 +574,7 @@ pub(super) async fn remove(
|
|||
}
|
||||
|
||||
pub(super) async fn remove_by_file_path(
|
||||
location_id: LocationId,
|
||||
location_id: location::id::Type,
|
||||
path: impl AsRef<Path>,
|
||||
file_path: &file_path::Data,
|
||||
library: &Library,
|
||||
|
@ -671,7 +671,7 @@ async fn generate_thumbnail(
|
|||
}
|
||||
|
||||
pub(super) async fn extract_inode_and_device_from_path(
|
||||
location_id: LocationId,
|
||||
location_id: location::id::Type,
|
||||
path: impl AsRef<Path>,
|
||||
library: &Library,
|
||||
) -> Result<INodeAndDevice, LocationManagerError> {
|
||||
|
@ -683,7 +683,7 @@ pub(super) async fn extract_inode_and_device_from_path(
|
|||
.ok_or(LocationManagerError::MissingLocation(location_id))?;
|
||||
|
||||
let Some(location_path) = &location.path else {
|
||||
return Err(LocationManagerError::MissingPath)
|
||||
return Err(LocationManagerError::MissingPath(location_id))
|
||||
};
|
||||
|
||||
library
|
||||
|
@ -715,7 +715,7 @@ pub(super) async fn extract_inode_and_device_from_path(
|
|||
}
|
||||
|
||||
pub(super) async fn extract_location_path(
|
||||
location_id: LocationId,
|
||||
location_id: location::id::Type,
|
||||
library: &Library,
|
||||
) -> Result<PathBuf, LocationManagerError> {
|
||||
find_location(library, location_id)
|
||||
|
@ -729,7 +729,7 @@ pub(super) async fn extract_location_path(
|
|||
location
|
||||
.path
|
||||
.map(PathBuf::from)
|
||||
.ok_or(LocationManagerError::MissingPath)
|
||||
.ok_or(LocationManagerError::MissingPath(location_id))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
@ -10,9 +10,8 @@
|
|||
use crate::{
|
||||
invalidate_query,
|
||||
library::Library,
|
||||
location::{
|
||||
file_path_helper::get_inode_and_device_from_path, manager::LocationManagerError, LocationId,
|
||||
},
|
||||
location::{file_path_helper::get_inode_and_device_from_path, manager::LocationManagerError},
|
||||
prisma::location,
|
||||
util::error::FileIOError,
|
||||
};
|
||||
|
||||
|
@ -37,7 +36,7 @@ use super::{
|
|||
/// Windows file system event handler
|
||||
#[derive(Debug)]
|
||||
pub(super) struct WindowsEventHandler<'lib> {
|
||||
location_id: LocationId,
|
||||
location_id: location::id::Type,
|
||||
library: &'lib Library,
|
||||
last_check_recently_files: Instant,
|
||||
recently_created_files: BTreeMap<PathBuf, Instant>,
|
||||
|
@ -50,7 +49,7 @@ pub(super) struct WindowsEventHandler<'lib> {
|
|||
|
||||
#[async_trait]
|
||||
impl<'lib> EventHandler<'lib> for WindowsEventHandler<'lib> {
|
||||
fn new(location_id: LocationId, library: &'lib Library) -> Self
|
||||
fn new(location_id: location::id::Type, library: &'lib Library) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use crate::{
|
||||
invalidate_query,
|
||||
job::{Job, JobManagerError},
|
||||
job::{Job, JobError, JobManagerError},
|
||||
library::Library,
|
||||
object::{
|
||||
file_identifier::{self, file_identifier_job::FileIdentifierJobInit},
|
||||
|
@ -37,8 +37,6 @@ use indexer::IndexerJobInit;
|
|||
pub use manager::{LocationManager, LocationManagerError};
|
||||
use metadata::SpacedriveLocationMetadataFile;
|
||||
|
||||
pub type LocationId = i32;
|
||||
|
||||
// Location includes!
|
||||
location::include!(location_with_indexer_rules {
|
||||
indexer_rules: select { indexer_rule }
|
||||
|
@ -211,7 +209,7 @@ impl LocationCreateArgs {
|
|||
/// Old rules that aren't in this vector will be purged.
|
||||
#[derive(Type, Deserialize)]
|
||||
pub struct LocationUpdateArgs {
|
||||
pub id: i32,
|
||||
pub id: location::id::Type,
|
||||
pub name: Option<String>,
|
||||
pub generate_preview_media: Option<bool>,
|
||||
pub sync_preview_media: Option<bool>,
|
||||
|
@ -336,7 +334,10 @@ impl LocationUpdateArgs {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn find_location(library: &Library, location_id: i32) -> location::FindUniqueQuery {
|
||||
pub fn find_location(
|
||||
library: &Library,
|
||||
location_id: location::id::Type,
|
||||
) -> location::FindUniqueQuery {
|
||||
library
|
||||
.db
|
||||
.location()
|
||||
|
@ -345,7 +346,7 @@ pub fn find_location(library: &Library, location_id: i32) -> location::FindUniqu
|
|||
|
||||
async fn link_location_and_indexer_rules(
|
||||
library: &Library,
|
||||
location_id: i32,
|
||||
location_id: location::id::Type,
|
||||
rules_ids: &[i32],
|
||||
) -> Result<(), LocationError> {
|
||||
library
|
||||
|
@ -432,7 +433,7 @@ pub async fn light_scan_location(
|
|||
library: Library,
|
||||
location: location_with_indexer_rules::Data,
|
||||
sub_path: impl AsRef<Path>,
|
||||
) -> Result<(), JobManagerError> {
|
||||
) -> Result<(), JobError> {
|
||||
let sub_path = sub_path.as_ref().to_path_buf();
|
||||
|
||||
if location.node_id != Some(library.node_local_id) {
|
||||
|
@ -620,7 +621,10 @@ async fn create_location(
|
|||
}))
|
||||
}
|
||||
|
||||
pub async fn delete_location(library: &Library, location_id: i32) -> Result<(), LocationError> {
|
||||
pub async fn delete_location(
|
||||
library: &Library,
|
||||
location_id: location::id::Type,
|
||||
) -> Result<(), LocationError> {
|
||||
let Library { db, .. } = library;
|
||||
|
||||
library
|
||||
|
@ -663,7 +667,7 @@ pub async fn delete_location(library: &Library, location_id: i32) -> Result<(),
|
|||
/// this function is used to delete a location and when ingesting directory deletion events
|
||||
pub async fn delete_directory(
|
||||
library: &Library,
|
||||
location_id: i32,
|
||||
location_id: location::id::Type,
|
||||
parent_materialized_path: Option<String>,
|
||||
) -> Result<(), QueryError> {
|
||||
let Library { db, .. } = library;
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use crate::{
|
||||
extract_job_data, extract_job_data_mut,
|
||||
job::{
|
||||
JobError, JobInitData, JobReportUpdate, JobResult, JobState, StatefulJob, WorkerContext,
|
||||
},
|
||||
|
@ -47,7 +48,7 @@ impl Hash for FileIdentifierJobInit {
|
|||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct FileIdentifierJobState {
|
||||
cursor: i32,
|
||||
cursor: file_path::id::Type,
|
||||
report: FileIdentifierReport,
|
||||
maybe_sub_iso_file_path: Option<IsolatedFilePathData<'static>>,
|
||||
}
|
||||
|
@ -119,10 +120,7 @@ impl StatefulJob for FileIdentifierJob {
|
|||
maybe_sub_iso_file_path,
|
||||
});
|
||||
|
||||
let data = state
|
||||
.data
|
||||
.as_mut()
|
||||
.expect("critical error: missing data on job state");
|
||||
let data = extract_job_data_mut!(state);
|
||||
|
||||
if orphan_count == 0 {
|
||||
return Err(JobError::EarlyFinish {
|
||||
|
@ -170,10 +168,7 @@ impl StatefulJob for FileIdentifierJob {
|
|||
ref mut cursor,
|
||||
ref mut report,
|
||||
ref maybe_sub_iso_file_path,
|
||||
} = state
|
||||
.data
|
||||
.as_mut()
|
||||
.expect("critical error: missing data on job state");
|
||||
} = extract_job_data_mut!(state);
|
||||
|
||||
let step_number = state.step_number;
|
||||
let location = &state.init.location;
|
||||
|
@ -223,11 +218,7 @@ impl StatefulJob for FileIdentifierJob {
|
|||
}
|
||||
|
||||
async fn finalize(&mut self, _: WorkerContext, state: &mut JobState<Self>) -> JobResult {
|
||||
let report = &state
|
||||
.data
|
||||
.as_ref()
|
||||
.expect("critical error: missing data on job state")
|
||||
.report;
|
||||
let report = &extract_job_data!(state).report;
|
||||
|
||||
info!("Finalizing identifier job: {report:?}");
|
||||
|
||||
|
@ -236,8 +227,8 @@ impl StatefulJob for FileIdentifierJob {
|
|||
}
|
||||
|
||||
fn orphan_path_filters(
|
||||
location_id: i32,
|
||||
file_path_id: Option<i32>,
|
||||
location_id: location::id::Type,
|
||||
file_path_id: Option<file_path::id::Type>,
|
||||
maybe_sub_iso_file_path: &Option<IsolatedFilePathData<'_>>,
|
||||
) -> Vec<file_path::WhereParam> {
|
||||
chain_optional_iter(
|
||||
|
@ -262,7 +253,7 @@ fn orphan_path_filters(
|
|||
|
||||
async fn count_orphan_file_paths(
|
||||
db: &PrismaClient,
|
||||
location_id: i32,
|
||||
location_id: location::id::Type,
|
||||
maybe_sub_materialized_path: &Option<IsolatedFilePathData<'_>>,
|
||||
) -> Result<usize, prisma_client_rust::QueryError> {
|
||||
db.file_path()
|
||||
|
@ -278,8 +269,8 @@ async fn count_orphan_file_paths(
|
|||
|
||||
async fn get_orphan_file_paths(
|
||||
db: &PrismaClient,
|
||||
location_id: i32,
|
||||
file_path_id: i32,
|
||||
location_id: location::id::Type,
|
||||
file_path_id: file_path::id::Type,
|
||||
maybe_sub_materialized_path: &Option<IsolatedFilePathData<'_>>,
|
||||
) -> Result<Vec<file_path_for_file_identifier::Data>, prisma_client_rust::QueryError> {
|
||||
info!(
|
||||
|
|
|
@ -14,13 +14,14 @@ use crate::{
|
|||
use sd_file_ext::{extensions::Extension, kind::ObjectKind};
|
||||
use sd_sync::CRDTOperation;
|
||||
|
||||
use futures::future::join_all;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use futures::future::join_all;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use thiserror::Error;
|
||||
use tokio::{fs, io};
|
||||
use tracing::{error, info};
|
||||
|
@ -345,7 +346,7 @@ async fn process_identifier_file_paths(
|
|||
location: &location::Data,
|
||||
file_paths: &[file_path_for_file_identifier::Data],
|
||||
step_number: usize,
|
||||
cursor: &mut i32,
|
||||
cursor: &mut file_path::id::Type,
|
||||
library: &Library,
|
||||
orphan_count: usize,
|
||||
) -> Result<(usize, usize), JobError> {
|
||||
|
@ -356,7 +357,7 @@ async fn process_identifier_file_paths(
|
|||
orphan_count
|
||||
);
|
||||
|
||||
let counts = identifier_job_step(&library, location, file_paths).await?;
|
||||
let counts = identifier_job_step(library, location, file_paths).await?;
|
||||
|
||||
// set the step data cursor to the last row of this chunk
|
||||
if let Some(last_row) = file_paths.last() {
|
||||
|
|
|
@ -19,7 +19,7 @@ use super::{process_identifier_file_paths, FileIdentifierJobError, CHUNK_SIZE};
|
|||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct ShallowFileIdentifierJobState {
|
||||
cursor: i32,
|
||||
cursor: file_path::id::Type,
|
||||
sub_iso_file_path: IsolatedFilePathData<'static>,
|
||||
}
|
||||
|
||||
|
@ -83,7 +83,7 @@ pub async fn shallow(
|
|||
.select(file_path::select!({ id }))
|
||||
.exec()
|
||||
.await?
|
||||
.unwrap(); // SAFETY: We already validated before that there are orphans `file_path`s
|
||||
.expect("We already validated before that there are orphans `file_path`s");
|
||||
|
||||
// Initializing `state.data` here because we need a complete state in case of early finish
|
||||
let mut data = ShallowFileIdentifierJobState {
|
||||
|
@ -106,7 +106,7 @@ pub async fn shallow(
|
|||
&file_paths,
|
||||
step_number,
|
||||
cursor,
|
||||
&library,
|
||||
library,
|
||||
orphan_count,
|
||||
)
|
||||
.await?;
|
||||
|
@ -118,8 +118,8 @@ pub async fn shallow(
|
|||
}
|
||||
|
||||
fn orphan_path_filters(
|
||||
location_id: i32,
|
||||
file_path_id: Option<i32>,
|
||||
location_id: location::id::Type,
|
||||
file_path_id: Option<file_path::id::Type>,
|
||||
sub_iso_file_path: &IsolatedFilePathData<'_>,
|
||||
) -> Vec<file_path::WhereParam> {
|
||||
chain_optional_iter(
|
||||
|
@ -139,7 +139,7 @@ fn orphan_path_filters(
|
|||
|
||||
async fn count_orphan_file_paths(
|
||||
db: &PrismaClient,
|
||||
location_id: i32,
|
||||
location_id: location::id::Type,
|
||||
sub_iso_file_path: &IsolatedFilePathData<'_>,
|
||||
) -> Result<usize, prisma_client_rust::QueryError> {
|
||||
db.file_path()
|
||||
|
@ -151,8 +151,8 @@ async fn count_orphan_file_paths(
|
|||
|
||||
async fn get_orphan_file_paths(
|
||||
db: &PrismaClient,
|
||||
location_id: i32,
|
||||
file_path_id_cursor: i32,
|
||||
location_id: location::id::Type,
|
||||
file_path_id_cursor: file_path::id::Type,
|
||||
sub_iso_file_path: &IsolatedFilePathData<'_>,
|
||||
) -> Result<Vec<file_path_for_file_identifier::Data>, prisma_client_rust::QueryError> {
|
||||
info!(
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
use crate::{
|
||||
invalidate_query,
|
||||
extract_job_data, invalidate_query,
|
||||
job::{
|
||||
JobError, JobInitData, JobReportUpdate, JobResult, JobState, StatefulJob, WorkerContext,
|
||||
},
|
||||
library::Library,
|
||||
location::file_path_helper::IsolatedFilePathData,
|
||||
prisma::{file_path, location},
|
||||
util::error::FileIOError,
|
||||
};
|
||||
|
||||
|
@ -10,46 +13,34 @@ use std::{hash::Hash, path::PathBuf};
|
|||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
use tokio::fs;
|
||||
use tokio::{fs, io};
|
||||
use tracing::{trace, warn};
|
||||
|
||||
use super::{context_menu_fs_info, get_location_path_from_location_id, osstr_to_string, FsInfo};
|
||||
use super::{
|
||||
construct_target_filename, error::FileSystemJobsError, fetch_source_and_target_location_paths,
|
||||
get_file_data_from_isolated_file_path, get_many_files_datas, FileData,
|
||||
};
|
||||
|
||||
pub struct FileCopierJob {}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct FileCopierJobState {
|
||||
pub target_path: PathBuf, // target dir prefix too
|
||||
pub source_fs_info: FsInfo,
|
||||
sources_location_path: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Hash, Type)]
|
||||
pub struct FileCopierJobInit {
|
||||
pub source_location_id: i32,
|
||||
pub source_path_id: i32,
|
||||
pub target_location_id: i32,
|
||||
pub target_path: PathBuf,
|
||||
pub source_location_id: location::id::Type,
|
||||
pub target_location_id: location::id::Type,
|
||||
pub sources_file_path_ids: Vec<file_path::id::Type>,
|
||||
pub target_location_relative_directory_path: PathBuf,
|
||||
pub target_file_name_suffix: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub enum FileCopierJobStep {
|
||||
Directory { path: PathBuf },
|
||||
File { path: PathBuf },
|
||||
}
|
||||
|
||||
impl From<FsInfo> for FileCopierJobStep {
|
||||
fn from(value: FsInfo) -> Self {
|
||||
if value.path_data.is_dir {
|
||||
Self::Directory {
|
||||
path: value.fs_path,
|
||||
}
|
||||
} else {
|
||||
Self::File {
|
||||
path: value.fs_path,
|
||||
}
|
||||
}
|
||||
}
|
||||
pub struct FileCopierJobStep {
|
||||
pub source_file_data: FileData,
|
||||
pub target_full_path: PathBuf,
|
||||
}
|
||||
|
||||
impl JobInitData for FileCopierJobInit {
|
||||
|
@ -69,50 +60,43 @@ impl StatefulJob for FileCopierJob {
|
|||
}
|
||||
|
||||
async fn init(&self, ctx: WorkerContext, state: &mut JobState<Self>) -> Result<(), JobError> {
|
||||
let source_fs_info = context_menu_fs_info(
|
||||
&ctx.library.db,
|
||||
state.init.source_location_id,
|
||||
state.init.source_path_id,
|
||||
let Library { db, .. } = &ctx.library;
|
||||
|
||||
let (sources_location_path, targets_location_path) =
|
||||
fetch_source_and_target_location_paths(
|
||||
db,
|
||||
state.init.source_location_id,
|
||||
state.init.target_location_id,
|
||||
)
|
||||
.await?;
|
||||
|
||||
state.steps = get_many_files_datas(
|
||||
db,
|
||||
&sources_location_path,
|
||||
&state.init.sources_file_path_ids,
|
||||
)
|
||||
.await?;
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|file_data| {
|
||||
// add the currently viewed subdirectory to the location root
|
||||
let mut full_target_path =
|
||||
targets_location_path.join(&state.init.target_location_relative_directory_path);
|
||||
|
||||
let mut full_target_path =
|
||||
get_location_path_from_location_id(&ctx.library.db, state.init.target_location_id)
|
||||
.await?;
|
||||
|
||||
// add the currently viewed subdirectory to the location root
|
||||
full_target_path.push(&state.init.target_path);
|
||||
|
||||
// extension wizardry for cloning and such
|
||||
// if no suffix has been selected, just use the file name
|
||||
// if a suffix is provided and it's a directory, use the directory name + suffix
|
||||
// if a suffix is provided and it's a file, use the (file name + suffix).extension
|
||||
let file_name = osstr_to_string(source_fs_info.fs_path.file_name())?;
|
||||
|
||||
let target_file_name = state.init.target_file_name_suffix.as_ref().map_or_else(
|
||||
|| Ok::<_, JobError>(file_name.clone()),
|
||||
|suffix| {
|
||||
Ok(if source_fs_info.path_data.is_dir {
|
||||
format!("{file_name}{suffix}")
|
||||
} else {
|
||||
osstr_to_string(source_fs_info.fs_path.file_stem())?
|
||||
+ suffix + &source_fs_info.fs_path.extension().map_or_else(
|
||||
|| Ok(String::new()),
|
||||
|ext| ext.to_str().map(|e| format!(".{e}")).ok_or(JobError::OsStr),
|
||||
)?
|
||||
})
|
||||
},
|
||||
)?;
|
||||
|
||||
full_target_path.push(target_file_name);
|
||||
full_target_path.push(construct_target_filename(
|
||||
&file_data,
|
||||
&state.init.target_file_name_suffix,
|
||||
));
|
||||
FileCopierJobStep {
|
||||
source_file_data: file_data,
|
||||
target_full_path: full_target_path,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
state.data = Some(FileCopierJobState {
|
||||
target_path: full_target_path,
|
||||
source_fs_info: source_fs_info.clone(),
|
||||
sources_location_path,
|
||||
});
|
||||
|
||||
state.steps.push_back(source_fs_info.into());
|
||||
|
||||
ctx.progress(vec![JobReportUpdate::TaskCount(state.steps.len())]);
|
||||
|
||||
Ok(())
|
||||
|
@ -123,109 +107,95 @@ impl StatefulJob for FileCopierJob {
|
|||
ctx: WorkerContext,
|
||||
state: &mut JobState<Self>,
|
||||
) -> Result<(), JobError> {
|
||||
let data = state
|
||||
.data
|
||||
.as_ref()
|
||||
.expect("critical error: missing data on job state");
|
||||
let FileCopierJobStep {
|
||||
source_file_data,
|
||||
target_full_path,
|
||||
} = &state.steps[0];
|
||||
|
||||
match &state.steps[0] {
|
||||
FileCopierJobStep::File { path } => {
|
||||
let mut target_path = data.target_path.clone();
|
||||
let data = extract_job_data!(state);
|
||||
|
||||
if data.source_fs_info.path_data.is_dir {
|
||||
// if root type is a dir, we need to preserve structure by making paths relative
|
||||
target_path.push(
|
||||
path.strip_prefix(&data.source_fs_info.fs_path)
|
||||
.map_err(|_| JobError::Path)?,
|
||||
);
|
||||
}
|
||||
if source_file_data.file_path.is_dir {
|
||||
fs::create_dir_all(target_full_path)
|
||||
.await
|
||||
.map_err(|e| FileIOError::from((target_full_path, e)))?;
|
||||
|
||||
let parent_path = path.parent().ok_or(JobError::Path)?;
|
||||
let parent_target_path = target_path.parent().ok_or(JobError::Path)?;
|
||||
let mut read_dir = fs::read_dir(&source_file_data.full_path)
|
||||
.await
|
||||
.map_err(|e| FileIOError::from((&source_file_data.full_path, e)))?;
|
||||
|
||||
if fs::canonicalize(parent_path)
|
||||
.await
|
||||
.map_err(|e| FileIOError::from((parent_path, e)))?
|
||||
== fs::canonicalize(parent_target_path)
|
||||
.await
|
||||
.map_err(|e| FileIOError::from((parent_target_path, e)))?
|
||||
{
|
||||
return Err(JobError::MatchingSrcDest(path.clone()));
|
||||
}
|
||||
// Can't use the `steps` borrow from here ownwards, or you feel the wrath of the borrow checker
|
||||
while let Some(children_entry) = read_dir
|
||||
.next_entry()
|
||||
.await
|
||||
.map_err(|e| FileIOError::from((&state.steps[0].source_file_data.full_path, e)))?
|
||||
{
|
||||
let children_path = children_entry.path();
|
||||
let target_children_full_path = state.steps[0].target_full_path.join(
|
||||
children_path
|
||||
.strip_prefix(&state.steps[0].source_file_data.full_path)
|
||||
.map_err(|_| JobError::Path)?,
|
||||
);
|
||||
|
||||
if fs::metadata(&target_path).await.is_ok() {
|
||||
// Currently not supporting file_name suffixes children files in a directory being copied
|
||||
state.steps.push_back(FileCopierJobStep {
|
||||
target_full_path: target_children_full_path,
|
||||
source_file_data: get_file_data_from_isolated_file_path(
|
||||
&ctx.library.db,
|
||||
&data.sources_location_path,
|
||||
&IsolatedFilePathData::new(
|
||||
state.init.source_location_id,
|
||||
&data.sources_location_path,
|
||||
&children_path,
|
||||
children_entry
|
||||
.metadata()
|
||||
.await
|
||||
.map_err(|e| FileIOError::from((&children_path, e)))?
|
||||
.is_dir(),
|
||||
)
|
||||
.map_err(FileSystemJobsError::from)?,
|
||||
)
|
||||
.await?,
|
||||
});
|
||||
|
||||
ctx.progress(vec![JobReportUpdate::TaskCount(state.steps.len())]);
|
||||
}
|
||||
} else {
|
||||
if source_file_data.full_path.parent().ok_or(JobError::Path)?
|
||||
== target_full_path.parent().ok_or(JobError::Path)?
|
||||
{
|
||||
return Err(FileSystemJobsError::MatchingSrcDest(
|
||||
source_file_data.full_path.clone().into_boxed_path(),
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
match fs::metadata(target_full_path).await {
|
||||
Ok(_) => {
|
||||
// only skip as it could be half way through a huge directory copy and run into an issue
|
||||
warn!(
|
||||
"Skipping {} as it would be overwritten",
|
||||
target_path.display()
|
||||
target_full_path.display()
|
||||
);
|
||||
// TODO(brxken128): could possibly return an error if the skipped file was the *only* file to be copied?
|
||||
} else {
|
||||
}
|
||||
Err(e) if e.kind() == io::ErrorKind::NotFound => {
|
||||
trace!(
|
||||
"Copying from {} to {}",
|
||||
path.display(),
|
||||
target_path.display()
|
||||
source_file_data.full_path.display(),
|
||||
target_full_path.display()
|
||||
);
|
||||
|
||||
fs::copy(&path, &target_path)
|
||||
fs::copy(&source_file_data.full_path, &target_full_path)
|
||||
.await
|
||||
.map_err(|e| FileIOError::from((&target_path, e)))?;
|
||||
.map_err(|e| FileIOError::from((target_full_path, e)))?;
|
||||
}
|
||||
Err(e) => return Err(FileIOError::from((target_full_path, e)).into()),
|
||||
}
|
||||
FileCopierJobStep::Directory { path } => {
|
||||
// if this is the very first path, create the target dir
|
||||
// fixes copying dirs with no child directories
|
||||
if data.source_fs_info.path_data.is_dir && &data.source_fs_info.fs_path == path {
|
||||
fs::create_dir_all(&data.target_path)
|
||||
.await
|
||||
.map_err(|e| FileIOError::from((&data.target_path, e)))?;
|
||||
}
|
||||
|
||||
let path = path.clone(); // To appease the borrowck
|
||||
|
||||
let mut dir = fs::read_dir(&path)
|
||||
.await
|
||||
.map_err(|e| FileIOError::from((&path, e)))?;
|
||||
|
||||
while let Some(entry) = dir
|
||||
.next_entry()
|
||||
.await
|
||||
.map_err(|e| FileIOError::from((&path, e)))?
|
||||
{
|
||||
let entry_path = entry.path();
|
||||
if entry
|
||||
.metadata()
|
||||
.await
|
||||
.map_err(|e| FileIOError::from((&entry_path, e)))?
|
||||
.is_dir()
|
||||
{
|
||||
state
|
||||
.steps
|
||||
.push_back(FileCopierJobStep::Directory { path: entry.path() });
|
||||
|
||||
let full_path = data.target_path.join(
|
||||
entry_path
|
||||
.strip_prefix(&data.source_fs_info.fs_path)
|
||||
.map_err(|_| JobError::Path)?,
|
||||
);
|
||||
|
||||
fs::create_dir_all(&full_path)
|
||||
.await
|
||||
.map_err(|e| FileIOError::from((full_path, e)))?;
|
||||
} else {
|
||||
state
|
||||
.steps
|
||||
.push_back(FileCopierJobStep::File { path: entry.path() });
|
||||
};
|
||||
|
||||
ctx.progress(vec![JobReportUpdate::TaskCount(state.steps.len())]);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
ctx.progress(vec![JobReportUpdate::CompletedTaskCount(
|
||||
state.step_number + 1,
|
||||
)]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
// // if the location is remote, we queue a job for that client specifically
|
||||
// // the actual create_folder function should be an option on an enum for all vfs actions
|
||||
// pub async fn create_folder(
|
||||
// location_id: i32,
|
||||
// location_id: location::id::Type,
|
||||
// path: &str,
|
||||
// name: Option<&str>,
|
||||
// library: &LibraryContext,
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
use crate::{
|
||||
invalidate_query,
|
||||
extract_job_data, invalidate_query,
|
||||
job::{
|
||||
JobError, JobInitData, JobReportUpdate, JobResult, JobState, StatefulJob, WorkerContext,
|
||||
},
|
||||
library::Library,
|
||||
object::fs::{construct_target_filename, error::FileSystemJobsError},
|
||||
prisma::{file_path, location},
|
||||
util::error::FileIOError,
|
||||
};
|
||||
|
||||
|
@ -10,25 +13,24 @@ use std::{hash::Hash, path::PathBuf};
|
|||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
use tokio::fs;
|
||||
use tokio::{fs, io};
|
||||
use tracing::{trace, warn};
|
||||
|
||||
use super::{context_menu_fs_info, get_location_path_from_location_id, FsInfo};
|
||||
use super::{fetch_source_and_target_location_paths, get_many_files_datas, FileData};
|
||||
|
||||
pub struct FileCutterJob {}
|
||||
|
||||
#[derive(Serialize, Deserialize, Hash, Type)]
|
||||
pub struct FileCutterJobInit {
|
||||
pub source_location_id: i32,
|
||||
pub source_path_id: i32,
|
||||
pub target_location_id: i32,
|
||||
pub target_path: PathBuf,
|
||||
pub source_location_id: location::id::Type,
|
||||
pub target_location_id: location::id::Type,
|
||||
pub sources_file_path_ids: Vec<file_path::id::Type>,
|
||||
pub target_location_relative_directory_path: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct FileCutterJobStep {
|
||||
pub source_fs_info: FsInfo,
|
||||
pub target_directory: PathBuf,
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct FileCutterJobState {
|
||||
full_target_directory_path: PathBuf,
|
||||
}
|
||||
|
||||
impl JobInitData for FileCutterJobInit {
|
||||
|
@ -38,8 +40,8 @@ impl JobInitData for FileCutterJobInit {
|
|||
#[async_trait::async_trait]
|
||||
impl StatefulJob for FileCutterJob {
|
||||
type Init = FileCutterJobInit;
|
||||
type Data = ();
|
||||
type Step = FileCutterJobStep;
|
||||
type Data = FileCutterJobState;
|
||||
type Step = FileData;
|
||||
|
||||
const NAME: &'static str = "file_cutter";
|
||||
|
||||
|
@ -48,23 +50,30 @@ impl StatefulJob for FileCutterJob {
|
|||
}
|
||||
|
||||
async fn init(&self, ctx: WorkerContext, state: &mut JobState<Self>) -> Result<(), JobError> {
|
||||
let source_fs_info = context_menu_fs_info(
|
||||
&ctx.library.db,
|
||||
state.init.source_location_id,
|
||||
state.init.source_path_id,
|
||||
)
|
||||
.await?;
|
||||
let Library { db, .. } = &ctx.library;
|
||||
|
||||
let mut full_target_path =
|
||||
get_location_path_from_location_id(&ctx.library.db, state.init.target_location_id)
|
||||
.await?;
|
||||
full_target_path.push(&state.init.target_path);
|
||||
let (sources_location_path, mut targets_location_path) =
|
||||
fetch_source_and_target_location_paths(
|
||||
db,
|
||||
state.init.source_location_id,
|
||||
state.init.target_location_id,
|
||||
)
|
||||
.await?;
|
||||
|
||||
state.steps.push_back(FileCutterJobStep {
|
||||
source_fs_info,
|
||||
target_directory: full_target_path,
|
||||
targets_location_path.push(&state.init.target_location_relative_directory_path);
|
||||
|
||||
state.data = Some(FileCutterJobState {
|
||||
full_target_directory_path: targets_location_path,
|
||||
});
|
||||
|
||||
state.steps = get_many_files_datas(
|
||||
db,
|
||||
&sources_location_path,
|
||||
&state.init.sources_file_path_ids,
|
||||
)
|
||||
.await?
|
||||
.into();
|
||||
|
||||
ctx.progress(vec![JobReportUpdate::TaskCount(state.steps.len())]);
|
||||
|
||||
Ok(())
|
||||
|
@ -75,50 +84,52 @@ impl StatefulJob for FileCutterJob {
|
|||
ctx: WorkerContext,
|
||||
state: &mut JobState<Self>,
|
||||
) -> Result<(), JobError> {
|
||||
let data = extract_job_data!(state);
|
||||
|
||||
let step = &state.steps[0];
|
||||
let source_info = &step.source_fs_info;
|
||||
|
||||
let full_output = step
|
||||
.target_directory
|
||||
.join(source_info.fs_path.file_name().ok_or(JobError::OsStr)?);
|
||||
let full_output = data
|
||||
.full_target_directory_path
|
||||
.join(construct_target_filename(step, &None));
|
||||
|
||||
let parent_source = source_info.fs_path.parent().ok_or(JobError::Path)?;
|
||||
|
||||
let parent_output = full_output.parent().ok_or(JobError::Path)?;
|
||||
|
||||
if fs::canonicalize(parent_source)
|
||||
.await
|
||||
.map_err(|e| FileIOError::from((parent_source, e)))?
|
||||
== fs::canonicalize(parent_output)
|
||||
.await
|
||||
.map_err(|e| FileIOError::from((parent_output, e)))?
|
||||
if step.full_path.parent().ok_or(JobError::Path)?
|
||||
== full_output.parent().ok_or(JobError::Path)?
|
||||
{
|
||||
return Err(JobError::MatchingSrcDest(source_info.fs_path.clone()));
|
||||
return Err(FileSystemJobsError::MatchingSrcDest(
|
||||
step.full_path.clone().into_boxed_path(),
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
if fs::metadata(&full_output).await.is_ok() {
|
||||
warn!(
|
||||
"Skipping {} as it would be overwritten",
|
||||
full_output.display()
|
||||
);
|
||||
match fs::metadata(&full_output).await {
|
||||
Ok(_) => {
|
||||
warn!(
|
||||
"Skipping {} as it would be overwritten",
|
||||
full_output.display()
|
||||
);
|
||||
|
||||
return Err(JobError::WouldOverwrite(full_output));
|
||||
Err(FileSystemJobsError::WouldOverwrite(full_output.into_boxed_path()).into())
|
||||
}
|
||||
Err(e) if e.kind() == io::ErrorKind::NotFound => {
|
||||
trace!(
|
||||
"Cutting {} to {}",
|
||||
step.full_path.display(),
|
||||
full_output.display()
|
||||
);
|
||||
|
||||
fs::rename(&step.full_path, &full_output)
|
||||
.await
|
||||
.map_err(|e| FileIOError::from((&step.full_path, e)))?;
|
||||
|
||||
ctx.progress(vec![JobReportUpdate::CompletedTaskCount(
|
||||
state.step_number + 1,
|
||||
)]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Err(e) => Err(FileIOError::from((&full_output, e)).into()),
|
||||
}
|
||||
|
||||
trace!(
|
||||
"Cutting {} to {}",
|
||||
source_info.fs_path.display(),
|
||||
full_output.display()
|
||||
);
|
||||
|
||||
fs::rename(&source_info.fs_path, &full_output)
|
||||
.await
|
||||
.map_err(|e| FileIOError::from((&source_info.fs_path, e)))?;
|
||||
|
||||
ctx.progress(vec![JobReportUpdate::CompletedTaskCount(
|
||||
state.step_number + 1,
|
||||
)]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn finalize(&mut self, ctx: WorkerContext, state: &mut JobState<Self>) -> JobResult {
|
||||
|
|
|
@ -1,38 +1,32 @@
|
|||
// use sd_crypto::{crypto::Decryptor, header::file::FileHeader, Protected};
|
||||
// use serde::{Deserialize, Serialize};
|
||||
// use specta::Type;
|
||||
// use std::path::PathBuf;
|
||||
// use tokio::fs::File;
|
||||
|
||||
// use crate::{
|
||||
// invalidate_query,
|
||||
// job::{
|
||||
// JobError, JobInitData, JobReportUpdate, JobResult, JobState, StatefulJob, WorkerContext,
|
||||
// },
|
||||
// library::Library,
|
||||
// location::{file_path_helper:: location::id::Type},
|
||||
// util::error::FileIOError,
|
||||
// };
|
||||
|
||||
// use super::{context_menu_fs_info, FsInfo, BYTES_EXT};
|
||||
// use sd_crypto::{crypto::Decryptor, header::file::FileHeader, Protected};
|
||||
|
||||
// use serde::{Deserialize, Serialize};
|
||||
// use specta::Type;
|
||||
// use tokio::fs::File;
|
||||
|
||||
// use super::{get_location_path_from_location_id, get_many_files_datas, FileData, BYTES_EXT};
|
||||
// pub struct FileDecryptorJob;
|
||||
// #[derive(Serialize, Deserialize, Debug)]
|
||||
// pub struct FileDecryptorJobState {}
|
||||
|
||||
// // decrypt could have an option to restore metadata (and another specific option for file name? - would turn "output file" into "output path" in the UI)
|
||||
// #[derive(Serialize, Deserialize, Debug, Type, Hash)]
|
||||
// pub struct FileDecryptorJobInit {
|
||||
// pub location_id: i32,
|
||||
// pub path_id: i32,
|
||||
// pub location_id: location::id::Type,
|
||||
// pub file_path_ids: Vec<file_path::id::Type>,
|
||||
// pub mount_associated_key: bool,
|
||||
// pub output_path: Option<PathBuf>,
|
||||
// pub password: Option<String>, // if this is set, we can assume the user chose password decryption
|
||||
// pub save_to_library: Option<bool>,
|
||||
// }
|
||||
|
||||
// #[derive(Serialize, Deserialize, Debug)]
|
||||
// pub struct FileDecryptorJobStep {
|
||||
// pub fs_info: FsInfo,
|
||||
// }
|
||||
|
||||
// impl JobInitData for FileDecryptorJobInit {
|
||||
// type Job = FileDecryptorJob;
|
||||
// }
|
||||
|
@ -40,8 +34,8 @@
|
|||
// #[async_trait::async_trait]
|
||||
// impl StatefulJob for FileDecryptorJob {
|
||||
// type Init = FileDecryptorJobInit;
|
||||
// type Data = FileDecryptorJobState;
|
||||
// type Step = FileDecryptorJobStep;
|
||||
// type Data = ();
|
||||
// type Step = FileData;
|
||||
|
||||
// const NAME: &'static str = "file_decryptor";
|
||||
|
||||
|
@ -50,13 +44,15 @@
|
|||
// }
|
||||
|
||||
// async fn init(&self, ctx: WorkerContext, state: &mut JobState<Self>) -> Result<(), JobError> {
|
||||
// // enumerate files to decrypt
|
||||
// // populate the steps with them (local file paths)
|
||||
// let fs_info =
|
||||
// context_menu_fs_info(&ctx.library.db, state.init.location_id, state.init.path_id)
|
||||
// .await?;
|
||||
// let Library { db, .. } = &ctx.library;
|
||||
|
||||
// state.steps.push_back(FileDecryptorJobStep { fs_info });
|
||||
// state.steps = get_many_files_datas(
|
||||
// db,
|
||||
// get_location_path_from_location_id(db, state.init.location_id).await?,
|
||||
// &state.init.file_path_ids,
|
||||
// )
|
||||
// .await?
|
||||
// .into();
|
||||
|
||||
// ctx.progress(vec![JobReportUpdate::TaskCount(state.steps.len())]);
|
||||
|
||||
|
@ -68,29 +64,26 @@
|
|||
// ctx: WorkerContext,
|
||||
// state: &mut JobState<Self>,
|
||||
// ) -> Result<(), JobError> {
|
||||
// let info = &&state.steps[0].fs_info;
|
||||
// let step = &state.steps[0];
|
||||
// let key_manager = &ctx.library.key_manager;
|
||||
|
||||
// // handle overwriting checks, and making sure there's enough available space
|
||||
// let output_path = state.init.output_path.clone().map_or_else(
|
||||
// || {
|
||||
// let mut path = info.fs_path.clone();
|
||||
// let extension = path.extension().map_or("decrypted", |ext| {
|
||||
// if ext == BYTES_EXT {
|
||||
// ""
|
||||
// } else {
|
||||
// "decrypted"
|
||||
// }
|
||||
// });
|
||||
// path.set_extension(extension);
|
||||
// path
|
||||
// },
|
||||
// |p| p,
|
||||
// );
|
||||
// let output_path = {
|
||||
// let mut path = step.full_path.clone();
|
||||
// let extension = path.extension().map_or("decrypted", |ext| {
|
||||
// if ext == BYTES_EXT {
|
||||
// ""
|
||||
// } else {
|
||||
// "decrypted"
|
||||
// }
|
||||
// });
|
||||
// path.set_extension(extension);
|
||||
// path
|
||||
// };
|
||||
|
||||
// let mut reader = File::open(info.fs_path.clone())
|
||||
// let mut reader = File::open(&step.full_path)
|
||||
// .await
|
||||
// .map_err(|e| FileIOError::from((&info.fs_path, e)))?;
|
||||
// .map_err(|e| FileIOError::from((&step.full_path, e)))?;
|
||||
// let mut writer = File::create(&output_path)
|
||||
// .await
|
||||
// .map_err(|e| FileIOError::from((output_path, e)))?;
|
||||
|
|
|
@ -3,6 +3,8 @@ use crate::{
|
|||
job::{
|
||||
JobError, JobInitData, JobReportUpdate, JobResult, JobState, StatefulJob, WorkerContext,
|
||||
},
|
||||
library::Library,
|
||||
prisma::{file_path, location},
|
||||
util::error::FileIOError,
|
||||
};
|
||||
|
||||
|
@ -10,18 +12,16 @@ use std::hash::Hash;
|
|||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
use tokio::fs;
|
||||
|
||||
use super::{context_menu_fs_info, FsInfo};
|
||||
use super::{get_location_path_from_location_id, get_many_files_datas, FileData};
|
||||
|
||||
pub struct FileDeleterJob {}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct FileDeleterJobState {}
|
||||
|
||||
#[derive(Serialize, Deserialize, Hash, Type)]
|
||||
pub struct FileDeleterJobInit {
|
||||
pub location_id: i32,
|
||||
pub path_id: i32,
|
||||
pub location_id: location::id::Type,
|
||||
pub file_path_ids: Vec<file_path::id::Type>,
|
||||
}
|
||||
|
||||
impl JobInitData for FileDeleterJobInit {
|
||||
|
@ -31,8 +31,8 @@ impl JobInitData for FileDeleterJobInit {
|
|||
#[async_trait::async_trait]
|
||||
impl StatefulJob for FileDeleterJob {
|
||||
type Init = FileDeleterJobInit;
|
||||
type Data = FileDeleterJobState;
|
||||
type Step = FsInfo;
|
||||
type Data = ();
|
||||
type Step = FileData;
|
||||
|
||||
const NAME: &'static str = "file_deleter";
|
||||
|
||||
|
@ -41,11 +41,16 @@ impl StatefulJob for FileDeleterJob {
|
|||
}
|
||||
|
||||
async fn init(&self, ctx: WorkerContext, state: &mut JobState<Self>) -> Result<(), JobError> {
|
||||
let fs_info =
|
||||
context_menu_fs_info(&ctx.library.db, state.init.location_id, state.init.path_id)
|
||||
.await?;
|
||||
let Library { db, .. } = &ctx.library;
|
||||
|
||||
state.steps.push_back(fs_info);
|
||||
state.steps = get_many_files_datas(
|
||||
db,
|
||||
get_location_path_from_location_id(db, state.init.location_id).await?,
|
||||
&state.init.file_path_ids,
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.collect();
|
||||
|
||||
ctx.progress(vec![JobReportUpdate::TaskCount(state.steps.len())]);
|
||||
|
||||
|
@ -57,21 +62,19 @@ impl StatefulJob for FileDeleterJob {
|
|||
ctx: WorkerContext,
|
||||
state: &mut JobState<Self>,
|
||||
) -> Result<(), JobError> {
|
||||
let info = &state.steps[0];
|
||||
let step = &state.steps[0];
|
||||
|
||||
// need to handle stuff such as querying prisma for all paths of a file, and deleting all of those if requested (with a checkbox in the ui)
|
||||
// maybe a files.countOccurances/and or files.getPath(location_id, path_id) to show how many of these files would be deleted (and where?)
|
||||
|
||||
if info.path_data.is_dir {
|
||||
tokio::fs::remove_dir_all(&info.fs_path).await
|
||||
if step.file_path.is_dir {
|
||||
fs::remove_dir_all(&step.full_path).await
|
||||
} else {
|
||||
tokio::fs::remove_file(&info.fs_path).await
|
||||
fs::remove_file(&step.full_path).await
|
||||
}
|
||||
.map_err(|e| FileIOError::from((&info.fs_path, e)))?;
|
||||
.map_err(|e| FileIOError::from((&step.full_path, e)))?;
|
||||
|
||||
ctx.progress(vec![JobReportUpdate::CompletedTaskCount(
|
||||
state.step_number + 1,
|
||||
)]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
|
@ -1,38 +1,48 @@
|
|||
// use crate::{invalidate_query, job::*, library::Library, util::error::FileIOError};
|
||||
// use crate::{
|
||||
// invalidate_query,
|
||||
// job::*,
|
||||
// library::Library,
|
||||
// location::{file_path_helper:: location::id::Type},
|
||||
// util::error::{FileIOError, NonUtf8PathError},
|
||||
// };
|
||||
|
||||
// use std::path::PathBuf;
|
||||
|
||||
// use chrono::FixedOffset;
|
||||
// use sd_crypto::{
|
||||
// crypto::Encryptor,
|
||||
// header::{file::FileHeader, keyslot::Keyslot},
|
||||
// primitives::{LATEST_FILE_HEADER, LATEST_KEYSLOT, LATEST_METADATA, LATEST_PREVIEW_MEDIA},
|
||||
// types::{Algorithm, Key},
|
||||
// };
|
||||
|
||||
// use chrono::FixedOffset;
|
||||
// use serde::{Deserialize, Serialize};
|
||||
// use specta::Type;
|
||||
// use tokio::{fs::File, io::AsyncReadExt};
|
||||
// use tokio::{
|
||||
// fs::{self, File},
|
||||
// io,
|
||||
// };
|
||||
// use tracing::{error, warn};
|
||||
// use uuid::Uuid;
|
||||
|
||||
// use super::{context_menu_fs_info, FsInfo, BYTES_EXT};
|
||||
// use super::{
|
||||
// error::FileSystemJobsError, get_location_path_from_location_id, get_many_files_datas, FileData,
|
||||
// BYTES_EXT,
|
||||
// };
|
||||
|
||||
// pub struct FileEncryptorJob;
|
||||
|
||||
// #[derive(Serialize, Deserialize, Type, Hash)]
|
||||
// pub struct FileEncryptorJobInit {
|
||||
// pub location_id: i32,
|
||||
// pub path_id: i32,
|
||||
// pub location_id: location::id::Type,
|
||||
// pub file_path_ids: Vec<file_path::id::Type>,
|
||||
// pub key_uuid: Uuid,
|
||||
// pub algorithm: Algorithm,
|
||||
// pub metadata: bool,
|
||||
// pub preview_media: bool,
|
||||
// pub output_path: Option<PathBuf>,
|
||||
// }
|
||||
|
||||
// #[derive(Serialize, Deserialize)]
|
||||
// pub struct Metadata {
|
||||
// pub path_id: i32,
|
||||
// pub file_path_id: file_path::id::Type,
|
||||
// pub name: String,
|
||||
// pub hidden: bool,
|
||||
// pub favorite: bool,
|
||||
|
@ -49,7 +59,7 @@
|
|||
// impl StatefulJob for FileEncryptorJob {
|
||||
// type Init = FileEncryptorJobInit;
|
||||
// type Data = ();
|
||||
// type Step = FsInfo;
|
||||
// type Step = FileData;
|
||||
|
||||
// const NAME: &'static str = "file_encryptor";
|
||||
|
||||
|
@ -58,13 +68,15 @@
|
|||
// }
|
||||
|
||||
// async fn init(&self, ctx: WorkerContext, state: &mut JobState<Self>) -> Result<(), JobError> {
|
||||
// state.steps.push_back(
|
||||
// context_menu_fs_info(&ctx.library.db, state.init.location_id, state.init.path_id)
|
||||
// .await
|
||||
// .map_err(|_| JobError::MissingData {
|
||||
// value: String::from("file_path that matches both location id and path id"),
|
||||
// })?,
|
||||
// );
|
||||
// let Library { db, .. } = &ctx.library;
|
||||
|
||||
// state.steps = get_many_files_datas(
|
||||
// db,
|
||||
// get_location_path_from_location_id(db, state.init.location_id).await?,
|
||||
// &state.init.file_path_ids,
|
||||
// )
|
||||
// .await?
|
||||
// .into();
|
||||
|
||||
// ctx.progress(vec![JobReportUpdate::TaskCount(state.steps.len())]);
|
||||
|
||||
|
@ -76,11 +88,11 @@
|
|||
// ctx: WorkerContext,
|
||||
// state: &mut JobState<Self>,
|
||||
// ) -> Result<(), JobError> {
|
||||
// let info = &state.steps[0];
|
||||
// let step = &state.steps[0];
|
||||
|
||||
// let Library { key_manager, .. } = &ctx.library;
|
||||
|
||||
// if !info.path_data.is_dir {
|
||||
// if !step.file_path.is_dir {
|
||||
// // handle overwriting checks, and making sure there's enough available space
|
||||
|
||||
// let user_key = key_manager
|
||||
|
@ -90,30 +102,23 @@
|
|||
|
||||
// let user_key_details = key_manager.access_keystore(state.init.key_uuid).await?;
|
||||
|
||||
// let output_path = state.init.output_path.clone().map_or_else(
|
||||
// || {
|
||||
// let mut path = info.fs_path.clone();
|
||||
// let extension = path.extension().map_or_else(
|
||||
// || Ok("bytes".to_string()),
|
||||
// |extension| {
|
||||
// Ok::<String, JobError>(
|
||||
// extension
|
||||
// .to_str()
|
||||
// .ok_or(JobError::MissingData {
|
||||
// value: String::from(
|
||||
// "path contents when converted to string",
|
||||
// ),
|
||||
// })?
|
||||
// .to_string() + BYTES_EXT,
|
||||
// )
|
||||
// },
|
||||
// )?;
|
||||
// let output_path = {
|
||||
// let mut path = step.full_path.clone();
|
||||
// let extension = path.extension().map_or_else(
|
||||
// || Ok("bytes".to_string()),
|
||||
// |extension| {
|
||||
// Ok::<String, JobError>(format!(
|
||||
// "{}{BYTES_EXT}",
|
||||
// extension.to_str().ok_or(FileSystemJobsError::FilePath(
|
||||
// NonUtf8PathError(step.full_path.clone().into_boxed_path()).into()
|
||||
// ))?
|
||||
// ))
|
||||
// },
|
||||
// )?;
|
||||
|
||||
// path.set_extension(extension);
|
||||
// Ok::<PathBuf, JobError>(path)
|
||||
// },
|
||||
// Ok,
|
||||
// )?;
|
||||
// path.set_extension(extension);
|
||||
// path
|
||||
// };
|
||||
|
||||
// let _guard = ctx
|
||||
// .library
|
||||
|
@ -135,9 +140,9 @@
|
|||
// Some,
|
||||
// );
|
||||
|
||||
// let mut reader = File::open(&info.fs_path)
|
||||
// let mut reader = File::open(&step.full_path)
|
||||
// .await
|
||||
// .map_err(|e| FileIOError::from((&info.fs_path, e)))?;
|
||||
// .map_err(|e| FileIOError::from((&step.full_path, e)))?;
|
||||
// let mut writer = File::create(&output_path)
|
||||
// .await
|
||||
// .map_err(|e| FileIOError::from((output_path, e)))?;
|
||||
|
@ -162,11 +167,11 @@
|
|||
|
||||
// if state.init.metadata || state.init.preview_media {
|
||||
// // if any are requested, we can make the query as it'll be used at least once
|
||||
// if let Some(ref object) = info.path_data.object {
|
||||
// if let Some(ref object) = step.file_path.object {
|
||||
// if state.init.metadata {
|
||||
// let metadata = Metadata {
|
||||
// path_id: state.init.path_id,
|
||||
// name: info.path_data.materialized_path.clone(),
|
||||
// file_path_id: step.file_path.id,
|
||||
// name: step.file_path.materialized_path.clone(),
|
||||
// hidden: object.hidden,
|
||||
// favorite: object.favorite,
|
||||
// important: object.important,
|
||||
|
@ -188,38 +193,37 @@
|
|||
// // && (object.has_thumbnail
|
||||
// // || object.has_video_preview || object.has_thumbstrip)
|
||||
|
||||
// // may not be the best - pvm isn't guaranteed to be webp
|
||||
// let pvm_path = ctx
|
||||
// // may not be the best - preview media (thumbnail) isn't guaranteed to be webp
|
||||
// let thumbnail_path = ctx
|
||||
// .library
|
||||
// .config()
|
||||
// .data_directory()
|
||||
// .join("thumbnails")
|
||||
// .join(
|
||||
// info.path_data
|
||||
// step.file_path
|
||||
// .cas_id
|
||||
// .as_ref()
|
||||
// .ok_or(JobError::MissingCasId)?,
|
||||
// )
|
||||
// .with_extension("wepb");
|
||||
|
||||
// if tokio::fs::metadata(&pvm_path).await.is_ok() {
|
||||
// let mut pvm_bytes = Vec::new();
|
||||
// let mut pvm_file = File::open(&pvm_path)
|
||||
// .await
|
||||
// .map_err(|e| FileIOError::from((&pvm_path, e)))?;
|
||||
// pvm_file
|
||||
// .read_to_end(&mut pvm_bytes)
|
||||
// .await
|
||||
// .map_err(|e| FileIOError::from((pvm_path, e)))?;
|
||||
|
||||
// header
|
||||
// .add_preview_media(
|
||||
// LATEST_PREVIEW_MEDIA,
|
||||
// state.init.algorithm,
|
||||
// master_key.clone(),
|
||||
// &pvm_bytes,
|
||||
// )
|
||||
// .await?;
|
||||
// match fs::read(&thumbnail_path).await {
|
||||
// Ok(thumbnail_bytes) => {
|
||||
// header
|
||||
// .add_preview_media(
|
||||
// LATEST_PREVIEW_MEDIA,
|
||||
// state.init.algorithm,
|
||||
// master_key.clone(),
|
||||
// &thumbnail_bytes,
|
||||
// )
|
||||
// .await?;
|
||||
// }
|
||||
// Err(e) if e.kind() == io::ErrorKind::NotFound => {
|
||||
// // If the file just doesn't exist, then we don't care
|
||||
// }
|
||||
// Err(e) => {
|
||||
// return Err(FileIOError::from((thumbnail_path, e)).into());
|
||||
// }
|
||||
// }
|
||||
// } else {
|
||||
// // should use container encryption if it's a directory
|
||||
|
@ -236,8 +240,8 @@
|
|||
// .await?;
|
||||
// } else {
|
||||
// warn!(
|
||||
// "encryption is skipping {} as it isn't a file",
|
||||
// info.path_data.materialized_path
|
||||
// "encryption is skipping {}/{} as it isn't a file",
|
||||
// step.file_path.materialized_path, step.file_path.name
|
||||
// )
|
||||
// }
|
||||
|
||||
|
|
|
@ -1,62 +1,58 @@
|
|||
use crate::{
|
||||
invalidate_query,
|
||||
extract_job_data_mut, invalidate_query,
|
||||
job::{
|
||||
JobError, JobInitData, JobReportUpdate, JobResult, JobState, StatefulJob, WorkerContext,
|
||||
},
|
||||
library::Library,
|
||||
location::file_path_helper::IsolatedFilePathData,
|
||||
prisma::{file_path, location},
|
||||
util::error::FileIOError,
|
||||
};
|
||||
|
||||
use std::{hash::Hash, path::PathBuf};
|
||||
|
||||
use futures::future::try_join_all;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_with::{serde_as, DisplayFromStr};
|
||||
use specta::Type;
|
||||
use tokio::{fs::OpenOptions, io::AsyncWriteExt};
|
||||
use tokio::{
|
||||
fs::{self, OpenOptions},
|
||||
io::AsyncWriteExt,
|
||||
};
|
||||
use tracing::trace;
|
||||
|
||||
use super::{context_menu_fs_info, FsInfo};
|
||||
use super::{
|
||||
error::FileSystemJobsError, get_file_data_from_isolated_file_path,
|
||||
get_location_path_from_location_id, get_many_files_datas, FileData,
|
||||
};
|
||||
|
||||
pub struct FileEraserJob {}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Serialize, Deserialize, Hash, Type)]
|
||||
pub struct FileEraserJobInit {
|
||||
pub location_id: i32,
|
||||
pub path_id: i32,
|
||||
pub location_id: location::id::Type,
|
||||
pub file_path_ids: Vec<file_path::id::Type>,
|
||||
#[specta(type = String)]
|
||||
#[serde_as(as = "DisplayFromStr")]
|
||||
pub passes: usize,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub enum FileEraserJobStep {
|
||||
Directory { path: PathBuf },
|
||||
File { path: PathBuf },
|
||||
}
|
||||
|
||||
impl From<FsInfo> for FileEraserJobStep {
|
||||
fn from(value: FsInfo) -> Self {
|
||||
if value.path_data.is_dir {
|
||||
Self::Directory {
|
||||
path: value.fs_path,
|
||||
}
|
||||
} else {
|
||||
Self::File {
|
||||
path: value.fs_path,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl JobInitData for FileEraserJobInit {
|
||||
type Job = FileEraserJob;
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct FileEraserJobData {
|
||||
location_path: PathBuf,
|
||||
diretories_to_remove: Vec<PathBuf>,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl StatefulJob for FileEraserJob {
|
||||
type Init = FileEraserJobInit;
|
||||
type Data = FsInfo;
|
||||
type Step = FileEraserJobStep;
|
||||
type Data = FileEraserJobData;
|
||||
type Step = FileData;
|
||||
|
||||
const NAME: &'static str = "file_eraser";
|
||||
|
||||
|
@ -65,13 +61,18 @@ impl StatefulJob for FileEraserJob {
|
|||
}
|
||||
|
||||
async fn init(&self, ctx: WorkerContext, state: &mut JobState<Self>) -> Result<(), JobError> {
|
||||
let fs_info =
|
||||
context_menu_fs_info(&ctx.library.db, state.init.location_id, state.init.path_id)
|
||||
.await?;
|
||||
let Library { db, .. } = &ctx.library;
|
||||
|
||||
state.data = Some(fs_info.clone());
|
||||
let location_path = get_location_path_from_location_id(db, state.init.location_id).await?;
|
||||
|
||||
state.steps.push_back(fs_info.into());
|
||||
state.steps = get_many_files_datas(db, &location_path, &state.init.file_path_ids)
|
||||
.await?
|
||||
.into();
|
||||
|
||||
state.data = Some(FileEraserJobData {
|
||||
location_path,
|
||||
diretories_to_remove: vec![],
|
||||
});
|
||||
|
||||
ctx.progress(vec![JobReportUpdate::TaskCount(state.steps.len())]);
|
||||
|
||||
|
@ -86,84 +87,96 @@ impl StatefulJob for FileEraserJob {
|
|||
// need to handle stuff such as querying prisma for all paths of a file, and deleting all of those if requested (with a checkbox in the ui)
|
||||
// maybe a files.countOccurances/and or files.getPath(location_id, path_id) to show how many of these files would be erased (and where?)
|
||||
|
||||
match &state.steps[0] {
|
||||
FileEraserJobStep::File { path } => {
|
||||
let mut file = OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.open(path)
|
||||
.await
|
||||
.map_err(|e| FileIOError::from((path, e)))?;
|
||||
let file_len = file
|
||||
.metadata()
|
||||
.await
|
||||
.map_err(|e| FileIOError::from((path, e)))?
|
||||
.len();
|
||||
let step = &state.steps[0];
|
||||
|
||||
sd_crypto::fs::erase::erase(&mut file, file_len as usize, state.init.passes)
|
||||
.await?;
|
||||
// Had to use `state.steps[0]` all over the place to appease the borrow checker
|
||||
if step.file_path.is_dir {
|
||||
let data = extract_job_data_mut!(state);
|
||||
|
||||
file.set_len(0)
|
||||
.await
|
||||
.map_err(|e| FileIOError::from((path, e)))?;
|
||||
file.flush()
|
||||
.await
|
||||
.map_err(|e| FileIOError::from((path, e)))?;
|
||||
drop(file);
|
||||
let mut dir = tokio::fs::read_dir(&step.full_path)
|
||||
.await
|
||||
.map_err(|e| FileIOError::from((&step.full_path, e)))?;
|
||||
|
||||
trace!("Erasing file: {:?}", path);
|
||||
// Can't use the `step` borrow from here ownwards, or you feel the wrath of the borrow checker
|
||||
while let Some(children_entry) = dir
|
||||
.next_entry()
|
||||
.await
|
||||
.map_err(|e| FileIOError::from((&state.steps[0].full_path, e)))?
|
||||
{
|
||||
let children_path = children_entry.path();
|
||||
|
||||
tokio::fs::remove_file(path)
|
||||
.await
|
||||
.map_err(|e| FileIOError::from((path, e)))?;
|
||||
state.steps.push_back(
|
||||
get_file_data_from_isolated_file_path(
|
||||
&ctx.library.db,
|
||||
&data.location_path,
|
||||
&IsolatedFilePathData::new(
|
||||
state.init.location_id,
|
||||
&data.location_path,
|
||||
&children_path,
|
||||
children_entry
|
||||
.metadata()
|
||||
.await
|
||||
.map_err(|e| FileIOError::from((&children_path, e)))?
|
||||
.is_dir(),
|
||||
)
|
||||
.map_err(FileSystemJobsError::from)?,
|
||||
)
|
||||
.await?,
|
||||
);
|
||||
|
||||
ctx.progress(vec![JobReportUpdate::TaskCount(state.steps.len())]);
|
||||
}
|
||||
FileEraserJobStep::Directory { path } => {
|
||||
let path = path.clone(); // To appease the borrowck
|
||||
data.diretories_to_remove
|
||||
.push(state.steps[0].full_path.clone());
|
||||
} else {
|
||||
let mut file = OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.open(&step.full_path)
|
||||
.await
|
||||
.map_err(|e| FileIOError::from((&step.full_path, e)))?;
|
||||
let file_len = file
|
||||
.metadata()
|
||||
.await
|
||||
.map_err(|e| FileIOError::from((&step.full_path, e)))?
|
||||
.len();
|
||||
|
||||
let mut dir = tokio::fs::read_dir(&path)
|
||||
.await
|
||||
.map_err(|e| FileIOError::from((&path, e)))?;
|
||||
sd_crypto::fs::erase::erase(&mut file, file_len as usize, state.init.passes).await?;
|
||||
|
||||
while let Some(entry) = dir
|
||||
.next_entry()
|
||||
.await
|
||||
.map_err(|e| FileIOError::from((&path, e)))?
|
||||
{
|
||||
let entry_path = entry.path();
|
||||
state.steps.push_back(
|
||||
if entry
|
||||
.metadata()
|
||||
.await
|
||||
.map_err(|e| FileIOError::from((&entry_path, e)))?
|
||||
.is_dir()
|
||||
{
|
||||
FileEraserJobStep::Directory { path: entry_path }
|
||||
} else {
|
||||
FileEraserJobStep::File { path: entry_path }
|
||||
},
|
||||
);
|
||||
file.set_len(0)
|
||||
.await
|
||||
.map_err(|e| FileIOError::from((&step.full_path, e)))?;
|
||||
file.flush()
|
||||
.await
|
||||
.map_err(|e| FileIOError::from((&step.full_path, e)))?;
|
||||
drop(file);
|
||||
|
||||
ctx.progress(vec![JobReportUpdate::TaskCount(state.steps.len())]);
|
||||
}
|
||||
}
|
||||
};
|
||||
trace!("Erasing file: {}", step.full_path.display());
|
||||
|
||||
fs::remove_file(&step.full_path)
|
||||
.await
|
||||
.map_err(|e| FileIOError::from((&step.full_path, e)))?;
|
||||
}
|
||||
|
||||
ctx.progress(vec![JobReportUpdate::CompletedTaskCount(
|
||||
state.step_number + 1,
|
||||
)]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn finalize(&mut self, ctx: WorkerContext, state: &mut JobState<Self>) -> JobResult {
|
||||
let data = state
|
||||
.data
|
||||
.as_ref()
|
||||
.expect("critical error: missing data on job state");
|
||||
if data.path_data.is_dir {
|
||||
tokio::fs::remove_dir_all(&data.fs_path)
|
||||
.await
|
||||
.map_err(|e| FileIOError::from((&data.fs_path, e)))?;
|
||||
}
|
||||
try_join_all(
|
||||
extract_job_data_mut!(state)
|
||||
.diretories_to_remove
|
||||
.drain(..)
|
||||
.map(|data| async {
|
||||
fs::remove_dir_all(&data)
|
||||
.await
|
||||
.map_err(|e| FileIOError::from((data, e)))
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
|
||||
invalidate_query!(ctx.library, "search.paths");
|
||||
|
||||
|
|
|
@ -1,14 +1,31 @@
|
|||
use crate::{
|
||||
location::{file_path_helper::FilePathError, LocationError},
|
||||
prisma::file_path,
|
||||
util::error::FileIOError,
|
||||
};
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use prisma_client_rust::QueryError;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::location::LocationError;
|
||||
|
||||
/// Error type for location related errors
|
||||
/// Error type for file system related jobs errors
|
||||
#[derive(Error, Debug)]
|
||||
pub enum VirtualFSError {
|
||||
pub enum FileSystemJobsError {
|
||||
#[error("Location error: {0}")]
|
||||
LocationError(#[from] LocationError),
|
||||
#[error("Failed to create file or folder on disk at path (path: {0:?})")]
|
||||
CreateFileOrFolder(#[from] std::io::Error),
|
||||
#[error("Database error (error: {0:?})")]
|
||||
DatabaseError(#[from] prisma_client_rust::QueryError),
|
||||
Location(#[from] LocationError),
|
||||
#[error("file_path not in database: <path='{}'>", .0.display())]
|
||||
FilePathNotFound(Box<Path>),
|
||||
#[error("file_path id not in database: <id='{0}'>")]
|
||||
FilePathIdNotFound(file_path::id::Type),
|
||||
#[error("failed to create file or folder on disk")]
|
||||
CreateFileOrFolder(FileIOError),
|
||||
#[error("database error: {0}")]
|
||||
Database(#[from] QueryError),
|
||||
#[error(transparent)]
|
||||
FilePath(#[from] FilePathError),
|
||||
#[error("source and destination path are the same: {}", .0.display())]
|
||||
MatchingSrcDest(Box<Path>),
|
||||
#[error("action would overwrite another file: {}", .0.display())]
|
||||
WouldOverwrite(Box<Path>),
|
||||
}
|
||||
|
|
|
@ -1,25 +1,28 @@
|
|||
use crate::{
|
||||
job::JobError,
|
||||
location::file_path_helper::{file_path_with_object, IsolatedFilePathData},
|
||||
location::{
|
||||
file_path_helper::{file_path_with_object, IsolatedFilePathData},
|
||||
LocationError,
|
||||
},
|
||||
prisma::{file_path, location, PrismaClient},
|
||||
};
|
||||
|
||||
use std::{ffi::OsStr, path::PathBuf};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub mod create;
|
||||
pub mod delete;
|
||||
pub mod erase;
|
||||
|
||||
pub mod copy;
|
||||
pub mod cut;
|
||||
|
||||
// pub mod decrypt;
|
||||
pub mod delete;
|
||||
// pub mod encrypt;
|
||||
|
||||
pub mod error;
|
||||
|
||||
pub mod erase;
|
||||
use error::FileSystemJobsError;
|
||||
|
||||
// pub const BYTES_EXT: &str = ".bytes";
|
||||
|
||||
|
@ -30,54 +33,144 @@ pub enum ObjectType {
|
|||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct FsInfo {
|
||||
pub path_data: file_path_with_object::Data,
|
||||
pub fs_path: PathBuf,
|
||||
}
|
||||
|
||||
pub fn osstr_to_string(os_str: Option<&OsStr>) -> Result<String, JobError> {
|
||||
os_str
|
||||
.and_then(OsStr::to_str)
|
||||
.map(str::to_string)
|
||||
.ok_or(JobError::OsStr)
|
||||
pub struct FileData {
|
||||
pub file_path: file_path_with_object::Data,
|
||||
pub full_path: PathBuf,
|
||||
}
|
||||
|
||||
pub async fn get_location_path_from_location_id(
|
||||
db: &PrismaClient,
|
||||
location_id: i32,
|
||||
) -> Result<PathBuf, JobError> {
|
||||
Ok(db
|
||||
.location()
|
||||
location_id: file_path::id::Type,
|
||||
) -> Result<PathBuf, FileSystemJobsError> {
|
||||
db.location()
|
||||
.find_unique(location::id::equals(location_id))
|
||||
.exec()
|
||||
.await?
|
||||
.ok_or(JobError::MissingData {
|
||||
value: String::from("location which matches location_id"),
|
||||
})?
|
||||
.path
|
||||
.ok_or(JobError::MissingPath)?
|
||||
.into())
|
||||
.and_then(|location| location.path.map(PathBuf::from))
|
||||
.ok_or(FileSystemJobsError::Location(LocationError::IdNotFound(
|
||||
location_id,
|
||||
)))
|
||||
}
|
||||
|
||||
pub async fn context_menu_fs_info(
|
||||
pub async fn get_many_files_datas(
|
||||
db: &PrismaClient,
|
||||
location_id: i32,
|
||||
file_path_id: i32,
|
||||
) -> Result<FsInfo, JobError> {
|
||||
let path_data = db
|
||||
.file_path()
|
||||
.find_unique(file_path::id::equals(file_path_id))
|
||||
location_path: impl AsRef<Path>,
|
||||
file_path_ids: &[file_path::id::Type],
|
||||
) -> Result<Vec<FileData>, FileSystemJobsError> {
|
||||
let location_path = location_path.as_ref();
|
||||
|
||||
db._batch(
|
||||
file_path_ids
|
||||
.iter()
|
||||
.map(|file_path_id| {
|
||||
db.file_path()
|
||||
.find_unique(file_path::id::equals(*file_path_id))
|
||||
.include(file_path_with_object::include())
|
||||
})
|
||||
// FIXME:(fogodev -> Brendonovich) this collect is a workaround to a weird higher ranker lifetime error on
|
||||
// the _batch function, it should be removed once the error is fixed
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.zip(file_path_ids.iter())
|
||||
.map(|(maybe_file_path, file_path_id)| {
|
||||
maybe_file_path
|
||||
.ok_or(FileSystemJobsError::FilePathIdNotFound(*file_path_id))
|
||||
.map(|path_data| FileData {
|
||||
full_path: location_path.join(IsolatedFilePathData::from(&path_data)),
|
||||
file_path: path_data,
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
}
|
||||
|
||||
pub async fn get_file_data_from_isolated_file_path(
|
||||
db: &PrismaClient,
|
||||
location_path: impl AsRef<Path>,
|
||||
iso_file_path: &IsolatedFilePathData<'_>,
|
||||
) -> Result<FileData, FileSystemJobsError> {
|
||||
db.file_path()
|
||||
.find_unique(iso_file_path.into())
|
||||
.include(file_path_with_object::include())
|
||||
.exec()
|
||||
.await?
|
||||
.ok_or(JobError::MissingData {
|
||||
value: String::from("file_path that matches both location id and path id"),
|
||||
})?;
|
||||
|
||||
Ok(FsInfo {
|
||||
fs_path: get_location_path_from_location_id(db, location_id)
|
||||
.await?
|
||||
.join(IsolatedFilePathData::from(&path_data)),
|
||||
path_data,
|
||||
})
|
||||
.ok_or_else(|| {
|
||||
FileSystemJobsError::FilePathNotFound(
|
||||
AsRef::<Path>::as_ref(iso_file_path)
|
||||
.to_path_buf()
|
||||
.into_boxed_path(),
|
||||
)
|
||||
})
|
||||
.map(|path_data| FileData {
|
||||
full_path: location_path
|
||||
.as_ref()
|
||||
.join(IsolatedFilePathData::from(&path_data)),
|
||||
file_path: path_data,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn fetch_source_and_target_location_paths(
|
||||
db: &PrismaClient,
|
||||
source_location_id: location::id::Type,
|
||||
target_location_id: location::id::Type,
|
||||
) -> Result<(PathBuf, PathBuf), FileSystemJobsError> {
|
||||
match db
|
||||
._batch((
|
||||
db.location()
|
||||
.find_unique(location::id::equals(source_location_id)),
|
||||
db.location()
|
||||
.find_unique(location::id::equals(target_location_id)),
|
||||
))
|
||||
.await?
|
||||
{
|
||||
(Some(source_location), Some(target_location)) => Ok((
|
||||
source_location
|
||||
.path
|
||||
.map(PathBuf::from)
|
||||
.ok_or(FileSystemJobsError::Location(LocationError::MissingPath(
|
||||
source_location_id,
|
||||
)))?,
|
||||
target_location
|
||||
.path
|
||||
.map(PathBuf::from)
|
||||
.ok_or(FileSystemJobsError::Location(LocationError::MissingPath(
|
||||
target_location_id,
|
||||
)))?,
|
||||
)),
|
||||
(None, _) => Err(FileSystemJobsError::Location(LocationError::IdNotFound(
|
||||
source_location_id,
|
||||
))),
|
||||
(_, None) => Err(FileSystemJobsError::Location(LocationError::IdNotFound(
|
||||
target_location_id,
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
fn construct_target_filename(
|
||||
source_file_data: &FileData,
|
||||
target_file_name_suffix: &Option<String>,
|
||||
) -> String {
|
||||
// extension wizardry for cloning and such
|
||||
// if no suffix has been selected, just use the file name
|
||||
// if a suffix is provided and it's a directory, use the directory name + suffix
|
||||
// if a suffix is provided and it's a file, use the (file name + suffix).extension
|
||||
|
||||
if let Some(ref suffix) = target_file_name_suffix {
|
||||
if source_file_data.file_path.is_dir {
|
||||
format!("{}{suffix}", source_file_data.file_path.name)
|
||||
} else {
|
||||
format!(
|
||||
"{}{suffix}.{}",
|
||||
source_file_data.file_path.name, source_file_data.file_path.extension,
|
||||
)
|
||||
}
|
||||
} else if source_file_data.file_path.is_dir {
|
||||
source_file_data.file_path.name.clone()
|
||||
} else {
|
||||
format!(
|
||||
"{}.{}",
|
||||
source_file_data.file_path.name, source_file_data.file_path.extension
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,10 +3,7 @@ use crate::{
|
|||
invalidate_query,
|
||||
job::{JobError, JobReportUpdate, JobResult, JobState, WorkerContext},
|
||||
library::Library,
|
||||
location::{
|
||||
file_path_helper::{file_path_for_thumbnailer, FilePathError, IsolatedFilePathData},
|
||||
LocationId,
|
||||
},
|
||||
location::file_path_helper::{file_path_for_thumbnailer, FilePathError, IsolatedFilePathData},
|
||||
prisma::location,
|
||||
util::{error::FileIOError, version_manager::VersionManagerError},
|
||||
};
|
||||
|
@ -106,7 +103,7 @@ pub enum ThumbnailerError {
|
|||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct ThumbnailerJobReport {
|
||||
location_id: LocationId,
|
||||
location_id: location::id::Type,
|
||||
path: PathBuf,
|
||||
thumbnails_created: u32,
|
||||
}
|
||||
|
@ -242,7 +239,7 @@ async fn process_step(
|
|||
.expect("critical error: missing data on job state");
|
||||
|
||||
let step_result = inner_process_step(
|
||||
&step,
|
||||
step,
|
||||
&data.location_path,
|
||||
&data.thumbnail_dir,
|
||||
&state.init.location,
|
||||
|
@ -261,12 +258,14 @@ async fn process_step(
|
|||
|
||||
pub async fn inner_process_step(
|
||||
step: &ThumbnailerJobStep,
|
||||
location_path: &PathBuf,
|
||||
thumbnail_dir: &PathBuf,
|
||||
location_path: impl AsRef<Path>,
|
||||
thumbnail_dir: impl AsRef<Path>,
|
||||
location: &location::Data,
|
||||
library: &Library,
|
||||
) -> Result<(), JobError> {
|
||||
let ThumbnailerJobStep { file_path, kind } = step;
|
||||
let location_path = location_path.as_ref();
|
||||
let thumbnail_dir = thumbnail_dir.as_ref();
|
||||
|
||||
// assemble the file path
|
||||
let path = location_path.join(IsolatedFilePathData::from((location.id, file_path)));
|
||||
|
|
|
@ -5,12 +5,9 @@ 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_thumbnailer, IsolatedFilePathData,
|
||||
},
|
||||
LocationId,
|
||||
location::file_path_helper::{
|
||||
ensure_file_path_exists, ensure_sub_path_is_directory, ensure_sub_path_is_in_location,
|
||||
file_path_for_thumbnailer, IsolatedFilePathData,
|
||||
},
|
||||
object::preview::thumbnail,
|
||||
prisma::{file_path, location, PrismaClient},
|
||||
|
@ -117,7 +114,7 @@ pub async fn shallow_thumbnailer(
|
|||
.flatten();
|
||||
|
||||
for file in all_files {
|
||||
thumbnail::inner_process_step(&file, &location_path, &thumbnail_dir, location, &library)
|
||||
thumbnail::inner_process_step(&file, &location_path, &thumbnail_dir, location, library)
|
||||
.await?;
|
||||
}
|
||||
|
||||
|
@ -128,7 +125,7 @@ pub async fn shallow_thumbnailer(
|
|||
|
||||
async fn get_files_by_extensions(
|
||||
db: &PrismaClient,
|
||||
location_id: LocationId,
|
||||
location_id: location::id::Type,
|
||||
parent_isolated_file_path_data: &IsolatedFilePathData<'_>,
|
||||
extensions: &[Extension],
|
||||
kind: ThumbnailerJobStepKind,
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use crate::{
|
||||
extract_job_data,
|
||||
job::{
|
||||
JobError, JobInitData, JobReportUpdate, JobResult, JobState, StatefulJob, WorkerContext,
|
||||
},
|
||||
|
@ -161,13 +162,7 @@ impl StatefulJob for ThumbnailerJob {
|
|||
}
|
||||
|
||||
async fn finalize(&mut self, ctx: WorkerContext, state: &mut JobState<Self>) -> JobResult {
|
||||
finalize_thumbnailer(
|
||||
state
|
||||
.data
|
||||
.as_ref()
|
||||
.expect("critical error: missing data on job state"),
|
||||
ctx,
|
||||
)
|
||||
finalize_thumbnailer(extract_job_data!(state), ctx)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
use crate::{
|
||||
extract_job_data,
|
||||
job::{
|
||||
JobError, JobInitData, JobReportUpdate, JobResult, JobState, StatefulJob, WorkerContext,
|
||||
},
|
||||
library::Library,
|
||||
location::file_path_helper::{file_path_for_object_validator, IsolatedFilePathData},
|
||||
prisma::file_path,
|
||||
prisma::{file_path, location},
|
||||
sync,
|
||||
util::error::FileIOError,
|
||||
};
|
||||
|
@ -32,7 +33,7 @@ pub struct ObjectValidatorJobState {
|
|||
// The validator can
|
||||
#[derive(Serialize, Deserialize, Debug, Hash)]
|
||||
pub struct ObjectValidatorJobInit {
|
||||
pub location_id: i32,
|
||||
pub location_id: location::id::Type,
|
||||
pub path: PathBuf,
|
||||
pub background: bool,
|
||||
}
|
||||
|
@ -86,10 +87,7 @@ impl StatefulJob for ObjectValidatorJob {
|
|||
let Library { db, sync, .. } = &ctx.library;
|
||||
|
||||
let file_path = &state.steps[0];
|
||||
let data = state
|
||||
.data
|
||||
.as_ref()
|
||||
.expect("critical error: missing data on job state");
|
||||
let data = extract_job_data!(state);
|
||||
|
||||
// this is to skip files that already have checksums
|
||||
// i'm unsure what the desired behaviour is in this case
|
||||
|
@ -129,10 +127,7 @@ impl StatefulJob for ObjectValidatorJob {
|
|||
}
|
||||
|
||||
async fn finalize(&mut self, _ctx: WorkerContext, state: &mut JobState<Self>) -> JobResult {
|
||||
let data = state
|
||||
.data
|
||||
.as_ref()
|
||||
.expect("critical error: missing data on job state");
|
||||
let data = extract_job_data!(state);
|
||||
info!(
|
||||
"finalizing validator job at {}: {} tasks",
|
||||
data.root_path.display(),
|
||||
|
|
|
@ -54,7 +54,7 @@ pub trait Migrate: Sized + DeserializeOwned + Serialize + Default {
|
|||
};
|
||||
|
||||
if let Some(obj) = y.as_object_mut() {
|
||||
if let Some(_) = obj.get("version").and_then(|v| v.as_str()) {
|
||||
if obj.contains_key("version") {
|
||||
return Err(MigratorError::HasSuperLegacyConfig); // This is just to make the error nicer
|
||||
} else {
|
||||
return Err(err.into());
|
||||
|
@ -74,7 +74,7 @@ pub trait Migrate: Sized + DeserializeOwned + Serialize + Default {
|
|||
let is_latest = cfg.version == Self::CURRENT_VERSION;
|
||||
for v in (cfg.version + 1)..=Self::CURRENT_VERSION {
|
||||
cfg.version = v;
|
||||
match Self::migrate(v, &mut cfg.other, &ctx).await {
|
||||
match Self::migrate(v, &mut cfg.other, ctx).await {
|
||||
Ok(()) => (),
|
||||
Err(err) => {
|
||||
file.write_all(serde_json::to_string(&cfg)?.as_bytes())?; // Writes updated version
|
||||
|
@ -133,6 +133,7 @@ pub enum MigratorError {
|
|||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::unwrap_used, clippy::panic)]
|
||||
mod test {
|
||||
use std::{fs, io::Read, path::PathBuf};
|
||||
|
||||
|
|
|
@ -5,9 +5,9 @@ authors = ["Jake Robinson <jake@spacedrive.com>"]
|
|||
readme = "README.md"
|
||||
description = "A library to handle cryptographic functions within Spacedrive"
|
||||
rust-version = "1.67.0"
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
edition.workspace = true
|
||||
license = { workspace = true }
|
||||
repository = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
[features]
|
||||
rspc = ["dep:rspc", "dep:specta"]
|
||||
|
|
|
@ -3,9 +3,9 @@ name = "deps-generator"
|
|||
version = "0.0.0"
|
||||
authors = ["Jake Robinson <jake@spacedrive.com>"]
|
||||
description = "A tool to compile all Spacedrive dependencies and their respective licenses"
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
edition.workspace = true
|
||||
license = { workspace = true }
|
||||
repository = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
[dependencies]
|
||||
reqwest = { version = "0.11.18", features = ["blocking"] }
|
||||
|
|
|
@ -5,9 +5,9 @@ authors = ["Ericson Soares <ericson.ds999@gmail.com>"]
|
|||
readme = "README.md"
|
||||
description = "A simple library to generate video thumbnails using ffmpeg with the webp format"
|
||||
rust-version = "1.64.0"
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
edition.workspace = true
|
||||
license = { workspace = true }
|
||||
repository = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
|
|
|
@ -5,9 +5,9 @@ authors = [
|
|||
"Brendan Allen <brendan@spacedrive.com>",
|
||||
"Jamie Pine <jamie@spacedrive.com>",
|
||||
]
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
edition.workspace = true
|
||||
license = { workspace = true }
|
||||
repository = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
|
|
|
@ -2,9 +2,9 @@
|
|||
name = "sd-heif"
|
||||
version = "0.1.0"
|
||||
authors = ["Jake Robinson <jake@spacedrive.com>"]
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
edition.workspace = true
|
||||
license = { workspace = true }
|
||||
repository = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
[dependencies]
|
||||
libheif-rs = "0.19.2"
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
[package]
|
||||
name = "sd-macos"
|
||||
version = "0.1.0"
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
edition.workspace = true
|
||||
license = { workspace = true }
|
||||
repository = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
swift-rs.workspace = true
|
||||
swift-rs = { workspace = true }
|
||||
|
||||
[build-dependencies]
|
||||
swift-rs = { workspace = true, features = ["build"] }
|
||||
|
|
|
@ -3,9 +3,9 @@ name = "sd-p2p"
|
|||
version = "0.1.0"
|
||||
description = "Rust Peer to Peer Networking Library"
|
||||
authors = ["Oscar Beaumont <oscar@otbeaumont.me>"]
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
edition.workspace = true
|
||||
license = { workspace = true }
|
||||
repository = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
[package]
|
||||
name = "prisma-cli"
|
||||
version = "0.1.0"
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
edition.workspace = true
|
||||
license = { workspace = true }
|
||||
repository = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
[dependencies]
|
||||
prisma-client-rust-cli = { workspace = true }
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
[package]
|
||||
name = "sd-sync-generator"
|
||||
version = "0.1.0"
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
edition.workspace = true
|
||||
license = { workspace = true }
|
||||
repository = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
[package]
|
||||
name = "sd-sync"
|
||||
version = "0.1.0"
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
edition.workspace = true
|
||||
license = { workspace = true }
|
||||
repository = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
[dependencies]
|
||||
rand = "0.8.5"
|
||||
|
|
|
@ -3,9 +3,9 @@ name = "sd-sync-example"
|
|||
version = "0.1.0"
|
||||
rust-version = "1.64"
|
||||
publish = false
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
edition.workspace = true
|
||||
license = { workspace = true }
|
||||
repository = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
[dependencies]
|
||||
serde_json = "1.0.85"
|
||||
|
|
|
@ -87,7 +87,7 @@ export default (props: { objectId: number }) => {
|
|||
e.preventDefault();
|
||||
assignTag.mutate({
|
||||
tag_id: tag.id,
|
||||
object_id: props.objectId,
|
||||
object_ids: [props.objectId],
|
||||
unassign: active
|
||||
});
|
||||
}}
|
||||
|
|
|
@ -92,9 +92,9 @@ export default (props: PropsWithChildren) => {
|
|||
params.path &&
|
||||
copyFiles.mutate({
|
||||
source_location_id: store.cutCopyState.sourceLocationId,
|
||||
source_path_id: store.cutCopyState.sourcePathId,
|
||||
sources_file_path_ids: [store.cutCopyState.sourcePathId],
|
||||
target_location_id: store.locationId,
|
||||
target_path: params.path,
|
||||
target_location_relative_directory_path: params.path,
|
||||
target_file_name_suffix: null
|
||||
});
|
||||
} else {
|
||||
|
@ -102,9 +102,9 @@ export default (props: PropsWithChildren) => {
|
|||
params.path &&
|
||||
cutFiles.mutate({
|
||||
source_location_id: store.cutCopyState.sourceLocationId,
|
||||
source_path_id: store.cutCopyState.sourcePathId,
|
||||
sources_file_path_ids: [store.cutCopyState.sourcePathId],
|
||||
target_location_id: store.locationId,
|
||||
target_path: params.path
|
||||
target_location_relative_directory_path: params.path
|
||||
});
|
||||
}
|
||||
}}
|
||||
|
|
|
@ -82,7 +82,7 @@ export default ({ data }: Props) => {
|
|||
<ContextMenu.Item
|
||||
label="Remove from recents"
|
||||
onClick={() =>
|
||||
data.item.object_id && removeFromRecents.mutate(data.item.object_id)
|
||||
data.item.object_id && removeFromRecents.mutate([data.item.object_id])
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
@ -129,9 +129,9 @@ export default ({ data }: Props) => {
|
|||
|
||||
copyFiles.mutate({
|
||||
source_location_id: store.locationId!,
|
||||
source_path_id: data.item.id,
|
||||
sources_file_path_ids: [data.item.id],
|
||||
target_location_id: store.locationId!,
|
||||
target_path: params.path,
|
||||
target_location_relative_directory_path: params.path,
|
||||
target_file_name_suffix: ' copy'
|
||||
});
|
||||
}}
|
||||
|
@ -303,7 +303,9 @@ const OpenOrDownloadOptions = (props: { data: ExplorerItem }) => {
|
|||
props.data.type === 'Path' &&
|
||||
props.data.item.object_id &&
|
||||
updateAccessTime.mutate(props.data.item.object_id);
|
||||
openFilePath(library.uuid, filePath.id);
|
||||
|
||||
// FIXME: treat error properly
|
||||
openFilePath(library.uuid, [filePath.id]);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -34,29 +34,29 @@ const Items = ({
|
|||
}) => {
|
||||
const { library } = useLibraryContext();
|
||||
|
||||
const items = useQuery(
|
||||
const items = useQuery<any[]>(
|
||||
['openWith', filePath.id],
|
||||
() => actions.getFilePathOpenWithApps(library.uuid, filePath.id),
|
||||
() => actions.getFilePathOpenWithApps(library.uuid, [filePath.id]),
|
||||
{ suspense: true }
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{items.data?.map((d) => (
|
||||
{items.data?.map((data) => (
|
||||
<ContextMenu.Item
|
||||
key={d.name}
|
||||
key={data.name}
|
||||
onClick={async () => {
|
||||
try {
|
||||
await actions.openFilePathWith(library.uuid, filePath.id, d.url);
|
||||
await actions.openFilePathWith(library.uuid, [(filePath.id, data.c.url)]);
|
||||
} catch {
|
||||
showAlertDialog({
|
||||
title: 'Error',
|
||||
value: `Failed to open file, with: ${d.url}`
|
||||
value: `Failed to open file, with: ${data.url}`
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
{d.name}
|
||||
{data.name}
|
||||
</ContextMenu.Item>
|
||||
)) ?? <p> No apps available </p>}
|
||||
</>
|
||||
|
|
|
@ -69,7 +69,7 @@
|
|||
// onSubmit={form.handleSubmit((data) =>
|
||||
// decryptFile.mutateAsync({
|
||||
// location_id: props.location_id,
|
||||
// path_id: props.path_id,
|
||||
// file_path_ids: [props.path_id],
|
||||
// output_path: data.outputPath !== '' ? data.outputPath : null,
|
||||
// mount_associated_key: data.mountAssociatedKey,
|
||||
// password: data.type === 'password' ? data.password : null,
|
||||
|
|
|
@ -18,7 +18,7 @@ export default (props: Propps) => {
|
|||
onSubmit={form.handleSubmit(() =>
|
||||
deleteFile.mutateAsync({
|
||||
location_id: props.location_id,
|
||||
path_id: props.path_id
|
||||
file_path_ids: [props.path_id]
|
||||
})
|
||||
)}
|
||||
dialog={useDialog(props)}
|
||||
|
|
|
@ -71,10 +71,9 @@
|
|||
// algorithm: data.encryptionAlgo as Algorithm,
|
||||
// key_uuid: data.key,
|
||||
// location_id: props.location_id,
|
||||
// path_id: props.path_id,
|
||||
// file_path_ids: [props.path_id],
|
||||
// metadata: data.metadata,
|
||||
// preview_media: data.previewMedia,
|
||||
// output_path: data.outputPath || null
|
||||
// preview_media: data.previewMedia
|
||||
// })
|
||||
// )}
|
||||
// dialog={useDialog(props)}
|
||||
|
|
|
@ -30,7 +30,7 @@ export default (props: Props) => {
|
|||
onSubmit={form.handleSubmit((data) =>
|
||||
eraseFile.mutateAsync({
|
||||
location_id: props.location_id,
|
||||
path_id: props.path_id,
|
||||
file_path_ids: [props.path_id],
|
||||
passes: data.passes.toString()
|
||||
})
|
||||
)}
|
||||
|
|
|
@ -58,8 +58,12 @@ export default ({
|
|||
if (newName !== oldName) {
|
||||
renameFile.mutate({
|
||||
location_id: filePathData.location_id,
|
||||
file_name: oldName,
|
||||
new_file_name: newName
|
||||
kind: {
|
||||
One: {
|
||||
from_file_path_id: filePathData.id,
|
||||
to: newName
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -51,13 +51,13 @@ const Thumbnail = memo(
|
|||
videoBarsSize
|
||||
? size && size.height >= size.width
|
||||
? {
|
||||
borderLeftWidth: videoBarsSize,
|
||||
borderRightWidth: videoBarsSize
|
||||
}
|
||||
borderLeftWidth: videoBarsSize,
|
||||
borderRightWidth: videoBarsSize
|
||||
}
|
||||
: {
|
||||
borderTopWidth: videoBarsSize,
|
||||
borderBottomWidth: videoBarsSize
|
||||
}
|
||||
borderTopWidth: videoBarsSize,
|
||||
borderBottomWidth: videoBarsSize
|
||||
}
|
||||
: {}
|
||||
}
|
||||
onLoad={props.onLoad}
|
||||
|
@ -75,11 +75,11 @@ const Thumbnail = memo(
|
|||
props.cover
|
||||
? {}
|
||||
: size
|
||||
? {
|
||||
? {
|
||||
marginTop: Math.floor(size.height / 2) - 2,
|
||||
marginLeft: Math.floor(size.width / 2) - 2
|
||||
}
|
||||
: { display: 'none' }
|
||||
}
|
||||
: { display: 'none' }
|
||||
}
|
||||
className={clsx(
|
||||
props.cover
|
||||
|
@ -200,9 +200,9 @@ function FileThumb({ size, cover, ...props }: ThumbProps) {
|
|||
className={clsx(
|
||||
'relative flex shrink-0 items-center justify-center',
|
||||
size &&
|
||||
kind !== 'Video' &&
|
||||
thumbType !== ThumbType.Icon &&
|
||||
'border-2 border-transparent',
|
||||
kind !== 'Video' &&
|
||||
thumbType !== ThumbType.Icon &&
|
||||
'border-2 border-transparent',
|
||||
size || ['h-full', cover ? 'w-full overflow-hidden' : 'w-[90%]'],
|
||||
props.className
|
||||
)}
|
||||
|
@ -299,9 +299,9 @@ function FileThumb({ size, cover, ...props }: ThumbProps) {
|
|||
'shadow shadow-black/30'
|
||||
],
|
||||
size &&
|
||||
(kind === 'Video'
|
||||
? 'border-x-0 border-black'
|
||||
: size > 60 && 'border-2 border-app-line'),
|
||||
(kind === 'Video'
|
||||
? 'border-x-0 border-black'
|
||||
: size > 60 && 'border-2 border-app-line'),
|
||||
props.className
|
||||
)}
|
||||
crossOrigin={ThumbType.Original && 'anonymous'} // Here it is ok, because it is not a react attr
|
||||
|
|
|
@ -61,7 +61,7 @@ export const ViewItem = ({ data, children, ...props }: ViewItemProps) => {
|
|||
updateAccessTime.mutate(data.item.object_id);
|
||||
}
|
||||
|
||||
openFilePath(library.uuid, filePath.id);
|
||||
openFilePath(library.uuid, [filePath.id]);
|
||||
} else {
|
||||
const { kind } = getExplorerItemData(data);
|
||||
|
||||
|
|
|
@ -32,16 +32,17 @@ const getNiceData = (
|
|||
name: isGroup
|
||||
? 'Indexing paths'
|
||||
: job.metadata?.location_path
|
||||
? `Indexed paths at ${job.metadata?.location_path} `
|
||||
: `Processing added location...`,
|
||||
? `Indexed paths at ${job.metadata?.location_path} `
|
||||
: `Processing added location...`,
|
||||
icon: Folder,
|
||||
subtext: `${numberWithCommas(job.metadata?.total_paths || 0)} ${appendPlural(job, 'path')}`
|
||||
},
|
||||
thumbnailer: {
|
||||
name: `${job.status === 'Running' || job.status === 'Queued'
|
||||
? 'Generating thumbnails'
|
||||
: 'Generated thumbnails'
|
||||
}`,
|
||||
name: `${
|
||||
job.status === 'Running' || job.status === 'Queued'
|
||||
? 'Generating thumbnails'
|
||||
: 'Generated thumbnails'
|
||||
}`,
|
||||
icon: Camera,
|
||||
subtext: `${numberWithCommas(job.completed_task_count)} of ${numberWithCommas(
|
||||
job.task_count
|
||||
|
@ -53,10 +54,11 @@ const getNiceData = (
|
|||
subtext: `${numberWithCommas(job.task_count)} ${appendPlural(job, 'item')}`
|
||||
},
|
||||
file_identifier: {
|
||||
name: `${job.status === 'Running' || job.status === 'Queued'
|
||||
? 'Extracting metadata'
|
||||
: 'Extracted metadata'
|
||||
}`,
|
||||
name: `${
|
||||
job.status === 'Running' || job.status === 'Queued'
|
||||
? 'Extracting metadata'
|
||||
: 'Extracted metadata'
|
||||
}`,
|
||||
icon: Eye,
|
||||
subtext:
|
||||
job.message ||
|
||||
|
|
|
@ -72,8 +72,9 @@ function JobGroup({ data, clearJob }: JobGroupProps) {
|
|||
<div className="truncate">
|
||||
<p className="truncate font-semibold">
|
||||
{allJobsCompleted
|
||||
? `Added location "${data.metadata.init.location.name || ''
|
||||
}"`
|
||||
? `Added location "${
|
||||
data.metadata.init.location.name || ''
|
||||
}"`
|
||||
: `Indexing "${data.metadata.init.location.name || ''}"`}
|
||||
</p>
|
||||
<p className="my-[2px] text-sidebar-inkFaint">
|
||||
|
|
|
@ -14,7 +14,7 @@ export default () => {
|
|||
<div
|
||||
className={clsx(
|
||||
'relative flex min-h-full w-44 shrink-0 grow-0 flex-col gap-2.5 border-r border-sidebar-divider bg-sidebar px-2.5 pb-2 pt-2.5',
|
||||
macOnly(os, 'bg-opacity-[0.65]'),
|
||||
macOnly(os, 'bg-opacity-[0.65]')
|
||||
)}
|
||||
>
|
||||
{showControls && <MacTrafficLights className="absolute left-[13px] top-[13px] z-50" />}
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
import { useInfiniteQuery } from '@tanstack/react-query';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { z } from 'zod';
|
||||
import {
|
||||
ExplorerItem,
|
||||
useLibraryContext,
|
||||
|
@ -6,9 +9,6 @@ import {
|
|||
useRspcLibraryContext
|
||||
} from '@sd/client';
|
||||
import { Folder } from '~/components/Folder';
|
||||
import { useInfiniteQuery } from '@tanstack/react-query';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { z } from 'zod';
|
||||
import {
|
||||
getExplorerStore,
|
||||
useExplorerStore,
|
||||
|
@ -57,14 +57,16 @@ export const Component = () => {
|
|||
<>
|
||||
<TopBarPortal
|
||||
left={
|
||||
<div className='group flex flex-row items-center space-x-2'>
|
||||
<div className="group flex flex-row items-center space-x-2">
|
||||
<span>
|
||||
<Folder size={22} className="ml-3 mr-2 mt-[-1px] inline-block" />
|
||||
<span className="text-sm font-medium">
|
||||
{path ? getLastSectionOfPath(path) : location.data?.name}
|
||||
</span>
|
||||
</span>
|
||||
{location.data && <LocationOptions location={location.data} path={path || ""} />}
|
||||
{location.data && (
|
||||
<LocationOptions location={location.data} path={path || ''} />
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
right={
|
||||
|
|
|
@ -58,7 +58,7 @@ export default function LocationOptions({ location, path }: { location: Location
|
|||
autoFocus
|
||||
className="mb-2"
|
||||
value={currentPath ?? ''}
|
||||
onChange={() => {}}
|
||||
onChange={() => { }}
|
||||
right={
|
||||
<Tooltip label="Copy path to clipboard" className="flex">
|
||||
<Button
|
||||
|
|
|
@ -19,7 +19,7 @@ export default (props: UseDialogProps & { assignToObject?: number }) => {
|
|||
if (props.assignToObject !== undefined) {
|
||||
assignTag.mutate({
|
||||
tag_id: tag.id,
|
||||
object_id: props.assignToObject,
|
||||
object_ids: [props.assignToObject],
|
||||
unassign: false
|
||||
});
|
||||
}
|
||||
|
|
|
@ -8,11 +8,11 @@ export function useIsDark(): boolean {
|
|||
const [isDark, setIsDark] = useState(themeStore.theme === 'dark');
|
||||
|
||||
useEffect(() => {
|
||||
if (themeStore.syncThemeWithSystem) {
|
||||
if (themeStore.syncThemeWithSystem) {
|
||||
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||||
setIsDark(true);
|
||||
} else setIsDark(false);
|
||||
} else {
|
||||
} else {
|
||||
if (themeStore.theme === 'dark') {
|
||||
setIsDark(true);
|
||||
} else if (themeStore.theme === 'vanilla') {
|
||||
|
|
|
@ -3,42 +3,42 @@ import { useEffect } from 'react';
|
|||
import { usePlatform } from '..';
|
||||
|
||||
export function useTheme() {
|
||||
const themeStore = useThemeStore();
|
||||
const { lockAppTheme } = usePlatform();
|
||||
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
const themeStore = useThemeStore();
|
||||
const { lockAppTheme } = usePlatform();
|
||||
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
|
||||
useEffect(() => {
|
||||
const handleThemeChange = () => {
|
||||
if (themeStore.syncThemeWithSystem) {
|
||||
lockAppTheme?.('Auto');
|
||||
if (systemTheme.matches) {
|
||||
document.documentElement.classList.remove('vanilla-theme');
|
||||
document.documentElement.style.setProperty('--dark-hue', getThemeStore().hueValue.toString());
|
||||
getThemeStore().theme = 'dark';
|
||||
} else {
|
||||
document.documentElement.classList.add('vanilla-theme');
|
||||
document.documentElement.style.setProperty('--light-hue', getThemeStore().hueValue.toString());
|
||||
getThemeStore().theme = 'vanilla';
|
||||
}
|
||||
} else {
|
||||
if (themeStore.theme === 'dark') {
|
||||
document.documentElement.classList.remove('vanilla-theme');
|
||||
document.documentElement.style.setProperty('--dark-hue', getThemeStore().hueValue.toString());
|
||||
lockAppTheme?.('Dark');
|
||||
} else if (themeStore.theme === 'vanilla') {
|
||||
document.documentElement.classList.add('vanilla-theme');
|
||||
document.documentElement.style.setProperty('--light-hue', getThemeStore().hueValue.toString());
|
||||
lockAppTheme?.('Light');
|
||||
}
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
const handleThemeChange = () => {
|
||||
if (themeStore.syncThemeWithSystem) {
|
||||
lockAppTheme?.('Auto');
|
||||
if (systemTheme.matches) {
|
||||
document.documentElement.classList.remove('vanilla-theme');
|
||||
document.documentElement.style.setProperty('--dark-hue', getThemeStore().hueValue.toString());
|
||||
getThemeStore().theme = 'dark';
|
||||
} else {
|
||||
document.documentElement.classList.add('vanilla-theme');
|
||||
document.documentElement.style.setProperty('--light-hue', getThemeStore().hueValue.toString());
|
||||
getThemeStore().theme = 'vanilla';
|
||||
}
|
||||
} else {
|
||||
if (themeStore.theme === 'dark') {
|
||||
document.documentElement.classList.remove('vanilla-theme');
|
||||
document.documentElement.style.setProperty('--dark-hue', getThemeStore().hueValue.toString());
|
||||
lockAppTheme?.('Dark');
|
||||
} else if (themeStore.theme === 'vanilla') {
|
||||
document.documentElement.classList.add('vanilla-theme');
|
||||
document.documentElement.style.setProperty('--light-hue', getThemeStore().hueValue.toString());
|
||||
lockAppTheme?.('Light');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
handleThemeChange();
|
||||
handleThemeChange();
|
||||
|
||||
systemTheme.addEventListener('change', handleThemeChange);
|
||||
systemTheme.addEventListener('change', handleThemeChange);
|
||||
|
||||
return () => {
|
||||
systemTheme.removeEventListener('change', handleThemeChange);
|
||||
};
|
||||
}, [themeStore, lockAppTheme, systemTheme]);
|
||||
return () => {
|
||||
systemTheme.removeEventListener('change', handleThemeChange);
|
||||
};
|
||||
}, [themeStore, lockAppTheme, systemTheme]);
|
||||
}
|
||||
|
|
|
@ -23,9 +23,9 @@ export type Platform = {
|
|||
openPath?(path: string): void;
|
||||
openLogsDir?(): void;
|
||||
// Opens a file path with a given ID
|
||||
openFilePath?(library: string, id: number): any;
|
||||
getFilePathOpenWithApps?(library: string, id: number): Promise<{ name: string; url: string }[]>;
|
||||
openFilePathWith?(library: string, id: number, appUrl: string): Promise<unknown>;
|
||||
openFilePath?(library: string, ids: number[]): any;
|
||||
getFilePathOpenWithApps?(library: string, ids: number[]): any;
|
||||
openFilePathWith?(library: string, fileIdsAndAppUrls: ([number, string])[]): any;
|
||||
lockAppTheme?(themeType: 'Auto' | 'Light' | 'Dark'): any;
|
||||
};
|
||||
|
||||
|
|
|
@ -28,11 +28,10 @@ export type Procedures = {
|
|||
mutations:
|
||||
{ key: "files.copyFiles", input: LibraryArgs<FileCopierJobInit>, result: null } |
|
||||
{ key: "files.cutFiles", input: LibraryArgs<FileCutterJobInit>, result: null } |
|
||||
{ key: "files.delete", input: LibraryArgs<number>, result: null } |
|
||||
{ key: "files.deleteFiles", input: LibraryArgs<FileDeleterJobInit>, result: null } |
|
||||
{ key: "files.duplicateFiles", input: LibraryArgs<FileCopierJobInit>, result: null } |
|
||||
{ key: "files.eraseFiles", input: LibraryArgs<FileEraserJobInit>, result: null } |
|
||||
{ key: "files.removeAccessTime", input: LibraryArgs<number>, result: null } |
|
||||
{ key: "files.removeAccessTime", input: LibraryArgs<number[]>, result: null } |
|
||||
{ key: "files.renameFile", input: LibraryArgs<RenameFileArgs>, result: null } |
|
||||
{ key: "files.setFavorite", input: LibraryArgs<SetFavoriteArgs>, result: null } |
|
||||
{ key: "files.setNote", input: LibraryArgs<SetNoteArgs>, result: null } |
|
||||
|
@ -91,13 +90,13 @@ export type EditLibraryArgs = { id: string; name: string | null; description: st
|
|||
|
||||
export type ExplorerItem = { type: "Path"; has_local_thumbnail: boolean; thumbnail_key: string[] | null; item: FilePathWithObject } | { type: "Object"; has_local_thumbnail: boolean; thumbnail_key: string[] | null; item: ObjectWithFilePaths }
|
||||
|
||||
export type FileCopierJobInit = { source_location_id: number; source_path_id: number; target_location_id: number; target_path: string; target_file_name_suffix: string | null }
|
||||
export type FileCopierJobInit = { source_location_id: number; target_location_id: number; sources_file_path_ids: number[]; target_location_relative_directory_path: string; target_file_name_suffix: string | null }
|
||||
|
||||
export type FileCutterJobInit = { source_location_id: number; source_path_id: number; target_location_id: number; target_path: string }
|
||||
export type FileCutterJobInit = { source_location_id: number; target_location_id: number; sources_file_path_ids: number[]; target_location_relative_directory_path: string }
|
||||
|
||||
export type FileDeleterJobInit = { location_id: number; path_id: number }
|
||||
export type FileDeleterJobInit = { location_id: number; file_path_ids: number[] }
|
||||
|
||||
export type FileEraserJobInit = { location_id: number; path_id: number; passes: string }
|
||||
export type FileEraserJobInit = { location_id: number; file_path_ids: number[]; passes: string }
|
||||
|
||||
export type FilePath = { id: number; pub_id: number[]; is_dir: boolean; cas_id: string | null; integrity_checksum: string | null; location_id: number; materialized_path: string; name: string; extension: string; size_in_bytes: string; inode: number[]; device: number[]; object_id: number | null; key_id: number | null; date_created: string; date_modified: string; date_indexed: string }
|
||||
|
||||
|
@ -109,6 +108,8 @@ export type FilePathSearchOrdering = { name: SortOrder } | { sizeInBytes: SortOr
|
|||
|
||||
export type FilePathWithObject = { id: number; pub_id: number[]; is_dir: boolean; cas_id: string | null; integrity_checksum: string | null; location_id: number; materialized_path: string; name: string; extension: string; size_in_bytes: string; inode: number[]; device: number[]; object_id: number | null; key_id: number | null; date_created: string; date_modified: string; date_indexed: string; object: Object | null }
|
||||
|
||||
export type FromPattern = { pattern: string; replace_all: boolean }
|
||||
|
||||
export type GenerateThumbsForLocationArgs = { id: number; path: string }
|
||||
|
||||
export type GetArgs = { id: number }
|
||||
|
@ -224,7 +225,13 @@ export type RelationOperation = { relation_item: string; relation_group: string;
|
|||
|
||||
export type RelationOperationData = "Create" | { Update: { field: string; value: any } } | "Delete"
|
||||
|
||||
export type RenameFileArgs = { location_id: number; file_name: string; new_file_name: string }
|
||||
export type RenameFileArgs = { location_id: number; kind: RenameKind }
|
||||
|
||||
export type RenameKind = { One: RenameOne } | { Many: RenameMany }
|
||||
|
||||
export type RenameMany = { from_pattern: FromPattern; to_pattern: string; from_file_path_ids: number[] }
|
||||
|
||||
export type RenameOne = { from_file_path_id: number; to: string }
|
||||
|
||||
export type RuleKind = "AcceptFilesByGlob" | "RejectFilesByGlob" | "AcceptIfChildrenDirectoriesArePresent" | "RejectIfChildrenDirectoriesArePresent"
|
||||
|
||||
|
@ -250,7 +257,7 @@ export type Statistics = { id: number; date_captured: string; total_object_count
|
|||
|
||||
export type Tag = { id: number; pub_id: number[]; name: string | null; color: string | null; total_objects: number | null; redundancy_goal: number | null; date_created: string; date_modified: string }
|
||||
|
||||
export type TagAssignArgs = { object_id: number; tag_id: number; unassign: boolean }
|
||||
export type TagAssignArgs = { object_ids: number[]; tag_id: number; unassign: boolean }
|
||||
|
||||
export type TagCreateArgs = { name: string; color: string }
|
||||
|
||||
|
|
|
@ -108,7 +108,7 @@ const AnimatedDialogOverlay = animated(RDialog.Overlay);
|
|||
|
||||
export interface DialogProps<S extends FieldValues>
|
||||
extends RDialog.DialogProps,
|
||||
Omit<FormProps<S>, 'onSubmit'> {
|
||||
Omit<FormProps<S>, 'onSubmit'> {
|
||||
title?: string;
|
||||
dialog: ReturnType<typeof useDialog>;
|
||||
loading?: boolean;
|
||||
|
|
|
@ -51,60 +51,60 @@
|
|||
--color-menu-shade: var(--dark-hue), 5%, 0%;
|
||||
}
|
||||
|
||||
.vanilla-theme {
|
||||
// global
|
||||
--color-black: 0, 0%, 0%;
|
||||
--color-white: 0, 0%, 100%;
|
||||
// accent theme colors
|
||||
--color-accent: 208, 100%, 57%;
|
||||
--color-accent-faint: 208, 100%, 67%;
|
||||
--color-accent-deep: 208, 100%, 47%;
|
||||
// text
|
||||
--color-ink: var(--light-hue), 5%, 20%;
|
||||
--color-ink-dull: var(--light-hue), 5%, 30%;
|
||||
--color-ink-faint: var(--light-hue), 5%, 40%;
|
||||
// sidebar
|
||||
--color-sidebar: var(--light-hue), 5%, 96%;
|
||||
--color-sidebar-box: var(--light-hue), 5%, 100%;
|
||||
--color-sidebar-line: var(--light-hue), 10%, 85%;
|
||||
--color-sidebar-ink: var(--light-hue), 5%, 20%;
|
||||
--color-sidebar-ink-dull: var(--light-hue), 5%, 30%;
|
||||
--color-sidebar-ink-faint: var(--light-hue), 5%, 40%;
|
||||
--color-sidebar-divider: var(--light-hue), 15%, 93%;
|
||||
--color-sidebar-button: var(--light-hue), 15%, 100%;
|
||||
--color-sidebar-selected: var(--light-hue), 10%, 80%;
|
||||
--color-sidebar-shade: var(--light-hue), 15%, 100%;
|
||||
// main
|
||||
--color-app: var(--light-hue), 5%, 100%;
|
||||
--color-app-box: var(--light-hue), 5%, 98%;
|
||||
--color-app-dark-box: var(--light-hue), 5%, 97%;
|
||||
--color-app-light-box: var(--light-hue), 5%, 100%;
|
||||
--color-app-overlay: var(--light-hue), 5%, 100%;
|
||||
--color-app-input: var(--light-hue), 5%, 100%;
|
||||
--color-app-focus: var(--light-hue), 5%, 98%;
|
||||
--color-app-line: var(--light-hue), 5%, 90%;
|
||||
--color-app-button: var(--light-hue), 5%, 100%;
|
||||
--color-app-divider: var(--light-hue), 5%, 80%;
|
||||
--color-app-selected: var(--light-hue), 5%, 93%;
|
||||
--color-app-selected-item: var(--light-hue), 5%, 96%;
|
||||
--color-app-hover: var(--light-hue), 5%, 97%;
|
||||
--color-app-active: var(--light-hue), 5%, 87%;
|
||||
--color-app-shade: var(--light-hue), 15%, 50%;
|
||||
--color-app-frame: 0, 0%, 100%;
|
||||
// menu
|
||||
--color-menu: var(--light-hue), 5%, 100%;
|
||||
--color-menu-line: var(--light-hue), 5%, 95%;
|
||||
--color-menu-ink: var(--light-hue), 5%, 20%;
|
||||
--color-menu-faint: var(--light-hue), 5%, 80%;
|
||||
--color-menu-hover: var(--light-hue), 15%, 20%;
|
||||
--color-menu-selected: var(--light-hue), 5%, 30%;
|
||||
--color-menu-shade: var(--light-hue), 5%, 0%;
|
||||
.vanilla-theme {
|
||||
// global
|
||||
--color-black: 0, 0%, 0%;
|
||||
--color-white: 0, 0%, 100%;
|
||||
// accent theme colors
|
||||
--color-accent: 208, 100%, 57%;
|
||||
--color-accent-faint: 208, 100%, 67%;
|
||||
--color-accent-deep: 208, 100%, 47%;
|
||||
// text
|
||||
--color-ink: var(--light-hue), 5%, 20%;
|
||||
--color-ink-dull: var(--light-hue), 5%, 30%;
|
||||
--color-ink-faint: var(--light-hue), 5%, 40%;
|
||||
// sidebar
|
||||
--color-sidebar: var(--light-hue), 5%, 96%;
|
||||
--color-sidebar-box: var(--light-hue), 5%, 100%;
|
||||
--color-sidebar-line: var(--light-hue), 10%, 85%;
|
||||
--color-sidebar-ink: var(--light-hue), 5%, 20%;
|
||||
--color-sidebar-ink-dull: var(--light-hue), 5%, 30%;
|
||||
--color-sidebar-ink-faint: var(--light-hue), 5%, 40%;
|
||||
--color-sidebar-divider: var(--light-hue), 15%, 93%;
|
||||
--color-sidebar-button: var(--light-hue), 15%, 100%;
|
||||
--color-sidebar-selected: var(--light-hue), 10%, 80%;
|
||||
--color-sidebar-shade: var(--light-hue), 15%, 100%;
|
||||
// main
|
||||
--color-app: var(--light-hue), 5%, 100%;
|
||||
--color-app-box: var(--light-hue), 5%, 98%;
|
||||
--color-app-dark-box: var(--light-hue), 5%, 97%;
|
||||
--color-app-light-box: var(--light-hue), 5%, 100%;
|
||||
--color-app-overlay: var(--light-hue), 5%, 100%;
|
||||
--color-app-input: var(--light-hue), 5%, 100%;
|
||||
--color-app-focus: var(--light-hue), 5%, 98%;
|
||||
--color-app-line: var(--light-hue), 5%, 90%;
|
||||
--color-app-button: var(--light-hue), 5%, 100%;
|
||||
--color-app-divider: var(--light-hue), 5%, 80%;
|
||||
--color-app-selected: var(--light-hue), 5%, 93%;
|
||||
--color-app-selected-item: var(--light-hue), 5%, 96%;
|
||||
--color-app-hover: var(--light-hue), 5%, 97%;
|
||||
--color-app-active: var(--light-hue), 5%, 87%;
|
||||
--color-app-shade: var(--light-hue), 15%, 50%;
|
||||
--color-app-frame: 0, 0%, 100%;
|
||||
// menu
|
||||
--color-menu: var(--light-hue), 5%, 100%;
|
||||
--color-menu-line: var(--light-hue), 5%, 95%;
|
||||
--color-menu-ink: var(--light-hue), 5%, 20%;
|
||||
--color-menu-faint: var(--light-hue), 5%, 80%;
|
||||
--color-menu-hover: var(--light-hue), 15%, 20%;
|
||||
--color-menu-selected: var(--light-hue), 5%, 30%;
|
||||
--color-menu-shade: var(--light-hue), 5%, 0%;
|
||||
|
||||
// --color-menu: var(--light-hue), 16%, 99%;
|
||||
// --color-menu-line: var(--light-hue), 5%, 90%;
|
||||
// --color-menu-ink: var(--light-hue), 5%, 30%;
|
||||
// --color-menu-faint: var(--light-hue), 5%, 80%;
|
||||
// --color-menu-hover: var(--light-hue), 15%, 20%;
|
||||
// --color-menu-selected: var(--light-hue), 5%, 30%;
|
||||
// --color-menu-shade: var(--light-hue), 5%, 0%;
|
||||
}
|
||||
// --color-menu: var(--light-hue), 16%, 99%;
|
||||
// --color-menu-line: var(--light-hue), 5%, 90%;
|
||||
// --color-menu-ink: var(--light-hue), 5%, 30%;
|
||||
// --color-menu-faint: var(--light-hue), 5%, 80%;
|
||||
// --color-menu-hover: var(--light-hue), 15%, 20%;
|
||||
// --color-menu-selected: var(--light-hue), 5%, 30%;
|
||||
// --color-menu-shade: var(--light-hue), 5%, 0%;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue