Library manager (#258)

This commit is contained in:
Oscar Beaumont 2022-07-11 10:05:24 +08:00 committed by GitHub
parent 70ea568530
commit c685ce5fe9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
100 changed files with 2517 additions and 1681 deletions

285
Cargo.lock generated
View file

@ -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",

View file

@ -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

View file

@ -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 ">

View file

@ -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

View file

@ -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

View file

@ -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))
}

View file

@ -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;

View file

@ -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"

View file

@ -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, } };

View file

@ -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, } };

View 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, }

View file

@ -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 };

View 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, } };

View 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, }

View 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, }

View 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" };

View 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, }

View file

@ -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, }

View file

@ -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';

View file

@ -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'
);

View file

@ -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 {

View file

@ -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())

View file

@ -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)
}

View file

@ -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())

View file

@ -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() {

View file

@ -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);
}

View file

@ -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;

View file

@ -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![

View file

@ -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;
}
}

View file

@ -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>),

View 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,
}

View 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()
}
}

View 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,
})
}
}

View file

@ -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(())
}

View file

@ -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;

View file

@ -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
View 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(()),
}
}
}

View file

@ -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),
}

View file

@ -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)
}
}

View file

@ -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);

View file

@ -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![

View file

@ -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)
}

View file

@ -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"
}
}

View file

@ -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 });
}

View file

@ -0,0 +1 @@
export * from './AppPropsContext';

View file

@ -1,2 +0,0 @@
export * from './query';
export * from './state';

View file

@ -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'));
// }

View file

@ -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;
})
);
}
}));

View file

@ -0,0 +1 @@
export * from './useCoreEvents';

View 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]);
}

View file

@ -1,3 +1,5 @@
export * from './bridge';
export * from './files';
export * from './ClientProvider';
export * from './stores';
export * from './hooks';
export * from './context';

View file

@ -0,0 +1,4 @@
export * from './useLibraryStore';
export * from './useExplorerStore';
export * from './useInspectorStore';
export * from './useInspectorStore';

View file

@ -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(() => ({}))
}));

View file

@ -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
});

View 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 };
};

View file

@ -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": {

View file

@ -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();

View file

@ -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() {

View file

@ -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>
)}
</>
);
}

View file

@ -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)}>

View file

@ -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 ?? ''];

View file

@ -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';

View file

@ -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 =

View file

@ -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>

View 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>
);
}

View file

@ -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>

View file

@ -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);
},

View file

@ -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);
}

View file

@ -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>;
};

View file

@ -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>
);

View file

@ -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>
);
};

View file

@ -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]);
}

View file

@ -1,5 +1,6 @@
import { AppProps, Platform } from '@sd/client';
import App from './App';
import { AppProps, Platform } from './AppPropsContext';
export type { AppProps, Platform };

View file

@ -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>
);

View file

@ -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 }

View file

@ -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"

View file

@ -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>
);
};

View file

@ -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>
);
};

View file

@ -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>
);
}

View file

@ -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>
);
}

View file

@ -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>
);
}

View 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>
);
};

View file

@ -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 (

View file

@ -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>
);
}

View file

@ -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 (

View file

@ -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 (

View file

@ -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>
);
}

View file

@ -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>
);
}

View file

@ -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>
);
}

View file

@ -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 (

View file

@ -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 (

View file

@ -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 (

View file

@ -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 (

View file

@ -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>
);
}

View file

@ -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>
);
}

View 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>
);
}

View file

@ -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",

View file

@ -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
)}

View file

@ -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