mirror of
https://github.com/spacedriveapp/spacedrive
synced 2024-07-06 22:33:27 +00:00
Library manager (#258)
This commit is contained in:
parent
70ea568530
commit
c685ce5fe9
285
Cargo.lock
generated
285
Cargo.lock
generated
|
@ -51,9 +51,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "actix-http"
|
||||
version = "3.1.0"
|
||||
version = "3.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bd2e9f6794b5826aff6df65e3a0d0127b271d1c03629c774238f3582e903d4e4"
|
||||
checksum = "6f9ffb6db08c1c3a1f4aef540f1a63193adc73c4fbd40b75a95fc8c5258f6e51"
|
||||
dependencies = [
|
||||
"actix-codec",
|
||||
"actix-rt",
|
||||
|
@ -195,7 +195,7 @@ dependencies = [
|
|||
"serde_urlencoded",
|
||||
"smallvec",
|
||||
"socket2",
|
||||
"time 0.3.9",
|
||||
"time 0.3.11",
|
||||
"url",
|
||||
]
|
||||
|
||||
|
@ -306,9 +306,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.57"
|
||||
version = "1.0.58"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08f9b8508dccb7687a1d6c4ce66b2b0ecef467c94667de27d8d7fe1f8d2a9cdc"
|
||||
checksum = "bb07d2053ccdbe10e2af2995a2f116c1330396493dc1269f6a91d0ae82e19704"
|
||||
|
||||
[[package]]
|
||||
name = "arrayvec"
|
||||
|
@ -588,7 +588,7 @@ dependencies = [
|
|||
"serde",
|
||||
"serde_bytes",
|
||||
"serde_json",
|
||||
"time 0.3.9",
|
||||
"time 0.3.11",
|
||||
"uuid 0.8.2",
|
||||
]
|
||||
|
||||
|
@ -615,9 +615,9 @@ checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7"
|
|||
|
||||
[[package]]
|
||||
name = "bytemuck"
|
||||
version = "1.9.1"
|
||||
version = "1.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cdead85bdec19c194affaeeb670c0e41fe23de31459efd1c174d049269cf02cc"
|
||||
checksum = "c53dfa917ec274df8ed3c572698f381a24eef2efba9492d797301b72b6db408a"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
|
@ -642,9 +642,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "cairo-rs"
|
||||
version = "0.15.11"
|
||||
version = "0.15.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "62be3562254e90c1c6050a72aa638f6315593e98c5cdaba9017cedbabf0a5dee"
|
||||
checksum = "c76ee391b03d35510d9fa917357c7f1855bd9a6659c95a1b392e33f49b3369bc"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cairo-sys-rs",
|
||||
|
@ -882,7 +882,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "94d4706de1b0fa5b132270cddffa8585166037822e260a944fe161acd137ca05"
|
||||
dependencies = [
|
||||
"percent-encoding",
|
||||
"time 0.3.9",
|
||||
"time 0.3.11",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
|
@ -964,17 +964,17 @@ dependencies = [
|
|||
"crossbeam-deque",
|
||||
"crossbeam-epoch",
|
||||
"crossbeam-queue 0.3.5",
|
||||
"crossbeam-utils 0.8.8",
|
||||
"crossbeam-utils 0.8.10",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-channel"
|
||||
version = "0.5.4"
|
||||
version = "0.5.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5aaa7bd5fb665c6864b5f963dd9097905c54125909c7aa94c9e18507cdbe6c53"
|
||||
checksum = "4c02a4d71819009c192cf4872265391563fd6a84c81ff2c0f2a7026ca4c1d85c"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"crossbeam-utils 0.8.8",
|
||||
"crossbeam-utils 0.8.10",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -985,20 +985,20 @@ checksum = "6455c0ca19f0d2fbf751b908d5c55c1f5cbc65e03c4225427254b46890bdde1e"
|
|||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"crossbeam-epoch",
|
||||
"crossbeam-utils 0.8.8",
|
||||
"crossbeam-utils 0.8.10",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-epoch"
|
||||
version = "0.9.8"
|
||||
version = "0.9.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1145cf131a2c6ba0615079ab6a638f7e1973ac9c2634fcbeaaad6114246efe8c"
|
||||
checksum = "07db9d94cbd326813772c968ccd25999e5f8ae22f4f8d1b11effa37ef6ce281d"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"cfg-if 1.0.0",
|
||||
"crossbeam-utils 0.8.8",
|
||||
"lazy_static",
|
||||
"crossbeam-utils 0.8.10",
|
||||
"memoffset",
|
||||
"once_cell",
|
||||
"scopeguard",
|
||||
]
|
||||
|
||||
|
@ -1020,7 +1020,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "1f25d8400f4a7a5778f0e4e52384a48cbd9b5c495d110786187fc750075277a2"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"crossbeam-utils 0.8.8",
|
||||
"crossbeam-utils 0.8.10",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1036,19 +1036,19 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.8.8"
|
||||
version = "0.8.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0bf124c720b7686e3c2663cf54062ab0f68a88af2fb6a030e87e30bf721fcb38"
|
||||
checksum = "7d82ee10ce34d7bc12c2122495e7593a9c41347ecdd64185af4ecf72cb1a7f83"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"lazy_static",
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.1.3"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57952ca27b5e3606ff4dd79b0020231aaf9d6aa76dc05fd30137538c50bd3ce8"
|
||||
checksum = "5999502d32b9c48d492abe66392408144895020ec4709e549e840799f3bb74c0"
|
||||
dependencies = [
|
||||
"generic-array 0.14.5",
|
||||
"typenum",
|
||||
|
@ -1377,9 +1377,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.6.1"
|
||||
version = "1.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
|
||||
checksum = "3f107b87b6afc2a64fd13cac55fe06d6c8859f12d4b14cbcdd2c67d0976781be"
|
||||
|
||||
[[package]]
|
||||
name = "embed-resource"
|
||||
|
@ -1598,14 +1598,14 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "filetime"
|
||||
version = "0.2.16"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0408e2626025178a6a7f7ffc05a25bc47103229f19c113755de7bf63816290c"
|
||||
checksum = "e94a7bbaa59354bc20dd75b67f23e2797b4490e9d6928203fb105c79e448c86c"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"libc",
|
||||
"redox_syscall 0.2.13",
|
||||
"winapi",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -2010,9 +2010,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "gif"
|
||||
version = "0.11.3"
|
||||
version = "0.11.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3a7187e78088aead22ceedeee99779455b23fc231fe13ec443f99bb71694e5b"
|
||||
checksum = "3edd93c6756b4dfaf2709eafcc345ba2636565295c198a9cfbf75fa5e3e00b06"
|
||||
dependencies = [
|
||||
"color_quant",
|
||||
"weezl",
|
||||
|
@ -2026,9 +2026,9 @@ checksum = "78cc372d058dcf6d5ecd98510e7fbc9e5aec4d21de70f65fea8fecebcd881bd4"
|
|||
|
||||
[[package]]
|
||||
name = "gio"
|
||||
version = "0.15.11"
|
||||
version = "0.15.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0f132be35e05d9662b9fa0fee3f349c6621f7782e0105917f4cc73c1bf47eceb"
|
||||
checksum = "68fdbc90312d462781a395f7a16d96a2b379bb6ef8cd6310a2df272771c4283b"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"futures-channel",
|
||||
|
@ -2056,9 +2056,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "glib"
|
||||
version = "0.15.11"
|
||||
version = "0.15.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bd124026a2fa8c33a3d17a3fe59c103f2d9fa5bd92c19e029e037736729abeab"
|
||||
checksum = "edb0306fbad0ab5428b0ca674a23893db909a98582969c9b537be4ced78c505d"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"futures-channel",
|
||||
|
@ -2228,13 +2228,19 @@ dependencies = [
|
|||
"ahash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db0d4cf898abf0081f964436dc980e96670a0f36863e4b83aaacdb65c9d7ccc3"
|
||||
|
||||
[[package]]
|
||||
name = "hashlink"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7249a3129cbc1ffccd74857f81464a323a152173cdb134e0fd81bc803b29facf"
|
||||
dependencies = [
|
||||
"hashbrown",
|
||||
"hashbrown 0.11.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -2427,7 +2433,7 @@ version = "0.4.18"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "713f1b139373f96a2e0ce3ac931cd01ee973c3c5dd7c40c0c2efe96ad2b6751d"
|
||||
dependencies = [
|
||||
"crossbeam-utils 0.8.8",
|
||||
"crossbeam-utils 0.8.10",
|
||||
"globset",
|
||||
"lazy_static",
|
||||
"log",
|
||||
|
@ -2492,12 +2498,12 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "1.8.2"
|
||||
version = "1.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6012d540c5baa3589337a98ce73408de9b5a25ec9fc2c6fd6be8f0d39e0ca5a"
|
||||
checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"hashbrown",
|
||||
"hashbrown 0.12.1",
|
||||
"serde",
|
||||
]
|
||||
|
||||
|
@ -2866,10 +2872,19 @@ dependencies = [
|
|||
]
|
||||
|
||||
[[package]]
|
||||
name = "linked-hash-map"
|
||||
version = "0.5.4"
|
||||
name = "line-wrap"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3"
|
||||
checksum = "f30344350a2a51da54c1d53be93fade8a237e545dbcc4bdbe635413f2117cab9"
|
||||
dependencies = [
|
||||
"safemem",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linked-hash-map"
|
||||
version = "0.5.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
|
||||
|
||||
[[package]]
|
||||
name = "local-channel"
|
||||
|
@ -2934,11 +2949,11 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "lru"
|
||||
version = "0.7.6"
|
||||
version = "0.7.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8015d95cb7b2ddd3c0d32ca38283ceb1eea09b4713ee380bceb942d85a244228"
|
||||
checksum = "c84e6fe5655adc6ce00787cf7dcaf8dc4f998a0565d23eafc207a8b08ca3349a"
|
||||
dependencies = [
|
||||
"hashbrown",
|
||||
"hashbrown 0.11.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -2966,7 +2981,7 @@ dependencies = [
|
|||
"dirs-next",
|
||||
"objc-foundation",
|
||||
"objc_id",
|
||||
"time 0.3.9",
|
||||
"time 0.3.11",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -3093,9 +3108,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "0.8.3"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "713d550d9b44d89174e066b7a6217ae06234c10cb47819a88290d2b353c31799"
|
||||
checksum = "57ee1c23c7c63b0c9250c339ffdc69255f110b298b901b9f6c82547b7b87caaf"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
|
@ -3284,7 +3299,7 @@ dependencies = [
|
|||
"smallvec",
|
||||
"subprocess",
|
||||
"thiserror",
|
||||
"time 0.3.9",
|
||||
"time 0.3.11",
|
||||
"uuid 0.8.2",
|
||||
]
|
||||
|
||||
|
@ -3439,9 +3454,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "num-rational"
|
||||
version = "0.4.0"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d41702bd167c2df5520b384281bc111a4b5efcf7fbc4c9c222c815b07e0a6a6a"
|
||||
checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"num-integer",
|
||||
|
@ -3980,18 +3995,18 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "pin-project"
|
||||
version = "1.0.10"
|
||||
version = "1.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "58ad3879ad3baf4e44784bc6a718a8698867bb991f8ce24d1bcbe2cfb4c3a75e"
|
||||
checksum = "78203e83c48cffbe01e4a2d35d566ca4de445d79a85372fc64e378bfc812a260"
|
||||
dependencies = [
|
||||
"pin-project-internal",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-internal"
|
||||
version = "1.0.10"
|
||||
version = "1.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "744b6f092ba29c3650faf274db506afd39944f48420f6c86b17cfe0ee1cb36bb"
|
||||
checksum = "710faf75e1b33345361201d36d04e98ac1ed8909151a017ed384700836104c74"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
@ -4016,6 +4031,20 @@ version = "0.3.25"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae"
|
||||
|
||||
[[package]]
|
||||
name = "plist"
|
||||
version = "1.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bd39bc6cdc9355ad1dc5eeedefee696bb35c34caf21768741e81826c0bbd7225"
|
||||
dependencies = [
|
||||
"base64 0.13.0",
|
||||
"indexmap",
|
||||
"line-wrap",
|
||||
"serde",
|
||||
"time 0.3.11",
|
||||
"xml-rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "png"
|
||||
version = "0.11.0"
|
||||
|
@ -4247,9 +4276,9 @@ checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5"
|
|||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.39"
|
||||
version = "1.0.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c54b25569025b7fc9651de43004ae593a75ad88543b17178aa5e1b9c4f15f56f"
|
||||
checksum = "dd96a1e8ed2596c337f8eae5f24924ec83f5ad5ab21ea8e455d3566c69fbcaf7"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
@ -4361,9 +4390,9 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
|
|||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.18"
|
||||
version = "1.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1"
|
||||
checksum = "3bcdf212e9776fbcb2d23ab029360416bb1706b1aea2d1a5ba002727cbcab804"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
@ -4484,7 +4513,7 @@ checksum = "258bcdb5ac6dad48491bb2992db6b7cf74878b0384908af124823d118c99683f"
|
|||
dependencies = [
|
||||
"crossbeam-channel",
|
||||
"crossbeam-deque",
|
||||
"crossbeam-utils 0.8.8",
|
||||
"crossbeam-utils 0.8.10",
|
||||
"num_cpus",
|
||||
]
|
||||
|
||||
|
@ -4736,7 +4765,7 @@ version = "0.4.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366"
|
||||
dependencies = [
|
||||
"semver 1.0.10",
|
||||
"semver 1.0.12",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -4772,9 +4801,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.6"
|
||||
version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2cc38e8fa666e2de3c4aba7edeb5ffc5246c1c2ed0e3d17e560aeeba736b23f"
|
||||
checksum = "a0a5f7c728f5d284929a1cccb5bc19884422bfe6ef4d6c409da2c41838983fcf"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
|
@ -4782,6 +4811,12 @@ version = "1.0.10"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3f6f92acf49d1b98f7a81226834412ada05458b7364277387724a237f062695"
|
||||
|
||||
[[package]]
|
||||
name = "safemem"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072"
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
version = "1.0.6"
|
||||
|
@ -4862,7 +4897,6 @@ dependencies = [
|
|||
"image",
|
||||
"include_dir",
|
||||
"int-enum",
|
||||
"lazy_static",
|
||||
"log",
|
||||
"prisma-client-rust",
|
||||
"ring 0.17.0-alpha.11",
|
||||
|
@ -4940,9 +4974,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.10"
|
||||
version = "1.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a41d061efea015927ac527063765e73601444cdc344ba855bc7bd44578b25e1c"
|
||||
checksum = "a2333e6df6d6598f2b1974829f853c2b4c5f4a6e503c10af918081aa6f8564e1"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
@ -4964,9 +4998,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.137"
|
||||
version = "1.0.138"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "61ea8d54c77f8315140a05f4c7237403bf38b72704d031543aa1d16abbf517d1"
|
||||
checksum = "1578c6245786b9d168c5447eeacfb96856573ca56c9d68fdcf394be134882a47"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
@ -4982,9 +5016,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.137"
|
||||
version = "1.0.138"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f26faba0c3959972377d3b2d306ee9f71faee9714294e41bb777f83f88578be"
|
||||
checksum = "023e9b1467aef8a10fb88f25611870ada9800ef7e22afce356bb0d2387b6f27c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
@ -4993,9 +5027,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.81"
|
||||
version = "1.0.82"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b7ce2b32a1aed03c558dc61a5cd328f15aff2dbc17daad8fb8af04d2100e15c"
|
||||
checksum = "82c2c1fdcd807d1098552c5b9a36e425e42e9fbd7c6a37a8425f390f781f7fa7"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"itoa 1.0.2",
|
||||
|
@ -5204,9 +5238,9 @@ checksum = "eb703cfe953bccee95685111adeedb76fabe4e97549a58d16f03ea7b9367bb32"
|
|||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.8.0"
|
||||
version = "1.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83"
|
||||
checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1"
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
|
@ -5442,9 +5476,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.96"
|
||||
version = "1.0.98"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0748dd251e24453cb8717f0354206b91557e4ec8703673a4b30208f2abaf1ebf"
|
||||
checksum = "c50aef8a904de4c23c788f104b7dddc7d6f79c647c7c8ce4cc8f73eb0ca773dd"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
@ -5500,9 +5534,9 @@ checksum = "f764005d11ee5f36500a149ace24e00e3da98b0158b3e2d53a7495660d3f4d60"
|
|||
|
||||
[[package]]
|
||||
name = "tao"
|
||||
version = "0.11.2"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3bfe4c782f0543f667ee3b732d026b2f1c64af39cd52e726dec1ea1f2d8f6b80"
|
||||
checksum = "a71c32c2fa7bba46b01becf9cf470f6a781573af7e376c5e317a313ecce27545"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cairo-rs",
|
||||
|
@ -5537,7 +5571,6 @@ dependencies = [
|
|||
"raw-window-handle",
|
||||
"scopeguard",
|
||||
"serde",
|
||||
"tao-core-video-sys",
|
||||
"unicode-segmentation",
|
||||
"uuid 0.8.2",
|
||||
"windows 0.37.0",
|
||||
|
@ -5545,18 +5578,6 @@ dependencies = [
|
|||
"x11-dl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tao-core-video-sys"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "271450eb289cb4d8d0720c6ce70c72c8c858c93dd61fc625881616752e6b98f6"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
"objc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tap"
|
||||
version = "1.0.1"
|
||||
|
@ -5576,9 +5597,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "tauri"
|
||||
version = "1.0.0"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e1ebb60bb8f246d5351ff9b7728fdfa7a6eba72baa722ab6021d553981caba1"
|
||||
checksum = "421641ec549d34935530886151a42ce5ecbbb57beb30e5eec1b22f8e08e10ee9"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"attohttpc",
|
||||
|
@ -5605,7 +5626,7 @@ dependencies = [
|
|||
"raw-window-handle",
|
||||
"regex",
|
||||
"rfd",
|
||||
"semver 1.0.10",
|
||||
"semver 1.0.12",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_repr",
|
||||
|
@ -5629,14 +5650,14 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "tauri-build"
|
||||
version = "1.0.0"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7b26eb3523e962b90012fedbfb744ca153d9be85e7981e00737e106d5323941"
|
||||
checksum = "598bd36884ee15ac73dfca9921066fd87d13d9beea60384b99a66c3a5d800d70"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cargo_toml",
|
||||
"heck 0.4.0",
|
||||
"semver 1.0.10",
|
||||
"semver 1.0.12",
|
||||
"serde_json",
|
||||
"tauri-utils",
|
||||
"winres",
|
||||
|
@ -5644,32 +5665,34 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "tauri-codegen"
|
||||
version = "1.0.0"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9468c5189188c820ef605dfe4937c768cb2918e9460c8093dc4ee2cbd717b262"
|
||||
checksum = "048a7b404b92c86e7dc32458fd0963f042a76d520681e6f598d73a97c2feeeef"
|
||||
dependencies = [
|
||||
"base64 0.13.0",
|
||||
"brotli",
|
||||
"ico",
|
||||
"plist",
|
||||
"png 0.17.5",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"regex",
|
||||
"semver 1.0.10",
|
||||
"semver 1.0.12",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"tauri-utils",
|
||||
"thiserror",
|
||||
"time 0.3.11",
|
||||
"uuid 1.1.2",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-macros"
|
||||
version = "1.0.0"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "40e3ffddd7a274fc7baaa260888c971a0d95d2ef403aa16600c878b8b1c00ffe"
|
||||
checksum = "aaf70098bfab21efde9b2c089008b319ba333f4ee6e55c38bdea188dea86497f"
|
||||
dependencies = [
|
||||
"heck 0.4.0",
|
||||
"proc-macro2",
|
||||
|
@ -5681,14 +5704,15 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "tauri-runtime"
|
||||
version = "0.9.0"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fb7dc4db360bb40584187b6cb7834da736ce4ef2ab0914e2be98014444fa9920"
|
||||
checksum = "82d34f58c61a6790ba3de5753daea61b5beb6926b2384d1ad03b9dfe622c72be"
|
||||
dependencies = [
|
||||
"gtk",
|
||||
"http",
|
||||
"http-range",
|
||||
"infer",
|
||||
"raw-window-handle",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri-utils",
|
||||
|
@ -5700,14 +5724,15 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "tauri-runtime-wry"
|
||||
version = "0.9.0"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c876fb3a6e7c6fe2ac466b2a6ecd83658528844b4df0914558a9bc1501b31cf3"
|
||||
checksum = "cd9a56e25146ff1f13f37bdb010ed0d692e7e81c824b9f977ae439f446f37ab4"
|
||||
dependencies = [
|
||||
"cocoa",
|
||||
"gtk",
|
||||
"percent-encoding",
|
||||
"rand 0.8.5",
|
||||
"raw-window-handle",
|
||||
"tauri-runtime",
|
||||
"tauri-utils",
|
||||
"uuid 1.1.2",
|
||||
|
@ -5719,9 +5744,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "tauri-utils"
|
||||
version = "1.0.0"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "727145cb55b8897fa9f2bcea4fad31dc39394703d037c9669b40f2d1c0c2d7f3"
|
||||
checksum = "616f178da1e0466ca45963ed108a1567d4b8803662addaca313169d0dcd97715"
|
||||
dependencies = [
|
||||
"brotli",
|
||||
"ctor",
|
||||
|
@ -5734,13 +5759,14 @@ dependencies = [
|
|||
"phf 0.10.1",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"semver 1.0.10",
|
||||
"semver 1.0.12",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_with",
|
||||
"thiserror",
|
||||
"url",
|
||||
"walkdir",
|
||||
"windows 0.37.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -5877,9 +5903,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.3.9"
|
||||
version = "0.3.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2702e08a7a860f005826c6815dcac101b19b5eb330c27fe4a5928fec1d20ddd"
|
||||
checksum = "72c91f41dcb2f096c05f0873d667dceec1087ce5bcf984ec8ffb19acddbb3217"
|
||||
dependencies = [
|
||||
"itoa 1.0.2",
|
||||
"libc",
|
||||
|
@ -6031,9 +6057,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "tower-service"
|
||||
version = "0.3.1"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6"
|
||||
checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52"
|
||||
|
||||
[[package]]
|
||||
name = "tracing"
|
||||
|
@ -6050,9 +6076,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "tracing-attributes"
|
||||
version = "0.1.21"
|
||||
version = "0.1.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc6b8ad3567499f98a1db7a752b07a7c8c7c7c34c332ec00effb2b0027974b7c"
|
||||
checksum = "11c75893af559bc8e10716548bdef5cb2b983f8e637db9d0e15126b61b484ee2"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
@ -6061,9 +6087,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "tracing-core"
|
||||
version = "0.1.27"
|
||||
version = "0.1.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7709595b8878a4965ce5e87ebf880a7d39c9afc6837721b21a5a816a8117d921"
|
||||
checksum = "7b7358be39f2f274f322d2aaed611acc57f382e8eb1e5b48cb9ae30933495ce7"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"valuable",
|
||||
|
@ -6105,13 +6131,13 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "tracing-subscriber"
|
||||
version = "0.3.11"
|
||||
version = "0.3.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4bc28f93baff38037f64e6f43d34cfa1605f27a49c34e8a04c5e78b0babf2596"
|
||||
checksum = "3a713421342a5a666b7577783721d3117f1b69a393df803ee17bb73b1e122a59"
|
||||
dependencies = [
|
||||
"ansi_term",
|
||||
"lazy_static",
|
||||
"matchers",
|
||||
"once_cell",
|
||||
"regex",
|
||||
"sharded-slab",
|
||||
"smallvec",
|
||||
|
@ -6190,6 +6216,7 @@ dependencies = [
|
|||
"chrono",
|
||||
"thiserror",
|
||||
"ts-rs-macros",
|
||||
"uuid 0.8.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -6253,9 +6280,9 @@ checksum = "5bd2fe26506023ed7b5e1e315add59d6f584c621d037f9368fea9cfb988f368c"
|
|||
|
||||
[[package]]
|
||||
name = "unicode-normalization"
|
||||
version = "0.1.19"
|
||||
version = "0.1.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9"
|
||||
checksum = "854cbdc4f7bc6ae19c820d44abdc3277ac3e1b2b93db20a636825d9322fb60e6"
|
||||
dependencies = [
|
||||
"tinyvec",
|
||||
]
|
||||
|
@ -6647,9 +6674,9 @@ checksum = "17882f045410753661207383517a6f62ec3dbeb6a4ed2acce01f0728238d1983"
|
|||
|
||||
[[package]]
|
||||
name = "wildmatch"
|
||||
version = "2.1.0"
|
||||
version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6c48bd20df7e4ced539c12f570f937c6b4884928a87fee70a479d72f031d4e0"
|
||||
checksum = "ee583bdc5ff1cf9db20e9db5bb3ff4c3089a8f6b8b31aff265c9aba85812db86"
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
|
@ -6911,9 +6938,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "wry"
|
||||
version = "0.18.3"
|
||||
version = "0.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26b1ba327c7dd4292f46bf8e6ba8e6ec2db4443b2973c9d304a359d95e0aa856"
|
||||
checksum = "ce19dddbd3ce01dc8f14eb6d4c8f914123bf8379aaa838f6da4f981ff7104a3f"
|
||||
dependencies = [
|
||||
"block",
|
||||
"cocoa",
|
||||
|
|
|
@ -1,16 +1,15 @@
|
|||
use std::time::{Duration, Instant};
|
||||
|
||||
use dotenvy::dotenv;
|
||||
use sdcore::{ClientCommand, ClientQuery, CoreController, CoreEvent, CoreResponse, Node};
|
||||
use tauri::api::path;
|
||||
use tauri::Manager;
|
||||
use sdcore::{ClientCommand, ClientQuery, CoreEvent, CoreResponse, Node, NodeController};
|
||||
use tauri::{api::path, Manager};
|
||||
#[cfg(target_os = "macos")]
|
||||
mod macos;
|
||||
mod menu;
|
||||
|
||||
#[tauri::command(async)]
|
||||
async fn client_query_transport(
|
||||
core: tauri::State<'_, CoreController>,
|
||||
core: tauri::State<'_, NodeController>,
|
||||
data: ClientQuery,
|
||||
) -> Result<CoreResponse, String> {
|
||||
match core.query(data).await {
|
||||
|
@ -24,7 +23,7 @@ async fn client_query_transport(
|
|||
|
||||
#[tauri::command(async)]
|
||||
async fn client_command_transport(
|
||||
core: tauri::State<'_, CoreController>,
|
||||
core: tauri::State<'_, NodeController>,
|
||||
data: ClientCommand,
|
||||
) -> Result<CoreResponse, String> {
|
||||
match core.command(data).await {
|
||||
|
@ -48,17 +47,11 @@ async fn main() {
|
|||
dotenv().ok();
|
||||
env_logger::init();
|
||||
|
||||
let data_dir = path::data_dir().unwrap_or(std::path::PathBuf::from("./"));
|
||||
let mut data_dir = path::data_dir().unwrap_or(std::path::PathBuf::from("./"));
|
||||
data_dir = data_dir.join("spacedrive");
|
||||
// create an instance of the core
|
||||
let (mut node, mut event_receiver) = Node::new(data_dir).await;
|
||||
// run startup tasks
|
||||
node.initializer().await;
|
||||
// extract the node controller
|
||||
let controller = node.get_controller();
|
||||
// throw the node into a dedicated thread
|
||||
tokio::spawn(async move {
|
||||
node.start().await;
|
||||
});
|
||||
let (controller, mut event_receiver, node) = Node::new(data_dir).await;
|
||||
tokio::spawn(node.start());
|
||||
// create tauri app
|
||||
tauri::Builder::default()
|
||||
// pass controller to the tauri state manager
|
||||
|
|
|
@ -197,7 +197,7 @@ function Page() {
|
|||
style={{ transform: 'scale(2)' }}
|
||||
/>
|
||||
<div className="relative z-10">
|
||||
<h1 className="text-5xl leading-snug fade-in-heading ">
|
||||
<h1 className="text-5xl leading-tight sm:leading-snug fade-in-heading ">
|
||||
We believe file management should be <span className="title-gradient">universal</span>.
|
||||
</h1>
|
||||
<p className="text-gray-400 animation-delay-2 fade-in-heading ">
|
||||
|
|
|
@ -1,42 +0,0 @@
|
|||
# Infrastructure setups up the Kubernetes cluster for Spacedrive!
|
||||
#
|
||||
# To get the service account token use the following:
|
||||
# ```bash
|
||||
# TOKENNAME=`kubectl -n spacedrive get sa/spacedrive-ci -o jsonpath='{.secrets[0].name}'`
|
||||
# kubectl -n spacedrive get secret $TOKENNAME -o jsonpath='{.data.token}' | base64 -d
|
||||
# ```
|
||||
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: spacedrive
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: spacedrive-ci
|
||||
namespace: spacedrive
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: spacedrive-ns-full
|
||||
namespace: spacedrive
|
||||
rules:
|
||||
- apiGroups: ['apps']
|
||||
resources: ['deployments']
|
||||
verbs: ['get', 'patch']
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: spacedrive-ci-rb
|
||||
namespace: spacedrive
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: spacedrive-ci
|
||||
namespace: spacedrive
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: Role
|
||||
name: spacedrive-ns-full
|
|
@ -1,118 +0,0 @@
|
|||
# This will deploy the Spacedrive Server container to the `spacedrive`` namespace on Kubernetes.
|
||||
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: sdserver-ingress
|
||||
namespace: spacedrive
|
||||
labels:
|
||||
app.kubernetes.io/name: sdserver
|
||||
app.kubernetes.io/component: webserver
|
||||
annotations:
|
||||
traefik.ingress.kubernetes.io/router.tls.certresolver: le
|
||||
traefik.ingress.kubernetes.io/router.middlewares: kube-system-antiseo@kubernetescrd
|
||||
spec:
|
||||
rules:
|
||||
- host: spacedrive.otbeaumont.me
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: sdserver-service
|
||||
port:
|
||||
number: 8080
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: sdserver-service
|
||||
namespace: spacedrive
|
||||
labels:
|
||||
app.kubernetes.io/name: sdserver
|
||||
app.kubernetes.io/component: webserver
|
||||
spec:
|
||||
ports:
|
||||
- port: 8080
|
||||
targetPort: 8080
|
||||
protocol: TCP
|
||||
selector:
|
||||
app.kubernetes.io/name: sdserver
|
||||
app.kubernetes.io/component: webserver
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: sdserver-pvc
|
||||
namespace: spacedrive
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
storageClassName: local-path
|
||||
resources:
|
||||
requests:
|
||||
storage: 512M
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: sdserver-deployment
|
||||
namespace: spacedrive
|
||||
labels:
|
||||
app.kubernetes.io/name: sdserver
|
||||
app.kubernetes.io/component: webserver
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: sdserver
|
||||
app.kubernetes.io/component: webserver
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: sdserver
|
||||
app.kubernetes.io/component: webserver
|
||||
spec:
|
||||
restartPolicy: Always
|
||||
# refer to Dockerfile to find securityContext values
|
||||
securityContext:
|
||||
runAsUser: 101
|
||||
runAsGroup: 101
|
||||
fsGroup: 101
|
||||
containers:
|
||||
- name: sdserver
|
||||
image: ghcr.io/oscartbeaumont/spacedrive/server:staging
|
||||
imagePullPolicy: Always
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
volumeMounts:
|
||||
- name: data-volume
|
||||
mountPath: /data
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
resources:
|
||||
limits:
|
||||
memory: 100Mi
|
||||
cpu: 100m
|
||||
requests:
|
||||
memory: 5Mi
|
||||
cpu: 10m
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 8080
|
||||
initialDelaySeconds: 10
|
||||
failureThreshold: 4
|
||||
periodSeconds: 5
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 8080
|
||||
initialDelaySeconds: 20
|
||||
failureThreshold: 3
|
||||
periodSeconds: 10
|
||||
volumes:
|
||||
- name: data-volume
|
||||
persistentVolumeClaim:
|
||||
claimName: sdserver-pvc
|
|
@ -1,4 +1,4 @@
|
|||
use sdcore::{ClientCommand, ClientQuery, CoreController, CoreEvent, CoreResponse, Node};
|
||||
use sdcore::{ClientCommand, ClientQuery, CoreEvent, CoreResponse, Node, NodeController};
|
||||
use std::{env, path::Path};
|
||||
|
||||
use actix::{
|
||||
|
@ -19,7 +19,7 @@ const DATA_DIR_ENV_VAR: &'static str = "DATA_DIR";
|
|||
/// Define HTTP actor
|
||||
struct Socket {
|
||||
_event_receiver: web::Data<mpsc::Receiver<CoreEvent>>,
|
||||
core: web::Data<CoreController>,
|
||||
core: web::Data<NodeController>,
|
||||
}
|
||||
|
||||
impl Actor for Socket {
|
||||
|
@ -52,7 +52,15 @@ impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for Socket {
|
|||
match msg {
|
||||
Ok(ws::Message::Ping(msg)) => ctx.pong(&msg),
|
||||
Ok(ws::Message::Text(text)) => {
|
||||
let msg: SocketMessage = serde_json::from_str(&text).unwrap();
|
||||
let msg = serde_json::from_str::<SocketMessage>(&text);
|
||||
|
||||
let msg = match msg {
|
||||
Ok(msg) => msg,
|
||||
Err(err) => {
|
||||
println!("Error parsing message: {}", err);
|
||||
return;
|
||||
},
|
||||
};
|
||||
|
||||
let core = self.core.clone();
|
||||
|
||||
|
@ -133,7 +141,7 @@ async fn ws_handler(
|
|||
req: HttpRequest,
|
||||
stream: web::Payload,
|
||||
event_receiver: web::Data<mpsc::Receiver<CoreEvent>>,
|
||||
controller: web::Data<CoreController>,
|
||||
controller: web::Data<NodeController>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let resp = ws::start(
|
||||
Socket {
|
||||
|
@ -178,7 +186,7 @@ async fn main() -> std::io::Result<()> {
|
|||
|
||||
async fn setup() -> (
|
||||
web::Data<mpsc::Receiver<CoreEvent>>,
|
||||
web::Data<CoreController>,
|
||||
web::Data<NodeController>,
|
||||
) {
|
||||
let data_dir_path = match env::var(DATA_DIR_ENV_VAR) {
|
||||
Ok(path) => Path::new(&path).to_path_buf(),
|
||||
|
@ -196,15 +204,8 @@ async fn setup() -> (
|
|||
},
|
||||
};
|
||||
|
||||
let (mut node, event_receiver) = Node::new(data_dir_path).await;
|
||||
|
||||
node.initializer().await;
|
||||
|
||||
let controller = node.get_controller();
|
||||
|
||||
tokio::spawn(async move {
|
||||
node.start().await;
|
||||
});
|
||||
let (controller, event_receiver, node) = Node::new(data_dir_path).await;
|
||||
tokio::spawn(node.start());
|
||||
|
||||
(web::Data::new(event_receiver), web::Data::new(controller))
|
||||
}
|
||||
|
|
|
@ -31,6 +31,16 @@ class Transport extends BaseTransport {
|
|||
});
|
||||
}
|
||||
async query(query: ClientQuery) {
|
||||
if (websocket.readyState == 0) {
|
||||
let resolve: () => void;
|
||||
const promise = new Promise((res) => {
|
||||
resolve = () => res(undefined);
|
||||
});
|
||||
// @ts-ignore
|
||||
websocket.addEventListener('open', resolve);
|
||||
await promise;
|
||||
}
|
||||
|
||||
const id = randomId();
|
||||
let resolve: (data: any) => void;
|
||||
|
||||
|
|
|
@ -24,10 +24,9 @@ ring = "0.17.0-alpha.10"
|
|||
int-enum = "0.4.0"
|
||||
|
||||
# Project dependencies
|
||||
ts-rs = { version = "6.1", features = ["chrono-impl"] }
|
||||
ts-rs = { version = "6.1", features = ["chrono-impl", "uuid-impl"] }
|
||||
prisma-client-rust = { git = "https://github.com/Brendonovich/prisma-client-rust.git", tag = "0.5.0" }
|
||||
walkdir = "^2.3.2"
|
||||
lazy_static = "1.4.0"
|
||||
uuid = "0.8"
|
||||
sysinfo = "0.23.9"
|
||||
thiserror = "1.0.30"
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { LibraryCommand } from "./LibraryCommand";
|
||||
|
||||
export type ClientCommand = { key: "FileReadMetaData", params: { id: number, } } | { key: "FileSetNote", params: { id: number, note: string | null, } } | { key: "FileDelete", params: { id: number, } } | { key: "LibDelete", params: { id: number, } } | { key: "TagCreate", params: { name: string, color: string, } } | { key: "TagUpdate", params: { name: string, color: string, } } | { key: "TagAssign", params: { file_id: number, tag_id: number, } } | { key: "TagDelete", params: { id: number, } } | { key: "LocCreate", params: { path: string, } } | { key: "LocUpdate", params: { id: number, name: string | null, } } | { key: "LocDelete", params: { id: number, } } | { key: "LocRescan", params: { id: number, } } | { key: "SysVolumeUnmount", params: { id: number, } } | { key: "GenerateThumbsForLocation", params: { id: number, path: string, } } | { key: "IdentifyUniqueFiles", params: { id: number, path: string, } };
|
||||
export type ClientCommand = { key: "CreateLibrary", params: { name: string, } } | { key: "EditLibrary", params: { id: string, name: string | null, description: string | null, } } | { key: "DeleteLibrary", params: { id: string, } } | { key: "LibraryCommand", params: { library_id: string, command: LibraryCommand, } };
|
|
@ -1,3 +1,4 @@
|
|||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { LibraryQuery } from "./LibraryQuery";
|
||||
|
||||
export type ClientQuery = { key: "NodeGetState" } | { key: "SysGetVolumes" } | { key: "LibGetTags" } | { key: "JobGetRunning" } | { key: "JobGetHistory" } | { key: "SysGetLocations" } | { key: "SysGetLocation", params: { id: number, } } | { key: "LibGetExplorerDir", params: { location_id: number, path: string, limit: number, } } | { key: "GetLibraryStatistics" } | { key: "GetNodes" };
|
||||
export type ClientQuery = { key: "NodeGetLibraries" } | { key: "NodeGetState" } | { key: "SysGetVolumes" } | { key: "JobGetRunning" } | { key: "GetNodes" } | { key: "LibraryQuery", params: { library_id: string, query: LibraryQuery, } };
|
3
core/bindings/ConfigMetadata.ts
Normal file
3
core/bindings/ConfigMetadata.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export interface ConfigMetadata { version: string | null, }
|
|
@ -1,9 +1,10 @@
|
|||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { DirectoryWithContents } from "./DirectoryWithContents";
|
||||
import type { JobReport } from "./JobReport";
|
||||
import type { LibraryConfigWrapped } from "./LibraryConfigWrapped";
|
||||
import type { LocationResource } from "./LocationResource";
|
||||
import type { NodeState } from "./NodeState";
|
||||
import type { Statistics } from "./Statistics";
|
||||
import type { Volume } from "./Volume";
|
||||
|
||||
export type CoreResponse = { key: "Success", data: null } | { key: "SysGetVolumes", data: Array<Volume> } | { key: "SysGetLocation", data: LocationResource } | { key: "SysGetLocations", data: Array<LocationResource> } | { key: "LibGetExplorerDir", data: DirectoryWithContents } | { key: "NodeGetState", data: NodeState } | { key: "LocCreate", data: LocationResource } | { key: "JobGetRunning", data: Array<JobReport> } | { key: "JobGetHistory", data: Array<JobReport> } | { key: "GetLibraryStatistics", data: Statistics };
|
||||
export type CoreResponse = { key: "Success", data: null } | { key: "Error", data: string } | { key: "NodeGetLibraries", data: Array<LibraryConfigWrapped> } | { key: "SysGetVolumes", data: Array<Volume> } | { key: "SysGetLocation", data: LocationResource } | { key: "SysGetLocations", data: Array<LocationResource> } | { key: "LibGetExplorerDir", data: DirectoryWithContents } | { key: "NodeGetState", data: NodeState } | { key: "LocCreate", data: LocationResource } | { key: "JobGetRunning", data: Array<JobReport> } | { key: "JobGetHistory", data: Array<JobReport> } | { key: "GetLibraryStatistics", data: Statistics };
|
3
core/bindings/LibraryCommand.ts
Normal file
3
core/bindings/LibraryCommand.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type LibraryCommand = { key: "FileReadMetaData", params: { id: number, } } | { key: "FileSetNote", params: { id: number, note: string | null, } } | { key: "FileDelete", params: { id: number, } } | { key: "TagCreate", params: { name: string, color: string, } } | { key: "TagUpdate", params: { name: string, color: string, } } | { key: "TagAssign", params: { file_id: number, tag_id: number, } } | { key: "TagDelete", params: { id: number, } } | { key: "LocCreate", params: { path: string, } } | { key: "LocUpdate", params: { id: number, name: string | null, } } | { key: "LocDelete", params: { id: number, } } | { key: "LocRescan", params: { id: number, } } | { key: "SysVolumeUnmount", params: { id: number, } } | { key: "GenerateThumbsForLocation", params: { id: number, path: string, } } | { key: "IdentifyUniqueFiles", params: { id: number, path: string, } };
|
3
core/bindings/LibraryConfig.ts
Normal file
3
core/bindings/LibraryConfig.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export interface LibraryConfig { version: string | null, name: string, description: string, }
|
4
core/bindings/LibraryConfigWrapped.ts
Normal file
4
core/bindings/LibraryConfigWrapped.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { LibraryConfig } from "./LibraryConfig";
|
||||
|
||||
export interface LibraryConfigWrapped { uuid: string, config: LibraryConfig, }
|
3
core/bindings/LibraryQuery.ts
Normal file
3
core/bindings/LibraryQuery.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type LibraryQuery = { key: "LibGetTags" } | { key: "JobGetHistory" } | { key: "SysGetLocations" } | { key: "SysGetLocation", params: { id: number, } } | { key: "LibGetExplorerDir", params: { location_id: number, path: string, limit: number, } } | { key: "GetLibraryStatistics" };
|
3
core/bindings/NodeConfig.ts
Normal file
3
core/bindings/NodeConfig.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export interface NodeConfig { version: string | null, id: string, name: string, p2p_port: number | null, }
|
|
@ -1,4 +1,3 @@
|
|||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { LibraryState } from "./LibraryState";
|
||||
|
||||
export interface NodeState { node_pub_id: string, node_id: number, node_name: string, data_path: string, tcp_port: number, libraries: Array<LibraryState>, current_library_uuid: string, }
|
||||
export interface NodeState { version: string | null, id: string, name: string, p2p_port: number | null, data_path: string, }
|
|
@ -2,6 +2,7 @@ export * from './bindings/Client';
|
|||
export * from './bindings/ClientCommand';
|
||||
export * from './bindings/ClientQuery';
|
||||
export * from './bindings/ClientState';
|
||||
export * from './bindings/ConfigMetadata';
|
||||
export * from './bindings/CoreEvent';
|
||||
export * from './bindings/CoreResource';
|
||||
export * from './bindings/CoreResponse';
|
||||
|
@ -12,9 +13,14 @@ export * from './bindings/FileKind';
|
|||
export * from './bindings/FilePath';
|
||||
export * from './bindings/JobReport';
|
||||
export * from './bindings/JobStatus';
|
||||
export * from './bindings/LibraryCommand';
|
||||
export * from './bindings/LibraryConfig';
|
||||
export * from './bindings/LibraryConfigWrapped';
|
||||
export * from './bindings/LibraryNode';
|
||||
export * from './bindings/LibraryQuery';
|
||||
export * from './bindings/LibraryState';
|
||||
export * from './bindings/LocationResource';
|
||||
export * from './bindings/NodeConfig';
|
||||
export * from './bindings/NodeState';
|
||||
export * from './bindings/Platform';
|
||||
export * from './bindings/Statistics';
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the `libraries` table. If the table is not empty, all the data it contains will be lost.
|
||||
- You are about to drop the `library_statistics` table. If the table is not empty, all the data it contains will be lost.
|
||||
|
||||
*/
|
||||
-- DropTable
|
||||
PRAGMA foreign_keys=off;
|
||||
DROP TABLE "libraries";
|
||||
PRAGMA foreign_keys=on;
|
||||
|
||||
-- DropTable
|
||||
PRAGMA foreign_keys=off;
|
||||
DROP TABLE "library_statistics";
|
||||
PRAGMA foreign_keys=on;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "statistics" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"date_captured" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"total_file_count" INTEGER NOT NULL DEFAULT 0,
|
||||
"library_db_size" TEXT NOT NULL DEFAULT '0',
|
||||
"total_bytes_used" TEXT NOT NULL DEFAULT '0',
|
||||
"total_bytes_capacity" TEXT NOT NULL DEFAULT '0',
|
||||
"total_unique_bytes" TEXT NOT NULL DEFAULT '0',
|
||||
"total_bytes_free" TEXT NOT NULL DEFAULT '0',
|
||||
"preview_media_bytes" TEXT NOT NULL DEFAULT '0'
|
||||
);
|
|
@ -35,21 +35,9 @@ model SyncEvent {
|
|||
@@map("sync_events")
|
||||
}
|
||||
|
||||
model Library {
|
||||
id Int @id @default(autoincrement())
|
||||
pub_id String @unique
|
||||
name String
|
||||
is_primary Boolean @default(true)
|
||||
date_created DateTime @default(now())
|
||||
timezone String?
|
||||
|
||||
@@map("libraries")
|
||||
}
|
||||
|
||||
model LibraryStatistics {
|
||||
model Statistics {
|
||||
id Int @id @default(autoincrement())
|
||||
date_captured DateTime @default(now())
|
||||
library_id Int @unique
|
||||
total_file_count Int @default(0)
|
||||
library_db_size String @default("0")
|
||||
total_bytes_used String @default("0")
|
||||
|
@ -58,7 +46,7 @@ model LibraryStatistics {
|
|||
total_bytes_free String @default("0")
|
||||
preview_media_bytes String @default("0")
|
||||
|
||||
@@map("library_statistics")
|
||||
@@map("statistics")
|
||||
}
|
||||
|
||||
model Node {
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
use crate::job::JobReportUpdate;
|
||||
use crate::node::get_nodestate;
|
||||
use crate::job::{JobReportUpdate, JobResult};
|
||||
use crate::library::LibraryContext;
|
||||
use crate::{
|
||||
job::{Job, WorkerContext},
|
||||
prisma::file_path,
|
||||
CoreContext,
|
||||
};
|
||||
use crate::{sys, CoreEvent};
|
||||
use futures::executor::block_on;
|
||||
|
@ -29,11 +28,18 @@ impl Job for ThumbnailJob {
|
|||
fn name(&self) -> &'static str {
|
||||
"thumbnailer"
|
||||
}
|
||||
async fn run(&self, ctx: WorkerContext) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let config = get_nodestate();
|
||||
let core_ctx = ctx.core_ctx.clone();
|
||||
async fn run(&self, ctx: WorkerContext) -> JobResult {
|
||||
let library_ctx = ctx.library_ctx();
|
||||
let thumbnail_dir = Path::new(&library_ctx.config().data_directory())
|
||||
.join(THUMBNAIL_CACHE_DIR_NAME)
|
||||
.join(format!("{}", self.location_id));
|
||||
|
||||
let location = sys::get_location(&core_ctx, self.location_id).await?;
|
||||
let location = sys::get_location(&library_ctx, self.location_id).await?;
|
||||
|
||||
info!(
|
||||
"Searching for images in location {} at path {}",
|
||||
location.id, self.path
|
||||
);
|
||||
|
||||
info!(
|
||||
"Searching for images in location {} at path {}",
|
||||
|
@ -41,17 +47,12 @@ impl Job for ThumbnailJob {
|
|||
);
|
||||
|
||||
// create all necessary directories if they don't exist
|
||||
fs::create_dir_all(
|
||||
Path::new(&config.data_path)
|
||||
.join(THUMBNAIL_CACHE_DIR_NAME)
|
||||
.join(format!("{}", self.location_id)),
|
||||
)?;
|
||||
fs::create_dir_all(&thumbnail_dir)?;
|
||||
let root_path = location.path.unwrap();
|
||||
|
||||
// query database for all files in this location that need thumbnails
|
||||
let image_files = get_images(&core_ctx, self.location_id, &self.path).await?;
|
||||
let image_files = get_images(&library_ctx, self.location_id, &self.path).await?;
|
||||
info!("Found {:?} files", image_files.len());
|
||||
|
||||
let is_background = self.background.clone();
|
||||
|
||||
tokio::task::spawn_blocking(move || {
|
||||
|
@ -89,9 +90,7 @@ impl Job for ThumbnailJob {
|
|||
};
|
||||
|
||||
// Define and write the WebP-encoded file to a given path
|
||||
let output_path = Path::new(&config.data_path)
|
||||
.join(THUMBNAIL_CACHE_DIR_NAME)
|
||||
.join(format!("{}", location.id))
|
||||
let output_path = Path::new(&thumbnail_dir)
|
||||
.join(&cas_id)
|
||||
.with_extension("webp");
|
||||
|
||||
|
@ -107,7 +106,7 @@ impl Job for ThumbnailJob {
|
|||
ctx.progress(vec![JobReportUpdate::CompletedTaskCount(i + 1)]);
|
||||
|
||||
if !is_background {
|
||||
block_on(ctx.core_ctx.emit(CoreEvent::NewThumbnail { cas_id }));
|
||||
block_on(ctx.library_ctx().emit(CoreEvent::NewThumbnail { cas_id }));
|
||||
};
|
||||
} else {
|
||||
info!("Thumb exists, skipping... {}", output_path.display());
|
||||
|
@ -146,7 +145,7 @@ pub fn generate_thumbnail(
|
|||
}
|
||||
|
||||
pub async fn get_images(
|
||||
ctx: &CoreContext,
|
||||
ctx: &LibraryContext,
|
||||
location_id: i32,
|
||||
path: &str,
|
||||
) -> Result<Vec<file_path::Data>, std::io::Error> {
|
||||
|
@ -166,7 +165,7 @@ pub async fn get_images(
|
|||
}
|
||||
|
||||
let image_files = ctx
|
||||
.database
|
||||
.db
|
||||
.file_path()
|
||||
.find_many(params)
|
||||
.with(file_path::file::fetch())
|
||||
|
|
|
@ -2,10 +2,10 @@ use super::checksum::generate_cas_id;
|
|||
use crate::{
|
||||
file::FileError,
|
||||
job::JobReportUpdate,
|
||||
job::{Job, WorkerContext},
|
||||
job::{Job, JobResult, WorkerContext},
|
||||
library::LibraryContext,
|
||||
prisma::{file, file_path},
|
||||
sys::get_location,
|
||||
CoreContext,
|
||||
};
|
||||
use chrono::{DateTime, FixedOffset};
|
||||
use futures::executor::block_on;
|
||||
|
@ -33,13 +33,14 @@ impl Job for FileIdentifierJob {
|
|||
fn name(&self) -> &'static str {
|
||||
"file_identifier"
|
||||
}
|
||||
async fn run(&self, ctx: WorkerContext) -> Result<(), Box<dyn std::error::Error>> {
|
||||
|
||||
async fn run(&self, ctx: WorkerContext) -> JobResult {
|
||||
info!("Identifying orphan file paths...");
|
||||
|
||||
let location = get_location(&ctx.core_ctx, self.location_id).await?;
|
||||
let location = get_location(&ctx.library_ctx(), self.location_id).await?;
|
||||
let location_path = location.path.unwrap_or("".to_string());
|
||||
|
||||
let total_count = count_orphan_file_paths(&ctx.core_ctx, location.id.into()).await?;
|
||||
let total_count = count_orphan_file_paths(&ctx.library_ctx(), location.id.into()).await?;
|
||||
info!("Found {} orphan file paths", total_count);
|
||||
|
||||
let task_count = (total_count as f64 / CHUNK_SIZE as f64).ceil() as usize;
|
||||
|
@ -48,9 +49,9 @@ impl Job for FileIdentifierJob {
|
|||
// update job with total task count based on orphan file_paths count
|
||||
ctx.progress(vec![JobReportUpdate::TaskCount(task_count)]);
|
||||
|
||||
let db = ctx.core_ctx.database.clone();
|
||||
// dedicated tokio thread for task
|
||||
let _ctx = tokio::task::spawn_blocking(move || {
|
||||
let db = ctx.library_ctx().db;
|
||||
let mut completed: usize = 0;
|
||||
let mut cursor: i32 = 1;
|
||||
// loop until task count is complete
|
||||
|
@ -60,7 +61,7 @@ impl Job for FileIdentifierJob {
|
|||
let mut cas_lookup: HashMap<String, i32> = HashMap::new();
|
||||
|
||||
// get chunk of orphans to process
|
||||
let file_paths = match block_on(get_orphan_file_paths(&ctx.core_ctx, cursor)) {
|
||||
let file_paths = match block_on(get_orphan_file_paths(&ctx.library_ctx(), cursor)) {
|
||||
Ok(file_paths) => file_paths,
|
||||
Err(e) => {
|
||||
info!("Error getting orphan file paths: {}", e);
|
||||
|
@ -192,11 +193,10 @@ struct CountRes {
|
|||
}
|
||||
|
||||
pub async fn count_orphan_file_paths(
|
||||
ctx: &CoreContext,
|
||||
ctx: &LibraryContext,
|
||||
location_id: i64,
|
||||
) -> Result<usize, FileError> {
|
||||
let db = &ctx.database;
|
||||
let files_count = db
|
||||
let files_count = ctx.db
|
||||
._query_raw::<CountRes>(raw!(
|
||||
"SELECT COUNT(*) AS count FROM file_paths WHERE file_id IS NULL AND is_dir IS FALSE AND location_id = {}",
|
||||
PrismaValue::Int(location_id)
|
||||
|
@ -206,10 +206,10 @@ pub async fn count_orphan_file_paths(
|
|||
}
|
||||
|
||||
pub async fn get_orphan_file_paths(
|
||||
ctx: &CoreContext,
|
||||
ctx: &LibraryContext,
|
||||
cursor: i32,
|
||||
) -> Result<Vec<file_path::Data>, FileError> {
|
||||
let db = &ctx.database;
|
||||
let db = &ctx.db;
|
||||
info!(
|
||||
"discovering {} orphan file paths at cursor: {:?}",
|
||||
CHUNK_SIZE, cursor
|
||||
|
@ -225,6 +225,7 @@ pub async fn get_orphan_file_paths(
|
|||
.take(CHUNK_SIZE as i64)
|
||||
.exec()
|
||||
.await?;
|
||||
|
||||
Ok(files)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,25 +1,22 @@
|
|||
use crate::{
|
||||
encode::THUMBNAIL_CACHE_DIR_NAME,
|
||||
file::{DirectoryWithContents, FileError, FilePath},
|
||||
node::get_nodestate,
|
||||
library::LibraryContext,
|
||||
prisma::file_path,
|
||||
sys::get_location,
|
||||
CoreContext,
|
||||
};
|
||||
use std::path::Path;
|
||||
|
||||
pub async fn open_dir(
|
||||
ctx: &CoreContext,
|
||||
ctx: &LibraryContext,
|
||||
location_id: &i32,
|
||||
path: &str,
|
||||
) -> Result<DirectoryWithContents, FileError> {
|
||||
let db = &ctx.database;
|
||||
let config = get_nodestate();
|
||||
|
||||
// get location
|
||||
let location = get_location(ctx, location_id.clone()).await?;
|
||||
|
||||
let directory = db
|
||||
let directory = ctx
|
||||
.db
|
||||
.file_path()
|
||||
.find_first(vec![
|
||||
file_path::location_id::equals(Some(location.id)),
|
||||
|
@ -32,7 +29,8 @@ pub async fn open_dir(
|
|||
|
||||
println!("DIRECTORY: {:?}", directory);
|
||||
|
||||
let mut file_paths: Vec<FilePath> = db
|
||||
let mut file_paths: Vec<FilePath> = ctx
|
||||
.db
|
||||
.file_path()
|
||||
.find_many(vec![
|
||||
file_path::location_id::equals(Some(location.id)),
|
||||
|
@ -47,7 +45,7 @@ pub async fn open_dir(
|
|||
|
||||
for file_path in &mut file_paths {
|
||||
if let Some(file) = &mut file_path.file {
|
||||
let thumb_path = Path::new(&config.data_path)
|
||||
let thumb_path = Path::new(&ctx.config().data_directory())
|
||||
.join(THUMBNAIL_CACHE_DIR_NAME)
|
||||
.join(format!("{}", location.id))
|
||||
.join(file.cas_id.clone())
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use crate::job::{Job, JobReportUpdate, WorkerContext};
|
||||
use crate::job::{Job, JobReportUpdate, JobResult, WorkerContext};
|
||||
|
||||
use self::scan::ScanProgress;
|
||||
mod scan;
|
||||
|
@ -17,9 +17,8 @@ impl Job for IndexerJob {
|
|||
fn name(&self) -> &'static str {
|
||||
"indexer"
|
||||
}
|
||||
async fn run(&self, ctx: WorkerContext) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let core_ctx = ctx.core_ctx.clone();
|
||||
scan_path(&core_ctx, self.path.as_str(), move |p| {
|
||||
async fn run(&self, ctx: WorkerContext) -> JobResult {
|
||||
scan_path(&ctx.library_ctx(), self.path.as_str(), move |p| {
|
||||
ctx.progress(
|
||||
p.iter()
|
||||
.map(|p| match p.clone() {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
use crate::job::JobResult;
|
||||
use crate::library::LibraryContext;
|
||||
use crate::sys::{create_location, LocationResource};
|
||||
use crate::CoreContext;
|
||||
use chrono::{DateTime, FixedOffset, Utc};
|
||||
use chrono::{DateTime, Utc};
|
||||
use log::{error, info};
|
||||
use prisma_client_rust::prisma_models::PrismaValue;
|
||||
use prisma_client_rust::raw;
|
||||
|
@ -21,11 +22,10 @@ static BATCH_SIZE: usize = 100;
|
|||
|
||||
// creates a vector of valid path buffers from a directory
|
||||
pub async fn scan_path(
|
||||
ctx: &CoreContext,
|
||||
ctx: &LibraryContext,
|
||||
path: &str,
|
||||
on_progress: impl Fn(Vec<ScanProgress>) + Send + Sync + 'static,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let db = &ctx.database;
|
||||
) -> JobResult {
|
||||
let path = path.to_string();
|
||||
|
||||
let location = create_location(&ctx, &path).await?;
|
||||
|
@ -36,7 +36,8 @@ pub async fn scan_path(
|
|||
id: Option<i32>,
|
||||
}
|
||||
// grab the next id so we can increment in memory for batch inserting
|
||||
let first_file_id = match db
|
||||
let first_file_id = match ctx
|
||||
.db
|
||||
._query_raw::<QueryRes>(raw!("SELECT MAX(id) id FROM file_paths"))
|
||||
.await
|
||||
{
|
||||
|
@ -162,7 +163,7 @@ pub async fn scan_path(
|
|||
files
|
||||
);
|
||||
|
||||
let count = db._execute_raw(raw).await;
|
||||
let count = ctx.db._execute_raw(raw).await;
|
||||
|
||||
info!("Inserted {:?} records", count);
|
||||
}
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
use chrono::{DateTime, Utc};
|
||||
use int_enum::IntEnum;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
use ts_rs::TS;
|
||||
|
||||
use crate::{
|
||||
library::LibraryContext,
|
||||
prisma::{self, file, file_path},
|
||||
sys::SysError,
|
||||
ClientQuery, CoreContext, CoreError, CoreEvent, CoreResponse,
|
||||
ClientQuery, CoreError, CoreEvent, CoreResponse, LibraryQuery,
|
||||
};
|
||||
pub mod cas;
|
||||
pub mod explorer;
|
||||
|
@ -32,9 +34,9 @@ pub struct File {
|
|||
pub ipfs_id: Option<String>,
|
||||
pub note: Option<String>,
|
||||
|
||||
pub date_created: chrono::DateTime<chrono::Utc>,
|
||||
pub date_modified: chrono::DateTime<chrono::Utc>,
|
||||
pub date_indexed: chrono::DateTime<chrono::Utc>,
|
||||
pub date_created: DateTime<Utc>,
|
||||
pub date_modified: DateTime<Utc>,
|
||||
pub date_indexed: DateTime<Utc>,
|
||||
|
||||
pub paths: Vec<FilePath>,
|
||||
// pub media_data: Option<MediaData>,
|
||||
|
@ -55,9 +57,9 @@ pub struct FilePath {
|
|||
pub file_id: Option<i32>,
|
||||
pub parent_id: Option<i32>,
|
||||
|
||||
pub date_created: chrono::DateTime<chrono::Utc>,
|
||||
pub date_modified: chrono::DateTime<chrono::Utc>,
|
||||
pub date_indexed: chrono::DateTime<chrono::Utc>,
|
||||
pub date_created: DateTime<chrono::Utc>,
|
||||
pub date_modified: DateTime<chrono::Utc>,
|
||||
pub date_indexed: DateTime<chrono::Utc>,
|
||||
|
||||
pub file: Option<File>,
|
||||
}
|
||||
|
@ -141,12 +143,12 @@ pub enum FileError {
|
|||
}
|
||||
|
||||
pub async fn set_note(
|
||||
ctx: CoreContext,
|
||||
ctx: LibraryContext,
|
||||
id: i32,
|
||||
note: Option<String>,
|
||||
) -> Result<CoreResponse, CoreError> {
|
||||
let response = ctx
|
||||
.database
|
||||
let _response = ctx
|
||||
.db
|
||||
.file()
|
||||
.find_unique(file::id::equals(id))
|
||||
.update(vec![file::note::set(note.clone())])
|
||||
|
@ -154,10 +156,13 @@ pub async fn set_note(
|
|||
.await
|
||||
.unwrap();
|
||||
|
||||
ctx.emit(CoreEvent::InvalidateQuery(ClientQuery::LibGetExplorerDir {
|
||||
limit: 0,
|
||||
path: "".to_string(),
|
||||
location_id: 0,
|
||||
ctx.emit(CoreEvent::InvalidateQuery(ClientQuery::LibraryQuery {
|
||||
library_id: ctx.id.to_string(),
|
||||
query: LibraryQuery::LibGetExplorerDir {
|
||||
limit: 0,
|
||||
path: "".to_string(),
|
||||
location_id: 0,
|
||||
},
|
||||
}))
|
||||
.await;
|
||||
|
||||
|
|
|
@ -3,48 +3,69 @@ use super::{
|
|||
JobError,
|
||||
};
|
||||
use crate::{
|
||||
node::get_nodestate,
|
||||
library::LibraryContext,
|
||||
prisma::{job, node},
|
||||
CoreContext,
|
||||
};
|
||||
use int_enum::IntEnum;
|
||||
use log::info;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
collections::{HashMap, VecDeque},
|
||||
error::Error,
|
||||
fmt::Debug,
|
||||
sync::Arc,
|
||||
};
|
||||
use tokio::sync::Mutex;
|
||||
use tokio::sync::{mpsc, Mutex, RwLock};
|
||||
use ts_rs::TS;
|
||||
|
||||
// db is single threaded, nerd
|
||||
const MAX_WORKERS: usize = 1;
|
||||
|
||||
pub type JobResult = Result<(), Box<dyn Error + Send + Sync>>;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait Job: Send + Sync + Debug {
|
||||
async fn run(&self, ctx: WorkerContext) -> Result<(), Box<dyn std::error::Error>>;
|
||||
async fn run(&self, ctx: WorkerContext) -> JobResult;
|
||||
fn name(&self) -> &'static str;
|
||||
}
|
||||
|
||||
// jobs struct is maintained by the core
|
||||
pub struct Jobs {
|
||||
job_queue: VecDeque<Box<dyn Job>>,
|
||||
// workers are spawned when jobs are picked off the queue
|
||||
running_workers: HashMap<String, Arc<Mutex<Worker>>>,
|
||||
pub enum JobManagerEvent {
|
||||
IngestJob(LibraryContext, Box<dyn Job>),
|
||||
}
|
||||
|
||||
impl Jobs {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
job_queue: VecDeque::new(),
|
||||
running_workers: HashMap::new(),
|
||||
}
|
||||
// jobs struct is maintained by the core
|
||||
pub struct JobManager {
|
||||
job_queue: RwLock<VecDeque<Box<dyn Job>>>,
|
||||
// workers are spawned when jobs are picked off the queue
|
||||
running_workers: RwLock<HashMap<String, Arc<Mutex<Worker>>>>,
|
||||
internal_sender: mpsc::UnboundedSender<JobManagerEvent>,
|
||||
}
|
||||
|
||||
impl JobManager {
|
||||
pub fn new() -> Arc<Self> {
|
||||
let (internal_sender, mut internal_reciever) = mpsc::unbounded_channel();
|
||||
let this = Arc::new(Self {
|
||||
job_queue: RwLock::new(VecDeque::new()),
|
||||
running_workers: RwLock::new(HashMap::new()),
|
||||
internal_sender,
|
||||
});
|
||||
|
||||
let this2 = this.clone();
|
||||
tokio::spawn(async move {
|
||||
while let Some(event) = internal_reciever.recv().await {
|
||||
match event {
|
||||
JobManagerEvent::IngestJob(ctx, job) => this2.clone().ingest(&ctx, job).await,
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this
|
||||
}
|
||||
|
||||
pub async fn ingest(&mut self, ctx: &CoreContext, job: Box<dyn Job>) {
|
||||
pub async fn ingest(self: Arc<Self>, ctx: &LibraryContext, job: Box<dyn Job>) {
|
||||
// create worker to process job
|
||||
if self.running_workers.len() < MAX_WORKERS {
|
||||
let mut running_workers = self.running_workers.write().await;
|
||||
if running_workers.len() < MAX_WORKERS {
|
||||
info!("Running job: {:?}", job.name());
|
||||
|
||||
let worker = Worker::new(job);
|
||||
|
@ -52,51 +73,57 @@ impl Jobs {
|
|||
|
||||
let wrapped_worker = Arc::new(Mutex::new(worker));
|
||||
|
||||
Worker::spawn(wrapped_worker.clone(), ctx).await;
|
||||
Worker::spawn(self.clone(), wrapped_worker.clone(), ctx).await;
|
||||
|
||||
self.running_workers.insert(id, wrapped_worker);
|
||||
running_workers.insert(id, wrapped_worker);
|
||||
} else {
|
||||
self.job_queue.push_back(job);
|
||||
self.job_queue.write().await.push_back(job);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ingest_queue(&mut self, _ctx: &CoreContext, job: Box<dyn Job>) {
|
||||
self.job_queue.push_back(job);
|
||||
pub async fn ingest_queue(&self, _ctx: &LibraryContext, job: Box<dyn Job>) {
|
||||
self.job_queue.write().await.push_back(job);
|
||||
}
|
||||
pub async fn complete(&mut self, ctx: &CoreContext, job_id: String) {
|
||||
|
||||
pub async fn complete(self: Arc<Self>, ctx: &LibraryContext, job_id: String) {
|
||||
// remove worker from running workers
|
||||
self.running_workers.remove(&job_id);
|
||||
self.running_workers.write().await.remove(&job_id);
|
||||
// continue queue
|
||||
let job = self.job_queue.pop_front();
|
||||
let job = self.job_queue.write().await.pop_front();
|
||||
if let Some(job) = job {
|
||||
self.ingest(ctx, job).await;
|
||||
// We can't directly execute `self.ingest` here because it would cause an async cycle.
|
||||
self.internal_sender
|
||||
.send(JobManagerEvent::IngestJob(ctx.clone(), job))
|
||||
.unwrap_or_else(|_| {
|
||||
println!("Failed to ingest job!");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_running(&self) -> Vec<JobReport> {
|
||||
let mut ret = vec![];
|
||||
|
||||
for worker in self.running_workers.values() {
|
||||
for worker in self.running_workers.read().await.values() {
|
||||
let worker = worker.lock().await;
|
||||
ret.push(worker.job_report.clone());
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
pub async fn queue_pending_job(ctx: &CoreContext) -> Result<(), JobError> {
|
||||
let db = &ctx.database;
|
||||
// pub async fn queue_pending_job(ctx: &LibraryContext) -> Result<(), JobError> {
|
||||
// let db = &ctx.db;
|
||||
|
||||
let next_job = db
|
||||
.job()
|
||||
.find_first(vec![job::status::equals(JobStatus::Queued.int_value())])
|
||||
.exec()
|
||||
.await?;
|
||||
// let _next_job = db
|
||||
// .job()
|
||||
// .find_first(vec![job::status::equals(JobStatus::Queued.int_value())])
|
||||
// .exec()
|
||||
// .await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
// Ok(())
|
||||
// }
|
||||
|
||||
pub async fn get_history(ctx: &CoreContext) -> Result<Vec<JobReport>, JobError> {
|
||||
let db = &ctx.database;
|
||||
pub async fn get_history(ctx: &LibraryContext) -> Result<Vec<JobReport>, JobError> {
|
||||
let db = &ctx.db;
|
||||
let jobs = db
|
||||
.job()
|
||||
.find_many(vec![job::status::not(JobStatus::Running.int_value())])
|
||||
|
@ -172,30 +199,29 @@ impl JobReport {
|
|||
seconds_elapsed: 0,
|
||||
}
|
||||
}
|
||||
pub async fn create(&self, ctx: &CoreContext) -> Result<(), JobError> {
|
||||
let config = get_nodestate();
|
||||
|
||||
pub async fn create(&self, ctx: &LibraryContext) -> Result<(), JobError> {
|
||||
let mut params = Vec::new();
|
||||
|
||||
if let Some(_) = &self.data {
|
||||
params.push(job::data::set(self.data.clone()))
|
||||
}
|
||||
|
||||
ctx.database
|
||||
ctx.db
|
||||
.job()
|
||||
.create(
|
||||
job::id::set(self.id.clone()),
|
||||
job::name::set(self.name.clone()),
|
||||
job::action::set(1),
|
||||
job::nodes::link(node::id::equals(config.node_id)),
|
||||
job::nodes::link(node::id::equals(ctx.node_local_id)),
|
||||
params,
|
||||
)
|
||||
.exec()
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
pub async fn update(&self, ctx: &CoreContext) -> Result<(), JobError> {
|
||||
ctx.database
|
||||
pub async fn update(&self, ctx: &LibraryContext) -> Result<(), JobError> {
|
||||
ctx.db
|
||||
.job()
|
||||
.find_unique(job::id::equals(self.id.clone()))
|
||||
.update(vec![
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
use super::{
|
||||
jobs::{JobReport, JobReportUpdate, JobStatus},
|
||||
Job,
|
||||
Job, JobManager,
|
||||
};
|
||||
use crate::{ClientQuery, CoreContext, CoreEvent, InternalEvent};
|
||||
use crate::{library::LibraryContext, ClientQuery, CoreEvent, LibraryQuery};
|
||||
use std::{sync::Arc, time::Duration};
|
||||
use tokio::{
|
||||
sync::{
|
||||
|
@ -26,8 +26,8 @@ enum WorkerState {
|
|||
#[derive(Clone)]
|
||||
pub struct WorkerContext {
|
||||
pub uuid: String,
|
||||
pub core_ctx: CoreContext,
|
||||
pub sender: UnboundedSender<WorkerEvent>,
|
||||
library_ctx: LibraryContext,
|
||||
sender: UnboundedSender<WorkerEvent>,
|
||||
}
|
||||
|
||||
impl WorkerContext {
|
||||
|
@ -36,9 +36,13 @@ impl WorkerContext {
|
|||
.send(WorkerEvent::Progressed(updates))
|
||||
.unwrap_or(());
|
||||
}
|
||||
|
||||
pub fn library_ctx(&self) -> LibraryContext {
|
||||
self.library_ctx.clone()
|
||||
}
|
||||
|
||||
// save the job data to
|
||||
// pub fn save_data () {
|
||||
|
||||
// }
|
||||
}
|
||||
|
||||
|
@ -63,7 +67,11 @@ impl Worker {
|
|||
}
|
||||
}
|
||||
// spawns a thread and extracts channel sender to communicate with it
|
||||
pub async fn spawn(worker: Arc<Mutex<Self>>, ctx: &CoreContext) {
|
||||
pub async fn spawn(
|
||||
job_manager: Arc<JobManager>,
|
||||
worker: Arc<Mutex<Self>>,
|
||||
ctx: &LibraryContext,
|
||||
) {
|
||||
// we capture the worker receiver channel so state can be updated from inside the worker
|
||||
let mut worker_mut = worker.lock().await;
|
||||
// extract owned job and receiver from Self
|
||||
|
@ -76,10 +84,11 @@ impl Worker {
|
|||
WorkerState::Running => unreachable!(),
|
||||
};
|
||||
let worker_sender = worker_mut.worker_sender.clone();
|
||||
let core_ctx = ctx.clone();
|
||||
|
||||
worker_mut.job_report.status = JobStatus::Running;
|
||||
|
||||
let ctx = ctx.clone();
|
||||
|
||||
worker_mut.job_report.create(&ctx).await.unwrap_or(());
|
||||
|
||||
// spawn task to handle receiving events from the worker
|
||||
|
@ -94,7 +103,7 @@ impl Worker {
|
|||
tokio::spawn(async move {
|
||||
let worker_ctx = WorkerContext {
|
||||
uuid,
|
||||
core_ctx,
|
||||
library_ctx: ctx.clone(),
|
||||
sender: worker_sender,
|
||||
};
|
||||
let job_start = Instant::now();
|
||||
|
@ -113,20 +122,17 @@ impl Worker {
|
|||
}
|
||||
});
|
||||
|
||||
let result = job.run(worker_ctx.clone()).await;
|
||||
|
||||
if let Err(e) = result {
|
||||
println!("job failed {:?}", e);
|
||||
worker_ctx.sender.send(WorkerEvent::Failed).unwrap_or(());
|
||||
} else {
|
||||
// handle completion
|
||||
worker_ctx.sender.send(WorkerEvent::Completed).unwrap_or(());
|
||||
match job.run(worker_ctx.clone()).await {
|
||||
Ok(_) => {
|
||||
worker_ctx.sender.send(WorkerEvent::Completed).unwrap_or(());
|
||||
}
|
||||
Err(err) => {
|
||||
println!("job '{}' failed with error: {}", worker_ctx.uuid, err);
|
||||
worker_ctx.sender.send(WorkerEvent::Failed).unwrap_or(());
|
||||
}
|
||||
}
|
||||
worker_ctx
|
||||
.core_ctx
|
||||
.internal_sender
|
||||
.send(InternalEvent::JobComplete(worker_ctx.uuid.clone()))
|
||||
.unwrap_or(());
|
||||
|
||||
job_manager.complete(&ctx, worker_ctx.uuid).await;
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -137,7 +143,7 @@ impl Worker {
|
|||
async fn track_progress(
|
||||
worker: Arc<Mutex<Self>>,
|
||||
mut channel: UnboundedReceiver<WorkerEvent>,
|
||||
ctx: CoreContext,
|
||||
ctx: LibraryContext,
|
||||
) {
|
||||
while let Some(command) = channel.recv().await {
|
||||
let mut worker = worker.lock().await;
|
||||
|
@ -176,16 +182,23 @@ impl Worker {
|
|||
|
||||
ctx.emit(CoreEvent::InvalidateQuery(ClientQuery::JobGetRunning))
|
||||
.await;
|
||||
ctx.emit(CoreEvent::InvalidateQuery(ClientQuery::JobGetHistory))
|
||||
.await;
|
||||
|
||||
ctx.emit(CoreEvent::InvalidateQuery(ClientQuery::LibraryQuery {
|
||||
library_id: ctx.id.to_string(),
|
||||
query: LibraryQuery::JobGetHistory,
|
||||
}))
|
||||
.await;
|
||||
break;
|
||||
}
|
||||
WorkerEvent::Failed => {
|
||||
worker.job_report.status = JobStatus::Failed;
|
||||
worker.job_report.update(&ctx).await.unwrap_or(());
|
||||
|
||||
ctx.emit(CoreEvent::InvalidateQuery(ClientQuery::JobGetHistory))
|
||||
.await;
|
||||
ctx.emit(CoreEvent::InvalidateQuery(ClientQuery::LibraryQuery {
|
||||
library_id: ctx.id.to_string(),
|
||||
query: LibraryQuery::JobGetHistory,
|
||||
}))
|
||||
.await;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
464
core/src/lib.rs
464
core/src/lib.rs
|
@ -1,11 +1,13 @@
|
|||
use crate::{
|
||||
file::cas::FileIdentifierJob, library::get_library_path, node::NodeState,
|
||||
prisma::file as prisma_file, prisma::location, util::db::create_connection,
|
||||
};
|
||||
use job::{Job, JobReport, Jobs};
|
||||
use prisma::PrismaClient;
|
||||
use crate::{file::cas::FileIdentifierJob, prisma::file as prisma_file, prisma::location};
|
||||
use job::{JobManager, JobReport};
|
||||
use library::{LibraryConfig, LibraryConfigWrapped, LibraryManager};
|
||||
use node::{NodeConfig, NodeConfigManager};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{fs, sync::Arc};
|
||||
use std::{
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
use thiserror::Error;
|
||||
use tokio::sync::{
|
||||
mpsc::{self, unbounded_channel, UnboundedReceiver, UnboundedSender},
|
||||
|
@ -32,12 +34,12 @@ pub struct ReturnableMessage<D, R = Result<CoreResponse, CoreError>> {
|
|||
}
|
||||
|
||||
// core controller is passed to the client to communicate with the core which runs in a dedicated thread
|
||||
pub struct CoreController {
|
||||
pub struct NodeController {
|
||||
query_sender: UnboundedSender<ReturnableMessage<ClientQuery>>,
|
||||
command_sender: UnboundedSender<ReturnableMessage<ClientCommand>>,
|
||||
}
|
||||
|
||||
impl CoreController {
|
||||
impl NodeController {
|
||||
pub async fn query(&self, query: ClientQuery) -> Result<CoreResponse, CoreError> {
|
||||
// a one time use channel to send and await a response
|
||||
let (sender, recv) = oneshot::channel();
|
||||
|
@ -64,35 +66,14 @@ impl CoreController {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum InternalEvent {
|
||||
JobIngest(Box<dyn Job>),
|
||||
JobQueue(Box<dyn Job>),
|
||||
JobComplete(String),
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct CoreContext {
|
||||
pub database: Arc<PrismaClient>,
|
||||
pub struct NodeContext {
|
||||
pub event_sender: mpsc::Sender<CoreEvent>,
|
||||
pub internal_sender: UnboundedSender<InternalEvent>,
|
||||
pub config: Arc<NodeConfigManager>,
|
||||
pub jobs: Arc<JobManager>,
|
||||
}
|
||||
|
||||
impl CoreContext {
|
||||
pub fn spawn_job(&self, job: Box<dyn Job>) {
|
||||
self.internal_sender
|
||||
.send(InternalEvent::JobIngest(job))
|
||||
.unwrap_or_else(|e| {
|
||||
println!("Failed to spawn job. {:?}", e);
|
||||
});
|
||||
}
|
||||
pub fn queue_job(&self, job: Box<dyn Job>) {
|
||||
self.internal_sender
|
||||
.send(InternalEvent::JobQueue(job))
|
||||
.unwrap_or_else(|e| {
|
||||
println!("Failed to queue job. {:?}", e);
|
||||
});
|
||||
}
|
||||
impl NodeContext {
|
||||
pub async fn emit(&self, event: CoreEvent) {
|
||||
self.event_sender.send(event).await.unwrap_or_else(|e| {
|
||||
println!("Failed to emit event. {:?}", e);
|
||||
|
@ -101,11 +82,9 @@ impl CoreContext {
|
|||
}
|
||||
|
||||
pub struct Node {
|
||||
state: NodeState,
|
||||
jobs: job::Jobs,
|
||||
database: Arc<PrismaClient>,
|
||||
// filetype_registry: library::TypeRegistry,
|
||||
// extension_registry: library::ExtensionRegistry,
|
||||
config: Arc<NodeConfigManager>,
|
||||
library_manager: Arc<LibraryManager>,
|
||||
jobs: Arc<JobManager>,
|
||||
|
||||
// global messaging channels
|
||||
query_channel: (
|
||||
|
@ -117,73 +96,52 @@ pub struct Node {
|
|||
UnboundedReceiver<ReturnableMessage<ClientCommand>>,
|
||||
),
|
||||
event_sender: mpsc::Sender<CoreEvent>,
|
||||
|
||||
// a channel for child threads to send events back to the core
|
||||
internal_channel: (
|
||||
UnboundedSender<InternalEvent>,
|
||||
UnboundedReceiver<InternalEvent>,
|
||||
),
|
||||
}
|
||||
|
||||
impl Node {
|
||||
// create new instance of node, run startup tasks
|
||||
pub async fn new(mut data_dir: std::path::PathBuf) -> (Node, mpsc::Receiver<CoreEvent>) {
|
||||
let (event_sender, event_recv) = mpsc::channel(100);
|
||||
|
||||
data_dir = data_dir.join("spacedrive");
|
||||
let data_dir = data_dir.to_str().unwrap();
|
||||
// create data directory if it doesn't exist
|
||||
pub async fn new(data_dir: PathBuf) -> (NodeController, mpsc::Receiver<CoreEvent>, Node) {
|
||||
fs::create_dir_all(&data_dir).unwrap();
|
||||
// prepare basic client state
|
||||
let mut state = NodeState::new(data_dir, "diamond-mastering-space-dragon").unwrap();
|
||||
// load from disk
|
||||
state
|
||||
.read_disk()
|
||||
.unwrap_or(println!("Error: No node state found, creating new one..."));
|
||||
|
||||
state.save();
|
||||
|
||||
println!("Node State: {:?}", state);
|
||||
|
||||
// connect to default library
|
||||
let database = Arc::new(
|
||||
create_connection(&get_library_path(&data_dir))
|
||||
.await
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
let internal_channel = unbounded_channel::<InternalEvent>();
|
||||
|
||||
let node = Node {
|
||||
state,
|
||||
query_channel: unbounded_channel(),
|
||||
command_channel: unbounded_channel(),
|
||||
jobs: Jobs::new(),
|
||||
event_sender,
|
||||
database,
|
||||
internal_channel,
|
||||
let (event_sender, event_recv) = mpsc::channel(100);
|
||||
let config = NodeConfigManager::new(data_dir.clone()).await.unwrap();
|
||||
let jobs = JobManager::new();
|
||||
let node_ctx = NodeContext {
|
||||
event_sender: event_sender.clone(),
|
||||
config: config.clone(),
|
||||
jobs: jobs.clone(),
|
||||
};
|
||||
|
||||
(node, event_recv)
|
||||
let node = Node {
|
||||
config,
|
||||
library_manager: LibraryManager::new(Path::new(&data_dir).join("libraries"), node_ctx)
|
||||
.await
|
||||
.unwrap(),
|
||||
query_channel: unbounded_channel(),
|
||||
command_channel: unbounded_channel(),
|
||||
jobs,
|
||||
event_sender,
|
||||
};
|
||||
|
||||
(
|
||||
NodeController {
|
||||
query_sender: node.query_channel.0.clone(),
|
||||
command_sender: node.command_channel.0.clone(),
|
||||
},
|
||||
event_recv,
|
||||
node,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn get_context(&self) -> CoreContext {
|
||||
CoreContext {
|
||||
database: self.database.clone(),
|
||||
pub fn get_context(&self) -> NodeContext {
|
||||
NodeContext {
|
||||
event_sender: self.event_sender.clone(),
|
||||
internal_sender: self.internal_channel.0.clone(),
|
||||
config: self.config.clone(),
|
||||
jobs: self.jobs.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_controller(&self) -> CoreController {
|
||||
CoreController {
|
||||
query_sender: self.query_channel.0.clone(),
|
||||
command_sender: self.command_channel.0.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn start(&mut self) {
|
||||
let ctx = self.get_context();
|
||||
pub async fn start(mut self) {
|
||||
loop {
|
||||
// listen on global messaging channels for incoming messages
|
||||
tokio::select! {
|
||||
|
@ -195,174 +153,200 @@ impl Node {
|
|||
let res = self.exec_command(msg.data).await;
|
||||
msg.return_sender.send(res).unwrap_or(());
|
||||
}
|
||||
Some(event) = self.internal_channel.1.recv() => {
|
||||
match event {
|
||||
InternalEvent::JobIngest(job) => {
|
||||
self.jobs.ingest(&ctx, job).await;
|
||||
},
|
||||
InternalEvent::JobQueue(job) => {
|
||||
self.jobs.ingest_queue(&ctx, job);
|
||||
},
|
||||
InternalEvent::JobComplete(id) => {
|
||||
self.jobs.complete(&ctx, id).await;
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// load library database + initialize client with db
|
||||
pub async fn initializer(&self) {
|
||||
println!("Initializing...");
|
||||
let ctx = self.get_context();
|
||||
|
||||
if self.state.libraries.len() == 0 {
|
||||
match library::create(&ctx, None).await {
|
||||
Ok(library) => println!("Created new library: {:?}", library),
|
||||
Err(e) => println!("Error creating library: {:?}", e),
|
||||
}
|
||||
} else {
|
||||
for library in self.state.libraries.iter() {
|
||||
// init database for library
|
||||
match library::load(&ctx, &library.library_path, &library.library_uuid).await {
|
||||
Ok(library) => println!("Loaded library: {:?}", library),
|
||||
Err(e) => println!("Error loading library: {:?}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
// init node data within library
|
||||
match node::LibraryNode::create(&self).await {
|
||||
Ok(_) => println!("Spacedrive online"),
|
||||
Err(e) => println!("Error initializing node: {:?}", e),
|
||||
};
|
||||
}
|
||||
|
||||
async fn exec_command(&mut self, cmd: ClientCommand) -> Result<CoreResponse, CoreError> {
|
||||
println!("Core command: {:?}", cmd);
|
||||
let ctx = self.get_context();
|
||||
Ok(match cmd {
|
||||
// CRUD for locations
|
||||
ClientCommand::LocCreate { path } => {
|
||||
let loc = sys::new_location_and_scan(&ctx, &path).await?;
|
||||
// ctx.queue_job(Box::new(FileIdentifierJob));
|
||||
CoreResponse::LocCreate(loc)
|
||||
ClientCommand::CreateLibrary { name } => {
|
||||
self.library_manager
|
||||
.create(LibraryConfig {
|
||||
name: name.to_string(),
|
||||
..Default::default()
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
CoreResponse::Success(())
|
||||
}
|
||||
ClientCommand::LocUpdate { id, name } => {
|
||||
ctx.database
|
||||
.location()
|
||||
.find_unique(location::id::equals(id))
|
||||
.update(vec![location::name::set(name)])
|
||||
.exec()
|
||||
.await?;
|
||||
ClientCommand::EditLibrary {
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
} => {
|
||||
self.library_manager
|
||||
.edit_library(id, name, description)
|
||||
.await
|
||||
.unwrap();
|
||||
CoreResponse::Success(())
|
||||
}
|
||||
ClientCommand::DeleteLibrary { id } => {
|
||||
self.library_manager.delete_library(id).await.unwrap();
|
||||
CoreResponse::Success(())
|
||||
}
|
||||
ClientCommand::LibraryCommand {
|
||||
library_id,
|
||||
command,
|
||||
} => {
|
||||
let ctx = self.library_manager.get_ctx(library_id).await.unwrap();
|
||||
match command {
|
||||
// CRUD for locations
|
||||
LibraryCommand::LocCreate { path } => {
|
||||
let loc = sys::new_location_and_scan(&ctx, &path).await?;
|
||||
// ctx.queue_job(Box::new(FileIdentifierJob));
|
||||
CoreResponse::LocCreate(loc)
|
||||
}
|
||||
LibraryCommand::LocUpdate { id, name } => {
|
||||
ctx.db
|
||||
.location()
|
||||
.find_unique(location::id::equals(id))
|
||||
.update(vec![location::name::set(name)])
|
||||
.exec()
|
||||
.await?;
|
||||
|
||||
CoreResponse::Success(())
|
||||
}
|
||||
ClientCommand::LocDelete { id } => {
|
||||
sys::delete_location(&ctx, id).await?;
|
||||
CoreResponse::Success(())
|
||||
}
|
||||
ClientCommand::LocRescan { id } => {
|
||||
sys::scan_location(&ctx, id, String::new());
|
||||
CoreResponse::Success(())
|
||||
}
|
||||
// CRUD for files
|
||||
ClientCommand::FileReadMetaData { id: _ } => todo!(),
|
||||
ClientCommand::FileSetNote { id, note } => file::set_note(ctx, id, note).await?,
|
||||
// ClientCommand::FileEncrypt { id: _, algorithm: _ } => todo!(),
|
||||
ClientCommand::FileDelete { id } => {
|
||||
ctx.database
|
||||
.file()
|
||||
.find_unique(prisma_file::id::equals(id))
|
||||
.delete()
|
||||
.exec()
|
||||
.await?;
|
||||
CoreResponse::Success(())
|
||||
}
|
||||
LibraryCommand::LocDelete { id } => {
|
||||
sys::delete_location(&ctx, id).await?;
|
||||
CoreResponse::Success(())
|
||||
}
|
||||
LibraryCommand::LocRescan { id } => {
|
||||
sys::scan_location(&ctx, id, String::new()).await;
|
||||
CoreResponse::Success(())
|
||||
}
|
||||
// CRUD for files
|
||||
LibraryCommand::FileReadMetaData { id: _ } => todo!(),
|
||||
LibraryCommand::FileSetNote { id, note } => {
|
||||
file::set_note(ctx, id, note).await?
|
||||
}
|
||||
// ClientCommand::FileEncrypt { id: _, algorithm: _ } => todo!(),
|
||||
LibraryCommand::FileDelete { id } => {
|
||||
ctx.db
|
||||
.file()
|
||||
.find_unique(prisma_file::id::equals(id))
|
||||
.delete()
|
||||
.exec()
|
||||
.await?;
|
||||
|
||||
CoreResponse::Success(())
|
||||
}
|
||||
// CRUD for tags
|
||||
ClientCommand::TagCreate { name: _, color: _ } => todo!(),
|
||||
ClientCommand::TagAssign {
|
||||
file_id: _,
|
||||
tag_id: _,
|
||||
} => todo!(),
|
||||
ClientCommand::TagDelete { id: _ } => todo!(),
|
||||
// CRUD for libraries
|
||||
ClientCommand::SysVolumeUnmount { id: _ } => todo!(),
|
||||
ClientCommand::LibDelete { id: _ } => todo!(),
|
||||
ClientCommand::TagUpdate { name: _, color: _ } => todo!(),
|
||||
ClientCommand::GenerateThumbsForLocation { id, path } => {
|
||||
ctx.spawn_job(Box::new(ThumbnailJob {
|
||||
location_id: id,
|
||||
path,
|
||||
background: false, // fix
|
||||
}));
|
||||
CoreResponse::Success(())
|
||||
}
|
||||
// ClientCommand::PurgeDatabase => {
|
||||
// println!("Purging database...");
|
||||
// fs::remove_file(Path::new(&self.state.data_path).join("library.db")).unwrap();
|
||||
// CoreResponse::Success(())
|
||||
// }
|
||||
ClientCommand::IdentifyUniqueFiles { id, path } => {
|
||||
ctx.spawn_job(Box::new(FileIdentifierJob {
|
||||
location_id: id,
|
||||
path,
|
||||
}));
|
||||
CoreResponse::Success(())
|
||||
CoreResponse::Success(())
|
||||
}
|
||||
// CRUD for tags
|
||||
LibraryCommand::TagCreate { name: _, color: _ } => todo!(),
|
||||
LibraryCommand::TagAssign {
|
||||
file_id: _,
|
||||
tag_id: _,
|
||||
} => todo!(),
|
||||
LibraryCommand::TagUpdate { name: _, color: _ } => todo!(),
|
||||
LibraryCommand::TagDelete { id: _ } => todo!(),
|
||||
// CRUD for libraries
|
||||
LibraryCommand::SysVolumeUnmount { id: _ } => todo!(),
|
||||
LibraryCommand::GenerateThumbsForLocation { id, path } => {
|
||||
ctx.spawn_job(Box::new(ThumbnailJob {
|
||||
location_id: id,
|
||||
path,
|
||||
background: false, // fix
|
||||
}))
|
||||
.await;
|
||||
CoreResponse::Success(())
|
||||
}
|
||||
LibraryCommand::IdentifyUniqueFiles { id, path } => {
|
||||
ctx.spawn_job(Box::new(FileIdentifierJob {
|
||||
location_id: id,
|
||||
path,
|
||||
}))
|
||||
.await;
|
||||
CoreResponse::Success(())
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// query sources of data
|
||||
async fn exec_query(&self, query: ClientQuery) -> Result<CoreResponse, CoreError> {
|
||||
let ctx = self.get_context();
|
||||
Ok(match query {
|
||||
// return the client state from memory
|
||||
ClientQuery::NodeGetState => CoreResponse::NodeGetState(self.state.clone()),
|
||||
// get system volumes without saving to library
|
||||
ClientQuery::SysGetVolumes => CoreResponse::SysGetVolumes(sys::Volume::get_volumes()?),
|
||||
ClientQuery::SysGetLocations => {
|
||||
CoreResponse::SysGetLocations(sys::get_locations(&ctx).await?)
|
||||
}
|
||||
// get location from library
|
||||
ClientQuery::SysGetLocation { id } => {
|
||||
CoreResponse::SysGetLocation(sys::get_location(&ctx, id).await?)
|
||||
}
|
||||
// return contents of a directory for the explorer
|
||||
ClientQuery::LibGetExplorerDir {
|
||||
path,
|
||||
location_id,
|
||||
limit: _,
|
||||
} => CoreResponse::LibGetExplorerDir(
|
||||
file::explorer::open_dir(&ctx, &location_id, &path).await?,
|
||||
ClientQuery::NodeGetLibraries => CoreResponse::NodeGetLibraries(
|
||||
self.library_manager.get_all_libraries_config().await,
|
||||
),
|
||||
ClientQuery::LibGetTags => todo!(),
|
||||
ClientQuery::NodeGetState => CoreResponse::NodeGetState(NodeState {
|
||||
config: self.config.get().await,
|
||||
data_path: self.config.data_directory().to_str().unwrap().to_string(),
|
||||
}),
|
||||
ClientQuery::SysGetVolumes => CoreResponse::SysGetVolumes(sys::Volume::get_volumes()?),
|
||||
ClientQuery::JobGetRunning => {
|
||||
CoreResponse::JobGetRunning(self.jobs.get_running().await)
|
||||
}
|
||||
ClientQuery::JobGetHistory => {
|
||||
CoreResponse::JobGetHistory(Jobs::get_history(&ctx).await?)
|
||||
}
|
||||
ClientQuery::GetLibraryStatistics => {
|
||||
CoreResponse::GetLibraryStatistics(library::Statistics::calculate(&ctx).await?)
|
||||
}
|
||||
ClientQuery::GetNodes => todo!(),
|
||||
ClientQuery::LibraryQuery { library_id, query } => {
|
||||
let ctx = match self.library_manager.get_ctx(library_id.clone()).await {
|
||||
Some(ctx) => ctx,
|
||||
None => {
|
||||
println!("Library '{}' not found!", library_id);
|
||||
return Ok(CoreResponse::Error("Library not found".into()));
|
||||
}
|
||||
};
|
||||
match query {
|
||||
LibraryQuery::SysGetLocations => {
|
||||
CoreResponse::SysGetLocations(sys::get_locations(&ctx).await?)
|
||||
}
|
||||
// get location from library
|
||||
LibraryQuery::SysGetLocation { id } => {
|
||||
CoreResponse::SysGetLocation(sys::get_location(&ctx, id).await?)
|
||||
}
|
||||
// return contents of a directory for the explorer
|
||||
LibraryQuery::LibGetExplorerDir {
|
||||
path,
|
||||
location_id,
|
||||
limit: _,
|
||||
} => CoreResponse::LibGetExplorerDir(
|
||||
file::explorer::open_dir(&ctx, &location_id, &path).await?,
|
||||
),
|
||||
LibraryQuery::LibGetTags => todo!(),
|
||||
LibraryQuery::JobGetHistory => {
|
||||
CoreResponse::JobGetHistory(JobManager::get_history(&ctx).await?)
|
||||
}
|
||||
LibraryQuery::GetLibraryStatistics => CoreResponse::GetLibraryStatistics(
|
||||
library::Statistics::calculate(&ctx).await?,
|
||||
),
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// represents an event this library can emit
|
||||
/// is a command destined for the core
|
||||
#[derive(Serialize, Deserialize, Debug, TS)]
|
||||
#[serde(tag = "key", content = "params")]
|
||||
#[ts(export)]
|
||||
pub enum ClientCommand {
|
||||
// Libraries
|
||||
CreateLibrary {
|
||||
name: String,
|
||||
},
|
||||
EditLibrary {
|
||||
id: String,
|
||||
name: Option<String>,
|
||||
description: Option<String>,
|
||||
},
|
||||
DeleteLibrary {
|
||||
id: String,
|
||||
},
|
||||
LibraryCommand {
|
||||
library_id: String,
|
||||
command: LibraryCommand,
|
||||
},
|
||||
}
|
||||
|
||||
/// is a command destined for a specific library which is loaded into the core.
|
||||
#[derive(Serialize, Deserialize, Debug, TS)]
|
||||
#[serde(tag = "key", content = "params")]
|
||||
#[ts(export)]
|
||||
pub enum LibraryCommand {
|
||||
// Files
|
||||
FileReadMetaData { id: i32 },
|
||||
FileSetNote { id: i32, note: Option<String> },
|
||||
// FileEncrypt { id: i32, algorithm: EncryptionAlgorithm },
|
||||
FileDelete { id: i32 },
|
||||
// Library
|
||||
LibDelete { id: i32 },
|
||||
// Tags
|
||||
TagCreate { name: String, color: String },
|
||||
TagUpdate { name: String, color: String },
|
||||
|
@ -380,15 +364,28 @@ pub enum ClientCommand {
|
|||
IdentifyUniqueFiles { id: i32, path: String },
|
||||
}
|
||||
|
||||
// represents an event this library can emit
|
||||
/// is a query destined for the core
|
||||
#[derive(Serialize, Deserialize, Debug, TS)]
|
||||
#[serde(tag = "key", content = "params")]
|
||||
#[ts(export)]
|
||||
pub enum ClientQuery {
|
||||
NodeGetLibraries,
|
||||
NodeGetState,
|
||||
SysGetVolumes,
|
||||
LibGetTags,
|
||||
JobGetRunning,
|
||||
GetNodes,
|
||||
LibraryQuery {
|
||||
library_id: String,
|
||||
query: LibraryQuery,
|
||||
},
|
||||
}
|
||||
|
||||
/// is a query destined for a specific library which is loaded into the core.
|
||||
#[derive(Serialize, Deserialize, Debug, TS)]
|
||||
#[serde(tag = "key", content = "params")]
|
||||
#[ts(export)]
|
||||
pub enum LibraryQuery {
|
||||
LibGetTags,
|
||||
JobGetHistory,
|
||||
SysGetLocations,
|
||||
SysGetLocation {
|
||||
|
@ -400,7 +397,6 @@ pub enum ClientQuery {
|
|||
limit: i32,
|
||||
},
|
||||
GetLibraryStatistics,
|
||||
GetNodes,
|
||||
}
|
||||
|
||||
// represents an event this library can emit
|
||||
|
@ -417,11 +413,21 @@ pub enum CoreEvent {
|
|||
DatabaseDisconnected { reason: Option<String> },
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, TS)]
|
||||
#[ts(export)]
|
||||
pub struct NodeState {
|
||||
#[serde(flatten)]
|
||||
pub config: NodeConfig,
|
||||
pub data_path: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, TS)]
|
||||
#[serde(tag = "key", content = "data")]
|
||||
#[ts(export)]
|
||||
pub enum CoreResponse {
|
||||
Success(()),
|
||||
Error(String),
|
||||
NodeGetLibraries(Vec<LibraryConfigWrapped>),
|
||||
SysGetVolumes(Vec<sys::Volume>),
|
||||
SysGetLocation(sys::LocationResource),
|
||||
SysGetLocations(Vec<sys::LocationResource>),
|
||||
|
|
72
core/src/library/library_config.rs
Normal file
72
core/src/library/library_config.rs
Normal file
|
@ -0,0 +1,72 @@
|
|||
use std::{
|
||||
fs::File,
|
||||
io::{BufReader, Seek, SeekFrom},
|
||||
path::PathBuf,
|
||||
};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::io::Write;
|
||||
use ts_rs::TS;
|
||||
|
||||
use crate::node::ConfigMetadata;
|
||||
|
||||
use super::LibraryManagerError;
|
||||
|
||||
/// LibraryConfig holds the configuration for a specific library. This is stored as a '{uuid}.sdlibrary' file.
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, TS, Default)]
|
||||
#[ts(export)]
|
||||
pub struct LibraryConfig {
|
||||
#[serde(flatten)]
|
||||
pub metadata: ConfigMetadata,
|
||||
/// name is the display name of the library. This is used in the UI and is set by the user.
|
||||
pub name: String,
|
||||
/// description is a user set description of the library. This is used in the UI and is set by the user.
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
impl LibraryConfig {
|
||||
/// read will read the configuration from disk and return it.
|
||||
pub(super) async fn read(file_dir: PathBuf) -> Result<LibraryConfig, LibraryManagerError> {
|
||||
let mut file = File::open(&file_dir)?;
|
||||
let base_config: ConfigMetadata = serde_json::from_reader(BufReader::new(&mut file))?;
|
||||
|
||||
Self::migrate_config(base_config.version, file_dir)?;
|
||||
|
||||
file.seek(SeekFrom::Start(0))?;
|
||||
Ok(serde_json::from_reader(BufReader::new(&mut file))?)
|
||||
}
|
||||
|
||||
/// save will write the configuration back to disk
|
||||
pub(super) async fn save(
|
||||
file_dir: PathBuf,
|
||||
config: &LibraryConfig,
|
||||
) -> Result<(), LibraryManagerError> {
|
||||
File::create(file_dir)
|
||||
.map_err(LibraryManagerError::IOError)?
|
||||
.write_all(serde_json::to_string(config)?.as_bytes())
|
||||
.map_err(LibraryManagerError::IOError)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// migrate_config is a function used to apply breaking changes to the library config file.
|
||||
fn migrate_config(
|
||||
current_version: Option<String>,
|
||||
config_path: PathBuf,
|
||||
) -> Result<(), LibraryManagerError> {
|
||||
match current_version {
|
||||
None => Err(LibraryManagerError::MigrationError(format!(
|
||||
"Your Spacedrive library at '{}' is missing the `version` field",
|
||||
config_path.display()
|
||||
))),
|
||||
_ => Ok(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// used to return to the frontend with uuid context
|
||||
#[derive(Serialize, Deserialize, Debug, TS)]
|
||||
#[ts(export)]
|
||||
pub struct LibraryConfigWrapped {
|
||||
pub uuid: String,
|
||||
pub config: LibraryConfig,
|
||||
}
|
46
core/src/library/library_ctx.rs
Normal file
46
core/src/library/library_ctx.rs
Normal file
|
@ -0,0 +1,46 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{job::Job, node::NodeConfigManager, prisma::PrismaClient, CoreEvent, NodeContext};
|
||||
|
||||
use super::LibraryConfig;
|
||||
|
||||
/// LibraryContext holds context for a library which can be passed around the application.
|
||||
#[derive(Clone)]
|
||||
pub struct LibraryContext {
|
||||
/// id holds the ID of the current library.
|
||||
pub id: Uuid,
|
||||
/// config holds the configuration of the current library.
|
||||
pub config: LibraryConfig,
|
||||
/// db holds the database client for the current library.
|
||||
pub db: Arc<PrismaClient>,
|
||||
/// node_local_id holds the local ID of the node which is running the library.
|
||||
pub node_local_id: i32,
|
||||
/// node_context holds the node context for the node which this library is running on.
|
||||
pub(super) node_context: NodeContext,
|
||||
}
|
||||
|
||||
impl LibraryContext {
|
||||
pub(crate) async fn spawn_job(&self, job: Box<dyn Job>) {
|
||||
self.node_context.jobs.clone().ingest(self, job).await;
|
||||
}
|
||||
|
||||
pub(crate) async fn queue_job(&self, job: Box<dyn Job>) {
|
||||
self.node_context.jobs.ingest_queue(self, job).await;
|
||||
}
|
||||
|
||||
pub(crate) async fn emit(&self, event: CoreEvent) {
|
||||
self.node_context
|
||||
.event_sender
|
||||
.send(event)
|
||||
.await
|
||||
.unwrap_or_else(|e| {
|
||||
println!("Failed to emit event. {:?}", e);
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn config(&self) -> Arc<NodeConfigManager> {
|
||||
self.node_context.config.clone()
|
||||
}
|
||||
}
|
271
core/src/library/library_manager.rs
Normal file
271
core/src/library/library_manager.rs
Normal file
|
@ -0,0 +1,271 @@
|
|||
use std::{
|
||||
env, fs, io,
|
||||
path::{Path, PathBuf},
|
||||
str::FromStr,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use thiserror::Error;
|
||||
use tokio::sync::RwLock;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
node::Platform,
|
||||
prisma::{self, node},
|
||||
util::db::load_and_migrate,
|
||||
ClientQuery, CoreEvent, NodeContext,
|
||||
};
|
||||
|
||||
use super::{LibraryConfig, LibraryConfigWrapped, LibraryContext};
|
||||
|
||||
/// LibraryManager is a singleton that manages all libraries for a node.
|
||||
pub struct LibraryManager {
|
||||
/// libraries_dir holds the path to the directory where libraries are stored.
|
||||
libraries_dir: PathBuf,
|
||||
/// libraries holds the list of libraries which are currently loaded into the node.
|
||||
libraries: RwLock<Vec<LibraryContext>>,
|
||||
/// node_context holds the context for the node which this library manager is running on.
|
||||
node_context: NodeContext,
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum LibraryManagerError {
|
||||
#[error("error saving or loading the config from the filesystem")]
|
||||
IOError(#[from] io::Error),
|
||||
#[error("error serializing or deserializing the JSON in the config file")]
|
||||
JsonError(#[from] serde_json::Error),
|
||||
#[error("Database error")]
|
||||
DatabaseError(#[from] prisma::QueryError),
|
||||
#[error("Library not found error")]
|
||||
LibraryNotFoundError,
|
||||
#[error("error migrating the config file")]
|
||||
MigrationError(String),
|
||||
#[error("failed to parse uuid")]
|
||||
UuidError(#[from] uuid::Error),
|
||||
}
|
||||
|
||||
impl LibraryManager {
|
||||
pub(crate) async fn new(
|
||||
libraries_dir: PathBuf,
|
||||
node_context: NodeContext,
|
||||
) -> Result<Arc<Self>, LibraryManagerError> {
|
||||
fs::create_dir_all(&libraries_dir)?;
|
||||
|
||||
let mut libraries = Vec::new();
|
||||
for entry in fs::read_dir(&libraries_dir)?
|
||||
.into_iter()
|
||||
.filter_map(|entry| entry.ok())
|
||||
.filter(|entry| {
|
||||
entry.path().is_file()
|
||||
&& entry
|
||||
.path()
|
||||
.extension()
|
||||
.map(|v| &*v == "sdlibrary")
|
||||
.unwrap_or(false)
|
||||
}) {
|
||||
let config_path = entry.path();
|
||||
let library_id = match Path::new(&config_path)
|
||||
.file_stem()
|
||||
.map(|v| v.to_str().map(|v| Uuid::from_str(v)))
|
||||
{
|
||||
Some(Some(Ok(id))) => id,
|
||||
_ => {
|
||||
println!("Attempted to load library from path '{}' but it has an invalid filename. Skipping...", config_path.display());
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let db_path = config_path.clone().with_extension("db");
|
||||
if !db_path.exists() {
|
||||
println!(
|
||||
"Found library '{}' but no matching database file was found. Skipping...",
|
||||
config_path.display()
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
let config = LibraryConfig::read(config_path).await?;
|
||||
libraries.push(
|
||||
Self::load(
|
||||
library_id,
|
||||
db_path.to_str().unwrap(),
|
||||
config,
|
||||
node_context.clone(),
|
||||
)
|
||||
.await?,
|
||||
);
|
||||
}
|
||||
|
||||
let this = Arc::new(Self {
|
||||
libraries: RwLock::new(libraries),
|
||||
libraries_dir,
|
||||
node_context,
|
||||
});
|
||||
|
||||
// TODO: Remove this before merging PR -> Currently it exists to make the app usable
|
||||
if this.libraries.read().await.len() == 0 {
|
||||
this.create(LibraryConfig {
|
||||
name: "My Default Library".into(),
|
||||
..Default::default()
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
Ok(this)
|
||||
}
|
||||
|
||||
/// create creates a new library with the given config and mounts it into the running [LibraryManager].
|
||||
pub(crate) async fn create(&self, config: LibraryConfig) -> Result<(), LibraryManagerError> {
|
||||
let id = Uuid::new_v4();
|
||||
LibraryConfig::save(
|
||||
Path::new(&self.libraries_dir).join(format!("{}.sdlibrary", id.to_string())),
|
||||
&config,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let library = Self::load(
|
||||
id,
|
||||
&Path::new(&self.libraries_dir)
|
||||
.join(format!("{}.db", id.to_string()))
|
||||
.to_str()
|
||||
.unwrap(),
|
||||
config,
|
||||
self.node_context.clone(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
self.libraries.write().await.push(library);
|
||||
|
||||
self.node_context
|
||||
.emit(CoreEvent::InvalidateQuery(ClientQuery::NodeGetLibraries))
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn get_all_libraries_config(&self) -> Vec<LibraryConfigWrapped> {
|
||||
self.libraries
|
||||
.read()
|
||||
.await
|
||||
.iter()
|
||||
.map(|lib| LibraryConfigWrapped {
|
||||
config: lib.config.clone(),
|
||||
uuid: lib.id.to_string(),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) async fn edit_library(
|
||||
&self,
|
||||
id: String,
|
||||
name: Option<String>,
|
||||
description: Option<String>,
|
||||
) -> Result<(), LibraryManagerError> {
|
||||
// check library is valid
|
||||
let mut libraries = self.libraries.write().await;
|
||||
let library = libraries
|
||||
.iter_mut()
|
||||
.find(|lib| lib.id == Uuid::from_str(&id).unwrap())
|
||||
.ok_or(LibraryManagerError::LibraryNotFoundError)?;
|
||||
|
||||
// update the library
|
||||
if let Some(name) = name {
|
||||
library.config.name = name;
|
||||
}
|
||||
if let Some(description) = description {
|
||||
library.config.description = description;
|
||||
}
|
||||
|
||||
LibraryConfig::save(
|
||||
Path::new(&self.libraries_dir).join(format!("{}.sdlibrary", id.to_string())),
|
||||
&library.config,
|
||||
)
|
||||
.await?;
|
||||
|
||||
self.node_context
|
||||
.emit(CoreEvent::InvalidateQuery(ClientQuery::NodeGetLibraries))
|
||||
.await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete_library(&self, id: String) -> Result<(), LibraryManagerError> {
|
||||
let mut libraries = self.libraries.write().await;
|
||||
|
||||
let id = Uuid::parse_str(&id)?;
|
||||
|
||||
let library = libraries
|
||||
.iter()
|
||||
.find(|l| l.id == id)
|
||||
.ok_or(LibraryManagerError::LibraryNotFoundError)?;
|
||||
|
||||
fs::remove_file(
|
||||
Path::new(&self.libraries_dir).join(format!("{}.db", library.id.to_string())),
|
||||
)?;
|
||||
fs::remove_file(
|
||||
Path::new(&self.libraries_dir).join(format!("{}.sdlibrary", library.id.to_string())),
|
||||
)?;
|
||||
|
||||
libraries.retain(|l| l.id != id);
|
||||
|
||||
self.node_context
|
||||
.emit(CoreEvent::InvalidateQuery(ClientQuery::NodeGetLibraries))
|
||||
.await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// get_ctx will return the library context for the given library id.
|
||||
pub(crate) async fn get_ctx(&self, library_id: String) -> Option<LibraryContext> {
|
||||
self.libraries
|
||||
.read()
|
||||
.await
|
||||
.iter()
|
||||
.find(|lib| lib.id.to_string() == library_id)
|
||||
.map(|v| v.clone())
|
||||
}
|
||||
|
||||
/// load the library from a given path
|
||||
pub(crate) async fn load(
|
||||
id: Uuid,
|
||||
db_path: &str,
|
||||
config: LibraryConfig,
|
||||
node_context: NodeContext,
|
||||
) -> Result<LibraryContext, LibraryManagerError> {
|
||||
let db = Arc::new(
|
||||
load_and_migrate(&format!("file:{}", db_path))
|
||||
.await
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
let node_config = node_context.config.get().await;
|
||||
|
||||
let platform = match env::consts::OS {
|
||||
"windows" => Platform::Windows,
|
||||
"macos" => Platform::MacOS,
|
||||
"linux" => Platform::Linux,
|
||||
_ => Platform::Unknown,
|
||||
};
|
||||
|
||||
let node_data = db
|
||||
.node()
|
||||
.upsert(
|
||||
node::pub_id::equals(id.to_string()),
|
||||
(
|
||||
node::pub_id::set(id.to_string()),
|
||||
node::name::set(node_config.name.clone()),
|
||||
vec![node::platform::set(platform as i32)],
|
||||
),
|
||||
vec![node::name::set(node_config.name.clone())],
|
||||
)
|
||||
.exec()
|
||||
.await?;
|
||||
|
||||
Ok(LibraryContext {
|
||||
id,
|
||||
config,
|
||||
db,
|
||||
node_local_id: node_data.id,
|
||||
node_context,
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,99 +0,0 @@
|
|||
use uuid::Uuid;
|
||||
|
||||
use crate::node::{get_nodestate, LibraryState};
|
||||
use crate::prisma::library;
|
||||
use crate::util::db::{run_migrations, DatabaseError};
|
||||
use crate::CoreContext;
|
||||
|
||||
pub static LIBRARY_DB_NAME: &str = "library.db";
|
||||
pub static DEFAULT_NAME: &str = "My Library";
|
||||
|
||||
pub fn get_library_path(data_path: &str) -> String {
|
||||
let path = data_path.to_owned();
|
||||
format!("{}/{}", path, LIBRARY_DB_NAME)
|
||||
}
|
||||
|
||||
// pub async fn get(core: &Node) -> Result<library::Data, LibraryError> {
|
||||
// let config = get_nodestate();
|
||||
// let db = &core.database;
|
||||
|
||||
// let library_state = config.get_current_library();
|
||||
|
||||
// println!("{:?}", library_state);
|
||||
|
||||
// // get library from db
|
||||
// let library = match db
|
||||
// .library()
|
||||
// .find_unique(library::pub_id::equals(library_state.library_uuid.clone()))
|
||||
// .exec()
|
||||
// .await?
|
||||
// {
|
||||
// Some(library) => Ok(library),
|
||||
// None => {
|
||||
// // update config library state to offline
|
||||
// // config.libraries
|
||||
|
||||
// Err(anyhow::anyhow!("library_not_found"))
|
||||
// }
|
||||
// };
|
||||
|
||||
// Ok(library.unwrap())
|
||||
// }
|
||||
|
||||
pub async fn load(
|
||||
ctx: &CoreContext,
|
||||
library_path: &str,
|
||||
library_id: &str,
|
||||
) -> Result<(), DatabaseError> {
|
||||
let mut config = get_nodestate();
|
||||
|
||||
println!("Initializing library: {} {}", &library_id, library_path);
|
||||
|
||||
if config.current_library_uuid != library_id {
|
||||
config.current_library_uuid = library_id.to_string();
|
||||
config.save();
|
||||
}
|
||||
// create connection with library database & run migrations
|
||||
run_migrations(&ctx).await?;
|
||||
// if doesn't exist, mark as offline
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn create(ctx: &CoreContext, name: Option<String>) -> Result<(), ()> {
|
||||
let mut config = get_nodestate();
|
||||
|
||||
let uuid = Uuid::new_v4().to_string();
|
||||
|
||||
println!("Creating library {:?}, UUID: {:?}", name, uuid);
|
||||
|
||||
let library_state = LibraryState {
|
||||
library_uuid: uuid.clone(),
|
||||
library_path: get_library_path(&config.data_path),
|
||||
..LibraryState::default()
|
||||
};
|
||||
|
||||
run_migrations(&ctx).await.unwrap();
|
||||
|
||||
config.libraries.push(library_state);
|
||||
|
||||
config.current_library_uuid = uuid;
|
||||
|
||||
config.save();
|
||||
|
||||
let db = &ctx.database;
|
||||
|
||||
let library = db
|
||||
.library()
|
||||
.create(
|
||||
library::pub_id::set(config.current_library_uuid),
|
||||
library::name::set(name.unwrap_or(DEFAULT_NAME.into())),
|
||||
vec![],
|
||||
)
|
||||
.exec()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
println!("library created in database: {:?}", library);
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -1,7 +1,11 @@
|
|||
mod loader;
|
||||
mod library_config;
|
||||
mod library_ctx;
|
||||
mod library_manager;
|
||||
mod statistics;
|
||||
|
||||
pub use loader::*;
|
||||
pub use library_config::*;
|
||||
pub use library_ctx::*;
|
||||
pub use library_manager::*;
|
||||
pub use statistics::*;
|
||||
|
||||
use thiserror::Error;
|
||||
|
|
|
@ -1,15 +1,10 @@
|
|||
use crate::{
|
||||
node::get_nodestate,
|
||||
prisma::{library, library_statistics::*},
|
||||
sys::Volume,
|
||||
CoreContext,
|
||||
};
|
||||
use crate::{prisma::statistics::*, sys::Volume};
|
||||
use fs_extra::dir::get_size;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs;
|
||||
use ts_rs::TS;
|
||||
|
||||
use super::LibraryError;
|
||||
use super::{LibraryContext, LibraryError};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, TS, Clone)]
|
||||
#[ts(export)]
|
||||
|
@ -52,14 +47,11 @@ impl Default for Statistics {
|
|||
}
|
||||
|
||||
impl Statistics {
|
||||
pub async fn retrieve(ctx: &CoreContext) -> Result<Statistics, LibraryError> {
|
||||
let config = get_nodestate();
|
||||
let db = &ctx.database;
|
||||
let library_data = config.get_current_library();
|
||||
|
||||
let library_statistics_db = match db
|
||||
.library_statistics()
|
||||
.find_unique(id::equals(library_data.library_id))
|
||||
pub async fn retrieve(ctx: &LibraryContext) -> Result<Statistics, LibraryError> {
|
||||
let library_statistics_db = match ctx
|
||||
.db
|
||||
.statistics()
|
||||
.find_unique(id::equals(ctx.node_local_id))
|
||||
.exec()
|
||||
.await?
|
||||
{
|
||||
|
@ -70,31 +62,11 @@ impl Statistics {
|
|||
Ok(library_statistics_db.into())
|
||||
}
|
||||
|
||||
pub async fn calculate(ctx: &CoreContext) -> Result<Statistics, LibraryError> {
|
||||
let config = get_nodestate();
|
||||
let db = &ctx.database;
|
||||
// get library from client state
|
||||
let library_data = config.get_current_library();
|
||||
println!(
|
||||
"Calculating library statistics {:?}",
|
||||
library_data.library_uuid
|
||||
);
|
||||
// get library from db
|
||||
let library = db
|
||||
.library()
|
||||
.find_unique(library::pub_id::equals(
|
||||
library_data.library_uuid.to_string(),
|
||||
))
|
||||
.exec()
|
||||
.await?;
|
||||
|
||||
if library.is_none() {
|
||||
return Err(LibraryError::LibraryNotFound);
|
||||
}
|
||||
|
||||
let library_statistics = db
|
||||
.library_statistics()
|
||||
.find_unique(id::equals(library_data.library_id))
|
||||
pub async fn calculate(ctx: &LibraryContext) -> Result<Statistics, LibraryError> {
|
||||
let _statistics = ctx
|
||||
.db
|
||||
.statistics()
|
||||
.find_unique(id::equals(ctx.node_local_id))
|
||||
.exec()
|
||||
.await?;
|
||||
|
||||
|
@ -113,14 +85,15 @@ impl Statistics {
|
|||
}
|
||||
}
|
||||
|
||||
let library_db_size = match fs::metadata(library_data.library_path.as_str()) {
|
||||
let library_db_size = match fs::metadata(ctx.config().data_directory()) {
|
||||
Ok(metadata) => metadata.len(),
|
||||
Err(_) => 0,
|
||||
};
|
||||
|
||||
println!("{:?}", library_statistics);
|
||||
let mut thumbsnails_dir = ctx.config().data_directory();
|
||||
thumbsnails_dir.push("thumbnails");
|
||||
|
||||
let thumbnail_folder_size = get_size(&format!("{}/{}", config.data_path, "thumbnails"));
|
||||
let thumbnail_folder_size = get_size(&thumbsnails_dir);
|
||||
|
||||
let statistics = Statistics {
|
||||
library_db_size: library_db_size.to_string(),
|
||||
|
@ -130,18 +103,11 @@ impl Statistics {
|
|||
..Statistics::default()
|
||||
};
|
||||
|
||||
let library_local_id = match library {
|
||||
Some(library) => library.id,
|
||||
None => library_data.library_id,
|
||||
};
|
||||
|
||||
db.library_statistics()
|
||||
ctx.db
|
||||
.statistics()
|
||||
.upsert(
|
||||
library_id::equals(library_local_id),
|
||||
(
|
||||
library_id::set(library_local_id),
|
||||
vec![library_db_size::set(statistics.library_db_size.clone())],
|
||||
),
|
||||
id::equals(1),
|
||||
vec![library_db_size::set(statistics.library_db_size.clone())],
|
||||
vec![
|
||||
total_file_count::set(statistics.total_file_count.clone()),
|
||||
total_bytes_used::set(statistics.total_bytes_used.clone()),
|
||||
|
|
149
core/src/node/config.rs
Normal file
149
core/src/node/config.rs
Normal file
|
@ -0,0 +1,149 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use std::fs::File;
|
||||
use std::io::{self, BufReader, Seek, SeekFrom, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
use thiserror::Error;
|
||||
use tokio::sync::{RwLock, RwLockWriteGuard};
|
||||
use ts_rs::TS;
|
||||
use uuid::Uuid;
|
||||
|
||||
/// NODE_STATE_CONFIG_NAME is the name of the file which stores the NodeState
|
||||
pub const NODE_STATE_CONFIG_NAME: &str = "node_state.sdconfig";
|
||||
|
||||
/// ConfigMetadata is a part of node configuration that is loaded before the main configuration and contains information about the schema of the config.
|
||||
/// This allows us to migrate breaking changes to the config format between Spacedrive releases.
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, TS)]
|
||||
#[ts(export)]
|
||||
pub struct ConfigMetadata {
|
||||
/// version of Spacedrive. Determined from `CARGO_PKG_VERSION` environment variable.
|
||||
pub version: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for ConfigMetadata {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
version: Some(env!("CARGO_PKG_VERSION").into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// NodeConfig is the configuration for a node. This is shared between all libraries and is stored in a JSON file on disk.
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, TS)]
|
||||
#[ts(export)]
|
||||
pub struct NodeConfig {
|
||||
#[serde(flatten)]
|
||||
pub metadata: ConfigMetadata,
|
||||
/// id is a unique identifier for the current node. Each node has a public identifier (this one) and is given a local id for each library (done within the library code).
|
||||
pub id: Uuid,
|
||||
/// name is the display name of the current node. This is set by the user and is shown in the UI. // TODO: Length validation so it can fit in DNS record
|
||||
pub name: String,
|
||||
// the port this node uses for peer to peer communication. By default a random free port will be chosen each time the application is started.
|
||||
pub p2p_port: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum NodeConfigError {
|
||||
#[error("error saving or loading the config from the filesystem")]
|
||||
IOError(#[from] io::Error),
|
||||
#[error("error serializing or deserializing the JSON in the config file")]
|
||||
JsonError(#[from] serde_json::Error),
|
||||
#[error("error migrating the config file")]
|
||||
MigrationError(String),
|
||||
}
|
||||
|
||||
impl NodeConfig {
|
||||
fn default() -> Self {
|
||||
NodeConfig {
|
||||
id: Uuid::new_v4(),
|
||||
name: match hostname::get() {
|
||||
Ok(hostname) => hostname.to_string_lossy().into_owned(),
|
||||
Err(err) => {
|
||||
eprintln!("Falling back to default node name as an error occurred getting your systems hostname: '{}'", err);
|
||||
"my-spacedrive".into()
|
||||
}
|
||||
},
|
||||
p2p_port: None,
|
||||
metadata: ConfigMetadata {
|
||||
version: Some(env!("CARGO_PKG_VERSION").into()),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct NodeConfigManager(RwLock<NodeConfig>, PathBuf);
|
||||
|
||||
impl NodeConfigManager {
|
||||
/// new will create a new NodeConfigManager with the given path to the config file.
|
||||
pub(crate) async fn new(data_path: PathBuf) -> Result<Arc<Self>, NodeConfigError> {
|
||||
Ok(Arc::new(Self(
|
||||
RwLock::new(Self::read(&data_path).await?),
|
||||
data_path,
|
||||
)))
|
||||
}
|
||||
|
||||
/// get will return the current NodeConfig in a read only state.
|
||||
pub(crate) async fn get(&self) -> NodeConfig {
|
||||
self.0.read().await.clone()
|
||||
}
|
||||
|
||||
/// data_directory returns the path to the directory storing the configuration data.
|
||||
pub(crate) fn data_directory(&self) -> PathBuf {
|
||||
self.1.clone()
|
||||
}
|
||||
|
||||
/// write allows the user to update the configuration. This is done in a closure while a Mutex lock is held so that the user can't cause a race condition if the config were to be updated in multiple parts of the app at the same time.
|
||||
#[allow(unused)]
|
||||
pub(crate) async fn write<F: FnOnce(RwLockWriteGuard<NodeConfig>)>(
|
||||
&self,
|
||||
mutation_fn: F,
|
||||
) -> Result<NodeConfig, NodeConfigError> {
|
||||
mutation_fn(self.0.write().await);
|
||||
let config = self.0.read().await;
|
||||
Self::save(&self.1, &config).await?;
|
||||
Ok(config.clone())
|
||||
}
|
||||
|
||||
/// read will read the configuration from disk and return it.
|
||||
async fn read(base_path: &PathBuf) -> Result<NodeConfig, NodeConfigError> {
|
||||
let path = Path::new(base_path).join(NODE_STATE_CONFIG_NAME);
|
||||
|
||||
match path.exists() {
|
||||
true => {
|
||||
let mut file = File::open(&path)?;
|
||||
let base_config: ConfigMetadata =
|
||||
serde_json::from_reader(BufReader::new(&mut file))?;
|
||||
|
||||
Self::migrate_config(base_config.version, path)?;
|
||||
|
||||
file.seek(SeekFrom::Start(0))?;
|
||||
Ok(serde_json::from_reader(BufReader::new(&mut file))?)
|
||||
}
|
||||
false => {
|
||||
let config = NodeConfig::default();
|
||||
Self::save(base_path, &config).await?;
|
||||
Ok(config)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// save will write the configuration back to disk
|
||||
async fn save(base_path: &PathBuf, config: &NodeConfig) -> Result<(), NodeConfigError> {
|
||||
let path = Path::new(base_path).join(NODE_STATE_CONFIG_NAME);
|
||||
File::create(path)?.write_all(serde_json::to_string(config)?.as_bytes())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// migrate_config is a function used to apply breaking changes to the config file.
|
||||
fn migrate_config(
|
||||
current_version: Option<String>,
|
||||
config_path: PathBuf,
|
||||
) -> Result<(), NodeConfigError> {
|
||||
match current_version {
|
||||
None => {
|
||||
Err(NodeConfigError::MigrationError(format!("Your Spacedrive config file stored at '{}' is missing the `version` field. If you just upgraded please delete the file and restart Spacedrive! Please note this upgrade will stop using your old 'library.db' as the folder structure has changed.", config_path.display())))
|
||||
}
|
||||
_ => Ok(()),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,17 +1,10 @@
|
|||
use crate::{
|
||||
prisma::{self, node},
|
||||
Node,
|
||||
};
|
||||
use chrono::{DateTime, Utc};
|
||||
use int_enum::IntEnum;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::env;
|
||||
use thiserror::Error;
|
||||
use ts_rs::TS;
|
||||
|
||||
mod state;
|
||||
|
||||
pub use state::*;
|
||||
mod config;
|
||||
use crate::prisma::node;
|
||||
pub use config::*;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||
#[ts(export)]
|
||||
|
@ -44,65 +37,3 @@ pub enum Platform {
|
|||
IOS = 4,
|
||||
Android = 5,
|
||||
}
|
||||
|
||||
impl LibraryNode {
|
||||
pub async fn create(node: &Node) -> Result<(), NodeError> {
|
||||
println!("Creating node...");
|
||||
let mut config = state::get_nodestate();
|
||||
|
||||
let db = &node.database;
|
||||
|
||||
let hostname = match hostname::get() {
|
||||
Ok(hostname) => hostname.to_str().unwrap_or_default().to_owned(),
|
||||
Err(_) => "unknown".to_owned(),
|
||||
};
|
||||
|
||||
let platform = match env::consts::OS {
|
||||
"windows" => Platform::Windows,
|
||||
"macos" => Platform::MacOS,
|
||||
"linux" => Platform::Linux,
|
||||
_ => Platform::Unknown,
|
||||
};
|
||||
|
||||
let _node = match db
|
||||
.node()
|
||||
.find_unique(node::pub_id::equals(config.node_pub_id.clone()))
|
||||
.exec()
|
||||
.await?
|
||||
{
|
||||
Some(node) => node,
|
||||
None => {
|
||||
db.node()
|
||||
.create(
|
||||
node::pub_id::set(config.node_pub_id.clone()),
|
||||
node::name::set(hostname.clone()),
|
||||
vec![node::platform::set(platform as i32)],
|
||||
)
|
||||
.exec()
|
||||
.await?
|
||||
}
|
||||
};
|
||||
|
||||
config.node_name = hostname;
|
||||
config.node_id = _node.id;
|
||||
config.save();
|
||||
|
||||
println!("node: {:?}", &_node);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// pub async fn get_nodes(ctx: &CoreContext) -> Result<Vec<node::Data>, NodeError> {
|
||||
// let db = &ctx.database;
|
||||
|
||||
// let _node = db.node().find_many(vec![]).exec().await?;
|
||||
|
||||
// Ok(_node)
|
||||
// }
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum NodeError {
|
||||
#[error("Database error")]
|
||||
DatabaseError(#[from] prisma::QueryError),
|
||||
}
|
||||
|
|
|
@ -1,107 +0,0 @@
|
|||
use lazy_static::lazy_static;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs;
|
||||
use std::io::{BufReader, Write};
|
||||
use std::sync::RwLock;
|
||||
use ts_rs::TS;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, Default, TS)]
|
||||
#[ts(export)]
|
||||
pub struct NodeState {
|
||||
pub node_pub_id: String,
|
||||
pub node_id: i32,
|
||||
pub node_name: String,
|
||||
// config path is stored as struct can exist only in memory during startup and be written to disk later without supplying path
|
||||
pub data_path: String,
|
||||
// the port this node uses to listen for incoming connections
|
||||
pub tcp_port: u32,
|
||||
// all the libraries loaded by this node
|
||||
pub libraries: Vec<LibraryState>,
|
||||
// used to quickly find the default library
|
||||
pub current_library_uuid: String,
|
||||
}
|
||||
|
||||
pub static NODE_STATE_CONFIG_NAME: &str = "node_state.json";
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, Default, TS)]
|
||||
#[ts(export)]
|
||||
pub struct LibraryState {
|
||||
pub library_uuid: String,
|
||||
pub library_id: i32,
|
||||
pub library_path: String,
|
||||
pub offline: bool,
|
||||
}
|
||||
|
||||
// global, thread-safe storage for node state
|
||||
lazy_static! {
|
||||
static ref CONFIG: RwLock<Option<NodeState>> = RwLock::new(None);
|
||||
}
|
||||
|
||||
pub fn get_nodestate() -> NodeState {
|
||||
match CONFIG.read() {
|
||||
Ok(guard) => guard.clone().unwrap_or(NodeState::default()),
|
||||
Err(_) => return NodeState::default(),
|
||||
}
|
||||
}
|
||||
|
||||
impl NodeState {
|
||||
pub fn new(data_path: &str, node_name: &str) -> Result<Self, ()> {
|
||||
let uuid = Uuid::new_v4().to_string();
|
||||
// create struct and assign defaults
|
||||
let config = Self {
|
||||
node_pub_id: uuid,
|
||||
data_path: data_path.to_string(),
|
||||
node_name: node_name.to_string(),
|
||||
..Default::default()
|
||||
};
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
pub fn save(&self) {
|
||||
self.write_memory();
|
||||
// only write to disk if config path is set
|
||||
if !&self.data_path.is_empty() {
|
||||
let config_path = format!("{}/{}", &self.data_path, NODE_STATE_CONFIG_NAME);
|
||||
let mut file = fs::File::create(config_path).unwrap();
|
||||
let json = serde_json::to_string(&self).unwrap();
|
||||
file.write_all(json.as_bytes()).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_disk(&mut self) -> Result<(), ()> {
|
||||
let config_path = format!("{}/{}", &self.data_path, NODE_STATE_CONFIG_NAME);
|
||||
|
||||
// open the file and parse json
|
||||
match fs::File::open(config_path) {
|
||||
Ok(file) => {
|
||||
let reader = BufReader::new(file);
|
||||
let data = serde_json::from_reader(reader).unwrap();
|
||||
// assign to self
|
||||
*self = data;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_memory(&self) {
|
||||
let mut writeable = CONFIG.write().unwrap();
|
||||
*writeable = Some(self.clone());
|
||||
}
|
||||
|
||||
pub fn get_current_library(&self) -> LibraryState {
|
||||
match self
|
||||
.libraries
|
||||
.iter()
|
||||
.find(|lib| lib.library_uuid == self.current_library_uuid)
|
||||
{
|
||||
Some(lib) => lib.clone(),
|
||||
None => LibraryState::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_current_library_db_path(&self) -> String {
|
||||
format!("{}/library.db", &self.get_current_library().library_path)
|
||||
}
|
||||
}
|
|
@ -1,11 +1,10 @@
|
|||
use crate::{
|
||||
encode::ThumbnailJob,
|
||||
file::{cas::FileIdentifierJob, indexer::IndexerJob},
|
||||
node::{get_nodestate, LibraryNode},
|
||||
library::LibraryContext,
|
||||
node::LibraryNode,
|
||||
prisma::{file_path, location},
|
||||
ClientQuery, CoreContext, CoreEvent,
|
||||
ClientQuery, CoreEvent, LibraryQuery,
|
||||
};
|
||||
use prisma_client_rust::{raw, PrismaValue};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{fs, io, io::Write, path::Path};
|
||||
use thiserror::Error;
|
||||
|
@ -66,13 +65,12 @@ static DOTFILE_NAME: &str = ".spacedrive";
|
|||
// }
|
||||
|
||||
pub async fn get_location(
|
||||
ctx: &CoreContext,
|
||||
ctx: &LibraryContext,
|
||||
location_id: i32,
|
||||
) -> Result<LocationResource, SysError> {
|
||||
let db = &ctx.database;
|
||||
|
||||
// get location by location_id from db and include location_paths
|
||||
let location = match db
|
||||
let location = match ctx
|
||||
.db
|
||||
.location()
|
||||
.find_unique(location::id::equals(location_id))
|
||||
.exec()
|
||||
|
@ -84,9 +82,11 @@ pub async fn get_location(
|
|||
Ok(location.into())
|
||||
}
|
||||
|
||||
pub fn scan_location(ctx: &CoreContext, location_id: i32, path: String) {
|
||||
ctx.spawn_job(Box::new(IndexerJob { path: path.clone() }));
|
||||
ctx.queue_job(Box::new(FileIdentifierJob { location_id, path }));
|
||||
pub async fn scan_location(ctx: &LibraryContext, location_id: i32, path: String) {
|
||||
ctx.spawn_job(Box::new(IndexerJob { path: path.clone() }))
|
||||
.await;
|
||||
ctx.queue_job(Box::new(FileIdentifierJob { location_id, path }))
|
||||
.await;
|
||||
// TODO: make a way to stop jobs so this can be canceled without rebooting app
|
||||
// ctx.queue_job(Box::new(ThumbnailJob {
|
||||
// location_id,
|
||||
|
@ -96,18 +96,18 @@ pub fn scan_location(ctx: &CoreContext, location_id: i32, path: String) {
|
|||
}
|
||||
|
||||
pub async fn new_location_and_scan(
|
||||
ctx: &CoreContext,
|
||||
ctx: &LibraryContext,
|
||||
path: &str,
|
||||
) -> Result<LocationResource, SysError> {
|
||||
let location = create_location(&ctx, path).await?;
|
||||
|
||||
scan_location(&ctx, location.id, path.to_string());
|
||||
scan_location(&ctx, location.id, path.to_string()).await;
|
||||
|
||||
Ok(location)
|
||||
}
|
||||
|
||||
pub async fn get_locations(ctx: &CoreContext) -> Result<Vec<LocationResource>, SysError> {
|
||||
let db = &ctx.database;
|
||||
pub async fn get_locations(ctx: &LibraryContext) -> Result<Vec<LocationResource>, SysError> {
|
||||
let db = &ctx.db;
|
||||
|
||||
let locations = db
|
||||
.location()
|
||||
|
@ -125,10 +125,10 @@ pub async fn get_locations(ctx: &CoreContext) -> Result<Vec<LocationResource>, S
|
|||
Ok(locations)
|
||||
}
|
||||
|
||||
pub async fn create_location(ctx: &CoreContext, path: &str) -> Result<LocationResource, SysError> {
|
||||
let db = &ctx.database;
|
||||
let config = get_nodestate();
|
||||
|
||||
pub async fn create_location(
|
||||
ctx: &LibraryContext,
|
||||
path: &str,
|
||||
) -> Result<LocationResource, SysError> {
|
||||
// check if we have access to this location
|
||||
if !Path::new(path).exists() {
|
||||
Err(LocationError::NotFound(path.to_string()))?;
|
||||
|
@ -155,7 +155,8 @@ pub async fn create_location(ctx: &CoreContext, path: &str) -> Result<LocationRe
|
|||
}
|
||||
|
||||
// check if location already exists
|
||||
let location = match db
|
||||
let location = match ctx
|
||||
.db
|
||||
.location()
|
||||
.find_first(vec![location::local_path::equals(Some(path.to_string()))])
|
||||
.exec()
|
||||
|
@ -171,7 +172,8 @@ pub async fn create_location(ctx: &CoreContext, path: &str) -> Result<LocationRe
|
|||
|
||||
let p = Path::new(&path);
|
||||
|
||||
let location = db
|
||||
let location = ctx
|
||||
.db
|
||||
.location()
|
||||
.create(
|
||||
location::pub_id::set(uuid.to_string()),
|
||||
|
@ -181,7 +183,7 @@ pub async fn create_location(ctx: &CoreContext, path: &str) -> Result<LocationRe
|
|||
)),
|
||||
location::is_online::set(true),
|
||||
location::local_path::set(Some(path.to_string())),
|
||||
location::node_id::set(Some(config.node_id)),
|
||||
location::node_id::set(Some(ctx.node_local_id)),
|
||||
],
|
||||
)
|
||||
.exec()
|
||||
|
@ -197,7 +199,7 @@ pub async fn create_location(ctx: &CoreContext, path: &str) -> Result<LocationRe
|
|||
|
||||
let data = DotSpacedrive {
|
||||
location_uuid: uuid.to_string(),
|
||||
library_uuid: config.current_library_uuid,
|
||||
library_uuid: ctx.id.to_string(),
|
||||
};
|
||||
|
||||
let json = match serde_json::to_string(&data) {
|
||||
|
@ -210,8 +212,8 @@ pub async fn create_location(ctx: &CoreContext, path: &str) -> Result<LocationRe
|
|||
Err(e) => Err(LocationError::DotfileWriteFailure(e, path.to_string()))?,
|
||||
}
|
||||
|
||||
ctx.emit(CoreEvent::InvalidateQuery(ClientQuery::SysGetLocations))
|
||||
.await;
|
||||
// ctx.emit(CoreEvent::InvalidateQuery(ClientQuery::SysGetLocations))
|
||||
// .await;
|
||||
|
||||
location
|
||||
}
|
||||
|
@ -220,8 +222,8 @@ pub async fn create_location(ctx: &CoreContext, path: &str) -> Result<LocationRe
|
|||
Ok(location.into())
|
||||
}
|
||||
|
||||
pub async fn delete_location(ctx: &CoreContext, location_id: i32) -> Result<(), SysError> {
|
||||
let db = &ctx.database;
|
||||
pub async fn delete_location(ctx: &LibraryContext, location_id: i32) -> Result<(), SysError> {
|
||||
let db = &ctx.db;
|
||||
|
||||
db.file_path()
|
||||
.find_many(vec![file_path::location_id::equals(Some(location_id))])
|
||||
|
@ -235,8 +237,11 @@ pub async fn delete_location(ctx: &CoreContext, location_id: i32) -> Result<(),
|
|||
.exec()
|
||||
.await?;
|
||||
|
||||
ctx.emit(CoreEvent::InvalidateQuery(ClientQuery::SysGetLocations))
|
||||
.await;
|
||||
ctx.emit(CoreEvent::InvalidateQuery(ClientQuery::LibraryQuery {
|
||||
library_id: ctx.id.to_string(),
|
||||
query: LibraryQuery::SysGetLocations,
|
||||
}))
|
||||
.await;
|
||||
|
||||
println!("Location {} deleted", location_id);
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// use crate::native;
|
||||
use crate::{node::get_nodestate, prisma::volume::*};
|
||||
use crate::{library::LibraryContext, prisma::volume::*};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ts_rs::TS;
|
||||
// #[cfg(not(target_os = "macos"))]
|
||||
|
@ -7,8 +7,6 @@ use std::process::Command;
|
|||
// #[cfg(not(target_os = "macos"))]
|
||||
use sysinfo::{DiskExt, System, SystemExt};
|
||||
|
||||
use crate::CoreContext;
|
||||
|
||||
use super::SysError;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Default, Clone, TS)]
|
||||
|
@ -26,23 +24,21 @@ pub struct Volume {
|
|||
}
|
||||
|
||||
impl Volume {
|
||||
pub async fn save(ctx: &CoreContext) -> Result<(), SysError> {
|
||||
let db = &ctx.database;
|
||||
let config = get_nodestate();
|
||||
|
||||
pub async fn save(ctx: &LibraryContext) -> Result<(), SysError> {
|
||||
let volumes = Self::get_volumes()?;
|
||||
|
||||
// enter all volumes associate with this client add to db
|
||||
for volume in volumes {
|
||||
db.volume()
|
||||
ctx.db
|
||||
.volume()
|
||||
.upsert(
|
||||
node_id_mount_point_name(
|
||||
config.node_id.clone(),
|
||||
ctx.node_local_id.clone(),
|
||||
volume.mount_point.to_string(),
|
||||
volume.name.to_string(),
|
||||
),
|
||||
(
|
||||
node_id::set(config.node_id),
|
||||
node_id::set(ctx.node_local_id),
|
||||
name::set(volume.name),
|
||||
mount_point::set(volume.mount_point),
|
||||
vec![
|
||||
|
|
|
@ -1,159 +1,123 @@
|
|||
use crate::prisma::{self, migration, PrismaClient};
|
||||
use crate::CoreContext;
|
||||
use data_encoding::HEXLOWER;
|
||||
use include_dir::{include_dir, Dir};
|
||||
use prisma_client_rust::raw;
|
||||
use ring::digest::{Context, Digest, SHA256};
|
||||
use std::ffi::OsStr;
|
||||
use std::io::{self, BufReader, Read};
|
||||
use prisma_client_rust::{raw, NewClientError};
|
||||
use ring::digest::{Context, SHA256};
|
||||
use thiserror::Error;
|
||||
|
||||
const INIT_MIGRATION: &str = include_str!("../../prisma/migrations/migration_table/migration.sql");
|
||||
static MIGRATIONS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/prisma/migrations");
|
||||
|
||||
/// MigrationError represents an error that occurring while opening a initialising and running migrations on the database.
|
||||
#[derive(Error, Debug)]
|
||||
pub enum DatabaseError {
|
||||
#[error("Unable to initialize the Prisma client")]
|
||||
ClientError(#[from] prisma::NewClientError),
|
||||
pub enum MigrationError {
|
||||
#[error("An error occurred while initialising a new database connection")]
|
||||
DatabaseIntialisation(#[from] NewClientError),
|
||||
#[error("An error occurred with the database while applying migrations")]
|
||||
DatabaseError(#[from] prisma_client_rust::queries::Error),
|
||||
#[error("An error occured reading the embedded migration files. {0}. Please report to Spacedrive developers!")]
|
||||
InvalidEmbeddedMigration(&'static str),
|
||||
}
|
||||
|
||||
pub async fn create_connection(path: &str) -> Result<PrismaClient, DatabaseError> {
|
||||
println!("Creating database connection: {:?}", path);
|
||||
let client = prisma::new_client_with_url(&format!("file:{}", &path)).await?;
|
||||
/// load_and_migrate will load the database from the given path and migrate it to the latest version of the schema.
|
||||
pub async fn load_and_migrate(db_url: &str) -> Result<PrismaClient, MigrationError> {
|
||||
let client = prisma::new_client_with_url(db_url).await?;
|
||||
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
pub fn sha256_digest<R: Read>(mut reader: R) -> Result<Digest, io::Error> {
|
||||
let mut context = Context::new(&SHA256);
|
||||
let mut buffer = [0; 1024];
|
||||
loop {
|
||||
let count = reader.read(&mut buffer)?;
|
||||
if count == 0 {
|
||||
break;
|
||||
}
|
||||
context.update(&buffer[..count]);
|
||||
}
|
||||
Ok(context.finish())
|
||||
}
|
||||
|
||||
pub async fn run_migrations(ctx: &CoreContext) -> Result<(), DatabaseError> {
|
||||
let client = &ctx.database;
|
||||
|
||||
match client
|
||||
let migrations_table_missing = client
|
||||
._query_raw::<serde_json::Value>(raw!(
|
||||
"SELECT name FROM sqlite_master WHERE type='table' AND name='_migrations'"
|
||||
))
|
||||
.await
|
||||
{
|
||||
Ok(data) => {
|
||||
if data.len() == 0 {
|
||||
// execute migration
|
||||
match client._execute_raw(raw!(INIT_MIGRATION)).await {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
println!("Failed to create migration table: {}", e);
|
||||
}
|
||||
};
|
||||
.await?
|
||||
.len() == 0;
|
||||
|
||||
let value: Vec<serde_json::Value> = client
|
||||
._query_raw(raw!(
|
||||
"SELECT name FROM sqlite_master WHERE type='table' AND name='_migrations'"
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
if migrations_table_missing {
|
||||
client._execute_raw(raw!(INIT_MIGRATION)).await?;
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
println!("Migration table created: {:?}", value);
|
||||
}
|
||||
|
||||
let mut migration_subdirs = MIGRATIONS_DIR
|
||||
.dirs()
|
||||
.filter(|subdir| {
|
||||
subdir
|
||||
.path()
|
||||
.file_name()
|
||||
.map(|name| name != OsStr::new("migration_table"))
|
||||
.unwrap_or(false)
|
||||
let mut migration_directories = MIGRATIONS_DIR
|
||||
.dirs()
|
||||
.map(|dir| {
|
||||
dir.path()
|
||||
.file_name()
|
||||
.ok_or(MigrationError::InvalidEmbeddedMigration(
|
||||
"File has malformed name",
|
||||
))
|
||||
.and_then(|name| {
|
||||
name.to_str()
|
||||
.ok_or_else(|| {
|
||||
MigrationError::InvalidEmbeddedMigration(
|
||||
"File name contains malformed characters",
|
||||
)
|
||||
})
|
||||
.map(|name| (name, dir))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
})
|
||||
.filter_map(|v| match v {
|
||||
Ok((name, _)) if name == "migration_table" => None,
|
||||
Ok((name, dir)) => match name[..14].parse::<i64>() {
|
||||
Ok(timestamp) => Some(Ok((name, timestamp, dir))),
|
||||
Err(_) => Some(Err(MigrationError::InvalidEmbeddedMigration(
|
||||
"File name is incorrectly formatted",
|
||||
))),
|
||||
},
|
||||
Err(v) => Some(Err(v)),
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
migration_subdirs.sort_by(|a, b| {
|
||||
let a_name = a.path().file_name().unwrap().to_str().unwrap();
|
||||
let b_name = b.path().file_name().unwrap().to_str().unwrap();
|
||||
// We sort the migrations so they are always applied in the correct order
|
||||
migration_directories.sort_by(|(_, a_time, _), (_, b_time, _)| a_time.cmp(&b_time));
|
||||
|
||||
let a_time = a_name[..14].parse::<i64>().unwrap();
|
||||
let b_time = b_name[..14].parse::<i64>().unwrap();
|
||||
for (name, _, dir) in migration_directories {
|
||||
let migration_file_raw = dir
|
||||
.get_file(dir.path().join("./migration.sql"))
|
||||
.ok_or(MigrationError::InvalidEmbeddedMigration(
|
||||
"Failed to find 'migration.sql' file in '{}' migration subdirectory",
|
||||
))?
|
||||
.contents_utf8()
|
||||
.ok_or(
|
||||
MigrationError::InvalidEmbeddedMigration(
|
||||
"Failed to open the contents of 'migration.sql' file in '{}' migration subdirectory",
|
||||
)
|
||||
)?;
|
||||
|
||||
a_time.cmp(&b_time)
|
||||
});
|
||||
// Generate SHA256 checksum of migration
|
||||
let mut checksum = Context::new(&SHA256);
|
||||
checksum.update(migration_file_raw.as_bytes());
|
||||
let checksum = HEXLOWER.encode(checksum.finish().as_ref());
|
||||
|
||||
for subdir in migration_subdirs {
|
||||
println!("{:?}", subdir.path());
|
||||
let migration_file = subdir
|
||||
.get_file(subdir.path().join("./migration.sql"))
|
||||
.unwrap();
|
||||
let migration_sql = migration_file.contents_utf8().unwrap();
|
||||
// get existing migration by checksum, if it doesn't exist run the migration
|
||||
if client
|
||||
.migration()
|
||||
.find_unique(migration::checksum::equals(checksum.clone()))
|
||||
.exec()
|
||||
.await?
|
||||
.is_none()
|
||||
{
|
||||
// Create migration record
|
||||
client
|
||||
.migration()
|
||||
.create(
|
||||
migration::name::set(name.to_string()),
|
||||
migration::checksum::set(checksum.clone()),
|
||||
vec![],
|
||||
)
|
||||
.exec()
|
||||
.await?;
|
||||
|
||||
let digest = sha256_digest(BufReader::new(migration_file.contents())).unwrap();
|
||||
// create a lowercase hash from
|
||||
let checksum = HEXLOWER.encode(digest.as_ref());
|
||||
let name = subdir.path().file_name().unwrap().to_str().unwrap();
|
||||
|
||||
// get existing migration by checksum, if it doesn't exist run the migration
|
||||
let existing_migration = client
|
||||
// Split the migrations file up into each individual step and apply them all
|
||||
let steps = migration_file_raw.split(";").collect::<Vec<&str>>();
|
||||
let steps = &steps[0..steps.len() - 1];
|
||||
for (i, step) in steps.iter().enumerate() {
|
||||
client._execute_raw(raw!(*step)).await?;
|
||||
client
|
||||
.migration()
|
||||
.find_unique(migration::checksum::equals(checksum.clone()))
|
||||
.update(vec![migration::steps_applied::set(i as i32 + 1)])
|
||||
.exec()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
if existing_migration.is_none() {
|
||||
#[cfg(debug_assertions)]
|
||||
println!("Running migration: {}", name);
|
||||
|
||||
let steps = migration_sql.split(";").collect::<Vec<&str>>();
|
||||
let steps = &steps[0..steps.len() - 1];
|
||||
|
||||
client
|
||||
.migration()
|
||||
.create(
|
||||
migration::name::set(name.to_string()),
|
||||
migration::checksum::set(checksum.clone()),
|
||||
vec![],
|
||||
)
|
||||
.exec()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
for (i, step) in steps.iter().enumerate() {
|
||||
match client._execute_raw(raw!(*step)).await {
|
||||
Ok(_) => {
|
||||
client
|
||||
.migration()
|
||||
.find_unique(migration::checksum::equals(checksum.clone()))
|
||||
.update(vec![migration::steps_applied::set(i as i32 + 1)])
|
||||
.exec()
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Error running migration: {}", name);
|
||||
println!("{}", e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
println!("Migration {} recorded successfully", name);
|
||||
}
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
panic!("Failed to check migration table existence: {:?}", err);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
Ok(client)
|
||||
}
|
||||
|
|
|
@ -13,25 +13,27 @@
|
|||
"lint": "TIMING=1 eslint src --fix",
|
||||
"clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.0.9",
|
||||
"scripts": "*",
|
||||
"tsconfig": "*",
|
||||
"typescript": "^4.7.2"
|
||||
},
|
||||
"jest": {
|
||||
"preset": "scripts/jest/node"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sd/config": "workspace:*",
|
||||
"@sd/core": "workspace:*",
|
||||
"@sd/interface": "workspace:*",
|
||||
"eventemitter3": "^4.0.7",
|
||||
"immer": "^9.0.14",
|
||||
"react-query": "^3.39.1",
|
||||
"lodash": "^4.17.21",
|
||||
"react-query": "^3.34.19",
|
||||
"zustand": "4.0.0-rc.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.0.9",
|
||||
"scripts": "*",
|
||||
"tsconfig": "*",
|
||||
"typescript": "^4.7.2",
|
||||
"@types/lodash": "^4.14.182"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.0.0",
|
||||
"react-query": "^3.34.19"
|
||||
"react": "^18.0.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,8 @@
|
|||
import { ClientCommand, ClientQuery, CoreResponse } from '@sd/core';
|
||||
import { ClientCommand, ClientQuery, CoreResponse, LibraryCommand, LibraryQuery } from '@sd/core';
|
||||
import { EventEmitter } from 'eventemitter3';
|
||||
import {
|
||||
UseMutationOptions,
|
||||
UseQueryOptions,
|
||||
UseQueryResult,
|
||||
useMutation,
|
||||
useQuery
|
||||
} from 'react-query';
|
||||
import { UseMutationOptions, UseQueryOptions, useMutation, useQuery } from 'react-query';
|
||||
|
||||
import { useLibraryStore } from './stores';
|
||||
|
||||
// global var to store the transport TODO: not global :D
|
||||
export let transport: BaseTransport | null = null;
|
||||
|
@ -23,11 +19,15 @@ export function setTransport(_transport: BaseTransport) {
|
|||
|
||||
// extract keys from generated Rust query/command types
|
||||
type QueryKeyType = ClientQuery['key'];
|
||||
type LibraryQueryKeyType = LibraryQuery['key'];
|
||||
type CommandKeyType = ClientCommand['key'];
|
||||
type LibraryCommandKeyType = LibraryCommand['key'];
|
||||
|
||||
// extract the type from the union
|
||||
type CQType<K> = Extract<ClientQuery, { key: K }>;
|
||||
type LQType<K> = Extract<LibraryQuery, { key: K }>;
|
||||
type CCType<K> = Extract<ClientCommand, { key: K }>;
|
||||
type LCType<K> = Extract<LibraryCommand, { key: K }>;
|
||||
type CRType<K> = Extract<CoreResponse, { key: K }>;
|
||||
|
||||
// extract payload type
|
||||
|
@ -35,20 +35,18 @@ type ExtractParams<P> = P extends { params: any } ? P['params'] : never;
|
|||
type ExtractData<D> = D extends { data: any } ? D['data'] : never;
|
||||
|
||||
// vanilla method to call the transport
|
||||
export async function queryBridge<
|
||||
K extends QueryKeyType,
|
||||
CQ extends CQType<K>,
|
||||
CR extends CRType<K>
|
||||
>(key: K, params?: ExtractParams<CQ>): Promise<ExtractData<CR>> {
|
||||
async function queryBridge<K extends QueryKeyType, CQ extends CQType<K>, CR extends CRType<K>>(
|
||||
key: K,
|
||||
params?: ExtractParams<CQ>
|
||||
): Promise<ExtractData<CR>> {
|
||||
const result = (await transport?.query({ key, params } as any)) as any;
|
||||
return result?.data;
|
||||
}
|
||||
|
||||
export async function commandBridge<
|
||||
K extends CommandKeyType,
|
||||
CC extends CCType<K>,
|
||||
CR extends CRType<K>
|
||||
>(key: K, params?: ExtractParams<CC>): Promise<ExtractData<CR>> {
|
||||
async function commandBridge<K extends CommandKeyType, CC extends CCType<K>, CR extends CRType<K>>(
|
||||
key: K,
|
||||
params?: ExtractParams<CC>
|
||||
): Promise<ExtractData<CR>> {
|
||||
const result = (await transport?.command({ key, params } as any)) as any;
|
||||
return result?.data;
|
||||
}
|
||||
|
@ -66,6 +64,21 @@ export function useBridgeQuery<K extends QueryKeyType, CQ extends CQType<K>, CR
|
|||
);
|
||||
}
|
||||
|
||||
export function useLibraryQuery<
|
||||
K extends LibraryQueryKeyType,
|
||||
CQ extends LQType<K>,
|
||||
CR extends CRType<K>
|
||||
>(key: K, params?: ExtractParams<CQ>, options: UseQueryOptions<ExtractData<CR>> = {}) {
|
||||
const library_id = useLibraryStore((state) => state.currentLibraryUuid);
|
||||
if (!library_id) throw new Error(`Attempted to do library query '${key}' with no library set!`);
|
||||
|
||||
return useQuery<ExtractData<CR>>(
|
||||
[library_id, key, params],
|
||||
async () => await queryBridge('LibraryQuery', { library_id, query: { key, params } as any }),
|
||||
options
|
||||
);
|
||||
}
|
||||
|
||||
export function useBridgeCommand<
|
||||
K extends CommandKeyType,
|
||||
CC extends CCType<K>,
|
||||
|
@ -78,9 +91,35 @@ export function useBridgeCommand<
|
|||
);
|
||||
}
|
||||
|
||||
export function useLibraryCommand<
|
||||
K extends LibraryCommandKeyType,
|
||||
LC extends LCType<K>,
|
||||
CR extends CRType<K>
|
||||
>(key: K, options: UseMutationOptions<ExtractData<LC>> = {}) {
|
||||
const library_id = useLibraryStore((state) => state.currentLibraryUuid);
|
||||
if (!library_id) throw new Error(`Attempted to do library command '${key}' with no library set!`);
|
||||
|
||||
return useMutation<ExtractData<CR>, unknown, ExtractParams<LC>>(
|
||||
[library_id, key],
|
||||
async (vars?: ExtractParams<LC>) =>
|
||||
await commandBridge('LibraryCommand', { library_id, command: { key, params: vars } as any }),
|
||||
options
|
||||
);
|
||||
}
|
||||
|
||||
export function command<K extends CommandKeyType, CC extends CCType<K>, CR extends CRType<K>>(
|
||||
key: K,
|
||||
vars: ExtractParams<CC>
|
||||
): Promise<ExtractData<CR>> {
|
||||
return commandBridge(key, vars);
|
||||
}
|
||||
|
||||
export function libraryCommand<
|
||||
K extends LibraryCommandKeyType,
|
||||
LC extends LCType<K>,
|
||||
CR extends CRType<K>
|
||||
>(key: K, vars: ExtractParams<LC>): Promise<ExtractData<CR>> {
|
||||
const library_id = useLibraryStore((state) => state.currentLibraryUuid);
|
||||
if (!library_id) throw new Error(`Attempted to do library command '${key}' with no library set!`);
|
||||
return commandBridge('LibraryCommand', { library_id, command: { key, params: vars } as any });
|
||||
}
|
||||
|
|
1
packages/client/src/context/index.ts
Normal file
1
packages/client/src/context/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './AppPropsContext';
|
|
@ -1,2 +0,0 @@
|
|||
export * from './query';
|
||||
export * from './state';
|
|
@ -1,21 +0,0 @@
|
|||
import { useState } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
|
||||
import { useBridgeCommand, useBridgeQuery } from '../bridge';
|
||||
import { useFileExplorerState } from './state';
|
||||
|
||||
// this hook initializes the explorer state and queries the core
|
||||
export function useFileExplorer(initialPath = '/', initialLocation: number | null = null) {
|
||||
const fileState = useFileExplorerState();
|
||||
// file explorer hooks maintain their own local state relative to exploration
|
||||
const [path, setPath] = useState(initialPath);
|
||||
const [locationId, setLocationId] = useState(initialPath);
|
||||
|
||||
// const { data: volumes } = useQuery(['sys_get_volumes'], () => bridge('sys_get_volumes'));
|
||||
|
||||
return { setPath, setLocationId };
|
||||
}
|
||||
|
||||
// export function useVolumes() {
|
||||
// return useQuery(['SysGetVolumes'], () => bridge('SysGetVolumes'));
|
||||
// }
|
|
@ -1,23 +0,0 @@
|
|||
import produce from 'immer';
|
||||
import create from 'zustand';
|
||||
|
||||
export interface FileExplorerState {
|
||||
current_location_id: number | null;
|
||||
row_limit: number;
|
||||
}
|
||||
|
||||
interface FileExplorerStore extends FileExplorerState {
|
||||
update_row_limit: (new_limit: number) => void;
|
||||
}
|
||||
|
||||
export const useFileExplorerState = create<FileExplorerStore>((set, get) => ({
|
||||
current_location_id: null,
|
||||
row_limit: 10,
|
||||
update_row_limit: (new_limit: number) => {
|
||||
set((store) =>
|
||||
produce(store, (draft) => {
|
||||
draft.row_limit = new_limit;
|
||||
})
|
||||
);
|
||||
}
|
||||
}));
|
1
packages/client/src/hooks/index.ts
Normal file
1
packages/client/src/hooks/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './useCoreEvents';
|
59
packages/client/src/hooks/useCoreEvents.tsx
Normal file
59
packages/client/src/hooks/useCoreEvents.tsx
Normal file
|
@ -0,0 +1,59 @@
|
|||
import { CoreEvent } from '@sd/core';
|
||||
import { useContext, useEffect } from 'react';
|
||||
import { useQueryClient } from 'react-query';
|
||||
|
||||
import { transport, useExplorerStore } from '..';
|
||||
|
||||
export function useCoreEvents() {
|
||||
const client = useQueryClient();
|
||||
|
||||
const { addNewThumbnail } = useExplorerStore();
|
||||
useEffect(() => {
|
||||
function handleCoreEvent(e: CoreEvent) {
|
||||
switch (e?.key) {
|
||||
case 'NewThumbnail':
|
||||
addNewThumbnail(e.data.cas_id);
|
||||
break;
|
||||
case 'InvalidateQuery':
|
||||
case 'InvalidateQueryDebounced':
|
||||
let query = [];
|
||||
if (e.data.key === 'LibraryQuery') {
|
||||
query = [e.data.params.library_id, e.data.params.query.key];
|
||||
|
||||
// TODO: find a way to make params accessible in TS
|
||||
// also this method will only work for queries that use the whole params obj as the second key
|
||||
// @ts-expect-error
|
||||
if (e.data.params.query.params) {
|
||||
// @ts-expect-error
|
||||
query.push(e.data.params.query.params);
|
||||
}
|
||||
} else {
|
||||
query = [e.data.key];
|
||||
|
||||
// TODO: find a way to make params accessible in TS
|
||||
// also this method will only work for queries that use the whole params obj as the second key
|
||||
// @ts-expect-error
|
||||
if (e.data.params) {
|
||||
// @ts-expect-error
|
||||
query.push(e.data.params);
|
||||
}
|
||||
}
|
||||
|
||||
client.invalidateQueries(query);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
// check Tauri Event type
|
||||
transport?.on('core_event', handleCoreEvent);
|
||||
|
||||
return () => {
|
||||
transport?.off('core_event', handleCoreEvent);
|
||||
};
|
||||
|
||||
// listen('core_event', (e: { payload: CoreEvent }) => {
|
||||
// });
|
||||
}, [transport]);
|
||||
}
|
|
@ -1,3 +1,5 @@
|
|||
export * from './bridge';
|
||||
export * from './files';
|
||||
export * from './ClientProvider';
|
||||
export * from './stores';
|
||||
export * from './hooks';
|
||||
export * from './context';
|
||||
|
|
4
packages/client/src/stores/index.ts
Normal file
4
packages/client/src/stores/index.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
export * from './useLibraryStore';
|
||||
export * from './useExplorerStore';
|
||||
export * from './useInspectorStore';
|
||||
export * from './useInspectorStore';
|
|
@ -1,15 +1,16 @@
|
|||
import create from 'zustand';
|
||||
|
||||
type ExplorerState = {
|
||||
type ExplorerStore = {
|
||||
selectedRowIndex: number;
|
||||
setSelectedRowIndex: (index: number) => void;
|
||||
locationId: number;
|
||||
setLocationId: (index: number) => void;
|
||||
newThumbnails: Record<string, boolean>;
|
||||
addNewThumbnail: (cas_id: string) => void;
|
||||
reset: () => void;
|
||||
};
|
||||
|
||||
export const useExplorerState = create<ExplorerState>((set) => ({
|
||||
export const useExplorerStore = create<ExplorerStore>((set) => ({
|
||||
selectedRowIndex: 1,
|
||||
setSelectedRowIndex: (index) => set((state) => ({ ...state, selectedRowIndex: index })),
|
||||
locationId: -1,
|
||||
|
@ -19,5 +20,6 @@ export const useExplorerState = create<ExplorerState>((set) => ({
|
|||
set((state) => ({
|
||||
...state,
|
||||
newThumbnails: { ...state.newThumbnails, [cas_id]: true }
|
||||
}))
|
||||
})),
|
||||
reset: () => set(() => ({}))
|
||||
}));
|
|
@ -1,17 +1,18 @@
|
|||
import { command } from '@sd/client';
|
||||
import produce from 'immer';
|
||||
import { debounce } from 'lodash';
|
||||
import create from 'zustand';
|
||||
|
||||
import { libraryCommand } from '../bridge';
|
||||
|
||||
export type UpdateNoteFN = (vars: { id: number; note: string }) => void;
|
||||
|
||||
interface UseInspectorState {
|
||||
interface InspectorStore {
|
||||
notes: Record<number, string>;
|
||||
setNote: (file_id: number, note: string) => void;
|
||||
unCacheNote: (file_id: number) => void;
|
||||
}
|
||||
|
||||
export const useInspectorState = create<UseInspectorState>((set) => ({
|
||||
export const useInspectorStore = create<InspectorStore>((set) => ({
|
||||
notes: {},
|
||||
// set the note locally
|
||||
setNote: (file_id, note) => {
|
||||
|
@ -35,7 +36,7 @@ export const useInspectorState = create<UseInspectorState>((set) => ({
|
|||
|
||||
// direct command call to update note
|
||||
export const updateNote = debounce(async (file_id: number, note: string) => {
|
||||
return await command('FileSetNote', {
|
||||
return await libraryCommand('FileSetNote', {
|
||||
id: file_id,
|
||||
note
|
||||
});
|
67
packages/client/src/stores/useLibraryStore.ts
Normal file
67
packages/client/src/stores/useLibraryStore.ts
Normal file
|
@ -0,0 +1,67 @@
|
|||
import { LibraryConfigWrapped } from '@sd/core';
|
||||
import produce from 'immer';
|
||||
import { useMemo } from 'react';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import create from 'zustand';
|
||||
import { devtools, persist } from 'zustand/middleware';
|
||||
|
||||
import { useBridgeQuery } from '../bridge';
|
||||
import { useExplorerStore } from './useExplorerStore';
|
||||
|
||||
type LibraryStore = {
|
||||
// the uuid of the currently active library
|
||||
currentLibraryUuid: string | null;
|
||||
// for full functionality this should be triggered along-side query invalidation
|
||||
switchLibrary: (uuid: string) => void;
|
||||
// a function
|
||||
init: (libraries: LibraryConfigWrapped[]) => Promise<void>;
|
||||
};
|
||||
|
||||
export const useLibraryStore = create<LibraryStore>()(
|
||||
devtools(
|
||||
persist(
|
||||
(set) => ({
|
||||
currentLibraryUuid: null,
|
||||
switchLibrary: (uuid) => {
|
||||
set((state) =>
|
||||
produce(state, (draft) => {
|
||||
draft.currentLibraryUuid = uuid;
|
||||
})
|
||||
);
|
||||
// reset other stores
|
||||
useExplorerStore().reset();
|
||||
},
|
||||
init: async (libraries) => {
|
||||
set((state) =>
|
||||
produce(state, (draft) => {
|
||||
// use first library default if none set
|
||||
if (!state.currentLibraryUuid) {
|
||||
draft.currentLibraryUuid = libraries[0].uuid;
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}),
|
||||
{ name: 'sd-library-store' }
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
// this must be used at least once in the app to correct the initial state
|
||||
// is memorized and can be used safely in any component
|
||||
export const useCurrentLibrary = () => {
|
||||
const { currentLibraryUuid, switchLibrary } = useLibraryStore();
|
||||
const { data: libraries } = useBridgeQuery('NodeGetLibraries', undefined, {});
|
||||
|
||||
// memorize library to avoid re-running find function
|
||||
const currentLibrary = useMemo(() => {
|
||||
const current = libraries?.find((l) => l.uuid === currentLibraryUuid);
|
||||
// switch to first library if none set
|
||||
if (Array.isArray(libraries) && !current && libraries[0]?.uuid) {
|
||||
switchLibrary(libraries[0]?.uuid);
|
||||
}
|
||||
return current;
|
||||
}, [libraries, currentLibraryUuid]);
|
||||
|
||||
return { currentLibrary, libraries, currentLibraryUuid };
|
||||
};
|
|
@ -46,7 +46,7 @@
|
|||
"react-loading-icons": "^1.1.0",
|
||||
"react-loading-skeleton": "^3.1.0",
|
||||
"react-portal": "^4.2.2",
|
||||
"react-query": "^3.39.1",
|
||||
"react-query": "^3.34.19",
|
||||
"react-router": "6.3.0",
|
||||
"react-router-dom": "6.3.0",
|
||||
"react-scrollbars-custom": "^4.0.27",
|
||||
|
@ -55,6 +55,7 @@
|
|||
"react-virtuoso": "^2.12.1",
|
||||
"rooks": "^5.11.2",
|
||||
"tailwindcss": "^3.0.24",
|
||||
"use-debounce": "^8.0.1",
|
||||
"zustand": "4.0.0-rc.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
import '@fontsource/inter/variable.css';
|
||||
import { BaseTransport, ClientProvider, setTransport } from '@sd/client';
|
||||
import { useCoreEvents } from '@sd/client';
|
||||
import { AppProps, AppPropsContext } from '@sd/client';
|
||||
import React from 'react';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
|
||||
import { AppProps, AppPropsContext } from './AppPropsContext';
|
||||
import { AppRouter } from './AppRouter';
|
||||
import { ErrorFallback } from './ErrorFallback';
|
||||
import { useCoreEvents } from './hooks/useCoreEvents';
|
||||
import './style.scss';
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { AppPropsContext } from '@sd/client';
|
||||
import clsx from 'clsx';
|
||||
import React, { useContext } from 'react';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
|
||||
import { AppPropsContext } from './AppPropsContext';
|
||||
import { Sidebar } from './components/file/Sidebar';
|
||||
|
||||
export function AppLayout() {
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { useBridgeQuery } from '@sd/client';
|
||||
import { useLibraryStore } from '@sd/client';
|
||||
import React, { useEffect } from 'react';
|
||||
import { Route, Routes, useLocation } from 'react-router-dom';
|
||||
|
||||
|
@ -9,56 +11,81 @@ import { ExplorerScreen } from './screens/Explorer';
|
|||
import { OverviewScreen } from './screens/Overview';
|
||||
import { PhotosScreen } from './screens/Photos';
|
||||
import { RedirectPage } from './screens/Redirect';
|
||||
import { SettingsScreen } from './screens/Settings';
|
||||
import { TagScreen } from './screens/Tag';
|
||||
import AppearanceSettings from './screens/settings/AppearanceSettings';
|
||||
import ContactsSettings from './screens/settings/ContactsSettings';
|
||||
import ExperimentalSettings from './screens/settings/ExperimentalSettings';
|
||||
import GeneralSettings from './screens/settings/GeneralSettings';
|
||||
import KeysSettings from './screens/settings/KeysSetting';
|
||||
import LibrarySettings from './screens/settings/LibrarySettings';
|
||||
import LocationSettings from './screens/settings/LocationSettings';
|
||||
import SecuritySettings from './screens/settings/SecuritySettings';
|
||||
import SharingSettings from './screens/settings/SharingSettings';
|
||||
import SyncSettings from './screens/settings/SyncSettings';
|
||||
import TagsSettings from './screens/settings/TagsSettings';
|
||||
import { CurrentLibrarySettings } from './screens/settings/CurrentLibrarySettings';
|
||||
import { SettingsScreen } from './screens/settings/Settings';
|
||||
import AppearanceSettings from './screens/settings/client/AppearanceSettings';
|
||||
import GeneralSettings from './screens/settings/client/GeneralSettings';
|
||||
import ContactsSettings from './screens/settings/library/ContactsSettings';
|
||||
import KeysSettings from './screens/settings/library/KeysSetting';
|
||||
import LibraryGeneralSettings from './screens/settings/library/LibraryGeneralSettings';
|
||||
import LocationSettings from './screens/settings/library/LocationSettings';
|
||||
import SecuritySettings from './screens/settings/library/SecuritySettings';
|
||||
import SharingSettings from './screens/settings/library/SharingSettings';
|
||||
import SyncSettings from './screens/settings/library/SyncSettings';
|
||||
import TagsSettings from './screens/settings/library/TagsSettings';
|
||||
import ExperimentalSettings from './screens/settings/node/ExperimentalSettings';
|
||||
import LibrarySettings from './screens/settings/node/LibrariesSettings';
|
||||
import NodesSettings from './screens/settings/node/NodesSettings';
|
||||
import P2PSettings from './screens/settings/node/P2PSettings';
|
||||
|
||||
export function AppRouter() {
|
||||
let location = useLocation();
|
||||
let state = location.state as { backgroundLocation?: Location };
|
||||
const libraryState = useLibraryStore();
|
||||
const { data: libraries } = useBridgeQuery('NodeGetLibraries');
|
||||
|
||||
// TODO: This can be removed once we add a setup flow to the app
|
||||
useEffect(() => {
|
||||
console.log({ url: location.pathname });
|
||||
}, [state]);
|
||||
if (libraryState.currentLibraryUuid === null && libraries && libraries.length > 0) {
|
||||
libraryState.switchLibrary(libraries[0].uuid);
|
||||
}
|
||||
}, [libraryState.currentLibraryUuid, libraries]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Routes location={state?.backgroundLocation || location}>
|
||||
<Route path="/" element={<AppLayout />}>
|
||||
<Route index element={<RedirectPage to="/overview" />} />
|
||||
<Route path="overview" element={<OverviewScreen />} />
|
||||
<Route path="content" element={<ContentScreen />} />
|
||||
<Route path="photos" element={<PhotosScreen />} />
|
||||
<Route path="debug" element={<DebugScreen />} />
|
||||
<Route path={'settings'} element={<SettingsScreen />}>
|
||||
<Route index element={<GeneralSettings />} />
|
||||
<Route path="appearance" element={<AppearanceSettings />} />
|
||||
<Route path="contacts" element={<ContactsSettings />} />
|
||||
<Route path="experimental" element={<ExperimentalSettings />} />
|
||||
<Route path="general" element={<GeneralSettings />} />
|
||||
<Route path="keys" element={<KeysSettings />} />
|
||||
<Route path="library" element={<LibrarySettings />} />
|
||||
<Route path="security" element={<SecuritySettings />} />
|
||||
<Route path="locations" element={<LocationSettings />} />
|
||||
<Route path="sharing" element={<SharingSettings />} />
|
||||
<Route path="sync" element={<SyncSettings />} />
|
||||
<Route path="tags" element={<TagsSettings />} />
|
||||
{libraryState.currentLibraryUuid === null ? (
|
||||
<>
|
||||
{/* TODO: Remove this when adding app setup flow */}
|
||||
<h1>No Library Loaded...</h1>
|
||||
</>
|
||||
) : (
|
||||
<Routes location={state?.backgroundLocation || location}>
|
||||
<Route path="/" element={<AppLayout />}>
|
||||
<Route index element={<RedirectPage to="/overview" />} />
|
||||
<Route path="overview" element={<OverviewScreen />} />
|
||||
<Route path="content" element={<ContentScreen />} />
|
||||
<Route path="photos" element={<PhotosScreen />} />
|
||||
<Route path="debug" element={<DebugScreen />} />
|
||||
<Route path={'library-settings'} element={<CurrentLibrarySettings />}>
|
||||
<Route index element={<LocationSettings />} />
|
||||
<Route path="general" element={<LibraryGeneralSettings />} />
|
||||
<Route path="locations" element={<LocationSettings />} />
|
||||
<Route path="tags" element={<TagsSettings />} />
|
||||
<Route path="keys" element={<KeysSettings />} />
|
||||
</Route>
|
||||
<Route path={'settings'} element={<SettingsScreen />}>
|
||||
<Route index element={<GeneralSettings />} />
|
||||
<Route path="general" element={<GeneralSettings />} />
|
||||
<Route path="appearance" element={<AppearanceSettings />} />
|
||||
<Route path="nodes" element={<NodesSettings />} />
|
||||
<Route path="p2p" element={<P2PSettings />} />
|
||||
<Route path="contacts" element={<ContactsSettings />} />
|
||||
<Route path="experimental" element={<ExperimentalSettings />} />
|
||||
<Route path="keys" element={<KeysSettings />} />
|
||||
<Route path="library" element={<LibrarySettings />} />
|
||||
<Route path="security" element={<SecuritySettings />} />
|
||||
<Route path="locations" element={<LocationSettings />} />
|
||||
<Route path="sharing" element={<SharingSettings />} />
|
||||
<Route path="sync" element={<SyncSettings />} />
|
||||
<Route path="tags" element={<TagsSettings />} />
|
||||
</Route>
|
||||
<Route path="explorer/:id" element={<ExplorerScreen />} />
|
||||
<Route path="tag/:id" element={<TagScreen />} />
|
||||
<Route path="*" element={<NotFound />} />
|
||||
</Route>
|
||||
<Route path="explorer/:id" element={<ExplorerScreen />} />
|
||||
<Route path="tag/:id" element={<TagScreen />} />
|
||||
<Route path="*" element={<NotFound />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</Routes>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ export function NotFound() {
|
|||
role="alert"
|
||||
className="flex flex-col items-center justify-center w-full h-full p-4 rounded-lg dark:text-white"
|
||||
>
|
||||
<p className="m-3 mt-20 text-sm font-semibold text-gray-500 uppercase">Error: 404</p>
|
||||
<p className="m-3 text-sm font-semibold text-gray-500 uppercase">Error: 404</p>
|
||||
<h1 className="text-4xl font-bold">You chose nothingness.</h1>
|
||||
<div className="flex flex-row space-x-2">
|
||||
<Button variant="primary" className="mt-4" onClick={() => navigate(-1)}>
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import { DotsVerticalIcon } from '@heroicons/react/solid';
|
||||
import { useBridgeQuery } from '@sd/client';
|
||||
import { useBridgeQuery, useLibraryQuery } from '@sd/client';
|
||||
import { useExplorerStore } from '@sd/client';
|
||||
import { AppPropsContext } from '@sd/client';
|
||||
import { FilePath } from '@sd/core';
|
||||
import clsx from 'clsx';
|
||||
import React, { useContext, useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
@ -7,8 +9,6 @@ import { useSearchParams } from 'react-router-dom';
|
|||
import { Virtuoso, VirtuosoHandle } from 'react-virtuoso';
|
||||
import { useKey, useWindowSize } from 'rooks';
|
||||
|
||||
import { AppPropsContext } from '../../AppPropsContext';
|
||||
import { useExplorerState } from '../../hooks/useExplorerState';
|
||||
import FileThumb from './FileThumb';
|
||||
|
||||
interface IColumn {
|
||||
|
@ -51,10 +51,10 @@ export const FileList: React.FC<{ location_id: number; path: string; limit: numb
|
|||
|
||||
const path = props.path;
|
||||
|
||||
const { selectedRowIndex, setSelectedRowIndex, setLocationId } = useExplorerState();
|
||||
const { selectedRowIndex, setSelectedRowIndex, setLocationId } = useExplorerStore();
|
||||
const [goingUp, setGoingUp] = useState(false);
|
||||
|
||||
const { data: currentDir } = useBridgeQuery('LibGetExplorerDir', {
|
||||
const { data: currentDir } = useLibraryQuery('LibGetExplorerDir', {
|
||||
location_id: props.location_id,
|
||||
path,
|
||||
limit: props.limit
|
||||
|
@ -148,7 +148,7 @@ const RenderRow: React.FC<{
|
|||
rowIndex: number;
|
||||
dirId: number;
|
||||
}> = ({ row, rowIndex, dirId }) => {
|
||||
const { selectedRowIndex, setSelectedRowIndex } = useExplorerState();
|
||||
const { selectedRowIndex, setSelectedRowIndex } = useExplorerStore();
|
||||
const isActive = selectedRowIndex === rowIndex;
|
||||
|
||||
let [_, setSearchParams] = useSearchParams();
|
||||
|
@ -202,7 +202,7 @@ const RenderCell: React.FC<{
|
|||
if (!value) return <></>;
|
||||
|
||||
const location = useContext(LocationContext);
|
||||
const { newThumbnails } = useExplorerState();
|
||||
const { newThumbnails } = useExplorerStore();
|
||||
|
||||
const hasNewThumbnail = !!newThumbnails[row?.file?.cas_id ?? ''];
|
||||
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { useBridgeQuery } from '@sd/client';
|
||||
import { AppPropsContext } from '@sd/client';
|
||||
import { FilePath } from '@sd/core';
|
||||
import clsx from 'clsx';
|
||||
import React, { useContext } from 'react';
|
||||
|
||||
import { AppPropsContext } from '../../AppPropsContext';
|
||||
import icons from '../../assets/icons';
|
||||
import { Folder } from '../icons/Folder';
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { Transition } from '@headlessui/react';
|
||||
import { ShareIcon } from '@heroicons/react/solid';
|
||||
import { useInspectorStore } from '@sd/client';
|
||||
import { FilePath, LocationResource } from '@sd/core';
|
||||
import { Button, TextArea } from '@sd/ui';
|
||||
import moment from 'moment';
|
||||
|
@ -7,7 +8,6 @@ import { Heart, Link } from 'phosphor-react';
|
|||
import React, { useEffect } from 'react';
|
||||
|
||||
import { default as types } from '../../constants/file-types.json';
|
||||
import { useInspectorState } from '../../hooks/useInspectorState';
|
||||
import FileThumb from './FileThumb';
|
||||
|
||||
interface MetaItemProps {
|
||||
|
@ -42,7 +42,7 @@ export const Inspector = (props: {
|
|||
// notes are cached in a store by their file id
|
||||
// this is so we can ensure every note has been sent to Rust even
|
||||
// when quickly navigating files, which cancels update function
|
||||
const { notes, setNote, unCacheNote } = useInspectorState();
|
||||
const { notes, setNote, unCacheNote } = useInspectorStore();
|
||||
|
||||
// show cached note over server note, important to check for undefined not falsey
|
||||
const note =
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
import { LockClosedIcon, PhotographIcon } from '@heroicons/react/outline';
|
||||
import { CogIcon, EyeOffIcon, PlusIcon } from '@heroicons/react/solid';
|
||||
import { useBridgeCommand, useBridgeQuery } from '@sd/client';
|
||||
import { useLibraryCommand, useLibraryQuery } from '@sd/client';
|
||||
import { useCurrentLibrary, useLibraryStore } from '@sd/client';
|
||||
import { AppPropsContext } from '@sd/client';
|
||||
import { Button, Dropdown } from '@sd/ui';
|
||||
import clsx from 'clsx';
|
||||
import { CirclesFour, Code, Planet } from 'phosphor-react';
|
||||
import React, { useContext } from 'react';
|
||||
import { NavLink, NavLinkProps } from 'react-router-dom';
|
||||
import React, { useContext, useEffect, useMemo } from 'react';
|
||||
import { NavLink, NavLinkProps, useNavigate } from 'react-router-dom';
|
||||
|
||||
import { AppPropsContext } from '../../AppPropsContext';
|
||||
import { useNodeStore } from '../device/Stores';
|
||||
import { Folder } from '../icons/Folder';
|
||||
import RunningJobsWidget from '../jobs/RunningJobsWidget';
|
||||
|
@ -76,11 +77,30 @@ const macOnly = (platform: string | undefined, classnames: string) =>
|
|||
export const Sidebar: React.FC<SidebarProps> = (props) => {
|
||||
const { isExperimental } = useNodeStore();
|
||||
|
||||
const appProps = useContext(AppPropsContext);
|
||||
const { data: locations } = useBridgeQuery('SysGetLocations');
|
||||
const { data: clientState } = useBridgeQuery('NodeGetState');
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { mutate: createLocation } = useBridgeCommand('LocCreate');
|
||||
const appProps = useContext(AppPropsContext);
|
||||
|
||||
const { data: locationsResponse, isError: isLocationsError } = useLibraryQuery('SysGetLocations');
|
||||
|
||||
let locations = Array.isArray(locationsResponse) ? locationsResponse : [];
|
||||
|
||||
// initialize libraries
|
||||
const { init: initLibraries, switchLibrary: _switchLibrary } = useLibraryStore();
|
||||
|
||||
const switchLibrary = (uuid: string) => {
|
||||
navigate('overview');
|
||||
|
||||
_switchLibrary(uuid);
|
||||
};
|
||||
|
||||
const { currentLibrary, libraries, currentLibraryUuid } = useCurrentLibrary();
|
||||
|
||||
useEffect(() => {
|
||||
if (libraries && !currentLibraryUuid) initLibraries(libraries);
|
||||
}, [libraries, currentLibraryUuid]);
|
||||
|
||||
const { mutate: createLocation } = useLibraryCommand('LocCreate');
|
||||
|
||||
const tags = [
|
||||
{ id: 1, name: 'Keepsafe', color: '#FF6788' },
|
||||
|
@ -122,7 +142,6 @@ export const Sidebar: React.FC<SidebarProps> = (props) => {
|
|||
appProps?.platform === 'macOS' &&
|
||||
'dark:!bg-opacity-40 dark:hover:!bg-opacity-70 dark:!border-[#333949] dark:hover:!border-[#394052]'
|
||||
),
|
||||
|
||||
variant: 'gray'
|
||||
}}
|
||||
// to support the transparent sidebar on macOS we use slightly adjusted styles
|
||||
|
@ -133,17 +152,22 @@ export const Sidebar: React.FC<SidebarProps> = (props) => {
|
|||
)}
|
||||
// this shouldn't default to "My Library", it is only this way for landing demo
|
||||
// TODO: implement demo mode for the sidebar and show loading indicator instead of "My Library"
|
||||
buttonText={clientState?.node_name || 'My Library'}
|
||||
buttonText={currentLibrary?.config.name || ' '}
|
||||
items={[
|
||||
libraries?.map((library) => ({
|
||||
name: library.config.name,
|
||||
selected: library.uuid === currentLibraryUuid,
|
||||
onPress: () => switchLibrary(library.uuid)
|
||||
})) || [],
|
||||
[
|
||||
{ name: clientState?.node_name || 'My Library', selected: true },
|
||||
{ name: 'Private Library' }
|
||||
],
|
||||
[
|
||||
{ name: 'Library Settings', icon: CogIcon },
|
||||
{
|
||||
name: 'Library Settings',
|
||||
icon: CogIcon,
|
||||
onPress: () => navigate('library-settings/general')
|
||||
},
|
||||
{ name: 'Add Library', icon: PlusIcon },
|
||||
{ name: 'Lock', icon: LockClosedIcon },
|
||||
{ name: 'Hide', icon: EyeOffIcon }
|
||||
{ name: 'Lock', icon: LockClosedIcon }
|
||||
// { name: 'Hide', icon: EyeOffIcon }
|
||||
]
|
||||
]}
|
||||
/>
|
||||
|
@ -204,21 +228,23 @@ export const Sidebar: React.FC<SidebarProps> = (props) => {
|
|||
);
|
||||
})}
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
appProps?.openDialog({ directory: true }).then((result) => {
|
||||
if (result) createLocation({ path: result as string });
|
||||
});
|
||||
}}
|
||||
className={clsx(
|
||||
'w-full px-2 py-1.5 mt-1 text-xs font-bold text-center text-gray-400 border border-dashed rounded border-transparent cursor-normal border-gray-350 transition',
|
||||
appProps?.platform === 'macOS'
|
||||
? 'dark:text-gray-450 dark:border-gray-450 hover:dark:border-gray-400 dark:border-opacity-60'
|
||||
: 'dark:text-gray-450 dark:border-gray-550 hover:dark:border-gray-500'
|
||||
)}
|
||||
>
|
||||
Add Location
|
||||
</button>
|
||||
{(locations?.length || 0) < 1 && (
|
||||
<button
|
||||
onClick={() => {
|
||||
appProps?.openDialog({ directory: true }).then((result) => {
|
||||
if (result) createLocation({ path: result as string });
|
||||
});
|
||||
}}
|
||||
className={clsx(
|
||||
'w-full px-2 py-1.5 mt-1 text-xs font-bold text-center text-gray-400 border border-dashed rounded border-transparent cursor-normal border-gray-350 transition',
|
||||
appProps?.platform === 'macOS'
|
||||
? 'dark:text-gray-450 dark:border-gray-450 hover:dark:border-gray-400 dark:border-opacity-60'
|
||||
: 'dark:text-gray-450 dark:border-gray-550 hover:dark:border-gray-500'
|
||||
)}
|
||||
>
|
||||
Add Location
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<Heading>Tags</Heading>
|
||||
|
|
15
packages/interface/src/components/layout/Card.tsx
Normal file
15
packages/interface/src/components/layout/Card.tsx
Normal file
|
@ -0,0 +1,15 @@
|
|||
import clsx from 'clsx';
|
||||
import React, { ReactNode } from 'react';
|
||||
|
||||
export default function Card(props: { children: ReactNode; className?: string }) {
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'flex w-full px-4 py-2 border border-gray-500 rounded-lg bg-gray-550',
|
||||
props.className
|
||||
)}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -5,7 +5,7 @@ import React, { ReactNode } from 'react';
|
|||
|
||||
import Loader from '../primitive/Loader';
|
||||
|
||||
export interface DialogProps {
|
||||
export interface DialogProps extends DialogPrimitive.DialogProps {
|
||||
trigger: ReactNode;
|
||||
ctaLabel?: string;
|
||||
ctaDanger?: boolean;
|
||||
|
@ -18,13 +18,15 @@ export interface DialogProps {
|
|||
|
||||
export default function Dialog(props: DialogProps) {
|
||||
return (
|
||||
<DialogPrimitive.Root>
|
||||
<DialogPrimitive.Root open={props.open} onOpenChange={props.onOpenChange}>
|
||||
<DialogPrimitive.Trigger asChild>{props.trigger}</DialogPrimitive.Trigger>
|
||||
<DialogPrimitive.Portal>
|
||||
<DialogPrimitive.Overlay className="fixed top-0 dialog-overlay bottom-0 left-0 right-0 z-50 grid overflow-y-auto bg-black bg-opacity-50 rounded-xl place-items-center m-[1px]">
|
||||
<DialogPrimitive.Content className="min-w-[300px] max-w-[400px] dialog-content rounded-md bg-gray-650 text-white border border-gray-550 shadow-deep">
|
||||
<div className="p-5">
|
||||
<DialogPrimitive.Title className="font-bold ">{props.title}</DialogPrimitive.Title>
|
||||
<DialogPrimitive.Title className="mb-2 font-bold">
|
||||
{props.title}
|
||||
</DialogPrimitive.Title>
|
||||
<DialogPrimitive.Description className="text-sm text-gray-300">
|
||||
{props.description}
|
||||
</DialogPrimitive.Description>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { ChevronLeftIcon, ChevronRightIcon } from '@heroicons/react/outline';
|
||||
import { useBridgeCommand } from '@sd/client';
|
||||
import { useLibraryCommand } from '@sd/client';
|
||||
import { useExplorerStore } from '@sd/client';
|
||||
import { Dropdown } from '@sd/ui';
|
||||
import clsx from 'clsx';
|
||||
import {
|
||||
|
@ -15,7 +16,6 @@ import {
|
|||
import React, { DetailedHTMLProps, HTMLAttributes } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { useExplorerState } from '../../hooks/useExplorerState';
|
||||
import { Shortcut } from '../primitive/Shortcut';
|
||||
import { DefaultProps } from '../primitive/types';
|
||||
|
||||
|
@ -50,14 +50,14 @@ const TopBarButton: React.FC<TopBarButtonProps> = ({ icon: Icon, ...props }) =>
|
|||
};
|
||||
|
||||
export const TopBar: React.FC<TopBarProps> = (props) => {
|
||||
const { locationId } = useExplorerState();
|
||||
const { mutate: generateThumbsForLocation } = useBridgeCommand('GenerateThumbsForLocation', {
|
||||
const { locationId } = useExplorerStore();
|
||||
const { mutate: generateThumbsForLocation } = useLibraryCommand('GenerateThumbsForLocation', {
|
||||
onMutate: (data) => {
|
||||
console.log('GenerateThumbsForLocation', data);
|
||||
}
|
||||
});
|
||||
|
||||
const { mutate: identifyUniqueFiles } = useBridgeCommand('IdentifyUniqueFiles', {
|
||||
const { mutate: identifyUniqueFiles } = useLibraryCommand('IdentifyUniqueFiles', {
|
||||
onMutate: (data) => {
|
||||
console.log('IdentifyUniqueFiles', data);
|
||||
},
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { DotsVerticalIcon, RefreshIcon } from '@heroicons/react/outline';
|
||||
import { CogIcon, TrashIcon } from '@heroicons/react/solid';
|
||||
import { command, useBridgeCommand } from '@sd/client';
|
||||
import { TrashIcon } from '@heroicons/react/solid';
|
||||
import { useLibraryCommand } from '@sd/client';
|
||||
import { LocationResource } from '@sd/core';
|
||||
import { Button } from '@sd/ui';
|
||||
import clsx from 'clsx';
|
||||
|
@ -16,9 +16,9 @@ interface LocationListItemProps {
|
|||
export default function LocationListItem({ location }: LocationListItemProps) {
|
||||
const [hide, setHide] = useState(false);
|
||||
|
||||
const { mutate: locRescan } = useBridgeCommand('LocRescan');
|
||||
const { mutate: locRescan } = useLibraryCommand('LocRescan');
|
||||
|
||||
const { mutate: deleteLoc, isLoading: locDeletePending } = useBridgeCommand('LocDelete', {
|
||||
const { mutate: deleteLoc, isLoading: locDeletePending } = useLibraryCommand('LocDelete', {
|
||||
onSuccess: () => {
|
||||
setHide(true);
|
||||
}
|
||||
|
|
|
@ -5,5 +5,5 @@ interface SettingsContainerProps {
|
|||
}
|
||||
|
||||
export const SettingsContainer: React.FC<SettingsContainerProps> = (props) => {
|
||||
return <div className="flex flex-col flex-grow max-w-4xl space-y-4 w-ful">{props.children}</div>;
|
||||
return <div className="flex flex-col flex-grow max-w-4xl space-y-6 w-ful">{props.children}</div>;
|
||||
};
|
||||
|
|
|
@ -1,15 +1,19 @@
|
|||
import React from 'react';
|
||||
import React, { ReactNode } from 'react';
|
||||
|
||||
interface SettingsHeaderProps {
|
||||
title: string;
|
||||
description: string;
|
||||
rightArea?: ReactNode;
|
||||
}
|
||||
|
||||
export const SettingsHeader: React.FC<SettingsHeaderProps> = (props) => {
|
||||
return (
|
||||
<div className="mt-3 mb-3">
|
||||
<h1 className="text-2xl font-bold">{props.title}</h1>
|
||||
<p className="mt-1 text-sm text-gray-400">{props.description}</p>
|
||||
<div className="flex mt-3 mb-3">
|
||||
<div className="flex-grow">
|
||||
<h1 className="text-2xl font-bold">{props.title}</h1>
|
||||
<p className="mt-1 text-sm text-gray-400">{props.description}</p>
|
||||
</div>
|
||||
{props.rightArea}
|
||||
<hr className="mt-4 border-gray-550" />
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
import { Outlet } from 'react-router';
|
||||
|
||||
interface SettingsScreenContainerProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const SettingsIcon = ({ component: Icon, ...props }: any) => (
|
||||
<Icon weight="bold" {...props} className={clsx('w-4 h-4 mr-2', props.className)} />
|
||||
);
|
||||
|
||||
export const SettingsHeading: React.FC<{ className?: string; children: string }> = ({
|
||||
children,
|
||||
className
|
||||
}) => (
|
||||
<div className={clsx('mt-5 mb-1 ml-1 text-xs font-semibold text-gray-300', className)}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
export const SettingsScreenContainer: React.FC<SettingsScreenContainerProps> = (props) => {
|
||||
return (
|
||||
<div className="flex flex-row w-full">
|
||||
<div className="h-full border-r max-w-[200px] flex-shrink-0 border-gray-100 w-60 dark:border-gray-550">
|
||||
<div data-tauri-drag-region className="w-full h-7" />
|
||||
<div className="p-5 pt-0">{props.children}</div>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<div data-tauri-drag-region className="w-full h-7" />
|
||||
<div className="flex flex-grow-0 w-full h-full max-h-screen custom-scroll page-scroll">
|
||||
<div className="flex flex-grow px-12 pb-5">
|
||||
<Outlet />
|
||||
<div className="block h-20" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -1,46 +0,0 @@
|
|||
import { transport } from '@sd/client';
|
||||
import { CoreEvent } from '@sd/core';
|
||||
import { useContext, useEffect } from 'react';
|
||||
import { useQueryClient } from 'react-query';
|
||||
|
||||
import { AppPropsContext } from '../AppPropsContext';
|
||||
import { useExplorerState } from './useExplorerState';
|
||||
|
||||
export function useCoreEvents() {
|
||||
const client = useQueryClient();
|
||||
|
||||
const { addNewThumbnail } = useExplorerState();
|
||||
useEffect(() => {
|
||||
function handleCoreEvent(e: CoreEvent) {
|
||||
switch (e?.key) {
|
||||
case 'NewThumbnail':
|
||||
addNewThumbnail(e.data.cas_id);
|
||||
break;
|
||||
case 'InvalidateQuery':
|
||||
case 'InvalidateQueryDebounced':
|
||||
let query = [e.data.key];
|
||||
// TODO: find a way to make params accessible in TS
|
||||
// also this method will only work for queries that use the whole params obj as the second key
|
||||
// @ts-expect-error
|
||||
if (e.data.params) {
|
||||
// @ts-expect-error
|
||||
query.push(e.data.params);
|
||||
}
|
||||
client.invalidateQueries(e.data.key);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
// check Tauri Event type
|
||||
transport?.on('core_event', handleCoreEvent);
|
||||
|
||||
return () => {
|
||||
transport?.off('core_event', handleCoreEvent);
|
||||
};
|
||||
|
||||
// listen('core_event', (e: { payload: CoreEvent }) => {
|
||||
// });
|
||||
}, [transport]);
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
import { AppProps, Platform } from '@sd/client';
|
||||
|
||||
import App from './App';
|
||||
import { AppProps, Platform } from './AppPropsContext';
|
||||
|
||||
export type { AppProps, Platform };
|
||||
|
||||
|
|
|
@ -1,21 +1,22 @@
|
|||
import { useBridgeCommand, useBridgeQuery } from '@sd/client';
|
||||
import { useBridgeQuery, useLibraryCommand, useLibraryQuery } from '@sd/client';
|
||||
import { AppPropsContext } from '@sd/client';
|
||||
import { Button } from '@sd/ui';
|
||||
import React, { useContext } from 'react';
|
||||
|
||||
import { AppPropsContext } from '../AppPropsContext';
|
||||
import CodeBlock from '../components/primitive/Codeblock';
|
||||
|
||||
export const DebugScreen: React.FC<{}> = (props) => {
|
||||
const appPropsContext = useContext(AppPropsContext);
|
||||
const { data: client } = useBridgeQuery('NodeGetState');
|
||||
const { data: nodeState } = useBridgeQuery('NodeGetState');
|
||||
const { data: libraryState } = useBridgeQuery('NodeGetLibraries');
|
||||
const { data: jobs } = useBridgeQuery('JobGetRunning');
|
||||
const { data: jobHistory } = useBridgeQuery('JobGetHistory');
|
||||
const { data: jobHistory } = useLibraryQuery('JobGetHistory');
|
||||
// const { mutate: purgeDB } = useBridgeCommand('PurgeDatabase', {
|
||||
// onMutate: () => {
|
||||
// alert('Database purged');
|
||||
// }
|
||||
// });
|
||||
const { mutate: identifyFiles } = useBridgeCommand('IdentifyUniqueFiles');
|
||||
const { mutate: identifyFiles } = useLibraryCommand('IdentifyUniqueFiles');
|
||||
return (
|
||||
<div className="flex flex-col w-full h-screen custom-scroll page-scroll">
|
||||
<div data-tauri-drag-region className="flex flex-shrink-0 w-full h-5" />
|
||||
|
@ -27,8 +28,8 @@ export const DebugScreen: React.FC<{}> = (props) => {
|
|||
variant="gray"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
if (client && appPropsContext?.onOpen) {
|
||||
appPropsContext.onOpen(client.data_path);
|
||||
if (nodeState && appPropsContext?.onOpen) {
|
||||
appPropsContext.onOpen(nodeState.data_path);
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
@ -39,8 +40,10 @@ export const DebugScreen: React.FC<{}> = (props) => {
|
|||
<CodeBlock src={{ ...jobs }} />
|
||||
<h1 className="text-sm font-bold ">Job History</h1>
|
||||
<CodeBlock src={{ ...jobHistory }} />
|
||||
<h1 className="text-sm font-bold ">Client State</h1>
|
||||
<CodeBlock src={{ ...client }} />
|
||||
<h1 className="text-sm font-bold ">Node State</h1>
|
||||
<CodeBlock src={{ ...nodeState }} />
|
||||
<h1 className="text-sm font-bold ">Libraries</h1>
|
||||
<CodeBlock src={{ ...libraryState }} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { useBridgeQuery } from '@sd/client';
|
||||
import { useLibraryQuery } from '@sd/client';
|
||||
import { useExplorerStore } from '@sd/client';
|
||||
import React from 'react';
|
||||
import { useParams, useSearchParams } from 'react-router-dom';
|
||||
|
||||
import { FileList } from '../components/file/FileList';
|
||||
import { Inspector } from '../components/file/Inspector';
|
||||
import { TopBar } from '../components/layout/TopBar';
|
||||
import { useExplorerState } from '../hooks/useExplorerState';
|
||||
|
||||
export const ExplorerScreen: React.FC<{}> = () => {
|
||||
let [searchParams] = useSearchParams();
|
||||
|
@ -16,13 +16,13 @@ export const ExplorerScreen: React.FC<{}> = () => {
|
|||
|
||||
const [limit, setLimit] = React.useState(100);
|
||||
|
||||
const { selectedRowIndex } = useExplorerState();
|
||||
const { selectedRowIndex } = useExplorerStore();
|
||||
|
||||
// Current Location
|
||||
const { data: currentLocation } = useBridgeQuery('SysGetLocation', { id: location_id });
|
||||
const { data: currentLocation } = useLibraryQuery('SysGetLocation', { id: location_id });
|
||||
|
||||
// Current Directory
|
||||
const { data: currentDir } = useBridgeQuery(
|
||||
const { data: currentDir } = useLibraryQuery(
|
||||
'LibGetExplorerDir',
|
||||
{ location_id: location_id!, path, limit },
|
||||
{ enabled: !!location_id }
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { PlusIcon } from '@heroicons/react/solid';
|
||||
import { useBridgeQuery } from '@sd/client';
|
||||
import { DatabaseIcon, ExclamationCircleIcon, PlusIcon } from '@heroicons/react/solid';
|
||||
import { useBridgeQuery, useLibraryQuery } from '@sd/client';
|
||||
import { AppPropsContext } from '@sd/client';
|
||||
import { Statistics } from '@sd/core';
|
||||
import { Button, Input } from '@sd/ui';
|
||||
import byteSize from 'byte-size';
|
||||
|
@ -10,7 +11,6 @@ import Skeleton from 'react-loading-skeleton';
|
|||
import 'react-loading-skeleton/dist/skeleton.css';
|
||||
import create from 'zustand';
|
||||
|
||||
import { AppPropsContext } from '../AppPropsContext';
|
||||
import { Device } from '../components/device/Device';
|
||||
import Dialog from '../components/layout/Dialog';
|
||||
|
||||
|
@ -102,7 +102,7 @@ const StatItem: React.FC<StatItemProps> = (props) => {
|
|||
|
||||
export const OverviewScreen = () => {
|
||||
const { data: libraryStatistics, isLoading: isStatisticsLoading } =
|
||||
useBridgeQuery('GetLibraryStatistics');
|
||||
useLibraryQuery('GetLibraryStatistics');
|
||||
const { data: nodeState } = useBridgeQuery('NodeGetState');
|
||||
|
||||
const { overviewStats, setOverviewStats } = useOverviewState();
|
||||
|
@ -157,7 +157,17 @@ export const OverviewScreen = () => {
|
|||
{/* STAT HEADER */}
|
||||
<div className="flex w-full">
|
||||
{/* STAT CONTAINER */}
|
||||
<div className="flex pb-4 overflow-hidden">
|
||||
<div className="flex -mb-1 overflow-hidden">
|
||||
{!libraryStatistics && (
|
||||
<div className="mb-2 ml-2">
|
||||
<div className="font-semibold text-gray-200">
|
||||
<ExclamationCircleIcon className="inline w-4 h-4 mr-1 -mt-1 " /> Missing library
|
||||
</div>
|
||||
<span className="text-xs text-gray-400 ">
|
||||
Ensure the library you have loaded still exists on disk
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{Object.entries(overviewStats).map(([key, value]) => {
|
||||
if (!displayableStatItems.includes(key)) return null;
|
||||
|
||||
|
@ -171,8 +181,9 @@ export const OverviewScreen = () => {
|
|||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="flex-grow" />
|
||||
<div className="space-x-2">
|
||||
<div className="space-x-2 ">
|
||||
<Dialog
|
||||
title="Add Device"
|
||||
description="Connect a new device to your library. Either enter another device's code or copy this one."
|
||||
|
@ -205,7 +216,7 @@ export const OverviewScreen = () => {
|
|||
</Dialog>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col pb-4 space-y-4">
|
||||
<div className="flex flex-col pb-4 mt-4 space-y-4">
|
||||
<Device
|
||||
name={`James' MacBook Pro`}
|
||||
size="1TB"
|
||||
|
|
|
@ -1,92 +0,0 @@
|
|||
import {
|
||||
CloudIcon,
|
||||
CogIcon,
|
||||
KeyIcon,
|
||||
LockClosedIcon,
|
||||
TagIcon,
|
||||
TerminalIcon,
|
||||
UsersIcon
|
||||
} from '@heroicons/react/outline';
|
||||
import clsx from 'clsx';
|
||||
import { Database, HardDrive, PaintBrush } from 'phosphor-react';
|
||||
import React from 'react';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
|
||||
import { SidebarLink } from '../components/file/Sidebar';
|
||||
|
||||
const Icon = ({ component: Icon, ...props }: any) => (
|
||||
<Icon weight="bold" {...props} className={clsx('w-4 h-4 mr-2', props.className)} />
|
||||
);
|
||||
|
||||
const Heading: React.FC<{ className?: string; children: string }> = ({ children, className }) => (
|
||||
<div className={clsx('mt-5 mb-1 ml-1 text-xs font-semibold text-gray-300', className)}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
export const SettingsScreen: React.FC<{}> = () => {
|
||||
return (
|
||||
<div className="flex flex-row w-full">
|
||||
<div className="h-full border-r max-w-[200px] flex-shrink-0 border-gray-100 w-60 dark:border-gray-550">
|
||||
<div data-tauri-drag-region className="w-full h-7" />
|
||||
<div className="p-5 pt-0">
|
||||
<Heading className="mt-0">Client</Heading>
|
||||
<SidebarLink to="/settings/general">
|
||||
<Icon component={CogIcon} />
|
||||
General
|
||||
</SidebarLink>
|
||||
<SidebarLink to="/settings/security">
|
||||
<Icon component={LockClosedIcon} />
|
||||
Security
|
||||
</SidebarLink>
|
||||
<SidebarLink to="/settings/appearance">
|
||||
<Icon component={PaintBrush} />
|
||||
Appearance
|
||||
</SidebarLink>
|
||||
<SidebarLink to="/settings/experimental">
|
||||
<Icon component={TerminalIcon} />
|
||||
Experimental
|
||||
</SidebarLink>
|
||||
|
||||
<Heading>Library</Heading>
|
||||
<SidebarLink to="/settings/library">
|
||||
<Icon component={Database} />
|
||||
Database
|
||||
</SidebarLink>
|
||||
<SidebarLink to="/settings/locations">
|
||||
<Icon component={HardDrive} />
|
||||
Locations
|
||||
</SidebarLink>
|
||||
|
||||
<SidebarLink to="/settings/keys">
|
||||
<Icon component={KeyIcon} />
|
||||
Keys
|
||||
</SidebarLink>
|
||||
<SidebarLink to="/settings/tags">
|
||||
<Icon component={TagIcon} />
|
||||
Tags
|
||||
</SidebarLink>
|
||||
|
||||
<Heading>Cloud</Heading>
|
||||
<SidebarLink to="/settings/sync">
|
||||
<Icon component={CloudIcon} />
|
||||
Sync
|
||||
</SidebarLink>
|
||||
<SidebarLink to="/settings/contacts">
|
||||
<Icon component={UsersIcon} />
|
||||
Contacts
|
||||
</SidebarLink>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<div data-tauri-drag-region className="w-full h-7" />
|
||||
<div className="flex flex-grow-0 w-full h-full max-h-screen custom-scroll page-scroll">
|
||||
<div className="flex flex-grow px-12 pb-5">
|
||||
<Outlet />
|
||||
<div className="block h-20" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,42 @@
|
|||
import { CogIcon, DatabaseIcon, KeyIcon, TagIcon } from '@heroicons/react/outline';
|
||||
import { HardDrive, ShareNetwork } from 'phosphor-react';
|
||||
import React from 'react';
|
||||
|
||||
import { SidebarLink } from '../../components/file/Sidebar';
|
||||
import {
|
||||
SettingsHeading,
|
||||
SettingsIcon,
|
||||
SettingsScreenContainer
|
||||
} from '../../components/settings/SettingsScreenContainer';
|
||||
|
||||
export const CurrentLibrarySettings: React.FC = () => {
|
||||
return (
|
||||
<SettingsScreenContainer>
|
||||
<SettingsHeading className="!mt-0">Library Settings</SettingsHeading>
|
||||
<SidebarLink to="/library-settings/general">
|
||||
<SettingsIcon component={CogIcon} />
|
||||
General
|
||||
</SidebarLink>
|
||||
<SidebarLink to="/library-settings/locations">
|
||||
<SettingsIcon component={HardDrive} />
|
||||
Locations
|
||||
</SidebarLink>
|
||||
<SidebarLink to="/library-settings/tags">
|
||||
<SettingsIcon component={TagIcon} />
|
||||
Tags
|
||||
</SidebarLink>
|
||||
<SidebarLink to="/library-settings/keys">
|
||||
<SettingsIcon component={KeyIcon} />
|
||||
Keys
|
||||
</SidebarLink>
|
||||
<SidebarLink to="/library-settings/backups">
|
||||
<SettingsIcon component={DatabaseIcon} />
|
||||
Backups
|
||||
</SidebarLink>
|
||||
<SidebarLink to="/library-settings/backups">
|
||||
<SettingsIcon component={ShareNetwork} />
|
||||
Sync
|
||||
</SidebarLink>
|
||||
</SettingsScreenContainer>
|
||||
);
|
||||
};
|
|
@ -1,40 +0,0 @@
|
|||
import { useBridgeQuery } from '@sd/client';
|
||||
import React from 'react';
|
||||
|
||||
import { InputContainer } from '../../components/primitive/InputContainer';
|
||||
import Listbox from '../../components/primitive/Listbox';
|
||||
import { SettingsContainer } from '../../components/settings/SettingsContainer';
|
||||
import { SettingsHeader } from '../../components/settings/SettingsHeader';
|
||||
|
||||
export default function GeneralSettings() {
|
||||
const { data: volumes } = useBridgeQuery('SysGetVolumes');
|
||||
|
||||
return (
|
||||
<SettingsContainer>
|
||||
<SettingsHeader
|
||||
title="General Settings"
|
||||
description="Basic settings related to this client."
|
||||
/>
|
||||
<InputContainer title="Volumes" description="A list of volumes running on this device.">
|
||||
<div className="flex flex-row space-x-2">
|
||||
<div className="flex flex-grow">
|
||||
<Listbox
|
||||
options={
|
||||
volumes?.map((volume) => {
|
||||
const name = volume.name && volume.name.length ? volume.name : volume.mount_point;
|
||||
return {
|
||||
key: name,
|
||||
option: name,
|
||||
description: volume.mount_point
|
||||
};
|
||||
}) ?? []
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</InputContainer>
|
||||
|
||||
{/* <div className="">{JSON.stringify({ config })}</div> */}
|
||||
</SettingsContainer>
|
||||
);
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
import React from 'react';
|
||||
|
||||
import { Toggle } from '../../components/primitive';
|
||||
import { InputContainer } from '../../components/primitive/InputContainer';
|
||||
import { SettingsContainer } from '../../components/settings/SettingsContainer';
|
||||
import { SettingsHeader } from '../../components/settings/SettingsHeader';
|
||||
|
||||
// type LibrarySecurity = 'public' | 'password' | 'vault';
|
||||
|
||||
export default function LibrarySettings() {
|
||||
// const locations = useBridgeQuery("SysGetLocation")
|
||||
const [encryptOnCloud, setEncryptOnCloud] = React.useState<boolean>(false);
|
||||
|
||||
return (
|
||||
<SettingsContainer>
|
||||
{/* <Button size="sm">Add Location</Button> */}
|
||||
<SettingsHeader
|
||||
title="Library database"
|
||||
description="The database contains all library data and file metadata."
|
||||
/>
|
||||
<InputContainer
|
||||
mini
|
||||
title="Encrypt on cloud"
|
||||
description="Enable if library contains sensitive data and should not be synced to the cloud without full encryption."
|
||||
>
|
||||
<div className="flex items-center h-full pl-10">
|
||||
<Toggle value={encryptOnCloud} onChange={setEncryptOnCloud} size={'sm'} />
|
||||
</div>
|
||||
</InputContainer>
|
||||
</SettingsContainer>
|
||||
);
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
import { Button } from '@sd/ui';
|
||||
import React from 'react';
|
||||
|
||||
import { InputContainer } from '../../components/primitive/InputContainer';
|
||||
import { SettingsContainer } from '../../components/settings/SettingsContainer';
|
||||
import { SettingsHeader } from '../../components/settings/SettingsHeader';
|
||||
|
||||
export default function SecuritySettings() {
|
||||
return (
|
||||
<SettingsContainer>
|
||||
<SettingsHeader title="Security" description="Keep your client safe." />
|
||||
<InputContainer
|
||||
title="Vault"
|
||||
description="You'll need to set a passphrase to enable the vault."
|
||||
>
|
||||
<div className="flex flex-row">
|
||||
<Button variant="primary">Enable Vault</Button>
|
||||
{/*<Input className="flex-grow" value="jeff" placeholder="/users/jamie/Desktop" />*/}
|
||||
</div>
|
||||
</InputContainer>
|
||||
</SettingsContainer>
|
||||
);
|
||||
}
|
83
packages/interface/src/screens/settings/Settings.tsx
Normal file
83
packages/interface/src/screens/settings/Settings.tsx
Normal file
|
@ -0,0 +1,83 @@
|
|||
import {
|
||||
CogIcon,
|
||||
CollectionIcon,
|
||||
GlobeAltIcon,
|
||||
KeyIcon,
|
||||
TerminalIcon
|
||||
} from '@heroicons/react/outline';
|
||||
import { HardDrive, PaintBrush, ShareNetwork } from 'phosphor-react';
|
||||
import React from 'react';
|
||||
|
||||
import { SidebarLink } from '../../components/file/Sidebar';
|
||||
import {
|
||||
SettingsHeading,
|
||||
SettingsIcon,
|
||||
SettingsScreenContainer
|
||||
} from '../../components/settings/SettingsScreenContainer';
|
||||
|
||||
export const SettingsScreen: React.FC = () => {
|
||||
return (
|
||||
<SettingsScreenContainer>
|
||||
<SettingsHeading className="!mt-0">Client</SettingsHeading>
|
||||
<SidebarLink to="/settings/general">
|
||||
<SettingsIcon component={CogIcon} />
|
||||
General
|
||||
</SidebarLink>
|
||||
<SidebarLink to="/settings/appearance">
|
||||
<SettingsIcon component={PaintBrush} />
|
||||
Appearance
|
||||
</SidebarLink>
|
||||
|
||||
<SettingsHeading>Node</SettingsHeading>
|
||||
<SidebarLink to="/settings/nodes">
|
||||
<SettingsIcon component={GlobeAltIcon} />
|
||||
Nodes
|
||||
</SidebarLink>
|
||||
<SidebarLink to="/settings/p2p">
|
||||
<SettingsIcon component={ShareNetwork} />
|
||||
P2P
|
||||
</SidebarLink>
|
||||
<SidebarLink to="/settings/library">
|
||||
<SettingsIcon component={CollectionIcon} />
|
||||
Libraries
|
||||
</SidebarLink>
|
||||
<SidebarLink to="/settings/security">
|
||||
<SettingsIcon component={KeyIcon} />
|
||||
Security
|
||||
</SidebarLink>
|
||||
<SettingsHeading>Developer</SettingsHeading>
|
||||
<SidebarLink to="/settings/experimental">
|
||||
<SettingsIcon component={TerminalIcon} />
|
||||
Experimental
|
||||
</SidebarLink>
|
||||
{/* <SettingsHeading>Library</SettingsHeading>
|
||||
<SidebarLink to="/settings/library">
|
||||
<SettingsIcon component={CollectionIcon} />
|
||||
My Libraries
|
||||
</SidebarLink>
|
||||
<SidebarLink to="/settings/locations">
|
||||
<SettingsIcon component={HardDrive} />
|
||||
Locations
|
||||
</SidebarLink>
|
||||
|
||||
<SidebarLink to="/settings/keys">
|
||||
<SettingsIcon component={KeyIcon} />
|
||||
Keys
|
||||
</SidebarLink>
|
||||
<SidebarLink to="/settings/tags">
|
||||
<SettingsIcon component={TagIcon} />
|
||||
Tags
|
||||
</SidebarLink> */}
|
||||
|
||||
{/* <SettingsHeading>Cloud</SettingsHeading>
|
||||
<SidebarLink to="/settings/sync">
|
||||
<SettingsIcon component={CloudIcon} />
|
||||
Sync
|
||||
</SidebarLink>
|
||||
<SidebarLink to="/settings/contacts">
|
||||
<SettingsIcon component={UsersIcon} />
|
||||
Contacts
|
||||
</SidebarLink> */}
|
||||
</SettingsScreenContainer>
|
||||
);
|
||||
};
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
|
||||
import { SettingsContainer } from '../../components/settings/SettingsContainer';
|
||||
import { SettingsHeader } from '../../components/settings/SettingsHeader';
|
||||
import { SettingsContainer } from '../../../components/settings/SettingsContainer';
|
||||
import { SettingsHeader } from '../../../components/settings/SettingsHeader';
|
||||
|
||||
export default function AppearanceSettings() {
|
||||
return (
|
|
@ -0,0 +1,35 @@
|
|||
import React from 'react';
|
||||
|
||||
import { SettingsContainer } from '../../../components/settings/SettingsContainer';
|
||||
import { SettingsHeader } from '../../../components/settings/SettingsHeader';
|
||||
|
||||
export default function GeneralSettings() {
|
||||
// const { data: volumes } = useBridgeQuery('SysGetVolumes');
|
||||
|
||||
return (
|
||||
<SettingsContainer>
|
||||
<SettingsHeader
|
||||
title="General Settings"
|
||||
description="General settings related to this client."
|
||||
/>
|
||||
{/* <InputContainer title="Volumes" description="A list of volumes running on this device.">
|
||||
<div className="flex flex-row space-x-2">
|
||||
<div className="flex flex-grow">
|
||||
<Listbox
|
||||
options={
|
||||
volumes?.map((volume) => {
|
||||
const name = volume.name && volume.name.length ? volume.name : volume.mount_point;
|
||||
return {
|
||||
key: name,
|
||||
option: name,
|
||||
description: volume.mount_point
|
||||
};
|
||||
}) ?? []
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</InputContainer> */}
|
||||
</SettingsContainer>
|
||||
);
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
|
||||
import { SettingsContainer } from '../../components/settings/SettingsContainer';
|
||||
import { SettingsHeader } from '../../components/settings/SettingsHeader';
|
||||
import { SettingsContainer } from '../../../components/settings/SettingsContainer';
|
||||
import { SettingsHeader } from '../../../components/settings/SettingsHeader';
|
||||
|
||||
export default function ContactsSettings() {
|
||||
return (
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
|
||||
import { SettingsContainer } from '../../components/settings/SettingsContainer';
|
||||
import { SettingsHeader } from '../../components/settings/SettingsHeader';
|
||||
import { SettingsContainer } from '../../../components/settings/SettingsContainer';
|
||||
import { SettingsHeader } from '../../../components/settings/SettingsHeader';
|
||||
|
||||
export default function KeysSettings() {
|
||||
return (
|
|
@ -0,0 +1,91 @@
|
|||
import { useBridgeCommand, useBridgeQuery } from '@sd/client';
|
||||
import { useCurrentLibrary } from '@sd/client';
|
||||
import { Button, Input } from '@sd/ui';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useDebounce } from 'use-debounce';
|
||||
|
||||
import { Toggle } from '../../../components/primitive';
|
||||
import { InputContainer } from '../../../components/primitive/InputContainer';
|
||||
import { SettingsContainer } from '../../../components/settings/SettingsContainer';
|
||||
import { SettingsHeader } from '../../../components/settings/SettingsHeader';
|
||||
|
||||
export default function LibraryGeneralSettings() {
|
||||
const { currentLibrary, libraries, currentLibraryUuid } = useCurrentLibrary();
|
||||
|
||||
const { mutate: editLibrary } = useBridgeCommand('EditLibrary');
|
||||
|
||||
const [name, setName] = useState('');
|
||||
const [description, setDescription] = useState('');
|
||||
const [encryptLibrary, setEncryptLibrary] = useState(false);
|
||||
|
||||
const [nameDebounced] = useDebounce(name, 500);
|
||||
const [descriptionDebounced] = useDebounce(description, 500);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentLibrary) {
|
||||
const { name, description } = currentLibrary.config;
|
||||
// currentLibrary must be loaded, name must not be empty, and must be different from the current
|
||||
if (nameDebounced && (nameDebounced !== name || descriptionDebounced !== description)) {
|
||||
editLibrary({
|
||||
id: currentLibraryUuid!,
|
||||
name: nameDebounced,
|
||||
description: descriptionDebounced
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [nameDebounced, descriptionDebounced]);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentLibrary) {
|
||||
setName(currentLibrary.config.name);
|
||||
setDescription(currentLibrary.config.description);
|
||||
}
|
||||
}, [libraries]);
|
||||
|
||||
return (
|
||||
<SettingsContainer>
|
||||
<SettingsHeader
|
||||
title="Library Settings"
|
||||
description="General settings related to the currently active library."
|
||||
/>
|
||||
<div className="flex flex-row pb-3 space-x-5">
|
||||
<div className="flex flex-col flex-grow ">
|
||||
<span className="mt-2 mb-1 text-xs font-semibold text-gray-300">Name</span>
|
||||
<Input
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
defaultValue="My Default Library"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col flex-grow">
|
||||
<span className="mt-2 mb-1 text-xs font-semibold text-gray-300">Description</span>
|
||||
<Input
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
placeholder=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<InputContainer
|
||||
mini
|
||||
title="Encrypt Library"
|
||||
description="Enable encryption for this library, this will only encrypt the Spacedrive database, not the files themselves."
|
||||
>
|
||||
<div className="flex items-center ml-3">
|
||||
<Toggle value={encryptLibrary} onChange={setEncryptLibrary} />
|
||||
</div>
|
||||
</InputContainer>
|
||||
<InputContainer
|
||||
title="Delete Library"
|
||||
description="This is permanent, your files will not be deleted, only the Spacedrive library."
|
||||
>
|
||||
<div className="mt-2">
|
||||
<Button size="sm" variant="colored" className="bg-red-500 border-red-500">
|
||||
Delete Library
|
||||
</Button>
|
||||
</div>
|
||||
</InputContainer>
|
||||
</SettingsContainer>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
import { PlusIcon } from '@heroicons/react/solid';
|
||||
import { useBridgeQuery, useLibraryCommand, useLibraryQuery } from '@sd/client';
|
||||
import { AppPropsContext } from '@sd/client';
|
||||
import { Button } from '@sd/ui';
|
||||
import React, { useContext } from 'react';
|
||||
|
||||
import LocationListItem from '../../../components/location/LocationListItem';
|
||||
import { InputContainer } from '../../../components/primitive/InputContainer';
|
||||
import { SettingsContainer } from '../../../components/settings/SettingsContainer';
|
||||
import { SettingsHeader } from '../../../components/settings/SettingsHeader';
|
||||
|
||||
// const exampleLocations = [
|
||||
// { option: 'Macintosh HD', key: 'macintosh_hd' },
|
||||
// { option: 'LaCie External', key: 'lacie_external' },
|
||||
// { option: 'Seagate 8TB', key: 'seagate_8tb' }
|
||||
// ];
|
||||
|
||||
export default function LocationSettings() {
|
||||
const { data: locations } = useLibraryQuery('SysGetLocations');
|
||||
|
||||
const appProps = useContext(AppPropsContext);
|
||||
|
||||
const { mutate: createLocation } = useLibraryCommand('LocCreate');
|
||||
|
||||
return (
|
||||
<SettingsContainer>
|
||||
{/*<Button size="sm">Add Location</Button>*/}
|
||||
<SettingsHeader
|
||||
title="Locations"
|
||||
description="Manage your storage locations."
|
||||
rightArea={
|
||||
<div className="flex-row space-x-2">
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
appProps?.openDialog({ directory: true }).then((result) => {
|
||||
if (result) createLocation({ path: result as string });
|
||||
});
|
||||
}}
|
||||
>
|
||||
Add Location
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="grid space-y-2">
|
||||
{locations?.map((location) => (
|
||||
<LocationListItem key={location.id} location={location} />
|
||||
))}
|
||||
</div>
|
||||
</SettingsContainer>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
import { Button } from '@sd/ui';
|
||||
import React from 'react';
|
||||
|
||||
import { InputContainer } from '../../../components/primitive/InputContainer';
|
||||
import { SettingsContainer } from '../../../components/settings/SettingsContainer';
|
||||
import { SettingsHeader } from '../../../components/settings/SettingsHeader';
|
||||
|
||||
export default function SecuritySettings() {
|
||||
return (
|
||||
<SettingsContainer>
|
||||
<SettingsHeader title="Security" description="Keep your client safe." />
|
||||
</SettingsContainer>
|
||||
);
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
|
||||
import { SettingsContainer } from '../../components/settings/SettingsContainer';
|
||||
import { SettingsHeader } from '../../components/settings/SettingsHeader';
|
||||
import { SettingsContainer } from '../../../components/settings/SettingsContainer';
|
||||
import { SettingsHeader } from '../../../components/settings/SettingsHeader';
|
||||
|
||||
export default function SharingSettings() {
|
||||
return (
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
|
||||
import { SettingsContainer } from '../../components/settings/SettingsContainer';
|
||||
import { SettingsHeader } from '../../components/settings/SettingsHeader';
|
||||
import { SettingsContainer } from '../../../components/settings/SettingsContainer';
|
||||
import { SettingsHeader } from '../../../components/settings/SettingsHeader';
|
||||
|
||||
export default function SyncSettings() {
|
||||
return (
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
|
||||
import { SettingsContainer } from '../../components/settings/SettingsContainer';
|
||||
import { SettingsHeader } from '../../components/settings/SettingsHeader';
|
||||
import { SettingsContainer } from '../../../components/settings/SettingsContainer';
|
||||
import { SettingsHeader } from '../../../components/settings/SettingsHeader';
|
||||
|
||||
export default function TagsSettings() {
|
||||
return (
|
|
@ -1,14 +1,12 @@
|
|||
import React from 'react';
|
||||
|
||||
import { useNodeStore } from '../../components/device/Stores';
|
||||
import { Toggle } from '../../components/primitive';
|
||||
import { InputContainer } from '../../components/primitive/InputContainer';
|
||||
import { SettingsContainer } from '../../components/settings/SettingsContainer';
|
||||
import { SettingsHeader } from '../../components/settings/SettingsHeader';
|
||||
import { useNodeStore } from '../../../components/device/Stores';
|
||||
import { Toggle } from '../../../components/primitive';
|
||||
import { InputContainer } from '../../../components/primitive/InputContainer';
|
||||
import { SettingsContainer } from '../../../components/settings/SettingsContainer';
|
||||
import { SettingsHeader } from '../../../components/settings/SettingsHeader';
|
||||
|
||||
export default function ExperimentalSettings() {
|
||||
// const locations = useBridgeQuery("SysGetLocation")
|
||||
|
||||
const { isExperimental, setIsExperimental } = useNodeStore();
|
||||
|
||||
return (
|
|
@ -0,0 +1,113 @@
|
|||
import { CollectionIcon, TrashIcon } from '@heroicons/react/outline';
|
||||
import { PlusIcon } from '@heroicons/react/solid';
|
||||
import { useBridgeCommand, useBridgeQuery } from '@sd/client';
|
||||
import { AppPropsContext } from '@sd/client';
|
||||
import { LibraryConfig, LibraryConfigWrapped } from '@sd/core';
|
||||
import { Button, Input } from '@sd/ui';
|
||||
import React, { useContext, useState } from 'react';
|
||||
|
||||
import Card from '../../../components/layout/Card';
|
||||
import Dialog from '../../../components/layout/Dialog';
|
||||
import { Toggle } from '../../../components/primitive';
|
||||
import { InputContainer } from '../../../components/primitive/InputContainer';
|
||||
import { SettingsContainer } from '../../../components/settings/SettingsContainer';
|
||||
import { SettingsHeader } from '../../../components/settings/SettingsHeader';
|
||||
|
||||
// type LibrarySecurity = 'public' | 'password' | 'vault';
|
||||
|
||||
function LibraryListItem(props: { library: LibraryConfigWrapped }) {
|
||||
const [openDeleteModal, setOpenDeleteModal] = useState(false);
|
||||
|
||||
const { mutate: deleteLib, isLoading: libDeletePending } = useBridgeCommand('DeleteLibrary', {
|
||||
onSuccess: () => {
|
||||
setOpenDeleteModal(false);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<div className="flex-grow my-0.5">
|
||||
<h3 className="font-semibold">{props.library.config.name}</h3>
|
||||
<p className="mt-0.5 text-xs text-gray-200">{props.library.uuid}</p>
|
||||
</div>
|
||||
<div>
|
||||
<Dialog
|
||||
open={openDeleteModal}
|
||||
onOpenChange={setOpenDeleteModal}
|
||||
title="Delete Library"
|
||||
description="Deleting a library will permanently the database, the files themselves will not be deleted."
|
||||
ctaAction={() => {
|
||||
deleteLib({ id: props.library.uuid });
|
||||
}}
|
||||
loading={libDeletePending}
|
||||
ctaDanger
|
||||
ctaLabel="Delete"
|
||||
trigger={
|
||||
<Button variant="gray" className="!p-1.5" onClick={() => {}}>
|
||||
<TrashIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default function LibrarySettings() {
|
||||
const [openCreateModal, setOpenCreateModal] = useState(false);
|
||||
const [newLibName, setNewLibName] = useState('');
|
||||
|
||||
const { mutate: createLibrary, isLoading: createLibLoading } = useBridgeCommand('CreateLibrary', {
|
||||
onSuccess: () => {
|
||||
setOpenCreateModal(false);
|
||||
}
|
||||
});
|
||||
|
||||
const { data: libraries } = useBridgeQuery('NodeGetLibraries');
|
||||
|
||||
function createNewLib() {
|
||||
if (newLibName) {
|
||||
createLibrary({ name: newLibName });
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingsContainer>
|
||||
<SettingsHeader
|
||||
title="Libraries"
|
||||
description="The database contains all library data and file metadata."
|
||||
rightArea={
|
||||
<div className="flex-row space-x-2">
|
||||
<Dialog
|
||||
open={openCreateModal}
|
||||
onOpenChange={setOpenCreateModal}
|
||||
title="Create New Library"
|
||||
description="Choose a name for your new library, you can configure this and more settings from the library settings later on."
|
||||
ctaAction={createNewLib}
|
||||
loading={createLibLoading}
|
||||
ctaLabel="Create"
|
||||
trigger={
|
||||
<Button variant="primary" size="sm">
|
||||
Add Library
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Input
|
||||
className="flex-grow w-full mt-3"
|
||||
value={newLibName}
|
||||
placeholder="My Cool Library"
|
||||
onChange={(e) => setNewLibName(e.target.value)}
|
||||
/>
|
||||
</Dialog>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="space-y-2">
|
||||
{libraries?.map((library) => (
|
||||
<LibraryListItem key={library.uuid} library={library} />
|
||||
))}
|
||||
</div>
|
||||
</SettingsContainer>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
import React from 'react';
|
||||
|
||||
import { SettingsContainer } from '../../../components/settings/SettingsContainer';
|
||||
import { SettingsHeader } from '../../../components/settings/SettingsHeader';
|
||||
|
||||
export default function NodesSettings() {
|
||||
return (
|
||||
<SettingsContainer>
|
||||
<SettingsHeader title="Nodes" description="Manage the nodes in your Spacedrive network." />
|
||||
</SettingsContainer>
|
||||
);
|
||||
}
|
40
packages/interface/src/screens/settings/node/P2PSettings.tsx
Normal file
40
packages/interface/src/screens/settings/node/P2PSettings.tsx
Normal file
|
@ -0,0 +1,40 @@
|
|||
import { useBridgeQuery } from '@sd/client';
|
||||
import { Button, Input } from '@sd/ui';
|
||||
import React from 'react';
|
||||
|
||||
import { Toggle } from '../../../components/primitive';
|
||||
import { InputContainer } from '../../../components/primitive/InputContainer';
|
||||
import Listbox from '../../../components/primitive/Listbox';
|
||||
import { SettingsContainer } from '../../../components/settings/SettingsContainer';
|
||||
import { SettingsHeader } from '../../../components/settings/SettingsHeader';
|
||||
|
||||
export default function P2PSettings() {
|
||||
return (
|
||||
<SettingsContainer>
|
||||
<SettingsHeader
|
||||
title="P2P Settings"
|
||||
description="Manage how this node communicates with other nodes."
|
||||
/>
|
||||
|
||||
<InputContainer
|
||||
mini
|
||||
title="Enable Node Discovery"
|
||||
description="Allow or block this node from calling an external server to assist in forming a peer-to-peer connection. "
|
||||
>
|
||||
<Toggle value />
|
||||
</InputContainer>
|
||||
|
||||
<InputContainer
|
||||
title="Discovery Server"
|
||||
description="Configuration server to aid with establishing peer-to-peer to connections between nodes over the internet. Disabling will result in nodes only being accessible over LAN and direct IP connections."
|
||||
>
|
||||
<div className="flex flex-col mt-1">
|
||||
<Input className="flex-grow" disabled defaultValue="https://p2p.spacedrive.com" />
|
||||
<div className="flex justify-end mt-1">
|
||||
<a className="p-1 text-sm font-bold text-primary-500 hover:text-primary-400">Change</a>
|
||||
</div>
|
||||
</div>
|
||||
</InputContainer>
|
||||
</SettingsContainer>
|
||||
);
|
||||
}
|
|
@ -17,7 +17,7 @@
|
|||
"storybook:build": "build-storybook"
|
||||
},
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^1.6.4",
|
||||
"@headlessui/react": "^1.6.6",
|
||||
"@heroicons/react": "^1.0.6",
|
||||
"@radix-ui/react-context-menu": "^0.1.6",
|
||||
"clsx": "^1.1.1",
|
||||
|
|
|
@ -39,7 +39,7 @@ export const Input = React.forwardRef<HTMLInputElement, InputProps>(({ ...props
|
|||
ref={ref}
|
||||
{...props}
|
||||
className={clsx(
|
||||
`px-3 py-1 rounded-md border leading-7 outline-none shadow-xs focus:ring-2 transition-all`,
|
||||
`px-3 py-1 text-sm rounded-md border leading-7 outline-none shadow-xs focus:ring-2 transition-all`,
|
||||
variants[props.variant || 'default'],
|
||||
props.className
|
||||
)}
|
||||
|
|
194
pnpm-lock.yaml
194
pnpm-lock.yaml
|
@ -50,7 +50,7 @@ importers:
|
|||
react-dom: 18.1.0_react@18.1.0
|
||||
devDependencies:
|
||||
'@tauri-apps/cli': 1.0.0
|
||||
'@tauri-apps/tauricon': github.com/tauri-apps/tauricon/f104e2af7a19e1cdf9ee8212d2d3f6456d3aa00f
|
||||
'@tauri-apps/tauricon': github.com/tauri-apps/tauricon/5eea916a4a8e13aa41a943beaa7b4b71977e190d
|
||||
'@types/babel-core': 6.25.7
|
||||
'@types/byte-size': 8.1.0
|
||||
'@types/react': 18.0.9
|
||||
|
@ -229,10 +229,13 @@ importers:
|
|||
specifiers:
|
||||
'@sd/config': workspace:*
|
||||
'@sd/core': workspace:*
|
||||
'@sd/interface': workspace:*
|
||||
'@types/lodash': ^4.14.182
|
||||
'@types/react': ^18.0.9
|
||||
eventemitter3: ^4.0.7
|
||||
immer: ^9.0.14
|
||||
react-query: ^3.39.1
|
||||
lodash: ^4.17.21
|
||||
react-query: ^3.34.19
|
||||
scripts: '*'
|
||||
tsconfig: '*'
|
||||
typescript: ^4.7.2
|
||||
|
@ -240,11 +243,14 @@ importers:
|
|||
dependencies:
|
||||
'@sd/config': link:../config
|
||||
'@sd/core': link:../../core
|
||||
'@sd/interface': link:../interface
|
||||
eventemitter3: 4.0.7
|
||||
immer: 9.0.15
|
||||
lodash: 4.17.21
|
||||
react-query: 3.39.1
|
||||
zustand: 4.0.0-rc.1_immer@9.0.15
|
||||
devDependencies:
|
||||
'@types/lodash': 4.14.182
|
||||
'@types/react': 18.0.9
|
||||
scripts: 0.1.0
|
||||
tsconfig: 7.0.0
|
||||
|
@ -300,7 +306,7 @@ importers:
|
|||
react-loading-icons: ^1.1.0
|
||||
react-loading-skeleton: ^3.1.0
|
||||
react-portal: ^4.2.2
|
||||
react-query: ^3.39.1
|
||||
react-query: ^3.34.19
|
||||
react-router: 6.3.0
|
||||
react-router-dom: 6.3.0
|
||||
react-scrollbars-custom: ^4.0.27
|
||||
|
@ -310,6 +316,7 @@ importers:
|
|||
rooks: ^5.11.2
|
||||
tailwindcss: ^3.0.24
|
||||
typescript: ^4.7.2
|
||||
use-debounce: ^8.0.1
|
||||
vite: ^2.9.9
|
||||
vite-plugin-svgr: ^2.1.0
|
||||
zustand: 4.0.0-rc.1
|
||||
|
@ -355,6 +362,7 @@ importers:
|
|||
react-virtuoso: 2.13.2_ef5jwxihqo6n7gxfmzogljlgcm
|
||||
rooks: 5.11.2_ef5jwxihqo6n7gxfmzogljlgcm
|
||||
tailwindcss: 3.1.3
|
||||
use-debounce: 8.0.1_react@18.1.0
|
||||
zustand: 4.0.0-rc.1_immer@9.0.15+react@18.1.0
|
||||
devDependencies:
|
||||
'@types/babel-core': 6.25.7
|
||||
|
@ -377,7 +385,7 @@ importers:
|
|||
packages/ui:
|
||||
specifiers:
|
||||
'@babel/core': ^7.18.2
|
||||
'@headlessui/react': ^1.6.4
|
||||
'@headlessui/react': ^1.6.6
|
||||
'@heroicons/react': ^1.0.6
|
||||
'@radix-ui/react-context-menu': ^0.1.6
|
||||
'@sd/config': workspace:*
|
||||
|
@ -408,7 +416,7 @@ importers:
|
|||
tailwindcss: ^3.0.24
|
||||
typescript: ^4.7.2
|
||||
dependencies:
|
||||
'@headlessui/react': 1.6.4_ef5jwxihqo6n7gxfmzogljlgcm
|
||||
'@headlessui/react': 1.6.6_ef5jwxihqo6n7gxfmzogljlgcm
|
||||
'@heroicons/react': 1.0.6_react@18.1.0
|
||||
'@radix-ui/react-context-menu': 0.1.6_ohobp6rpsmerwlq5ipwfh5yigy
|
||||
clsx: 1.1.1
|
||||
|
@ -2212,6 +2220,17 @@ packages:
|
|||
react-dom: 18.1.0_react@18.1.0
|
||||
dev: false
|
||||
|
||||
/@headlessui/react/1.6.6_ef5jwxihqo6n7gxfmzogljlgcm:
|
||||
resolution: {integrity: sha512-MFJtmj9Xh/hhBMhLccGbBoSk+sk61BlP6sJe4uQcVMtXZhCgGqd2GyIQzzmsdPdTEWGSF434CBi8mnhR6um46Q==}
|
||||
engines: {node: '>=10'}
|
||||
peerDependencies:
|
||||
react: ^16 || ^17 || ^18
|
||||
react-dom: ^16 || ^17 || ^18
|
||||
dependencies:
|
||||
react: 18.1.0
|
||||
react-dom: 18.1.0_react@18.1.0
|
||||
dev: false
|
||||
|
||||
/@heroicons/react/1.0.6_react@18.1.0:
|
||||
resolution: {integrity: sha512-JJCXydOFWMDpCP4q13iEplA503MQO3xLoZiKum+955ZCtHINWnx26CUxVxxFQu/uLb4LW3ge15ZpzIkXKkJ8oQ==}
|
||||
peerDependencies:
|
||||
|
@ -3784,6 +3803,7 @@ packages:
|
|||
webpack-hot-middleware: 2.25.1
|
||||
webpack-virtual-modules: 0.2.2
|
||||
transitivePeerDependencies:
|
||||
- bluebird
|
||||
- eslint
|
||||
- supports-color
|
||||
- vue-template-compiler
|
||||
|
@ -4159,6 +4179,7 @@ packages:
|
|||
x-default-browser: 0.4.0
|
||||
transitivePeerDependencies:
|
||||
- '@storybook/mdx2-csf'
|
||||
- bluebird
|
||||
- bufferutil
|
||||
- encoding
|
||||
- eslint
|
||||
|
@ -4196,6 +4217,7 @@ packages:
|
|||
webpack: 5.73.0
|
||||
transitivePeerDependencies:
|
||||
- '@storybook/mdx2-csf'
|
||||
- bluebird
|
||||
- bufferutil
|
||||
- encoding
|
||||
- eslint
|
||||
|
@ -4316,6 +4338,7 @@ packages:
|
|||
webpack-dev-middleware: 3.7.3_webpack@4.46.0
|
||||
webpack-virtual-modules: 0.2.2
|
||||
transitivePeerDependencies:
|
||||
- bluebird
|
||||
- encoding
|
||||
- eslint
|
||||
- supports-color
|
||||
|
@ -4546,6 +4569,7 @@ packages:
|
|||
- '@storybook/mdx2-csf'
|
||||
- '@swc/core'
|
||||
- '@types/webpack'
|
||||
- bluebird
|
||||
- bufferutil
|
||||
- encoding
|
||||
- esbuild
|
||||
|
@ -5970,6 +5994,8 @@ packages:
|
|||
dependencies:
|
||||
micromatch: 3.1.10
|
||||
normalize-path: 2.1.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/anymatch/3.1.2:
|
||||
|
@ -6603,6 +6629,8 @@ packages:
|
|||
qs: 6.5.2
|
||||
raw-body: 2.3.3
|
||||
type-is: 1.6.18
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/body-parser/1.20.0:
|
||||
|
@ -6621,6 +6649,8 @@ packages:
|
|||
raw-body: 2.5.1
|
||||
type-is: 1.6.18
|
||||
unpipe: 1.0.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
/boolbase/1.0.0:
|
||||
resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
|
||||
|
@ -6677,6 +6707,8 @@ packages:
|
|||
snapdragon-node: 2.1.1
|
||||
split-string: 3.1.0
|
||||
to-regex: 3.0.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/braces/3.0.2:
|
||||
|
@ -6798,7 +6830,7 @@ packages:
|
|||
dev: true
|
||||
|
||||
/buffer-equal/0.0.1:
|
||||
resolution: {integrity: sha512-RgSV6InVQ9ODPdLWJ5UAqBqJBOg370Nz6ZQtRzpt6nUjc8v0St97uJ4PYC6NztqIScrAXafKM3mZPMygSe1ggA==}
|
||||
resolution: {integrity: sha1-kbx0sR6kBbyRa8aqkI+q+ltKrEs=}
|
||||
engines: {node: '>=0.4.0'}
|
||||
dev: true
|
||||
|
||||
|
@ -6887,7 +6919,7 @@ packages:
|
|||
mississippi: 3.0.0
|
||||
mkdirp: 0.5.6
|
||||
move-concurrently: 1.0.1
|
||||
promise-inflight: 1.0.1
|
||||
promise-inflight: 1.0.1_bluebird@3.7.2
|
||||
rimraf: 2.7.1
|
||||
ssri: 6.0.2
|
||||
unique-filename: 1.1.1
|
||||
|
@ -6916,6 +6948,8 @@ packages:
|
|||
ssri: 8.0.1
|
||||
tar: 6.1.11
|
||||
unique-filename: 1.1.1
|
||||
transitivePeerDependencies:
|
||||
- bluebird
|
||||
dev: true
|
||||
|
||||
/cache-base/1.0.1:
|
||||
|
@ -7137,6 +7171,8 @@ packages:
|
|||
upath: 1.2.0
|
||||
optionalDependencies:
|
||||
fsevents: 1.2.13
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
|
@ -7417,6 +7453,8 @@ packages:
|
|||
on-headers: 1.0.2
|
||||
safe-buffer: 5.1.2
|
||||
vary: 1.1.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/compression/1.7.4:
|
||||
|
@ -7430,6 +7468,8 @@ packages:
|
|||
on-headers: 1.0.2
|
||||
safe-buffer: 5.1.2
|
||||
vary: 1.1.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
/concat-map/0.0.1:
|
||||
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
|
||||
|
@ -7627,6 +7667,8 @@ packages:
|
|||
p-all: 2.1.0
|
||||
p-filter: 2.1.0
|
||||
p-map: 3.0.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/create-ecdh/4.0.4:
|
||||
|
@ -7862,15 +7904,37 @@ packages:
|
|||
|
||||
/debug/2.6.9:
|
||||
resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}
|
||||
peerDependencies:
|
||||
supports-color: '*'
|
||||
peerDependenciesMeta:
|
||||
supports-color:
|
||||
optional: true
|
||||
dependencies:
|
||||
ms: 2.0.0
|
||||
|
||||
/debug/3.2.7:
|
||||
resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==}
|
||||
peerDependencies:
|
||||
supports-color: '*'
|
||||
peerDependenciesMeta:
|
||||
supports-color:
|
||||
optional: true
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
dev: true
|
||||
|
||||
/debug/3.2.7_supports-color@5.5.0:
|
||||
resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==}
|
||||
peerDependencies:
|
||||
supports-color: '*'
|
||||
peerDependenciesMeta:
|
||||
supports-color:
|
||||
optional: true
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
supports-color: 5.5.0
|
||||
dev: true
|
||||
|
||||
/debug/4.3.4:
|
||||
resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==}
|
||||
engines: {node: '>=6.0'}
|
||||
|
@ -8107,6 +8171,8 @@ packages:
|
|||
dependencies:
|
||||
address: 1.2.0
|
||||
debug: 2.6.9
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/detective/5.2.1:
|
||||
|
@ -8874,6 +8940,8 @@ packages:
|
|||
regex-not: 1.0.2
|
||||
snapdragon: 0.8.2
|
||||
to-regex: 3.0.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/expand-template/2.0.3:
|
||||
|
@ -8915,6 +8983,8 @@ packages:
|
|||
type-is: 1.6.18
|
||||
utils-merge: 1.0.1
|
||||
vary: 1.1.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/express/4.18.1:
|
||||
|
@ -8952,6 +9022,8 @@ packages:
|
|||
type-is: 1.6.18
|
||||
utils-merge: 1.0.1
|
||||
vary: 1.1.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
/ext-list/2.2.2:
|
||||
resolution: {integrity: sha512-u+SQgsubraE6zItfVA0tBuCBhfU9ogSRnsvygI7wht9TS510oLkBRXBsqopeUG/GBOIQyKZO9wjTqIu/sf5zFA==}
|
||||
|
@ -9008,6 +9080,8 @@ packages:
|
|||
regex-not: 1.0.2
|
||||
snapdragon: 0.8.2
|
||||
to-regex: 3.0.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/fast-deep-equal/2.0.1:
|
||||
|
@ -9027,6 +9101,8 @@ packages:
|
|||
is-glob: 4.0.3
|
||||
merge2: 1.4.1
|
||||
micromatch: 3.1.10
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/fast-glob/3.2.11:
|
||||
|
@ -9233,6 +9309,8 @@ packages:
|
|||
parseurl: 1.3.3
|
||||
statuses: 1.4.0
|
||||
unpipe: 1.0.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/finalhandler/1.2.0:
|
||||
|
@ -9246,6 +9324,8 @@ packages:
|
|||
parseurl: 1.3.3
|
||||
statuses: 2.0.1
|
||||
unpipe: 1.0.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
/find-cache-dir/2.1.0:
|
||||
resolution: {integrity: sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==}
|
||||
|
@ -9419,6 +9499,8 @@ packages:
|
|||
typescript: 4.7.2
|
||||
webpack: 4.46.0
|
||||
worker-rpc: 0.1.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/fork-ts-checker-webpack-plugin/6.5.2_2uut6pkjgoy643sdkylfmypqbm:
|
||||
|
@ -9737,7 +9819,7 @@ packages:
|
|||
dev: true
|
||||
|
||||
/github-from-package/0.0.0:
|
||||
resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==}
|
||||
resolution: {integrity: sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4=}
|
||||
dev: true
|
||||
|
||||
/github-slugger/1.4.0:
|
||||
|
@ -9876,10 +9958,12 @@ packages:
|
|||
ignore: 4.0.6
|
||||
pify: 4.0.1
|
||||
slash: 2.0.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/got/12.0.4:
|
||||
resolution: {integrity: sha512-2Eyz4iU/ktq7wtMFXxzK7g5p35uNYLLdiZarZ5/Yn3IJlNEpBd5+dCgcAyxN8/8guZLszffwe3wVyw+DEVrpBg==}
|
||||
/got/12.1.0:
|
||||
resolution: {integrity: sha512-hBv2ty9QN2RdbJJMK3hesmSkFTjVIHyIDDbssCKnSmq62edGgImJWD10Eb1k77TiV1bxloxqcFAVK8+9pkhOig==}
|
||||
engines: {node: '>=14.16'}
|
||||
dependencies:
|
||||
'@sindresorhus/is': 4.6.0
|
||||
|
@ -11099,6 +11183,8 @@ packages:
|
|||
walker: 1.0.8
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/jest-mock/27.5.1:
|
||||
|
@ -11917,6 +12003,8 @@ packages:
|
|||
regex-not: 1.0.2
|
||||
snapdragon: 0.8.2
|
||||
to-regex: 3.0.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/micromatch/4.0.5:
|
||||
|
@ -12112,6 +12200,8 @@ packages:
|
|||
depd: 1.1.2
|
||||
on-finished: 2.3.0
|
||||
on-headers: 1.0.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/move-concurrently/1.0.1:
|
||||
|
@ -12173,6 +12263,8 @@ packages:
|
|||
regex-not: 1.0.2
|
||||
snapdragon: 0.8.2
|
||||
to-regex: 3.0.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/napi-build-utils/1.0.2:
|
||||
|
@ -12186,6 +12278,9 @@ packages:
|
|||
rimraf: 2.7.1
|
||||
tracer: 0.8.15
|
||||
ws: 2.3.1
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- utf-8-validate
|
||||
dev: true
|
||||
|
||||
/negotiator/0.6.3:
|
||||
|
@ -12297,7 +12392,7 @@ packages:
|
|||
requiresBuild: true
|
||||
dependencies:
|
||||
chokidar: 3.5.3
|
||||
debug: 3.2.7
|
||||
debug: 3.2.7_supports-color@5.5.0
|
||||
ignore-by-default: 1.0.1
|
||||
minimatch: 3.1.2
|
||||
pstree.remy: 1.1.8
|
||||
|
@ -13437,6 +13532,22 @@ packages:
|
|||
|
||||
/promise-inflight/1.0.1:
|
||||
resolution: {integrity: sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==}
|
||||
peerDependencies:
|
||||
bluebird: '*'
|
||||
peerDependenciesMeta:
|
||||
bluebird:
|
||||
optional: true
|
||||
dev: true
|
||||
|
||||
/promise-inflight/1.0.1_bluebird@3.7.2:
|
||||
resolution: {integrity: sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==}
|
||||
peerDependencies:
|
||||
bluebird: '*'
|
||||
peerDependenciesMeta:
|
||||
bluebird:
|
||||
optional: true
|
||||
dependencies:
|
||||
bluebird: 3.7.2
|
||||
dev: true
|
||||
|
||||
/promise.allsettled/1.0.5:
|
||||
|
@ -14216,6 +14327,8 @@ packages:
|
|||
graceful-fs: 4.2.10
|
||||
micromatch: 3.1.10
|
||||
readable-stream: 2.3.7
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
|
@ -14626,6 +14739,8 @@ packages:
|
|||
micromatch: 3.1.10
|
||||
minimist: 1.2.6
|
||||
walker: 1.0.8
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/sass-loader/13.0.0_sass@1.52.1:
|
||||
|
@ -14789,6 +14904,8 @@ packages:
|
|||
on-finished: 2.3.0
|
||||
range-parser: 1.2.1
|
||||
statuses: 1.4.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/send/0.18.0:
|
||||
|
@ -14808,6 +14925,8 @@ packages:
|
|||
on-finished: 2.4.1
|
||||
range-parser: 1.2.1
|
||||
statuses: 2.0.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
/serialize-error/7.0.1:
|
||||
resolution: {integrity: sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==}
|
||||
|
@ -14853,6 +14972,8 @@ packages:
|
|||
escape-html: 1.0.3
|
||||
parseurl: 1.3.3
|
||||
send: 0.16.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/serve-static/1.15.0:
|
||||
|
@ -14863,6 +14984,8 @@ packages:
|
|||
escape-html: 1.0.3
|
||||
parseurl: 1.3.3
|
||||
send: 0.18.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
/set-blocking/2.0.0:
|
||||
resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
|
||||
|
@ -15029,6 +15152,8 @@ packages:
|
|||
source-map: 0.5.7
|
||||
source-map-resolve: 0.5.3
|
||||
use: 3.1.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/sort-keys-length/1.0.1:
|
||||
|
@ -15562,6 +15687,10 @@ packages:
|
|||
timer2: 1.0.0
|
||||
uuidv4: 3.0.1
|
||||
ws: 6.2.0
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- supports-color
|
||||
- utf-8-validate
|
||||
dev: true
|
||||
|
||||
/tailwindcss/3.1.3:
|
||||
|
@ -15710,6 +15839,8 @@ packages:
|
|||
terser: 5.14.1
|
||||
webpack: 4.46.0
|
||||
webpack-sources: 1.4.3
|
||||
transitivePeerDependencies:
|
||||
- bluebird
|
||||
dev: true
|
||||
|
||||
/terser-webpack-plugin/5.3.3_webpack@5.73.0:
|
||||
|
@ -16796,6 +16927,15 @@ packages:
|
|||
react: 18.1.0
|
||||
dev: false
|
||||
|
||||
/use-debounce/8.0.1_react@18.1.0:
|
||||
resolution: {integrity: sha512-6tGAFJKJ0qCalecaV7/gm/M6n238nmitNppvR89ff1yfwSFjwFKR7IQZzIZf1KZRQhqNireBzytzU6jgb29oVg==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
peerDependencies:
|
||||
react: '>=16.8.0'
|
||||
dependencies:
|
||||
react: 18.1.0
|
||||
dev: false
|
||||
|
||||
/use-isomorphic-layout-effect/1.1.2_7cpxmzzodpxnolj5zcc5cr63ji:
|
||||
resolution: {integrity: sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==}
|
||||
peerDependencies:
|
||||
|
@ -17102,6 +17242,8 @@ packages:
|
|||
requiresBuild: true
|
||||
dependencies:
|
||||
chokidar: 2.1.8
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
|
@ -17113,6 +17255,8 @@ packages:
|
|||
optionalDependencies:
|
||||
chokidar: 3.5.3
|
||||
watchpack-chokidar2: 2.0.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/watchpack/2.4.0:
|
||||
|
@ -17207,6 +17351,8 @@ packages:
|
|||
resolution: {integrity: sha512-kDUmfm3BZrei0y+1NTHJInejzxfhtU8eDj2M7OKb2IWrPFAeO1SOH2KuQ68MSZu9IGEHcxbkKKR1v18FrUSOmA==}
|
||||
dependencies:
|
||||
debug: 3.2.7
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/webpack-virtual-modules/0.4.3:
|
||||
|
@ -17249,6 +17395,8 @@ packages:
|
|||
terser-webpack-plugin: 1.4.5_webpack@4.46.0
|
||||
watchpack: 1.7.5
|
||||
webpack-sources: 1.4.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/webpack/5.73.0:
|
||||
|
@ -17378,6 +17526,14 @@ packages:
|
|||
|
||||
/ws/2.3.1:
|
||||
resolution: {integrity: sha512-61a+9LgtYZxTq1hAonhX8Xwpo2riK4IOR/BIVxioFbCfc3QFKmpE4x9dLExfLHKtUfVZigYa36tThVhO57erEw==}
|
||||
peerDependencies:
|
||||
bufferutil: ^4.0.1
|
||||
utf-8-validate: ^5.0.2
|
||||
peerDependenciesMeta:
|
||||
bufferutil:
|
||||
optional: true
|
||||
utf-8-validate:
|
||||
optional: true
|
||||
dependencies:
|
||||
safe-buffer: 5.0.1
|
||||
ultron: 1.1.1
|
||||
|
@ -17385,6 +17541,14 @@ packages:
|
|||
|
||||
/ws/6.2.0:
|
||||
resolution: {integrity: sha512-deZYUNlt2O4buFCa3t5bKLf8A7FPP/TVjwOeVNpw818Ma5nk4MLXls2eoEGS39o8119QIYxTrTDoPQ5B/gTD6w==}
|
||||
peerDependencies:
|
||||
bufferutil: ^4.0.1
|
||||
utf-8-validate: ^5.0.2
|
||||
peerDependenciesMeta:
|
||||
bufferutil:
|
||||
optional: true
|
||||
utf-8-validate:
|
||||
optional: true
|
||||
dependencies:
|
||||
async-limiter: 1.0.1
|
||||
dev: true
|
||||
|
@ -17588,10 +17752,10 @@ packages:
|
|||
resolution: {integrity: sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw==}
|
||||
dev: true
|
||||
|
||||
github.com/tauri-apps/tauricon/f104e2af7a19e1cdf9ee8212d2d3f6456d3aa00f:
|
||||
resolution: {tarball: https://codeload.github.com/tauri-apps/tauricon/tar.gz/f104e2af7a19e1cdf9ee8212d2d3f6456d3aa00f}
|
||||
github.com/tauri-apps/tauricon/5eea916a4a8e13aa41a943beaa7b4b71977e190d:
|
||||
resolution: {tarball: https://codeload.github.com/tauri-apps/tauricon/tar.gz/5eea916a4a8e13aa41a943beaa7b4b71977e190d}
|
||||
name: '@tauri-apps/tauricon'
|
||||
version: 1.0.2
|
||||
version: 1.0.3
|
||||
engines: {node: '>= 12.13.0', npm: '>= 6.6.0', yarn: '>= 1.19.1'}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
|
@ -17603,7 +17767,7 @@ packages:
|
|||
fs-extra: 10.1.0
|
||||
glob: 8.0.3
|
||||
global-agent: 3.0.0
|
||||
got: 12.0.4
|
||||
got: 12.1.0
|
||||
imagemin: 8.0.1
|
||||
imagemin-optipng: 8.0.0
|
||||
imagemin-zopfli: 7.0.0
|
||||
|
|
Loading…
Reference in a new issue