From 1fc94b4eb5e65951c11770827f2e17930f400e5e Mon Sep 17 00:00:00 2001 From: JakePIXL Date: Sun, 8 Oct 2023 14:19:02 -0400 Subject: [PATCH] Added backup handling (#5) * forgot to commit changes again :/ * very dirty commit (too many changes) * full backup viewing * added delete funcitonality * make backups async * added restore option to backups * added backups cache * fixed backup args error --- .DS_Store | Bin 8196 -> 8196 bytes Cargo.lock | 342 ++++++++++++- teller/Cargo.toml | 14 +- teller/src/handlers/backup.rs | 472 ++++++++++++++++++ teller/src/handlers/config/backup.rs | 122 +++++ .../{config.rs => config/instance.rs} | 81 +-- teller/src/handlers/config/mod.rs | 30 ++ teller/src/handlers/mod.rs | 1 + teller/src/handlers/player.rs | 221 +++++++- teller/src/handlers/search/backups.rs | 183 +++++++ teller/src/handlers/search/directories.rs | 7 +- teller/src/handlers/search/mod.rs | 1 + teller/src/handlers/search/worlds.rs | 324 +++++++++--- teller/src/handlers/snapshot.rs | 89 ++++ teller/src/handlers/world.rs | 151 +++++- teller/src/types/backup.rs | 46 ++ teller/src/types/config.rs | 12 + teller/src/types/error.rs | 14 + teller/src/types/mod.rs | 3 + teller/src/types/world.rs | 7 +- teller_desktop/package.json | 1 + teller_desktop/pnpm-lock.yaml | 11 + teller_desktop/src-tauri/Cargo.toml | 4 +- .../src-tauri/src/backend/backup_handler.rs | 158 ++++++ .../src-tauri/src/backend/folder_handler.rs | 15 +- teller_desktop/src-tauri/src/backend/mod.rs | 1 + .../src-tauri/src/backend/world_handler.rs | 47 +- teller_desktop/src-tauri/src/config/mod.rs | 36 +- teller_desktop/src-tauri/src/lib.rs | 1 + teller_desktop/src-tauri/src/main.rs | 1 + teller_desktop/src-tauri/src/types/events.rs | 11 + teller_desktop/src-tauri/src/types/mod.rs | 1 + teller_desktop/src/app.postcss | 13 +- teller_desktop/src/lib/backup_list.svelte | 27 + .../src/lib/backup_list_item.svelte | 104 ++++ teller_desktop/src/lib/cron_selector.svelte | 90 ++++ .../src/lib/directories_list.svelte | 10 +- .../src/lib/local_vault_list.svelte | 106 ++++ .../src/lib/modals/backup_modal.svelte | 89 ++++ .../src/lib/modals/delete_modal.svelte | 46 ++ .../modals/directories_modal.svelte} | 68 ++- .../src/lib/modals/local_vault_modal.svelte | 113 +++++ .../src/lib/modals/restore_modal.svelte | 160 ++++++ .../src/lib/modals/settings_modal.svelte | 109 ++++ teller_desktop/src/lib/snapshot_item.svelte | 119 +++++ teller_desktop/src/lib/stores.ts | 58 --- teller_desktop/src/lib/stores/caches.ts | 37 ++ teller_desktop/src/lib/stores/navigation.ts | 15 + teller_desktop/src/lib/stores/settings.ts | 16 + teller_desktop/src/lib/types/backups.ts | 33 ++ teller_desktop/src/lib/types/caches.ts | 18 + teller_desktop/src/lib/types/config.ts | 7 + teller_desktop/src/lib/types/events.ts | 3 + teller_desktop/src/lib/types/navigation.ts | 9 + teller_desktop/src/lib/types/worlds.ts | 46 ++ teller_desktop/src/lib/utils.ts | 54 -- teller_desktop/src/lib/world_list.svelte | 12 +- teller_desktop/src/lib/world_list_item.svelte | 88 +++- teller_desktop/src/routes/+page.svelte | 10 +- .../routes/api/images/[imageName]/+server.ts | 20 - .../src/routes/config/+layout.svelte | 7 - teller_desktop/src/routes/config/+layout.ts | 2 - .../src/routes/local/+layout.svelte | 189 +++++-- teller_desktop/src/routes/local/+page.svelte | 109 ---- .../[vaultName]}/+page.server.ts | 0 .../local/vaults/[vaultName]/+page.svelte | 168 +++++++ .../[vaultName]/[worldId]}/+page.server.ts | 0 .../vaults/[vaultName]/[worldId]/+page.svelte | 139 ++++++ .../[worldId]/[snapshotId]/+page.server.ts | 1 + .../[worldId]/[snapshotId]/+page.svelte | 240 +++++++++ .../[categoryName]/[pathName]/+page.server.ts | 1 + .../[categoryName]/[pathName]/+page.svelte | 186 +++++++ .../[pathName]/[worldId]/+page.server.ts | 1 + .../{ => [pathName]}/[worldId]/+page.svelte | 66 ++- .../player/[playerId]/+page.server.ts | 1 + .../[worldId]/player/[playerId]/+page.svelte | 20 +- teller_desktop/svelte.config.js | 5 + 77 files changed, 4431 insertions(+), 591 deletions(-) create mode 100644 teller/src/handlers/config/backup.rs rename teller/src/handlers/{config.rs => config/instance.rs} (82%) create mode 100644 teller/src/handlers/config/mod.rs create mode 100644 teller/src/handlers/search/backups.rs create mode 100644 teller/src/handlers/snapshot.rs create mode 100644 teller/src/types/backup.rs create mode 100644 teller/src/types/config.rs create mode 100644 teller/src/types/error.rs create mode 100644 teller_desktop/src-tauri/src/backend/backup_handler.rs create mode 100644 teller_desktop/src-tauri/src/types/events.rs create mode 100644 teller_desktop/src-tauri/src/types/mod.rs create mode 100644 teller_desktop/src/lib/backup_list.svelte create mode 100644 teller_desktop/src/lib/backup_list_item.svelte create mode 100644 teller_desktop/src/lib/cron_selector.svelte create mode 100644 teller_desktop/src/lib/local_vault_list.svelte create mode 100644 teller_desktop/src/lib/modals/backup_modal.svelte create mode 100644 teller_desktop/src/lib/modals/delete_modal.svelte rename teller_desktop/src/{routes/config/setDirs/+page@config.svelte => lib/modals/directories_modal.svelte} (52%) create mode 100644 teller_desktop/src/lib/modals/local_vault_modal.svelte create mode 100644 teller_desktop/src/lib/modals/restore_modal.svelte create mode 100644 teller_desktop/src/lib/modals/settings_modal.svelte create mode 100644 teller_desktop/src/lib/snapshot_item.svelte delete mode 100644 teller_desktop/src/lib/stores.ts create mode 100644 teller_desktop/src/lib/stores/caches.ts create mode 100644 teller_desktop/src/lib/stores/navigation.ts create mode 100644 teller_desktop/src/lib/stores/settings.ts create mode 100644 teller_desktop/src/lib/types/backups.ts create mode 100644 teller_desktop/src/lib/types/caches.ts create mode 100644 teller_desktop/src/lib/types/config.ts create mode 100644 teller_desktop/src/lib/types/events.ts create mode 100644 teller_desktop/src/lib/types/navigation.ts create mode 100644 teller_desktop/src/lib/types/worlds.ts delete mode 100644 teller_desktop/src/routes/api/images/[imageName]/+server.ts delete mode 100644 teller_desktop/src/routes/config/+layout.svelte delete mode 100644 teller_desktop/src/routes/config/+layout.ts delete mode 100644 teller_desktop/src/routes/local/+page.svelte rename teller_desktop/src/routes/local/{worlds/[categoryName]/[worldId] => vaults/[vaultName]}/+page.server.ts (100%) create mode 100644 teller_desktop/src/routes/local/vaults/[vaultName]/+page.svelte rename teller_desktop/src/routes/local/{worlds/[categoryName]/[worldId]/player/[playerId] => vaults/[vaultName]/[worldId]}/+page.server.ts (100%) create mode 100644 teller_desktop/src/routes/local/vaults/[vaultName]/[worldId]/+page.svelte create mode 100644 teller_desktop/src/routes/local/vaults/[vaultName]/[worldId]/[snapshotId]/+page.server.ts create mode 100644 teller_desktop/src/routes/local/vaults/[vaultName]/[worldId]/[snapshotId]/+page.svelte create mode 100644 teller_desktop/src/routes/local/worlds/[categoryName]/[pathName]/+page.server.ts create mode 100644 teller_desktop/src/routes/local/worlds/[categoryName]/[pathName]/+page.svelte create mode 100644 teller_desktop/src/routes/local/worlds/[categoryName]/[pathName]/[worldId]/+page.server.ts rename teller_desktop/src/routes/local/worlds/[categoryName]/{ => [pathName]}/[worldId]/+page.svelte (78%) create mode 100644 teller_desktop/src/routes/local/worlds/[categoryName]/[pathName]/[worldId]/player/[playerId]/+page.server.ts rename teller_desktop/src/routes/local/worlds/[categoryName]/{ => [pathName]}/[worldId]/player/[playerId]/+page.svelte (92%) diff --git a/.DS_Store b/.DS_Store index f0a5c684ff8a72017cb3b8e9b3196d87d6b32460..3e622ece9004ee7da73a5ef4e30f6b6be82f6dc7 100644 GIT binary patch delta 294 zcmZp1XmOa}&nUDpU^hRb&}JS1V@BCTh9ZV^hE#?U&z$_^q@4UD1_lNJ1_nl1Ag#CA zK(Lo_a=qZb$;G0wJekR51rQaChbAu;R^k*D6%!Ygkd%;~d`|d|bi4q6aYlZ*XL5dC zKv8O0W@>pvCXgMRS(Q5ZsEFF+FCr3LoXQRXK!br`vY4o%xg?_!qc)=nqb;L7qX(lW sV-#aFV*+C$LlF*d8M<>U}m*0&Cd&(6us%kQ4-CM3_;GkKqo0#9agSwT`xei8!%2*hG6203=~Cecu)i497d*(JWQNWxUm&LVjL+x$u6 diff --git a/Cargo.lock b/Cargo.lock index d35f1a8..f864e59 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,17 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "aes" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac1f845298e95f983ff1944b728ae08b8cebab80d684f0a832ed0fc74dfa27e2" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + [[package]] name = "ahash" version = "0.7.6" @@ -94,6 +105,23 @@ dependencies = [ "futures-core", ] +[[package]] +name = "async-compression" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "942c7cd7ae39e91bde4820d74132e9862e62c2f386c3aa90ccf55949f5bad63a" +dependencies = [ + "bzip2", + "flate2", + "futures-core", + "futures-io", + "memchr", + "pin-project-lite", + "xz2", + "zstd", + "zstd-safe", +] + [[package]] name = "async-executor" version = "1.5.1" @@ -195,6 +223,23 @@ dependencies = [ "syn 2.0.29", ] +[[package]] +name = "async_zip" +version = "0.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "795310de3218cde15219fc98c1cf7d8fe9db4865aab27fcf1d535d6cb61c6b54" +dependencies = [ + "async-compression", + "chrono", + "crc32fast", + "futures-util", + "log", + "pin-project", + "thiserror", + "tokio", + "tokio-util", +] + [[package]] name = "atk" version = "0.15.1" @@ -258,6 +303,12 @@ version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + [[package]] name = "bitflags" version = "1.3.2" @@ -374,6 +425,27 @@ dependencies = [ "serde", ] +[[package]] +name = "bzip2" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8" +dependencies = [ + "bzip2-sys", + "libc", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.11+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + [[package]] name = "cairo-rs" version = "0.15.12" @@ -414,6 +486,7 @@ version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" dependencies = [ + "jobserver", "libc", ] @@ -475,6 +548,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "cmake" version = "0.1.50" @@ -573,6 +656,12 @@ dependencies = [ "yaml-rust", ] +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + [[package]] name = "convert_case" version = "0.4.0" @@ -794,6 +883,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", + "subtle", ] [[package]] @@ -1539,6 +1629,15 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + [[package]] name = "html5ever" version = "0.25.2" @@ -1740,6 +1839,15 @@ dependencies = [ "cfb", ] +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + [[package]] name = "instant" version = "0.1.12" @@ -1827,6 +1935,15 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" +[[package]] +name = "jobserver" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2" +dependencies = [ + "libc", +] + [[package]] name = "js-sys" version = "0.3.64" @@ -1967,6 +2084,17 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "lzma-sys" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fda04ab3764e6cde78b9974eec4f779acaba7c4e84b36eca3cf77c581b85d27" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + [[package]] name = "mac" version = "0.1.1" @@ -2013,9 +2141,9 @@ checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" [[package]] name = "memchr" -version = "2.5.0" +version = "2.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c" [[package]] name = "memoffset" @@ -2432,12 +2560,35 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "password-hash" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "pathdiff" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" +[[package]] +name = "pbkdf2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" +dependencies = [ + "digest", + "hmac", + "password-hash", + "sha2", +] + [[package]] name = "percent-encoding" version = "2.3.0" @@ -2586,6 +2737,26 @@ dependencies = [ "siphasher", ] +[[package]] +name = "pin-project" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.29", +] + [[package]] name = "pin-project-lite" version = "0.2.12" @@ -2844,14 +3015,14 @@ dependencies = [ [[package]] name = "regex" -version = "1.9.3" +version = "1.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81bc1d4caf89fac26a70747fe603c130093b53c773888797a6329091246d651a" +checksum = "697061221ea1b4a94a624f67d0ae2bfe4e22b8a17b6a192afb11046542cc8c47" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.3.6", - "regex-syntax 0.7.4", + "regex-automata 0.3.8", + "regex-syntax 0.7.5", ] [[package]] @@ -2865,13 +3036,13 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.3.6" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fed1ceff11a1dddaee50c9dc8e4938bd106e9d89ae372f192311e7da498e3b69" +checksum = "c2f401f4955220693b56f8ec66ee9c78abffd8d1c4f23dc41a23839eb88f0795" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.7.4", + "regex-syntax 0.7.5", ] [[package]] @@ -2882,15 +3053,15 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2" +checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" [[package]] name = "reqwest" -version = "0.11.20" +version = "0.11.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e9ad3fe7488d7e34558a2033d45a0c90b72d97b4f80705666fea71472e2e6a1" +checksum = "78fdbab6a7e1d7b13cc8ff10197f47986b41c639300cc3c8158cac7847c9bbef" dependencies = [ "base64 0.21.2", "bytes", @@ -2913,6 +3084,7 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", + "system-configuration", "tokio", "tokio-native-tls", "tokio-util", @@ -2925,6 +3097,12 @@ dependencies = [ "winreg 0.50.0", ] +[[package]] +name = "result" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "194d8e591e405d1eecf28819740abed6d719d1a2db87fc0bcdedee9a26d55560" + [[package]] name = "rfd" version = "0.10.0" @@ -3146,6 +3324,17 @@ dependencies = [ "syn 2.0.29", ] +[[package]] +name = "serde_ini" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb236687e2bb073a7521c021949be944641e671b8505a94069ca37b656c81139" +dependencies = [ + "result", + "serde", + "void", +] + [[package]] name = "serde_json" version = "1.0.105" @@ -3444,6 +3633,12 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "subtle" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" + [[package]] name = "syn" version = "1.0.109" @@ -3466,6 +3661,27 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "system-deps" version = "5.0.0" @@ -3789,28 +4005,40 @@ dependencies = [ name = "teller" version = "0.1.0" dependencies = [ + "anyhow", + "async-recursion", + "async_zip", "base64 0.21.2", "chrono", "commandblock", "config", "directories", "log", + "regex", "reqwest", "serde", + "serde_ini", "serde_json", + "thiserror", + "tokio", + "tracing-error", + "url", "uuid", + "zip", ] [[package]] name = "teller_desktop" version = "0.1.0" dependencies = [ + "anyhow", "base64 0.21.2", "chrono", "commandblock", "config", "directories", "log", + "reqwest", "serde", "serde_json", "tauri", @@ -3854,18 +4082,18 @@ checksum = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c" [[package]] name = "thiserror" -version = "1.0.47" +version = "1.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97a802ec30afc17eee47b2855fc72e0c4cd62be9b4efe6591edde0ec5bd68d8f" +checksum = "9d6d7a740b8a666a7e828dd00da9c0dc290dff53154ea77ac109281de90589b7" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.47" +version = "1.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bb623b56e39ab7dcd4b1b98bb6c8f8d907ed255b18de254088016b27a8ee19b" +checksum = "49922ecae66cc8a249b77e68d1d0623c1b2c514f0060c27cdc68bd62a1219d35" dependencies = [ "proc-macro2", "quote", @@ -3986,6 +4214,7 @@ checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" dependencies = [ "bytes", "futures-core", + "futures-io", "futures-sink", "pin-project-lite", "tokio", @@ -4074,6 +4303,16 @@ dependencies = [ "valuable", ] +[[package]] +name = "tracing-error" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d686ec1c0f384b1277f097b2f279a2ecc11afe8c133c1aabf036a27cb4cd206e" +dependencies = [ + "tracing", + "tracing-subscriber", +] + [[package]] name = "tracing-log" version = "0.1.3" @@ -4169,9 +4408,9 @@ checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" [[package]] name = "url" -version = "2.4.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb" +checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5" dependencies = [ "form_urlencoded", "idna", @@ -4237,6 +4476,12 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "void" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" + [[package]] name = "vswhom" version = "0.1.0" @@ -4863,6 +5108,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "xz2" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388c44dc09d76f1536602ead6d325eb532f5c122f17782bd57fb47baeeb767e2" +dependencies = [ + "lzma-sys", +] + [[package]] name = "yaml-rust" version = "0.4.5" @@ -4938,6 +5192,56 @@ dependencies = [ "zvariant", ] +[[package]] +name = "zip" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" +dependencies = [ + "aes", + "byteorder", + "bzip2", + "constant_time_eq", + "crc32fast", + "crossbeam-utils", + "flate2", + "hmac", + "pbkdf2", + "sha1", + "time 0.3.25", + "zstd", +] + +[[package]] +name = "zstd" +version = "0.11.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "5.0.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d2a5585e04f9eea4b2a3d1eca508c4dee9592a89ef6f450c11719da0726f4db" +dependencies = [ + "libc", + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.8+zstd.1.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5556e6ee25d32df2586c098bbfa278803692a20d0ab9565e049480d52707ec8c" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + [[package]] name = "zvariant" version = "3.15.0" diff --git a/teller/Cargo.toml b/teller/Cargo.toml index 3fb9a1c..43c5ec9 100644 --- a/teller/Cargo.toml +++ b/teller/Cargo.toml @@ -13,8 +13,20 @@ config = "0.13.3" directories = "5.0.1" log = "0.4.20" reqwest = { version = "0.11.20", features = ["blocking"] } +tokio = { version = "1", features = ["full"] } + serde = { version = "1.0", features = ["derive"] } serde_json = "1.0.105" +serde_ini = "0.2.0" + uuid = { version = "1.4.1", features = ["v4"] } -base64 = "0.21.2" \ No newline at end of file +base64 = "0.21.2" +zip = "0.6.6" +anyhow = "1.0.75" +tracing-error = "0.2.0" +thiserror = "1.0.48" +url = "2.4.1" +regex = "1.9.5" +async_zip = { version = "0.0.15", features = ["full", "tokio"] } +async-recursion = "1.0.5" diff --git a/teller/src/handlers/backup.rs b/teller/src/handlers/backup.rs index 8b13789..39776e6 100644 --- a/teller/src/handlers/backup.rs +++ b/teller/src/handlers/backup.rs @@ -1 +1,473 @@ +use log::{error, info}; +use std::collections::HashMap; +use std::path::Path; +use std::path::PathBuf; +use tokio::io::AsyncWriteExt; +use async_recursion::async_recursion; +use async_zip::error::ZipError; +use async_zip::tokio::read::seek::ZipFileReader; +use async_zip::tokio::write::ZipFileWriter; +use async_zip::{Compression, ZipEntryBuilder}; +use tokio::fs::File; + +use serde_json::json; +use tokio::io::AsyncReadExt; +use tokio::io::AsyncSeek; +use tokio::io::AsyncWrite; + +use crate::handlers::config::backup::get_backup_config; +use crate::handlers::search::worlds::world_path_from_id; +use crate::types::backup::BackupMetadata; + +use super::config::get_config_folder; +use super::search::worlds::is_minecraft_world; +use super::world::{parse_world_entry_data, process_world_data}; + +async fn get_default_vault() -> PathBuf { + let config_dir = get_config_folder(); + + let vault_dir = config_dir.join("vault"); + + match vault_dir.exists() { + true => vault_dir, + false => { + let _ = tokio::fs::create_dir_all(vault_dir.clone()).await; + vault_dir + } + } +} + +pub async fn create_world_backup(world_path: PathBuf) -> Result { + let default_vault = get_default_vault().await; + + let temp_dir = match default_vault.join("temp").exists() { + true => default_vault.join("temp"), + false => { + let _ = tokio::fs::create_dir_all(default_vault.join("temp")).await; + default_vault.join("temp") + } + }; + + let mut world_entry_data = parse_world_entry_data(world_path.clone())?; + + world_entry_data.path = "".to_string(); + + let game_type = is_minecraft_world(&world_path); + + let world_data = match process_world_data(&world_path, game_type) { + Ok(data) => data, + Err(e) => { + return Err(format!("Could not process world data: {:?}", e)); + } + }; + + let metadata = json!({ + "entry": world_entry_data, + "data": world_data, + }); + + info!("Creating backup for world {}", world_entry_data.id); + + let backup_id = format!("{}.chunkvault-snapshot", chrono::Utc::now().timestamp()); + let backup_path = temp_dir.join(backup_id); + + let mut zip = ZipFileWriter::with_tokio(File::create(backup_path.clone()).await.unwrap()); + + let meta_builder = ZipEntryBuilder::new("metadata.json".into(), Compression::Stored); + + zip.write_entry_whole(meta_builder, metadata.to_string().as_bytes()) + .await + .unwrap(); + + let world_zip_path = temp_dir.join(format!("{}_data.zip", world_entry_data.id)); + let mut world_zip = ZipFileWriter::with_tokio(File::create(&world_zip_path).await.unwrap()); + add_directory_to_zip(&mut world_zip, &world_path, &world_path) + .await + .unwrap(); + world_zip.close().await.unwrap(); + + let mut world_zip_file = File::open(&world_zip_path).await.unwrap(); + let mut buffer = Vec::new(); + world_zip_file.read_to_end(&mut buffer).await.unwrap(); + + let world_zip_builder = ZipEntryBuilder::new( + format!("{}_data.zip", world_entry_data.id).into(), + Compression::Stored, + ); + + zip.write_entry_whole(world_zip_builder, &buffer) + .await + .unwrap(); + + zip.close().await.unwrap(); + + tokio::fs::remove_file(&world_zip_path).await.unwrap(); + + Ok(backup_path) +} + +#[async_recursion] +async fn add_directory_to_zip( + zip_writer: &mut ZipFileWriter, + directory: &Path, + prefix: &Path, +) -> Result<(), ZipError> { + let mut entries = tokio::fs::read_dir(directory).await?; + + while let Ok(entry) = entries.next_entry().await { + let entry = match entry { + Some(entry) => entry, + None => { + break; + } + }; + let path = entry.path(); + let name = path.strip_prefix(Path::new(prefix)).unwrap(); + if path.is_file() { + let builder = ZipEntryBuilder::new(name.to_str().unwrap().into(), Compression::Zstd) + .unix_permissions(0o755); + + let mut f = File::open(&path).await?; + let mut buffer = Vec::new(); + f.read_to_end(&mut buffer).await?; + + zip_writer + .write_entry_whole(builder, &buffer) + .await + .unwrap(); + } else if path.is_dir() { + add_directory_to_zip(zip_writer, &path, prefix).await?; + } + } + Ok(()) +} + +pub async fn create_backup_from_id( + world_id: &str, + category: Option<&str>, + instance: Option<&str>, + vaults: Option>, +) -> Result { + info!("Creating backup for world id: {}", world_id); + match world_path_from_id(world_id, category, instance) { + Ok(world_path) => { + let world_backup_path = match create_world_backup(world_path.clone()).await { + Ok(backup_path) => backup_path, + Err(e) => { + error!( + "Failed to create backup for world folder {}: {:?}", + world_path.display(), + e + ); + return Err(format!( + "Failed to create backup for world folder {}: {:?}", + world_path.display(), + e + )); + } + }; + + let backup_name = match world_backup_path.file_name() { + Some(name) => name, + None => { + error!( + "Could not get backup name from path: {:?}", + world_backup_path + ); + return Err(format!( + "Could not get backup name from path: {:?}", + world_backup_path + )); + } + }; + + if vaults.is_some() { + let mut vault_locations = HashMap::new(); + + let backup_settings = get_backup_config()?; + + for vault_id in vaults.unwrap() { + if let Some(vault) = backup_settings.vaults.get(&vault_id) { + vault_locations.insert(vault_id, vault); + } + } + + info!("Copying backup to all {} vaults", vault_locations.len()); + + for (vault_id, vault_path) in vault_locations { + let backup_location = vault_path.join(world_id); + if !backup_location.exists() { + match tokio::fs::create_dir_all(&backup_location).await { + Ok(_) => {} + Err(e) => { + error!("Failed to create vault folder {}: {:?}", vault_id, e); + continue; + } + }; + } + match tokio::fs::copy( + &world_backup_path, + backup_location.join(backup_name), + ) + .await + { + Ok(_) => {} + Err(e) => { + error!( + "Failed to move backup to vault folder {}: {:?}", + vault_id, e + ); + continue; + } + }; + } + + if let Err(e) = tokio::fs::remove_file(&world_backup_path).await { + error!( + "Failed to remove backup file {}: {:?}", + world_backup_path.display(), + e + ); + return Err(format!( + "Failed to remove backup file {}: {:?}", + world_backup_path.display(), + e + )); + } + } else { + let default_vault = get_default_vault().await; + match tokio::fs::rename(&world_backup_path, default_vault.join(backup_name)).await { + Ok(_) => {} + Err(e) => { + error!( + "Failed to move backup to default vault {}: {:?}", + default_vault.display(), + e + ); + return Err(format!( + "Failed to move backup to default vault {}: {:?}", + default_vault.display(), + e + )); + } + }; + } + Ok("Successfully Createad Backup.".to_string()) + } + Err(e) => { + error!("Failed to grab world by id {}: {:?}", world_id, e); + return Err(format!("Failed to grab world by id {}: {:?}", world_id, e)); + } + } +} + +pub async fn grab_backup_metadata(backup_path: PathBuf) -> Result { + let mut zip = + match ZipFileReader::with_tokio(File::open(backup_path.clone()).await.unwrap()).await { + Ok(zip) => zip, + Err(e) => { + return Err(format!( + "Failed to open backup file {}: {:?}", + backup_path.display(), + e + )); + } + }; + + let mut metadata = String::new(); + + let mut reader = match zip.reader_with_entry(0).await { + Ok(reader) => reader, + Err(e) => { + return Err(format!( + "Failed to open metadata file in backup {}: {:?}", + backup_path.display(), + e + )); + } + }; + + match reader.read_to_string_checked(&mut metadata).await { + Ok(_) => {} + Err(e) => { + return Err(format!( + "Failed to read metadata file in backup {}: {:?}", + backup_path.display(), + e + )); + } + }; + + let metadata: BackupMetadata = match serde_json::from_str(&metadata) { + Ok(metadata) => metadata, + Err(e) => { + return Err(format!( + "Failed to parse metadata file in backup {}: {:?}", + backup_path.display(), + e + )); + } + }; + + Ok(metadata) +} + +pub async fn extract_world_backup( + backup_path: PathBuf, + extract_path: PathBuf, +) -> Result<(), String> { + let mut zip = + match ZipFileReader::with_tokio(File::open(backup_path.clone()).await.unwrap()).await { + Ok(zip) => zip, + Err(e) => { + return Err(format!( + "Failed to open backup file {}: {:?}", + backup_path.display(), + e + )); + } + }; + + let mut reader = match zip.reader_with_entry(1).await { + Ok(reader) => reader, + Err(e) => { + return Err(format!( + "Failed to open metadata file in backup {}: {:?}", + backup_path.display(), + e + )); + } + }; + + let mut world_data = Vec::new(); + + match reader.read_to_end_checked(&mut world_data).await { + Ok(_) => {} + Err(e) => { + return Err(format!( + "Failed to read world data file in backup {}: {:?}", + backup_path.display(), + e + )); + } + }; + + let cursor = std::io::Cursor::new(world_data); + + let mut world_data_zip = match ZipFileReader::with_tokio(cursor).await { + Ok(zip) => zip, + Err(e) => { + return Err(format!( + "Failed to open world data zip in backup {}: {:?}", + backup_path.display(), + e + )); + } + }; + + let mut index = 0; + while let Ok(mut zip_entry) = world_data_zip.reader_with_entry(index).await { + let entry = zip_entry.entry(); + + let path = extract_path.join(entry.filename().as_str().unwrap()); + + if entry.dir().unwrap() { + tokio::fs::create_dir_all(&path) + .await + .map_err(|e| e.to_string())?; + } else { + if let Some(parent) = path.parent() { + if !parent.exists() { + tokio::fs::create_dir_all(&parent) + .await + .map_err(|e| e.to_string())?; + } + } + + let mut file = tokio::fs::File::create(&path) + .await + .map_err(|e| e.to_string())?; + + let mut buffer = Vec::new(); + zip_entry + .read_to_end_checked(&mut buffer) + .await + .map_err(|e| e.to_string())?; + file.write_all(&buffer).await.map_err(|e| e.to_string())?; + } + + index += 1; + } + + Ok(()) +} + +pub async fn delete_backup( + world_id: &str, + vault: Option<&str>, + snapshot_id: &str, +) -> Result<(), String> { + let backup_settings = get_backup_config()?; + + let vault_path = match vault { + Some(vault_id) => { + if let Some(vault) = backup_settings.vaults.get(vault_id) { + vault.to_owned() + } else { + return Err(format!("Vault {} does not exist.", vault_id)); + } + } + None => get_default_vault().await, + }; + + let backup_path = vault_path + .join(world_id) + .join(format!("{}.chunkvault-snapshot", snapshot_id)); + + info!("Removing backup {} for {}", snapshot_id, world_id); + + match tokio::fs::remove_file(backup_path.clone()).await { + Ok(_) => {} + Err(e) => { + return Err(format!( + "Failed to remove backup file {}: {:?}", + backup_path.display(), + e + )); + } + }; + Ok(()) +} + +pub async fn delete_all_backups(world_id: &str, vault: Option<&str>) -> Result<(), String> { + let backup_settings = get_backup_config()?; + + let vault_path = match vault { + Some(vault_id) => { + if let Some(vault) = backup_settings.vaults.get(vault_id) { + vault.to_owned() + } else { + return Err(format!("Vault {} does not exist.", vault_id)); + } + } + None => get_default_vault().await, + }; + + let backups_path = vault_path.join(world_id); + + info!("Removing all backups for {}", world_id); + + match tokio::fs::remove_dir_all(backups_path.clone()).await { + Ok(_) => {} + Err(e) => { + return Err(format!( + "Failed to remove backup folder {}: {:?}", + backups_path.display(), + e + )); + } + }; + + Ok(()) +} diff --git a/teller/src/handlers/config/backup.rs b/teller/src/handlers/config/backup.rs new file mode 100644 index 0000000..63cc204 --- /dev/null +++ b/teller/src/handlers/config/backup.rs @@ -0,0 +1,122 @@ +use std::fs; + +use log::{error, info}; + +use crate::{handlers::config::get_config_folder, types::backup::BackupSettings}; + +pub fn update_backup_config(settings_data: BackupSettings) -> Result { + let config_dir = get_config_folder(); + + let config_path = config_dir.join("backup_settings.json"); + + info!("Updating backup config file at {:?}", config_path); + + let settings = match config::Config::builder() + .add_source(config::File::from_str( + serde_json::to_string(&settings_data).unwrap().as_str(), + config::FileFormat::Json, + )) + .build() + { + Ok(s) => s, + Err(e) => { + error!( + "Could not load backup config file at {:?}: {:?}", + config_path, e + ); + return Err(format!( + "Could not load backup config file at {:?}: {:?}", + config_path, e + )); + } + }; + + let parsed_settings = match settings.try_deserialize::() { + Ok(s) => s, + Err(e) => { + error!( + "Could not parse backup config file at {:?}: {:?}", + config_path, e + ); + return Err(format!( + "Could not parse backup config file at {:?}: {:?}", + config_path, e + )); + } + }; + + match fs::write(&config_path, serde_json::to_string(&settings_data).unwrap()) { + Ok(_) => (), + Err(e) => { + error!( + "Could not write backup config file at {:?}: {:?}", + config_path, e + ); + return Err(format!( + "Could not write backup config file at {:?}: {:?}", + config_path, e + )); + } + } + + info!("Updated backup config file at {:?}", config_path); + + Ok(parsed_settings) +} + +pub fn get_backup_config() -> Result { + let config_dir = get_config_folder(); + + let config_path = config_dir.join("backup_settings.json"); + + if !config_path.exists() { + let default_settings = BackupSettings::default(); + match update_backup_config(default_settings) { + Ok(settings) => return Ok(settings), + Err(e) => { + error!( + "Could not create backup config file at {:?}: {:?}", + config_path, e + ); + return Err(format!( + "Could not create backup config file at {:?}: {:?}", + config_path, e + )); + } + } + } + + info!("Loading backup config file at {:?}", config_path); + + let settings = match config::Config::builder() + .add_source(config::File::from(config_path.clone())) + .build() + { + Ok(s) => s, + Err(e) => { + error!("Could not load config file at {:?}: {:?}", config_path, e); + return Err(format!( + "Could not load config file at {:?}: {:?}", + config_path, e + )); + } + }; + + let parsed_settings = match settings.try_deserialize::() { + Ok(s) => s, + Err(e) => { + error!( + "Could not parse backup config file at {:?}: {:?}", + config_path, e + ); + return Err(format!( + "Could not parse backup config file at {:?}: {:?}", + config_path, e + )); + } + }; + + info!("Loaded backup config file at {:?}", config_path); + + Ok(parsed_settings) +} diff --git a/teller/src/handlers/config.rs b/teller/src/handlers/config/instance.rs similarity index 82% rename from teller/src/handlers/config.rs rename to teller/src/handlers/config/instance.rs index be34c8f..a76dcc0 100644 --- a/teller/src/handlers/config.rs +++ b/teller/src/handlers/config/instance.rs @@ -1,22 +1,35 @@ -use std::path::Path; -use std::{env, fs}; +use std::{ + env, fs, + path::{Path, PathBuf}, +}; use log::{error, info}; -use std::collections::HashMap; -use std::path::PathBuf; +use crate::{handlers::config::get_config_folder, types::config::DirectorySettings}; -#[derive(serde::Deserialize, serde::Serialize, Clone)] -pub struct DirectorySettings { - pub categories: HashMap, -} +pub fn get_minecraft_save_location() -> Option { + let os = env::consts::OS; -#[derive(serde::Deserialize, serde::Serialize, Clone)] -pub struct VaultEntries { - pub paths: HashMap, + match os { + "windows" => Some(PathBuf::from(format!( + "{}\\.minecraft\\saves", + env::var("APPDATA").unwrap() + ))), + "macos" => Some(PathBuf::from(format!( + "{}/Library/Application Support/minecraft/saves", + env::var("HOME").unwrap() + ))), + "linux" => Some(PathBuf::from(format!( + "{}/.minecraft/saves", + env::var("HOME").unwrap() + ))), + _ => None, + } } -pub fn get_saves_config>(config_dir: P) -> Result { +pub fn get_local_directories_config>( + config_dir: P, +) -> Result { let config_path = config_dir.as_ref().join("local-directories.json"); info!("Loading config file at {:?}", config_path); @@ -172,47 +185,3 @@ pub fn update_local_directories_config( Ok(parsed_settings) } - -pub fn get_config_folder() -> PathBuf { - let config_dir = directories::ProjectDirs::from("io", "valink", "teller"); - - let config_dir = config_dir.unwrap().config_dir().to_path_buf(); - - // check if config directory exists - if !config_dir.exists() { - info!("Creating config folder at {:?}", config_dir); - - match fs::create_dir_all(&config_dir) { - Ok(_) => (), - Err(e) => { - error!( - "Could not create config directory at {:?}: {:?}", - config_dir, e - ); - return config_dir; - } - } - } - - config_dir -} - -pub fn get_minecraft_save_location() -> Option { - let os = env::consts::OS; - - match os { - "windows" => Some(PathBuf::from(format!( - "{}\\.minecraft\\saves", - env::var("APPDATA").unwrap() - ))), - "macos" => Some(PathBuf::from(format!( - "{}/Library/Application Support/minecraft/saves", - env::var("HOME").unwrap() - ))), - "linux" => Some(PathBuf::from(format!( - "{}/.minecraft/saves", - env::var("HOME").unwrap() - ))), - _ => None, - } -} diff --git a/teller/src/handlers/config/mod.rs b/teller/src/handlers/config/mod.rs new file mode 100644 index 0000000..afacabb --- /dev/null +++ b/teller/src/handlers/config/mod.rs @@ -0,0 +1,30 @@ +pub mod backup; +pub mod instance; + +use std::{fs, path::PathBuf}; + +use log::{error, info}; + +pub fn get_config_folder() -> PathBuf { + let config_dir = directories::ProjectDirs::from("io", "valink", "teller"); + + let config_dir = config_dir.unwrap().config_dir().to_path_buf(); + + // check if config directory exists + if !config_dir.exists() { + info!("Creating config folder at {:?}", config_dir); + + match fs::create_dir_all(&config_dir) { + Ok(_) => (), + Err(e) => { + error!( + "Could not create config directory at {:?}: {:?}", + config_dir, e + ); + return config_dir; + } + } + } + + config_dir +} diff --git a/teller/src/handlers/mod.rs b/teller/src/handlers/mod.rs index dbec287..f1d8579 100644 --- a/teller/src/handlers/mod.rs +++ b/teller/src/handlers/mod.rs @@ -2,4 +2,5 @@ pub mod backup; pub mod config; pub mod player; pub mod search; +pub mod snapshot; pub mod world; diff --git a/teller/src/handlers/player.rs b/teller/src/handlers/player.rs index d99f379..56b3f06 100644 --- a/teller/src/handlers/player.rs +++ b/teller/src/handlers/player.rs @@ -5,11 +5,17 @@ use uuid::Uuid; use std::{collections::HashMap, path::PathBuf}; use crate::{ - handlers::world::{is_minecraft_world, parse_world_data, read_dat_file, GameType}, + handlers::{ + search::worlds::is_minecraft_world, + world::{parse_world_data, read_dat_file, GameType}, + }, types::player::{Item, PlayerData}, }; -pub fn fetch_player_data_from_uuid(player_uuid_str: String) -> Result { +pub async fn fetch_player_data_from_uuid( + client: reqwest::Client, + player_uuid_str: String, +) -> Result { if player_uuid_str == "~local_player".to_string() { let player_avatar = "https://crafthead.net/avatar/8667ba71b85a4004af54457a9734eed7?scale=32&overlay=false"; @@ -30,10 +36,10 @@ pub fn fetch_player_data_from_uuid(player_uuid_str: String) -> Result match data.json::() { + Ok(data) => match data.json::().await { Ok(mut json) => { // info!("Fetched player data from playerdb.co: {:?}", json); if json @@ -68,14 +74,16 @@ pub fn fetch_player_data_from_uuid(player_uuid_str: String) -> Result, ) -> Result, String> { let mut player_data_map: HashMap = HashMap::new(); + let client = reqwest::Client::new(); for player_data in player_data_list { let player_uuid = player_data.id; - match fetch_player_data_from_uuid(player_uuid.clone()) { + let client = client.clone(); + match fetch_player_data_from_uuid(client, player_uuid.clone()).await { Ok(player) => { player_data_map.insert(player_uuid, player); } @@ -86,10 +94,7 @@ pub fn fetch_players_meta_data( Ok(player_data_map) } -pub fn grab_player_from_uuid( - player_uuid: String, - path: &PathBuf, -) -> Result> { +pub fn grab_player_from_uuid(player_uuid: String, path: &PathBuf) -> Result { info!("Grabbing player from UUID: {}", player_uuid); let game_type = is_minecraft_world(path); @@ -114,7 +119,8 @@ pub fn grab_player_from_uuid( if local_player_data.is_none() { return Err("Failed to read player data".into()); } - let player_data = serde_json::to_value(local_player_data.unwrap())?; + let player_data = serde_json::to_value(local_player_data.unwrap()) + .map_err(|e| format!("Failed to parse player data: {:?}", e))?; let player_data = PlayerData { id: player_uuid.to_owned(), @@ -153,7 +159,8 @@ pub fn grab_player_from_uuid( let player_data = if player_uuid == "~local_player" { let level_dat_path = path.join("level.dat"); - let level_data = read_dat_file(level_dat_path, game_type)?; + let level_data = read_dat_file(level_dat_path, game_type) + .map_err(|e| format!("Failed to read level.dat: {:?}", e))?; let level_data = parse_world_data(level_data, game_type)?; @@ -170,7 +177,8 @@ pub fn grab_player_from_uuid( commandblock::nbt::Endian::Big, false, ) { - Ok((_, data)) => serde_json::to_value(data)?, + Ok((_, data)) => serde_json::to_value(data) + .map_err(|e| format!("Failed to parse player data: {:?}", e))?, Err(e) => { return Err(format!("Failed to read player data: {:?}", e).into()); } @@ -219,3 +227,190 @@ pub fn grab_player_from_uuid( GameType::None => Err("Game type not found".into()), } } + +pub async fn get_player_data( + path: &PathBuf, + game_type: GameType, +) -> Result, Box> { + match game_type { + GameType::Bedrock => { + info!("Fetching Bedrock player data"); + + let player_uuid = "~local_player".to_string(); + let player_avatar = get_steve_image(); + + let db_path = path.join("db").to_str().unwrap().to_string(); + + let mut db_reader = commandblock::db::DbReader::new(&db_path, 0); + + let remote_player_data = db_reader.parse_remote_players(); + + let mut players: Vec = Vec::new(); + + if remote_player_data.is_some() { + for (uuid, _) in remote_player_data.unwrap().iter() { + info!("Fetching player data for: {:?}", uuid); + + let player_meta = json!({ + "username": "Remote Player", + "id": uuid.strip_prefix("player_server_").unwrap_or(uuid), + "avatar": player_avatar, + "meta": {} + }); + + players.push(player_meta); + } + } + + let local_player_data = json!({ + "username": "Local Player", + "id": player_uuid, + "avatar": player_avatar, + "meta": {} + }); + + players.push(local_player_data); + + Ok(players) + } + GameType::Java => { + info!("Fetching Java player data"); + + let player_data_path = path.join("playerdata"); + + if !player_data_path.exists() { + let level_dat_path = path.join("level.dat"); + + let level_data = read_dat_file(level_dat_path, game_type)?; + + let level_data = parse_world_data(level_data, game_type)?; + + let player_data = match level_data.get("Player") { + Some(data) => data, + None => return Err("Could not find Player in level.dat".into()), + }; + + let player_uuid = match player_data.get("UUID") { + Some(player_uuid_values) => { + let d1 = player_uuid_values[0].as_i64().unwrap_or_default() as u32; // Your most significant 32-bit value + let d2 = player_uuid_values[1].as_i64().unwrap_or_default() as u32; // Your second most significant 32-bit value + let d3 = player_uuid_values[2].as_i64().unwrap_or_default() as u32; // Your second least significant 32-bit value + let d4 = player_uuid_values[3].as_i64().unwrap_or_default() as u32; // Your least significant 32-bit value + + // Concatenate the four integers into a single 128-bit value + let uuid_int = ((d1 as u128) << 96) + | ((d2 as u128) << 64) + | ((d3 as u128) << 32) + | d4 as u128; + + // Create a UUID from the 128-bit value + let player_uuid = Uuid::from_u128(uuid_int).to_string(); + + player_uuid + } + None => "~local_player".to_string(), + }; + + let player_avatar = match player_uuid.contains("~local_player") { + true => get_steve_image(), + false => format!( + "https://crafthead.net/avatar/{}?scale=32&overlay=false", + player_uuid + ), + }; + + let player_meta = json!({ + "username": "Local Player", + "id": player_uuid, + "avatar": player_avatar, + "meta": {} + }); + + return Ok(vec![player_meta]); + } + + let player_data = match std::fs::read_dir(&player_data_path) { + Ok(data) => data, + Err(e) => { + return Err(format!("Failed to read player data: {:?}", e).into()); + } + }; + + let mut all_players: Vec = Vec::new(); + + for player in player_data { + let player = match player { + Ok(player) => player, + Err(e) => { + return Err(format!("Failed to read player data: {:?}", e).into()); + } + }; + + let player = player.path(); + + if !player.is_file() + || player.extension().and_then(std::ffi::OsStr::to_str) != Some("dat") + { + continue; + } + + let player_uuid = player.file_stem().unwrap().to_str().unwrap().to_string(); + // let player_meta = match fetch_player_data_from_uuid(client, player_uuid).await { + // Ok(data) => data, + // Err(e) => { + // return Err(format!("Failed to fetch player data: {:?}", e).into()); + // } + // }; + + let player_avatar = format!( + "https://crafthead.net/avatar/{}?scale=32&overlay=false", + player_uuid + ); + + let player_meta = json!({ + "username": "Remote Player", + "id": player_uuid, + "avatar": player_avatar, + "meta": {} + }); + + all_players.push(player_meta); + } + + Ok(all_players) + } + GameType::None => Err("Game type not specified".into()), + } +} + +// Literally the base64 encoded image of Steve's face +pub fn get_steve_image() -> String { + format!( + "{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}", + "data:image/png;base64,", + "iVBORw0KGgoAAAANSUhEUgAAALQAAAC0CAYAAAA9zQYyAAAFTUlEQVR4nO3cPcsnVwHG4fx3N89GxWjlW", + "ohaq6CCdrJN8AUsLLTWIkUKETsb0cpCtBEbGz9BRCxMoU1ASytBsFCML4hoERYWY1x8dtezSTH3PZBuzs", + "CB6yru6aYYflOec/noB9/5+KkJrh+NmeTW5cbY4z14eD12juvrh2OPd+PGnG/xxNO3bo493uMpxb1J0EH", + "QTdBB0E3QG0HvCHoj6CboIOgm6CDoJuiNoHcEvRF0E3QQdBN0EHQT9EbQO4LeCLoJOgi6CToIugl6I+gd", + "QW8E3QQdBN0EHQTdBL0R9I6gN4Jugg6CboIOgm6C3gh6R9AbQTdBB0E3QQdBN0FvBL0j6I2g2+XD758T9", + "Iqeubo1do53XV3GHu/hU3Oie+LVfz8Ye7zrR3N+7icEHQTdBL04QTdBL07QTdCLE3QT9OIE3QS9OEE3QS", + "9O0E3QixN0E/TiBN0EvThBN0EvTtBN0IsTdBP04gTdBL04QTdBL07QTdCLE3QT9OIE3QS9OEE3QS9O0E3", + "QixN0E/TiBN0EvThBN0GH73/1K2PnuP301djjve2ZZ8fO8fpr98dOcHPevRz3/vmPscf77s9+MXYOQQdB", + "N0EHQTdBbwS9I+gg6CLoIOgm6CDoJuiNoHcEHQRdBB0E3QQdBN0EvRH0jqCDoIugg6CboIOgm6A3gt4Rd", + "BB0EXQQdBN0EHQT9EbQO4IOgi6CDoJugg6CboLeCHpH0EHQRdBB0E3QQdBN0BtB7wg6CLpcXvzWC4/H83", + "Bvv301do7/PJhzvP7mzdtj57h3/97Y433gznvGzvHXv/xh7FoEHQTdBB0E3QR9DkEHQTdBB0E3QZ9D0EH", + "QTdBB0E3Q5xB0EHQTdBB0E/Q5BB0E3QQdBN0EfQ5BB0E3QQdBN0GfQ9BB0E3QQdBN0OcQdBB0E3QQdBP0", + "OQQdBN0EHQTdBH0OQQdBN0EHQTdBn0PQQdBN0EHQTdDnuLz0na9NCfrz3/zh2Dm+ePd7Y4/3/N1Xxs5x9", + "ew7xh7vRy/dGTvHT3/9jbHH+8m3Xxg7h6CDoJugg6CboDeC3hH0RtBN0EHQTdBB0E3QG0HvCHoj6CboIO", + "gm6CDoJuiNoHcEvRF0E3QQdBN0EHQT9EbQO4LeCLoJOgi6CToIugl6I+gdQW8E3QQdBN0EHQTdBL0R9I6", + "gN4Jugg6CboIOgm6C3gh6R9AbQbfLj5//zJSg77z3fWPnuHX73WOP9/OXXx67li997lNj5/j73/449niv", + "/OvVsXMIOgi6CToI+hyCboIOgm6CDoI+h6CboIOgm6CDoM8h6CboIOgm6CDocwi6CToIugk6CPocgm6CD", + "oJugg6CPoegm6CDoJugg6DPIegm6CDoJugg6HMIugk6CLoJOgj6HIJugg6CboIOgj6HoJugg6CboIOgzy", + "HodvnBl5+bEvTV5eHYOV781W/GHu+zH/rY2LX88ve/HTvHF+5+Yuzx7r/2v7FzCDoIugk6CPocgm6CDoJ", + "ugg6CPoegm6CDoJugg6DPIegm6CDoJugg6HMIugk6CLoJOgj6HIJugg6CboIOgj6HoJugg6CboIOgzyHo", + "Jugg6CboIOhzCLoJOgi6CToI+hyCboIOgm6CDoI+h6CboIOg25JBf/3TH5kS9KNHU177ht/96c9jme25T", + "3587PFef/DfsXMImrck6CDo9Qk6CHp9gg6CXp+gg6DXJ+gg6PUJOgh6fYIOgl6foIOg1yfoIOj1CToIen", + "2CDoJen6CDoNcn6CDo9Qk6CHp9gg6CXp+gg6DXJ+gg6PUJOgh6fYIOgl6foIOg1yfoIOj1rRj0/wEqn6O", + "eM+9YbQAAAABJRU5ErkJggg==" + ) +} diff --git a/teller/src/handlers/search/backups.rs b/teller/src/handlers/search/backups.rs new file mode 100644 index 0000000..bf56f8d --- /dev/null +++ b/teller/src/handlers/search/backups.rs @@ -0,0 +1,183 @@ +use std::{fs, path::PathBuf}; + +use crate::{ + handlers::{backup::grab_backup_metadata, config::backup::get_backup_config}, + types::{ + backup::{BackupMetadata, SnapshotInfo}, + world::WorldData, + }, +}; + +fn get_backups_from_path(directory_path: &str) -> Result, std::io::Error> { + let entries = fs::read_dir(directory_path)?; + let files: Vec = entries.filter_map(|entry| entry.ok()).collect(); + Ok(files) +} + +fn find_newest_backup(files: &[std::fs::DirEntry]) -> Option { + let mut newest_file: Option = None; + let mut newest_time = i64::MIN; + + for file in files { + if let Some(file_name) = file.file_name().to_str() { + let file_name = file_name.replace(".chunkvault-snapshot", ""); + + if let Ok(time) = file_name.parse::() { + if time > newest_time { + newest_time = time; + newest_file = Some(file.path()); + } + } + } + } + + newest_file +} + +pub async fn fetch_backups_list(vault: &str) -> Result, String> { + let backup_settings = get_backup_config()?; + + let local_backups_path = if let Some(vault_path) = backup_settings.vaults.get(vault) { + vault_path + } else { + return Err(format!("Vault {} does not exist", vault)); + }; + + let backup_entries = fs::read_dir(local_backups_path) + .map_err(|e| format!("Failed to read backups directory: {}", e))?; + + let mut backups: Vec = Vec::new(); + + for entry in backup_entries { + if entry.is_ok() { + let entry = entry.unwrap(); + let path = entry.path(); + + if path.is_dir() { + let all_backups = get_backups_from_path(path.to_str().unwrap()) + .map_err(|e| format!("Failed to read backups directory: {}", e))?; + + let newest_backup = find_newest_backup(&all_backups); + + if let Some(newest_backup) = newest_backup { + let metadata = grab_backup_metadata(newest_backup).await; + if metadata.is_ok() { + let world_data = metadata.unwrap(); + backups.push(world_data.entry); + } else { + continue; + } + } + } + } else { + continue; + } + } + + Ok(backups) +} + +pub fn fetch_backups_for_world( + world_id: &str, + selected_vault: Option<&str>, +) -> Result, String> { + let backup_settings = get_backup_config()?; + + let world_path = if let Some(selected_vault) = selected_vault { + if let Some(vault_path) = backup_settings.vaults.get(selected_vault) { + vault_path.join(world_id) + } else { + return Err(format!("Vault {} does not exist", selected_vault)); + } + } else { + return Err("No vault selected".to_string()); + }; + + let files = get_backups_from_path(world_path.to_str().unwrap()) + .map_err(|e| format!("Failed to read backups directory: {}", e))?; + + let mut backups = Vec::new(); + + for entry in files { + if entry + .file_name() + .to_str() + .unwrap() + .contains(".chunkvault-snapshot") + { + let path = entry.path(); + + let created = path + .file_name() + .unwrap() + .to_str() + .unwrap() + .replace(".chunkvault-snapshot", ""); + let created = created.parse::().unwrap(); + + let metadata = fs::metadata(&path).unwrap(); + let size = metadata.len(); + + let data = SnapshotInfo { + created, + size, + path, + }; + + backups.push(data); + } + } + + Ok(backups) +} + +pub async fn fetch_metadata_for_world( + world_id: &str, + selected_vault: Option<&str>, +) -> Result { + let backup_settings = get_backup_config()?; + + let world_path = if let Some(selected_vault) = selected_vault { + if let Some(vault_path) = backup_settings.vaults.get(selected_vault) { + vault_path.join(world_id).to_owned() + } else { + return Err(format!("Vault {} does not exist", selected_vault)); + } + } else { + return Err("No vault selected".to_string()); + }; + + let files = get_backups_from_path(world_path.to_str().unwrap()) + .map_err(|e| format!("Failed to read backups directory: {}", e))?; + + match find_newest_backup(&files) { + Some(newest_backup) => return grab_backup_metadata(newest_backup).await, + None => Err("No backups found".to_string()), + } +} + +pub async fn fetch_metadata_for_backup( + world_id: &str, + selected_vault: Option<&str>, + backup_id: &str, +) -> Result { + let backup_settings = get_backup_config()?; + + let world_path = if let Some(selected_vault) = selected_vault { + if let Some(vault_path) = backup_settings.vaults.get(selected_vault) { + vault_path.join(world_id).to_owned() + } else { + return Err(format!("Vault {} does not exist", selected_vault)); + } + } else { + return Err("No vault selected".to_string()); + }; + + let backup_path = world_path.join(format!("{}.chunkvault-snapshot", backup_id)); + + if backup_path.exists() { + return grab_backup_metadata(backup_path).await; + } else { + return Err("Backup does not exist".to_string()); + } +} diff --git a/teller/src/handlers/search/directories.rs b/teller/src/handlers/search/directories.rs index eef8389..784bfea 100644 --- a/teller/src/handlers/search/directories.rs +++ b/teller/src/handlers/search/directories.rs @@ -2,7 +2,10 @@ use std::path::PathBuf; use log::{error, info}; -use crate::handlers::config::{get_config_folder, get_minecraft_save_location, get_saves_config}; +use crate::handlers::config::{ + get_config_folder, + instance::{get_local_directories_config, get_minecraft_save_location}, +}; pub fn get_directory_by_name(dir_name: &str, category: Option<&str>) -> Option { info!("Getting path for {}", dir_name); @@ -14,7 +17,7 @@ pub fn get_directory_by_name(dir_name: &str, category: Option<&str>) -> Option

s, Err(e) => { error!("Could not get saves config: {:?}", e); diff --git a/teller/src/handlers/search/mod.rs b/teller/src/handlers/search/mod.rs index d117738..8375f5b 100644 --- a/teller/src/handlers/search/mod.rs +++ b/teller/src/handlers/search/mod.rs @@ -1,2 +1,3 @@ +pub mod backups; pub mod directories; pub mod worlds; diff --git a/teller/src/handlers/search/worlds.rs b/teller/src/handlers/search/worlds.rs index 8560a89..0313b48 100644 --- a/teller/src/handlers/search/worlds.rs +++ b/teller/src/handlers/search/worlds.rs @@ -1,26 +1,64 @@ -use std::path::PathBuf; +use std::{ + fs, + path::{Path, PathBuf}, +}; use log::{error, info}; -use serde_json::Value; use crate::{ handlers::{ - config::{get_config_folder, get_minecraft_save_location, get_saves_config}, - world::{ - get_level_name, get_vault_id, is_minecraft_world, process_world_data, read_dat_file, - GameType, + config::{ + get_config_folder, + instance::{get_local_directories_config, get_minecraft_save_location}, }, + world::{get_vault_id, parse_world_entry_data, process_world_data, GameType}, }, - types::world::WorldData, - utils::{calculate_dir_size, encode_image_to_base64}, + types::world::{WorldData, WorldLevelData}, }; -pub fn fetch_worlds_from_path(local_saves_path: PathBuf) -> Result, String> { +pub fn fetch_worlds_from_instance( + selected_category: &str, + instance: &str, +) -> Result, String> { let mut worlds_list: Vec = Vec::new(); - info!("Grabbing local worlds list from {:?}", local_saves_path); + let config_dir = get_config_folder(); - let local_saves_path = PathBuf::from(local_saves_path); + let config = match get_local_directories_config(&config_dir) { + Ok(config) => config, + Err(e) => { + error!("Could not get local directories config: {:?}", e); + return Err("Could not get local directories config".to_string()); + } + }; + let local_saves_path = if selected_category == "default" { + match get_minecraft_save_location() { + Some(path) => path, + None => { + error!("Could not find Minecraft save location"); + return Err("Could not find Minecraft save location".to_string()); + } + } + } else { + match config.categories.get(selected_category) { + Some(category) => match category.paths.get(instance) { + Some(path) => path.to_owned(), + None => { + error!( + "Could not find instance {} in category {}", + instance, selected_category + ); + return Err("Could not find instance".to_string()); + } + }, + None => { + error!("Could not find category {}", selected_category); + return Err("Could not find category".to_string()); + } + } + }; + + info!("Grabbing local worlds list from {:?}", local_saves_path); if !local_saves_path.exists() { error!( @@ -49,73 +87,84 @@ pub fn fetch_worlds_from_path(local_saves_path: PathBuf) -> Result continue, }; let path = entry.path(); - if path.is_dir() { - let game_type = is_minecraft_world(&path); - - let level_dat_path = path.join("level.dat"); - let level_dat_blob = match read_dat_file(level_dat_path, game_type) { - Ok(blob) => blob, - Err(e) => { - error!("Could not parse level.dat at {:?}: {:?}", path, e); - continue; - } - }; - - let level_name = match get_level_name(level_dat_blob, game_type) { - Ok(name) => name, - Err(e) => { - error!("Could not get level name at {:?}: {:?}", path, e); - continue; - } - }; - - let world_size = match calculate_dir_size(&path) { - Ok(size) => size, - Err(_) => 0, - }; - - let vault_id = match get_vault_id(&path) { - Ok(id) => id, - Err(e) => { - error!("Could not get vault id at {:?}: {:?}", path, e); - continue; - } - }; - - let world_data = WorldData { - id: vault_id, - name: level_name, - image: match game_type { - GameType::Java => { - encode_image_to_base64(path.join("icon.png")).unwrap_or("".to_string()) - } - GameType::Bedrock => encode_image_to_base64(path.join("world_icon.jpeg")) - .unwrap_or("".to_string()), - GameType::None => "".to_string(), - }, - path: path.to_string_lossy().into_owned(), - size: world_size, - }; - - worlds_list.push(world_data); + if path.is_dir() && path.extension().map_or(true, |ext| ext != "zip") { + match parse_world_entry_data(path.clone()) { + Ok(world_data) => worlds_list.push(world_data), + Err(_) => continue, + } } } Ok(worlds_list) } -pub fn grab_world_by_id( +// pub fn world_path_from_id(world_id: &str, category: Option<&str>) -> Result { +// let config_dir = get_config_folder(); + +// info!("Searching for world: {}", world_id); + +// let mut paths: Vec = Vec::new(); + +// match get_local_directories_config(&config_dir) { +// Ok(config) => { +// if let Some(category) = category { +// if category == "default" { +// match get_minecraft_save_location() { +// Some(path) => paths.push(path), +// None => {} +// }; +// } else if let Some(vault_entries) = config.categories.get(category) { +// for (_, path) in vault_entries.paths.iter() { +// paths.push(path.clone()); +// } +// } +// } +// } +// Err(_e) => {} +// }; + +// for save_location in paths { +// let world_folders = match std::fs::read_dir(&save_location) { +// Ok(folders) => folders, +// Err(_) => continue, +// }; + +// for entry in world_folders { +// if let Ok(world_folder) = entry { +// let world_folder = world_folder.path(); + +// if !world_folder.is_dir() { +// continue; +// } + +// let vault_id = match get_vault_id(&world_folder) { +// Ok(id) => id, +// Err(_) => continue, +// }; + +// if vault_id == world_id { +// info!("Found world: {world_id}"); +// return Ok(world_folder); +// } +// } +// } +// } + +// Err("Could not find world".to_string()) +// } + +pub fn world_path_from_id( world_id: &str, - return_path: Option, category: Option<&str>, -) -> Result { + instance: Option<&str>, +) -> Result { let config_dir = get_config_folder(); info!("Searching for world: {}", world_id); let mut paths: Vec = Vec::new(); - match get_saves_config(&config_dir) { + match get_local_directories_config(&config_dir) { Ok(config) => { if let Some(category) = category { if category == "default" { @@ -128,6 +177,12 @@ pub fn grab_world_by_id( paths.push(path.clone()); } } + } else if let Some(instance) = instance { + config.categories.iter().for_each(|(_, category)| { + if let Some(path) = category.paths.get(instance) { + paths.push(path.clone()); + } + }); } } Err(_e) => {} @@ -153,28 +208,137 @@ pub fn grab_world_by_id( }; if vault_id == world_id { - let game_type = is_minecraft_world(&world_folder); - info!("Found world: {world_id}"); + return Ok(world_folder); + } + } + } + } - if let Some(true) = return_path { - return Ok(Value::String(world_folder.to_string_lossy().into_owned())); - } else { - match process_world_data(&world_folder, game_type) { - Ok(data) => { - let data_value = serde_json::to_value(data).unwrap(); - return Ok(data_value); - } - Err(e) => { - error!("Could not process world data: {:?}", e); - continue; + Err("Could not find world".to_string()) +} + +pub async fn grab_world_by_id( + world_id: &str, + category: Option<&str>, + instance: Option<&str>, +) -> Result { + match world_path_from_id(world_id, category, instance) { + Ok(path) => { + let game_type = is_minecraft_world(&path.clone()); + match process_world_data(&path, game_type) { + Ok(data) => Ok(data), + Err(e) => { + error!("Could not process world data: {:?}", e); + Err("Could not process world data".to_string()) + } + } + } + Err(e) => { + error!("Could not find world: {:?}", e); + return Err("Could not find world".to_string()); + } + } +} + +pub fn is_minecraft_world(path: &Path) -> GameType { + if !path.is_dir() { + return GameType::None; + } + + let java_files = ["level.dat", "region", "data"]; + let bedrock_files = ["level.dat", "db"]; + + let is_java = java_files.iter().all(|file| path.join(file).exists()); + let is_bedrock = bedrock_files.iter().all(|file| path.join(file).exists()); + + if is_java { + info!("Found java world at {:?}", path); + return GameType::Java; + } else if is_bedrock { + info!("Found bedrock world at {:?}", path); + return GameType::Bedrock; + } else { + error!( + "Could not determine if path is a minecraft world: {:?}", + path + ); + + return GameType::None; + } +} + +pub fn is_minecraft_folder(path: &Path) -> GameType { + if path.is_dir() { + if path.file_name().unwrap() == ".minecraft" { + if !path.join("saves").exists() { + fs::create_dir_all(path.join("saves")).expect("Failed to create saves directory"); + } + return GameType::Java; + } else if path.join("minecraftWorlds").exists() { + return GameType::Bedrock; + } + } + + error!( + "Could not determine if path is a minecraft folder: {:?}", + path + ); + + GameType::None +} + +pub fn recursive_world_search( + path: &Path, + depth: usize, + max_depth: usize, + save_folders: &mut Vec, +) -> Result<(), String> { + if depth > max_depth { + return Ok(()); + } + + if !path.exists() { + return Err(format!("Path {:?} does not exist", path)); + } + + if path.ends_with("node_modules") || path.extension().map_or(false, |ext| ext == "zip") { + return Ok(()); + } + + match is_minecraft_world(path) { + GameType::Java => { + save_folders.push(path.parent().unwrap().to_path_buf()); + } + GameType::Bedrock => { + save_folders.push(path.parent().unwrap().to_path_buf()); + } + GameType::None => match is_minecraft_folder(path) { + GameType::Java => { + save_folders.push(path.join("saves")); + } + GameType::Bedrock => { + save_folders.push(path.join("minecraftWorlds")); + } + GameType::None => { + if let Ok(entries) = path.read_dir() { + for entry in entries { + if let Ok(entry) = entry { + let entry_path = entry.path(); + if entry_path.is_dir() { + recursive_world_search( + &entry_path, + depth + 1, + max_depth, + save_folders, + )?; } - }; + } } } } - } + }, } - Err("Could not find world".to_string()) + Ok(()) } diff --git a/teller/src/handlers/snapshot.rs b/teller/src/handlers/snapshot.rs new file mode 100644 index 0000000..6bcf43a --- /dev/null +++ b/teller/src/handlers/snapshot.rs @@ -0,0 +1,89 @@ +use std::{path::PathBuf, str::FromStr}; + +use log::info; + +use crate::handlers::{ + backup::grab_backup_metadata, search::directories::get_directory_by_name, world::new_vault_id, +}; + +use super::{ + backup::extract_world_backup, config::backup::get_backup_config, + search::worlds::world_path_from_id, +}; + +pub async fn snapshot_to_world( + snapshot_id: &str, + selected_vault: Option<&str>, + world_id: &str, + replace: bool, + instances: Vec, +) -> Result<(), String> { + let backup_settings = get_backup_config()?; + + let world_path = if let Some(selected_vault) = selected_vault { + if let Some(vault_path) = backup_settings.vaults.get(selected_vault) { + vault_path.join(world_id).to_owned() + } else { + return Err(format!("Vault {} does not exist", selected_vault)); + } + } else { + return Err("No vault selected".to_string()); + }; + + let backup_path = world_path.join(format!("{}.chunkvault-snapshot", snapshot_id)); + + info!("Restoring backup from {:?}", backup_path); + + if backup_path.exists() { + for instance in instances { + let mut world_path = match world_path_from_id(world_id, None, Some(&instance)) { + Ok(path) => path.to_owned(), + Err(_) => { + let instance_path = get_directory_by_name(&instance, None).unwrap(); + + let metadata = grab_backup_metadata(backup_path.clone()).await?; + + instance_path.join(&metadata.entry.name) + } + }; + + if replace { + if world_path.exists() { + tokio::fs::remove_dir_all(&world_path) + .await + .map_err(|e| e.to_string())?; + } + + tokio::fs::create_dir_all(&world_path) + .await + .map_err(|e| e.to_string())?; + + extract_world_backup(backup_path.clone(), world_path).await?; + } else { + let mut copy_counter = 1; + let original_world_path = world_path.clone(); + + while world_path.exists() { + let new_world_path = format!( + "{}-copy({})", + original_world_path.to_str().unwrap(), + copy_counter + ); + copy_counter += 1; + world_path = PathBuf::from_str(&new_world_path).unwrap(); + } + + tokio::fs::create_dir_all(&world_path) + .await + .map_err(|e| e.to_string())?; + extract_world_backup(backup_path.clone(), world_path.clone().to_owned()).await?; + + new_vault_id(&world_path)?; + } + } + } else { + return Err("Backup does not exist".to_string()); + } + + Ok(()) +} diff --git a/teller/src/handlers/world.rs b/teller/src/handlers/world.rs index 38b524e..fa1340d 100644 --- a/teller/src/handlers/world.rs +++ b/teller/src/handlers/world.rs @@ -4,14 +4,15 @@ use std::{ path::{Path, PathBuf}, }; +use chrono::NaiveDateTime; use commandblock::nbt::{read_from_file, Compression, Endian, NbtValue}; use log::{error, info}; use serde_json::{json, Value}; use uuid::Uuid; use crate::{ - handlers::player::fetch_player_data_from_uuid, - types::world::{GameRules, WorldLevelData}, + handlers::{player::get_steve_image, search::worlds::world_path_from_id}, + types::world::{GameRules, WorldData, WorldLevelData}, utils::{calculate_dir_size, encode_image_to_base64}, }; @@ -136,6 +137,16 @@ pub fn get_vault_id(path: &PathBuf) -> Result { Ok(vault_id.to_string()) } +pub fn new_vault_id(world_path: &PathBuf) -> Result<(), String> { + let mut vault_info = get_vault_file(world_path)?; + if let Some(id_pointer) = vault_info.pointer_mut("/id") { + *id_pointer = serde_json::Value::String(uuid::Uuid::new_v4().to_string()); + } + update_vault_file(vault_info, world_path)?; + + Ok(()) +} + // Minecraft save finder pub fn is_minecraft_world(path: &Path) -> GameType { @@ -445,7 +456,7 @@ pub fn get_player_data( info!("Fetching Bedrock player data"); let player_uuid = "~local_player".to_string(); - let player_avatar = "https://crafthead.net/avatar/8667ba71b85a4004af54457a9734eed7?scale=32&overlay=false"; + let player_avatar = get_steve_image(); let db_path = path.join("db").to_str().unwrap().to_string(); @@ -460,10 +471,8 @@ pub fn get_player_data( info!("Fetching player data for: {:?}", uuid); let player_meta = json!({ - "username": "Remote Player", "id": uuid.strip_prefix("player_server_").unwrap_or(uuid), "avatar": player_avatar, - "meta": {} }); players.push(player_meta); @@ -471,10 +480,8 @@ pub fn get_player_data( } let local_player_data = json!({ - "username": "Local Player", "id": player_uuid, "avatar": player_avatar, - "meta": {} }); players.push(local_player_data); @@ -518,13 +525,18 @@ pub fn get_player_data( } None => "~local_player".to_string(), }; - let player_avatar = "https://crafthead.net/avatar/8667ba71b85a4004af54457a9734eed7?scale=32&overlay=false"; + + let player_avatar = match player_uuid.contains("~local_player") { + true => get_steve_image(), + false => format!( + "https://crafthead.net/avatar/{}?scale=32&overlay=false", + player_uuid + ), + }; let player_meta = json!({ - "username": "Local Player", "id": player_uuid, "avatar": player_avatar, - "meta": {} }); return Ok(vec![player_meta]); @@ -557,12 +569,23 @@ pub fn get_player_data( let player_uuid = player.file_stem().unwrap().to_str().unwrap().to_string(); - let player_meta = match fetch_player_data_from_uuid(player_uuid) { - Ok(data) => data, - Err(e) => { - return Err(format!("Failed to fetch player data: {:?}", e).into()); - } - }; + let player_avatar = format!( + "https://crafthead.net/avatar/{}?scale=32&overlay=false", + player_uuid + ); + + let player_meta = json!({ + "id": player_uuid, + "avatar": player_avatar, + "meta": {} + }); + + // let player_meta = match fetch_player_data_from_uuid(player_uuid) { + // Ok(data) => data, + // Err(e) => { + // return Err(format!("Failed to fetch player data: {:?}", e).into()); + // } + // }; all_players.push(player_meta); } @@ -821,10 +844,10 @@ pub fn parse_game_rules( } } -pub fn get_level_name( +pub fn get_level_info( level_dat_blob: NbtValue, game_type: GameType, -) -> Result> { +) -> Result<(String, Option), Box> { let level_value: serde_json::Value = serde_json::to_value(level_dat_blob)?; match game_type { @@ -839,9 +862,16 @@ pub fn get_level_name( None => return Err("Could not find LevelName in level.dat".into()), }; + let last_played = match level_data.get("LastPlayed") { + Some(time) => time.as_i64().unwrap_or_default(), + None => return Err("Could not find LastPlayed in level.dat".into()), + }; + let parsed_level_name = level_name[1..level_name.len() - 1].to_string(); - Ok(parsed_level_name) + let parsed_last_played = chrono::NaiveDateTime::from_timestamp_millis(last_played); + + Ok((parsed_level_name, parsed_last_played)) } GameType::Bedrock => { let level_name = match level_value.get("LevelName") { @@ -849,10 +879,91 @@ pub fn get_level_name( None => return Err("Could not find levelName in level.dat".into()), }; + let last_played = match level_value.get("LastPlayed") { + Some(time) => time.as_i64().unwrap_or_default(), + None => return Err("Could not find LastPlayed in level.dat".into()), + }; + let parsed_level_name = level_name[1..level_name.len() - 1].to_string(); - Ok(parsed_level_name) + let parsed_last_played = chrono::NaiveDateTime::from_timestamp_opt(last_played, 0); + + Ok((parsed_level_name, parsed_last_played)) } GameType::None => Err("Could not find game type".into()), } } + +pub fn parse_world_entry_data(path: PathBuf) -> Result { + let game_type = is_minecraft_world(&path); + + let level_dat_path = path.join("level.dat"); + let level_dat_blob = match read_dat_file(level_dat_path, game_type) { + Ok(blob) => blob, + Err(e) => { + error!("Could not parse level.dat at {:?}: {:?}", path, e); + return Err(format!("Could not parse level.dat at {:?}: {:?}", path, e)); + } + }; + + let (level_name, last_played) = match get_level_info(level_dat_blob, game_type) { + Ok((name, time)) => (name, time), + Err(e) => { + error!("Could not get level name at {:?}: {:?}", path, e); + return Err(format!("Could not get level name at {:?}: {:?}", path, e)); + } + }; + + let world_size = match calculate_dir_size(&path) { + Ok(size) => size, + Err(_) => 0, + }; + + let vault_id = match get_vault_id(&path) { + Ok(id) => id, + Err(e) => { + error!("Could not get vault id at {:?}: {:?}", path, e); + return Err(format!("Could not get vault id at {:?}: {:?}", path, e)); + } + }; + + let world_data = WorldData { + id: vault_id, + name: level_name, + image: match game_type { + GameType::Java => { + encode_image_to_base64(path.join("icon.png")).unwrap_or("".to_string()) + } + GameType::Bedrock => { + encode_image_to_base64(path.join("world_icon.jpeg")).unwrap_or("".to_string()) + } + GameType::None => "".to_string(), + }, + path: path.to_string_lossy().into_owned(), + size: world_size, + last_played: last_played, + }; + + Ok(world_data) +} + +pub fn delete_world( + world_id: &str, + category: Option<&str>, + instance: Option<&str>, +) -> Result<(), String> { + let world_path = world_path_from_id(world_id, category, instance)?; + + if !world_path.exists() { + error!("World does not exist: {:?}", world_path); + return Err("World does not exist".into()); + } + + info!("Deleting world at {:?}", world_path); + + if let Err(e) = fs::remove_dir_all(world_path) { + return Err(format!("Failed to delete world: {:?}", e)); + } + + Ok(()) +} diff --git a/teller/src/types/backup.rs b/teller/src/types/backup.rs new file mode 100644 index 0000000..c86da4a --- /dev/null +++ b/teller/src/types/backup.rs @@ -0,0 +1,46 @@ +use std::collections::HashMap; +use std::path::PathBuf; + +use serde::{Deserialize, Serialize}; + +use super::world::{WorldData, WorldLevelData}; + +#[derive(Deserialize, Serialize, Clone)] +pub struct RemoteBackup { + pub remote_url: String, + pub api_key: String, +} + +#[derive(Deserialize, Serialize, Clone)] +pub struct BackupSettings { + pub schedule: String, + pub auto_backup: bool, + pub default_vaults: Option>, + pub vaults: HashMap, + pub remote_vaults: HashMap, +} + +impl Default for BackupSettings { + fn default() -> Self { + Self { + schedule: "0 0 * * * * *".to_string(), + auto_backup: false, + default_vaults: Some(Vec::new()), + vaults: HashMap::new(), + remote_vaults: HashMap::new(), + } + } +} + +#[derive(Deserialize, Serialize, Clone)] +pub struct BackupMetadata { + pub entry: WorldData, + pub data: WorldLevelData, +} + +#[derive(Deserialize, Serialize, Clone)] +pub struct SnapshotInfo { + pub created: i64, + pub size: u64, + pub path: PathBuf, +} diff --git a/teller/src/types/config.rs b/teller/src/types/config.rs new file mode 100644 index 0000000..b437932 --- /dev/null +++ b/teller/src/types/config.rs @@ -0,0 +1,12 @@ +use std::collections::HashMap; +use std::path::PathBuf; + +#[derive(serde::Deserialize, serde::Serialize, Clone)] +pub struct DirectorySettings { + pub categories: HashMap, +} + +#[derive(serde::Deserialize, serde::Serialize, Clone)] +pub struct VaultEntries { + pub paths: HashMap, +} diff --git a/teller/src/types/error.rs b/teller/src/types/error.rs new file mode 100644 index 0000000..29a3360 --- /dev/null +++ b/teller/src/types/error.rs @@ -0,0 +1,14 @@ +#[derive(Debug, thiserror::Error)] +enum Error { + #[error(transparent)] + Io(#[from] std::io::Error), +} + +impl serde::Serialize for Error { + fn serialize(&self, serializer: S) -> Result + where + S: serde::ser::Serializer, + { + serializer.serialize_str(self.to_string().as_ref()) + } +} diff --git a/teller/src/types/mod.rs b/teller/src/types/mod.rs index 50f34a8..6961875 100644 --- a/teller/src/types/mod.rs +++ b/teller/src/types/mod.rs @@ -1,2 +1,5 @@ +pub mod backup; +pub mod config; +pub mod error; pub mod player; pub mod world; diff --git a/teller/src/types/world.rs b/teller/src/types/world.rs index 7e33962..4bf7696 100644 --- a/teller/src/types/world.rs +++ b/teller/src/types/world.rs @@ -3,13 +3,14 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; use uuid::Uuid; -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub struct WorldData { pub id: String, pub name: String, pub image: String, pub path: String, pub size: u64, + pub last_played: Option, } #[allow(non_snake_case)] @@ -53,7 +54,7 @@ pub struct WorldVersion { pub additional_data: Option, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub struct WorldLevelData { pub name: String, pub folder: Option, @@ -67,7 +68,7 @@ pub struct WorldLevelData { pub game_rules: Option, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub struct GameRules { pub do_fire_tick: bool, pub mob_loot: bool, diff --git a/teller_desktop/package.json b/teller_desktop/package.json index 80efbe1..de8ae38 100644 --- a/teller_desktop/package.json +++ b/teller_desktop/package.json @@ -19,6 +19,7 @@ "@sveltejs/adapter-static": "1.0.0-next.50", "@sveltejs/kit": "^1.25.0", "@tauri-apps/cli": "^1.4.0", + "@zerodevx/svelte-toast": "^0.9.5", "autoprefixer": "^10.4.15", "daisyui": "^3.7.5", "eslint": "^8.49.0", diff --git a/teller_desktop/pnpm-lock.yaml b/teller_desktop/pnpm-lock.yaml index 4f62285..400763c 100644 --- a/teller_desktop/pnpm-lock.yaml +++ b/teller_desktop/pnpm-lock.yaml @@ -37,6 +37,9 @@ devDependencies: '@tauri-apps/cli': specifier: ^1.4.0 version: 1.4.0 + '@zerodevx/svelte-toast': + specifier: ^0.9.5 + version: 0.9.5(svelte@4.2.0) autoprefixer: specifier: ^10.4.15 version: 10.4.15(postcss@8.4.29) @@ -736,6 +739,14 @@ packages: resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} dev: true + /@zerodevx/svelte-toast@0.9.5(svelte@4.2.0): + resolution: {integrity: sha512-JLeB/oRdJfT+dz9A5bgd3Z7TuQnBQbeUtXrGIrNWMGqWbabpepBF2KxtWVhL2qtxpRqhae2f6NAOzH7xs4jUSw==} + peerDependencies: + svelte: ^3.57.0 || ^4.0.0 + dependencies: + svelte: 4.2.0 + dev: true + /acorn-jsx@5.3.2(acorn@8.10.0): resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: diff --git a/teller_desktop/src-tauri/Cargo.toml b/teller_desktop/src-tauri/Cargo.toml index 6efa7d1..c1c897d 100644 --- a/teller_desktop/src-tauri/Cargo.toml +++ b/teller_desktop/src-tauri/Cargo.toml @@ -18,7 +18,7 @@ tauri-build = { version = "1.2.1", features = [] } serde_json = "1.0" serde = { version = "1.0.183", features = ["derive"] } -tauri = { version = "1.3", features = [ "shell-all", "window-all", "dialog-all", "http-all", "fs-all", "devtools"] } +tauri = { version = "1.4", features = [ "shell-all", "window-all", "dialog-all", "http-all", "fs-all", "devtools"] } tauri-plugin-log = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" } tauri-plugin-single-instance = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" } @@ -34,6 +34,8 @@ chrono = { version = "0.4.26", features = ["serde"] } teller = { path = "../../teller" } # commandblock = { version = "0.4.1", features = ["serde"] } commandblock = { git = "https://github.com/Valink-Solutions/CommandBlock", features = ["serde"], branch = "add-bedrock-db-parsing" } +anyhow = "1.0.75" +reqwest = "0.11.21" [features] diff --git a/teller_desktop/src-tauri/src/backend/backup_handler.rs b/teller_desktop/src-tauri/src/backend/backup_handler.rs new file mode 100644 index 0000000..0cf53a9 --- /dev/null +++ b/teller_desktop/src-tauri/src/backend/backup_handler.rs @@ -0,0 +1,158 @@ +use tauri::{ + plugin::{Builder, TauriPlugin}, + Manager, Wry, +}; +use teller::types::{ + backup::{BackupMetadata, SnapshotInfo}, + world::WorldData, +}; + +use crate::types::events::ToastEvent; + +pub fn init() -> TauriPlugin { + Builder::new("backup_handler") + .invoke_handler(tauri::generate_handler![ + create_backup_from_id, + grab_local_backup_list, + grab_world_metadata, + grab_world_backups, + grab_backup_metadata, + delete_backup_from_id, + delete_world_backups, + restore_snapshot_to_world + ]) + .build() +} + +#[tauri::command] +async fn create_backup_from_id( + app: tauri::AppHandle, + world_id: String, + category: Option, + instance: Option, + vaults: Option>, +) -> String { + let world_id_clone = world_id.clone(); + + let result = teller::handlers::backup::create_backup_from_id( + &world_id, + category.as_deref(), + instance.as_deref(), + vaults, + ) + .await; + + match result { + Ok(path) => { + let _ = app.emit_all("world_backup_list_updated", &world_id_clone); + + let _ = app.emit_all("backup_list_updated", ()); + + let _ = app.emit_all( + "toast", + ToastEvent { + message: "Backup created successfully".to_string(), + }, + ); + path + } + Err(e) => { + let _ = app.emit_all( + "error", + ToastEvent { + message: format!("Error creating backup: {}", e), + }, + ); + e + } + } +} + +#[tauri::command] +async fn grab_local_backup_list(vault: &str) -> Result, String> { + teller::handlers::search::backups::fetch_backups_list(vault).await +} + +#[tauri::command] +async fn grab_world_metadata( + world_id: &str, + selected_vault: Option<&str>, +) -> Result { + teller::handlers::search::backups::fetch_metadata_for_world(world_id, selected_vault).await +} + +#[tauri::command] +fn grab_world_backups( + world_id: &str, + selected_vault: Option<&str>, +) -> Result, String> { + teller::handlers::search::backups::fetch_backups_for_world(world_id, selected_vault) +} + +#[tauri::command] +async fn grab_backup_metadata( + world_id: &str, + selected_vault: Option<&str>, + backup_id: &str, +) -> Result { + teller::handlers::search::backups::fetch_metadata_for_backup( + world_id, + selected_vault, + backup_id, + ) + .await +} + +#[tauri::command] +async fn delete_backup_from_id( + world_id: &str, + selected_vault: Option<&str>, + backup_id: &str, +) -> Result<(), String> { + teller::handlers::backup::delete_backup(world_id, selected_vault, backup_id).await +} + +#[tauri::command] +async fn delete_world_backups(world_id: &str, selected_vault: Option<&str>) -> Result<(), String> { + teller::handlers::backup::delete_all_backups(world_id, selected_vault).await +} + +#[tauri::command] +async fn restore_snapshot_to_world( + app: tauri::AppHandle, + snapshot_id: &str, + selected_vault: Option<&str>, + world_id: &str, + replace: bool, + instances: Vec, +) -> Result<(), String> { + match teller::handlers::snapshot::snapshot_to_world( + snapshot_id, + selected_vault, + world_id, + replace, + instances, + ) + .await + { + Ok(_) => { + let _ = app.emit_all( + "toast", + ToastEvent { + message: "Succesfully restored backup to isntance.".to_string(), + }, + ); + + Ok(()) + } + Err(e) => { + let _ = app.emit_all( + "error", + ToastEvent { + message: format!("Error restoring backup: {}", e), + }, + ); + Err(e) + } + } +} diff --git a/teller_desktop/src-tauri/src/backend/folder_handler.rs b/teller_desktop/src-tauri/src/backend/folder_handler.rs index 463ff12..0d5db3d 100644 --- a/teller_desktop/src-tauri/src/backend/folder_handler.rs +++ b/teller_desktop/src-tauri/src/backend/folder_handler.rs @@ -6,9 +6,8 @@ use tauri::{ Manager, Wry, }; use teller::{ - handlers::{ - search::worlds::{fetch_worlds_from_path, grab_world_by_id}, - world::recursive_world_search, + handlers::search::worlds::{ + fetch_worlds_from_instance, recursive_world_search, world_path_from_id, }, types::world::WorldData, }; @@ -40,8 +39,8 @@ fn check_path_for_save_folders(path: PathBuf) -> Result, String> { } #[tauri::command] -fn grab_local_worlds_list(local_saves_path: PathBuf) -> Result, String> { - fetch_worlds_from_path(local_saves_path) +fn grab_local_worlds_list(category: &str, instance: &str) -> Result, String> { + fetch_worlds_from_instance(category, instance) } #[tauri::command] @@ -49,11 +48,9 @@ fn open_world_in_explorer( handle: tauri::AppHandle, world_id: &str, category: Option<&str>, + instance: Option<&str>, ) -> Result<(), String> { - let path_str = grab_world_by_id(world_id, Some(true), category)?.to_string(); - let path_str = path_str.replace(" ", r" ").replace("\"", ""); - - let path = PathBuf::from(path_str); + let path = world_path_from_id(world_id, category, instance)?; if path.is_dir() { match tauri::api::shell::open(&handle.shell_scope(), &path.to_string_lossy(), None) diff --git a/teller_desktop/src-tauri/src/backend/mod.rs b/teller_desktop/src-tauri/src/backend/mod.rs index 2ba0a88..e3a2641 100644 --- a/teller_desktop/src-tauri/src/backend/mod.rs +++ b/teller_desktop/src-tauri/src/backend/mod.rs @@ -1,2 +1,3 @@ +pub mod backup_handler; pub mod folder_handler; pub mod world_handler; diff --git a/teller_desktop/src-tauri/src/backend/world_handler.rs b/teller_desktop/src-tauri/src/backend/world_handler.rs index 524e862..a2d2a3e 100644 --- a/teller_desktop/src-tauri/src/backend/world_handler.rs +++ b/teller_desktop/src-tauri/src/backend/world_handler.rs @@ -1,12 +1,15 @@ -use std::{collections::HashMap, path::Path}; +use std::{ + collections::HashMap, + path::{Path, PathBuf}, +}; use serde_json::Value; use teller::{ handlers::{ player::{fetch_player_data_from_uuid, fetch_players_meta_data, grab_player_from_uuid}, - search::worlds::grab_world_by_id, + search::worlds::{grab_world_by_id, world_path_from_id}, }, - types::player::PlayerData, + types::{player::PlayerData, world::WorldLevelData}, }; use tauri::{ @@ -18,32 +21,43 @@ pub fn init() -> TauriPlugin { Builder::new("world_handler") .invoke_handler(tauri::generate_handler![ get_world_by_id, + get_world_path_by_id, get_player_meta_from_uuids, get_player_meta_from_uuid, - get_player_from_uuid + get_player_from_uuid, + delete_world_by_id, ]) .build() } #[tauri::command] -pub fn get_world_by_id( +pub async fn get_world_by_id( + world_id: &str, + category: Option<&str>, + instance: Option<&str>, +) -> Result { + grab_world_by_id(world_id, category, instance).await +} + +#[tauri::command] +fn get_world_path_by_id( world_id: &str, - return_path: Option, category: Option<&str>, -) -> Result { - grab_world_by_id(world_id, return_path, category) + instance: Option<&str>, +) -> Result { + world_path_from_id(world_id, category, instance) } #[tauri::command] -fn get_player_meta_from_uuids( +async fn get_player_meta_from_uuids( player_data_list: Vec, ) -> Result, String> { - fetch_players_meta_data(player_data_list) + fetch_players_meta_data(player_data_list).await } #[tauri::command] -fn get_player_meta_from_uuid(player_uuid: String) -> Result { - fetch_player_data_from_uuid(player_uuid) +async fn get_player_meta_from_uuid(player_uuid: String) -> Result { + fetch_player_data_from_uuid(reqwest::Client::new(), player_uuid).await } #[tauri::command] @@ -53,3 +67,12 @@ fn get_player_from_uuid(player_uuid: String, path: &Path) -> Result Err(e.to_string()), } } + +#[tauri::command] +fn delete_world_by_id( + world_id: &str, + category: Option<&str>, + instance: Option<&str>, +) -> Result<(), String> { + teller::handlers::world::delete_world(world_id, category, instance) +} diff --git a/teller_desktop/src-tauri/src/config/mod.rs b/teller_desktop/src-tauri/src/config/mod.rs index ad85940..2117b7b 100644 --- a/teller_desktop/src-tauri/src/config/mod.rs +++ b/teller_desktop/src-tauri/src/config/mod.rs @@ -1,12 +1,18 @@ use std::path::PathBuf; use log::error; -use teller::handlers::{ - config::{ - create_local_directories_config, get_config_folder, get_saves_config, - update_local_directories_config, DirectorySettings, +use teller::{ + handlers::{ + config::{ + get_config_folder, + instance::{ + create_local_directories_config, get_local_directories_config, + update_local_directories_config, + }, + }, + search::directories::get_directory_by_name, }, - search::directories::get_directory_by_name, + types::{backup::BackupSettings, config::DirectorySettings}, }; use tauri::{ @@ -22,7 +28,9 @@ pub fn init() -> TauriPlugin { get_folder_path, create_saves_config, update_saves_config, - get_minecraft_save_location + get_minecraft_save_location, + get_backup_settings, + update_backup_settings ]) .build() } @@ -32,7 +40,7 @@ async fn get_save_folders(handle: tauri::AppHandle) -> Result s, Err(e) => { let _config_saves_window = tauri::WindowBuilder::new( @@ -55,7 +63,7 @@ async fn get_save_folders(handle: tauri::AppHandle) -> Result Result { let config_dir = get_config_folder(); - let saves_config = match get_saves_config(&config_dir) { + let saves_config = match get_local_directories_config(&config_dir) { Ok(s) => s, Err(e) => { return Err(format!("Could not get saves config: {:?}", e)); @@ -82,5 +90,15 @@ fn update_saves_config(settings_data: DirectorySettings) -> Result Option { - teller::handlers::config::get_minecraft_save_location() + teller::handlers::config::instance::get_minecraft_save_location() +} + +#[tauri::command] +fn get_backup_settings() -> Result { + teller::handlers::config::backup::get_backup_config() +} + +#[tauri::command] +fn update_backup_settings(settings_data: BackupSettings) -> Result { + teller::handlers::config::backup::update_backup_config(settings_data) } diff --git a/teller_desktop/src-tauri/src/lib.rs b/teller_desktop/src-tauri/src/lib.rs index 8729764..d38bf2f 100644 --- a/teller_desktop/src-tauri/src/lib.rs +++ b/teller_desktop/src-tauri/src/lib.rs @@ -1,2 +1,3 @@ pub mod backend; pub mod config; +pub mod types; diff --git a/teller_desktop/src-tauri/src/main.rs b/teller_desktop/src-tauri/src/main.rs index 3cf1389..3fe261e 100644 --- a/teller_desktop/src-tauri/src/main.rs +++ b/teller_desktop/src-tauri/src/main.rs @@ -32,6 +32,7 @@ fn main() { .unwrap(); })) .plugin(teller_desktop::config::init()) + .plugin(teller_desktop::backend::backup_handler::init()) .plugin(teller_desktop::backend::folder_handler::init()) .plugin(teller_desktop::backend::world_handler::init()) .run(tauri::generate_context!()) diff --git a/teller_desktop/src-tauri/src/types/events.rs b/teller_desktop/src-tauri/src/types/events.rs new file mode 100644 index 0000000..18c3de0 --- /dev/null +++ b/teller_desktop/src-tauri/src/types/events.rs @@ -0,0 +1,11 @@ +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct BackupEvent { + pub world_id: String, + pub category: Option, + pub vaults: Option>, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct ToastEvent { + pub message: String, +} diff --git a/teller_desktop/src-tauri/src/types/mod.rs b/teller_desktop/src-tauri/src/types/mod.rs new file mode 100644 index 0000000..a9970c2 --- /dev/null +++ b/teller_desktop/src-tauri/src/types/mod.rs @@ -0,0 +1 @@ +pub mod events; diff --git a/teller_desktop/src/app.postcss b/teller_desktop/src/app.postcss index 28fcc0f..57494c3 100644 --- a/teller_desktop/src/app.postcss +++ b/teller_desktop/src/app.postcss @@ -20,13 +20,9 @@ body { ::-webkit-scrollbar { width: 8px; height: 8px; /* Make horizontal scrollbar the same size as vertical */ + margin: 10px 0; } -/* This will apply to the horizontal scrollbar */ -/* ::-webkit-scrollbar-track { - padding: 0 1rem 0 0; -} */ - [data-theme='neubrutalism'] .btn { border-width: 4px; border-color: black; @@ -148,6 +144,13 @@ body { outline: none; } +[data-theme='neubrutalism'] .select:disabled, +[data-theme='neubrutalism-dark'] .select:disabled { + border-color: #a9a9a9 !important; + color: #a9a9a9 !important; + box-shadow: 0px 4px 0px 0px rgba(0, 0, 0, 0.33); +} + [data-theme='neubrutalism'] .checkbox { width: 30px; height: 30px; diff --git a/teller_desktop/src/lib/backup_list.svelte b/teller_desktop/src/lib/backup_list.svelte new file mode 100644 index 0000000..402d1be --- /dev/null +++ b/teller_desktop/src/lib/backup_list.svelte @@ -0,0 +1,27 @@ + + +

+
+ {#if worlds.length > 0} + {#each worlds as world} + + {/each} + {:else} +
+

No Backups Found

+

Try Creating Some.

+
+ {/if} +
+
diff --git a/teller_desktop/src/lib/backup_list_item.svelte b/teller_desktop/src/lib/backup_list_item.svelte new file mode 100644 index 0000000..3a068b9 --- /dev/null +++ b/teller_desktop/src/lib/backup_list_item.svelte @@ -0,0 +1,104 @@ + + +
  • +
    + 0 + ? world.image + : 'https://static.planetminecraft.com/files/image/minecraft/project/2020/194/13404399_l.jpg'} + alt={world.name} + /> +
    +
    +
    +
    +

    + {world.name} +

    +
    + + {formatBytes(world.size)} +
    + +
    + + +
    +
    +
  • diff --git a/teller_desktop/src/lib/cron_selector.svelte b/teller_desktop/src/lib/cron_selector.svelte new file mode 100644 index 0000000..4a2b537 --- /dev/null +++ b/teller_desktop/src/lib/cron_selector.svelte @@ -0,0 +1,90 @@ + + +
    +
    + + {#if selectedOption === 'custom'} +
    + at + + : + + every + + + {#if Number(day) > 1} + days + {:else} + day + {/if} + + +
    + {/if} +
    +
    diff --git a/teller_desktop/src/lib/directories_list.svelte b/teller_desktop/src/lib/directories_list.svelte index 8e5fabe..cbe30bc 100644 --- a/teller_desktop/src/lib/directories_list.svelte +++ b/teller_desktop/src/lib/directories_list.svelte @@ -2,8 +2,8 @@ import Sortable from 'sortablejs'; import { onMount, afterUpdate } from 'svelte'; import Icon from '@iconify/svelte'; - import type { VaultEntries } from './utils'; - import { directorySettings } from './stores'; + import { directorySettings } from './stores/settings'; + import type { VaultEntries } from './types/config'; // If you can find a better way to do this please implement it // I'm begging you @@ -127,12 +127,12 @@ }; -
    +
    {#each Object.entries($directorySettings.categories) as [category, value], i (category)}
    -
    +
    + + { + if (e.key === 'Enter') { + e.preventDefault(); + e.target.blur(); + } + }} + on:blur={(e) => { + if (e.target.textContent.length > 15) { + e.target.textContent = vault; // reset to old name if new name is too long + } else { + updateVaultName(vault, e.target.textContent); + } + }} + > + {vault} + +
    + {data.path} +
    +
    + +
    +
    + {/each} +
    diff --git a/teller_desktop/src/lib/modals/backup_modal.svelte b/teller_desktop/src/lib/modals/backup_modal.svelte new file mode 100644 index 0000000..1ea4e38 --- /dev/null +++ b/teller_desktop/src/lib/modals/backup_modal.svelte @@ -0,0 +1,89 @@ + + +{#if isOpen} + +{/if} diff --git a/teller_desktop/src/lib/modals/delete_modal.svelte b/teller_desktop/src/lib/modals/delete_modal.svelte new file mode 100644 index 0000000..d21c5e8 --- /dev/null +++ b/teller_desktop/src/lib/modals/delete_modal.svelte @@ -0,0 +1,46 @@ + + +{#if isOpen} + +{/if} diff --git a/teller_desktop/src/routes/config/setDirs/+page@config.svelte b/teller_desktop/src/lib/modals/directories_modal.svelte similarity index 52% rename from teller_desktop/src/routes/config/setDirs/+page@config.svelte rename to teller_desktop/src/lib/modals/directories_modal.svelte index 4429ec9..8ca4e50 100644 --- a/teller_desktop/src/routes/config/setDirs/+page@config.svelte +++ b/teller_desktop/src/lib/modals/directories_modal.svelte @@ -1,23 +1,16 @@ -
    - - - -
    +{#if isOpen} + +{/if} diff --git a/teller_desktop/src/lib/modals/local_vault_modal.svelte b/teller_desktop/src/lib/modals/local_vault_modal.svelte new file mode 100644 index 0000000..6661575 --- /dev/null +++ b/teller_desktop/src/lib/modals/local_vault_modal.svelte @@ -0,0 +1,113 @@ + + +{#if isOpen} + +{/if} diff --git a/teller_desktop/src/lib/modals/restore_modal.svelte b/teller_desktop/src/lib/modals/restore_modal.svelte new file mode 100644 index 0000000..ceee728 --- /dev/null +++ b/teller_desktop/src/lib/modals/restore_modal.svelte @@ -0,0 +1,160 @@ + + +{#if isOpen} + +{/if} diff --git a/teller_desktop/src/lib/modals/settings_modal.svelte b/teller_desktop/src/lib/modals/settings_modal.svelte new file mode 100644 index 0000000..c654dca --- /dev/null +++ b/teller_desktop/src/lib/modals/settings_modal.svelte @@ -0,0 +1,109 @@ + + +{#if isOpen} +