Full port: v1 renderer shim + folder monitor + remote server + updater + upload log fallback
Major additions:
Frontend — v1 renderer/app.js and renderer/index.html copied 1:1. A
new tauri-shim.js reconstructs window.api with all 54 methods v1 used,
each mapped to a matching Rust #[tauri::command] so the existing
renderer works unchanged. Drag-drop paths are bridged via the Tauri
tauri://drag-drop event.
Backend modules:
- folder_monitor.rs: notify-debouncer-full based recursive/
non-recursive watch, include/exclude extension filter, skip-dupes,
initial baseline scan, emits 'folder-monitor-new-files' to frontend.
Auto-restarts on launch if persisted settings have enabled=true.
- remote_server.rs: axum HTTP server with bearer-token auth exposing
/api/status, /api/control/cancel, /api/control/finish-after.
Auto-restarts on launch if enabled + token set.
- updater.rs: Gitea releases polling + semver compare, returns
download URL for current platform. install_update opens the URL
externally (true in-app update needs signing cert later).
- upload_log.rs: full fallback ladder (primary → Desktop → AppData),
daily-log suffix handling, auto-persists working fallback path into
globalSettings.logFilePath so next session writes there directly,
emits 'upload-log-fallback' to the renderer once per session.
Commands added (all wired into tauri::generate_handler!):
get_hoster_settings, save_hoster_settings, get_global_settings,
save_global_settings, set_always_on_top, get_always_on_top,
set_shutdown_after_finish, get_shutdown_after_finish, cancel_shutdown,
resolve_folder_files, copy_to_clipboard (Windows clipboard via
PowerShell pipe), start_upload, add_jobs_to_batch,
finish_after_active, run_health_check, export_backup, import_backup,
import_backup_saved (legacy-password path), read_own_upload_log,
import_upload_log, save_text_file, open_log_folder,
read_rotation_log, get_version, check_for_update, install_update,
folder_monitor_start/stop/status, remote_get_settings,
remote_save_settings, remote_generate_token, remote_status,
show_drop_target, hide_drop_target, debug_log.
Release build: exe 7.5 MB, NSIS 2.7 MB, MSI 3.7 MB.
This commit is contained in:
parent
c97c6b9469
commit
615161d747
532
src-tauri/Cargo.lock
generated
532
src-tauri/Cargo.lock
generated
@ -95,6 +95,15 @@ version = "1.0.102"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
||||
|
||||
[[package]]
|
||||
name = "arbitrary"
|
||||
version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
|
||||
dependencies = [
|
||||
"derive_arbitrary",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-broadcast"
|
||||
version = "0.7.2"
|
||||
@ -295,6 +304,61 @@ version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||
|
||||
[[package]]
|
||||
name = "axum"
|
||||
version = "0.7.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum-core",
|
||||
"bytes",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
"itoa",
|
||||
"matchit",
|
||||
"memchr",
|
||||
"mime",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"rustversion",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_path_to_error",
|
||||
"serde_urlencoded",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tower",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "axum-core"
|
||||
version = "0.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"bytes",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"mime",
|
||||
"pin-project-lite",
|
||||
"rustversion",
|
||||
"sync_wrapper",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.21.7"
|
||||
@ -422,6 +486,25 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bzip2"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49ecfb22d906f800d4fe833b6282cf4dc1c298f5057ca0b5445e5c209735ca47"
|
||||
dependencies = [
|
||||
"bzip2-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bzip2-sys"
|
||||
version = "0.1.13+1.0.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cairo-rs"
|
||||
version = "0.18.5"
|
||||
@ -496,6 +579,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"jobserver",
|
||||
"libc",
|
||||
"shlex",
|
||||
]
|
||||
|
||||
@ -598,6 +683,25 @@ dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "console"
|
||||
version = "0.15.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8"
|
||||
dependencies = [
|
||||
"encode_unicode",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"unicode-width",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "constant_time_eq"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6"
|
||||
|
||||
[[package]]
|
||||
name = "convert_case"
|
||||
version = "0.4.0"
|
||||
@ -682,6 +786,21 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crc"
|
||||
version = "3.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d"
|
||||
dependencies = [
|
||||
"crc-catalog",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crc-catalog"
|
||||
version = "2.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.5.0"
|
||||
@ -837,6 +956,12 @@ dependencies = [
|
||||
"parking_lot_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deflate64"
|
||||
version = "0.1.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac6b926516df9c60bfa16e107b21086399f8285a44ca9711344b9e553c5146e2"
|
||||
|
||||
[[package]]
|
||||
name = "deranged"
|
||||
version = "0.5.8"
|
||||
@ -847,6 +972,17 @@ dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_arbitrary"
|
||||
version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_more"
|
||||
version = "0.99.20"
|
||||
@ -1045,6 +1181,12 @@ version = "1.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7"
|
||||
|
||||
[[package]]
|
||||
name = "encode_unicode"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
|
||||
|
||||
[[package]]
|
||||
name = "encoding_rs"
|
||||
version = "0.8.35"
|
||||
@ -1154,6 +1296,26 @@ dependencies = [
|
||||
"rustc_version",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "file-id"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1fc6a637b6dc58414714eddd9170ff187ecb0933d4c7024d1abbd23a3cc26e9"
|
||||
dependencies = [
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "filetime"
|
||||
version = "0.2.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"libredox",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.9"
|
||||
@ -1224,6 +1386,15 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fsevent-sys"
|
||||
version = "4.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futf"
|
||||
version = "0.1.5"
|
||||
@ -1800,12 +1971,24 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http-range-header"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c"
|
||||
|
||||
[[package]]
|
||||
name = "httparse"
|
||||
version = "1.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
|
||||
|
||||
[[package]]
|
||||
name = "httpdate"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "1.9.0"
|
||||
@ -1819,6 +2002,7 @@ dependencies = [
|
||||
"http",
|
||||
"http-body",
|
||||
"httparse",
|
||||
"httpdate",
|
||||
"itoa",
|
||||
"pin-project-lite",
|
||||
"smallvec",
|
||||
@ -2037,6 +2221,19 @@ dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indicatif"
|
||||
version = "0.17.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "183b3088984b400f4cfac3620d5e076c84da5364016b4f49473de574b2586235"
|
||||
dependencies = [
|
||||
"console",
|
||||
"number_prefix",
|
||||
"portable-atomic",
|
||||
"unicode-width",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "infer"
|
||||
version = "0.19.0"
|
||||
@ -2046,6 +2243,26 @@ dependencies = [
|
||||
"cfb",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inotify"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fdd168d97690d0b8c412d6b6c10360277f4d7ee495c5d0d5d5fe0854923255cc"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"inotify-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inotify-sys"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inout"
|
||||
version = "0.1.4"
|
||||
@ -2055,6 +2272,15 @@ dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "instant"
|
||||
version = "0.1.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ipnet"
|
||||
version = "2.12.0"
|
||||
@ -2163,6 +2389,16 @@ dependencies = [
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jobserver"
|
||||
version = "0.1.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
|
||||
dependencies = [
|
||||
"getrandom 0.3.4",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.95"
|
||||
@ -2208,6 +2444,26 @@ dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kqueue"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a"
|
||||
dependencies = [
|
||||
"kqueue-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kqueue-sys"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kuchikiki"
|
||||
version = "0.8.8-speedreader"
|
||||
@ -2278,7 +2534,10 @@ version = "0.1.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c"
|
||||
dependencies = [
|
||||
"bitflags 2.11.1",
|
||||
"libc",
|
||||
"plain",
|
||||
"redox_syscall 0.7.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2320,6 +2579,27 @@ version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
|
||||
|
||||
[[package]]
|
||||
name = "lzma-rs"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "297e814c836ae64db86b36cf2a557ba54368d03f6afcd7d947c266692f71115e"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"crc",
|
||||
]
|
||||
|
||||
[[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"
|
||||
@ -2391,6 +2671,12 @@ version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5"
|
||||
|
||||
[[package]]
|
||||
name = "matchit"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.0"
|
||||
@ -2439,6 +2725,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"wasi 0.11.1+wasi-snapshot-preview1",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
@ -2471,6 +2758,7 @@ dependencies = [
|
||||
"aes-gcm",
|
||||
"anyhow",
|
||||
"async-stream",
|
||||
"axum",
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"chrono",
|
||||
@ -2479,6 +2767,8 @@ dependencies = [
|
||||
"futures-util",
|
||||
"hmac",
|
||||
"mime_guess",
|
||||
"notify",
|
||||
"notify-debouncer-full",
|
||||
"once_cell",
|
||||
"parking_lot",
|
||||
"pbkdf2",
|
||||
@ -2487,6 +2777,8 @@ dependencies = [
|
||||
"regex",
|
||||
"reqwest 0.12.28",
|
||||
"scraper",
|
||||
"self_update",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
@ -2501,12 +2793,15 @@ dependencies = [
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tower",
|
||||
"tower-http",
|
||||
"tracing",
|
||||
"tracing-appender",
|
||||
"tracing-subscriber",
|
||||
"urlencoding",
|
||||
"uuid",
|
||||
"windows 0.58.0",
|
||||
"zip",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2551,6 +2846,47 @@ version = "0.1.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb"
|
||||
|
||||
[[package]]
|
||||
name = "notify"
|
||||
version = "7.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c533b4c39709f9ba5005d8002048266593c1cfaf3c5f0739d5b8ab0c6c504009"
|
||||
dependencies = [
|
||||
"bitflags 2.11.1",
|
||||
"filetime",
|
||||
"fsevent-sys",
|
||||
"inotify",
|
||||
"kqueue",
|
||||
"libc",
|
||||
"log",
|
||||
"mio",
|
||||
"notify-types",
|
||||
"walkdir",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "notify-debouncer-full"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9dcf855483228259b2353f89e99df35fc639b2b2510d1166e4858e3f67ec1afb"
|
||||
dependencies = [
|
||||
"file-id",
|
||||
"log",
|
||||
"notify",
|
||||
"notify-types",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "notify-types"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "585d3cb5e12e01aed9e8a1f70d5c6b5e86fe2a6e48fc8cd0b3e0b8df6f6eb174"
|
||||
dependencies = [
|
||||
"instant",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nu-ansi-term"
|
||||
version = "0.50.3"
|
||||
@ -2597,6 +2933,12 @@ dependencies = [
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "number_prefix"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3"
|
||||
|
||||
[[package]]
|
||||
name = "objc2"
|
||||
version = "0.6.4"
|
||||
@ -2820,7 +3162,7 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
"redox_syscall 0.5.18",
|
||||
"smallvec",
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
@ -3080,6 +3422,12 @@ version = "0.3.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
|
||||
|
||||
[[package]]
|
||||
name = "plain"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
|
||||
|
||||
[[package]]
|
||||
name = "plist"
|
||||
version = "1.8.0"
|
||||
@ -3088,7 +3436,7 @@ checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"indexmap 2.14.0",
|
||||
"quick-xml",
|
||||
"quick-xml 0.38.4",
|
||||
"serde",
|
||||
"time",
|
||||
]
|
||||
@ -3132,6 +3480,12 @@ dependencies = [
|
||||
"universal-hash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic"
|
||||
version = "1.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
|
||||
|
||||
[[package]]
|
||||
name = "potential_utf"
|
||||
version = "0.1.5"
|
||||
@ -3256,6 +3610,15 @@ dependencies = [
|
||||
"psl-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.23.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "11bafc859c6815fbaffbbbf4229ecb767ac913fecb27f9ad4343662e9ef099ea"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.38.4"
|
||||
@ -3466,6 +3829,15 @@ dependencies = [
|
||||
"bitflags 2.11.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f450ad9c3b1da563fb6948a8e0fb0fb9269711c9c73d9ea1de5058c79c8d643a"
|
||||
dependencies = [
|
||||
"bitflags 2.11.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_users"
|
||||
version = "0.5.2"
|
||||
@ -3536,6 +3908,7 @@ dependencies = [
|
||||
"bytes",
|
||||
"cookie",
|
||||
"cookie_store",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"http",
|
||||
@ -3855,6 +4228,36 @@ dependencies = [
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "self-replace"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "03ec815b5eab420ab893f63393878d89c90fdd94c0bcc44c07abb8ad95552fb7"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"tempfile",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "self_update"
|
||||
version = "0.41.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "469a3970061380c19852269f393e74c0fe607a4e23d85267382cf25486aa8de5"
|
||||
dependencies = [
|
||||
"hyper",
|
||||
"indicatif",
|
||||
"log",
|
||||
"quick-xml 0.23.1",
|
||||
"regex",
|
||||
"reqwest 0.12.28",
|
||||
"self-replace",
|
||||
"semver",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
"urlencoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.28"
|
||||
@ -3931,6 +4334,17 @@ dependencies = [
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_path_to_error"
|
||||
version = "0.1.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_repr"
|
||||
version = "0.1.20"
|
||||
@ -4053,6 +4467,17 @@ dependencies = [
|
||||
"stable_deref_trait",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha1"
|
||||
version = "0.10.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha2"
|
||||
version = "0.10.9"
|
||||
@ -4176,7 +4601,7 @@ dependencies = [
|
||||
"objc2-foundation",
|
||||
"objc2-quartz-core",
|
||||
"raw-window-handle",
|
||||
"redox_syscall",
|
||||
"redox_syscall 0.5.18",
|
||||
"tracing",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
@ -5027,6 +5452,7 @@ dependencies = [
|
||||
"tokio",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -5043,13 +5469,19 @@ dependencies = [
|
||||
"http",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"http-range-header",
|
||||
"httpdate",
|
||||
"iri-string",
|
||||
"mime",
|
||||
"mime_guess",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tower",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -5070,6 +5502,7 @@ version = "0.1.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
|
||||
dependencies = [
|
||||
"log",
|
||||
"pin-project-lite",
|
||||
"tracing-attributes",
|
||||
"tracing-core",
|
||||
@ -6354,6 +6787,15 @@ dependencies = [
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xz2"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "388c44dc09d76f1536602ead6d325eb532f5c122f17782bd57fb47baeeb767e2"
|
||||
dependencies = [
|
||||
"lzma-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yoke"
|
||||
version = "0.8.2"
|
||||
@ -6484,6 +6926,20 @@ name = "zeroize"
|
||||
version = "1.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
|
||||
dependencies = [
|
||||
"zeroize_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeroize_derive"
|
||||
version = "1.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerotrie"
|
||||
@ -6518,12 +6974,82 @@ dependencies = [
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zip"
|
||||
version = "2.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fabe6324e908f85a1c52063ce7aa26b68dcb7eb6dbc83a2d148403c9bc3eba50"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"arbitrary",
|
||||
"bzip2",
|
||||
"constant_time_eq",
|
||||
"crc32fast",
|
||||
"crossbeam-utils",
|
||||
"deflate64",
|
||||
"displaydoc",
|
||||
"flate2",
|
||||
"getrandom 0.3.4",
|
||||
"hmac",
|
||||
"indexmap 2.14.0",
|
||||
"lzma-rs",
|
||||
"memchr",
|
||||
"pbkdf2",
|
||||
"sha1",
|
||||
"thiserror 2.0.18",
|
||||
"time",
|
||||
"xz2",
|
||||
"zeroize",
|
||||
"zopfli",
|
||||
"zstd",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||
|
||||
[[package]]
|
||||
name = "zopfli"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"crc32fast",
|
||||
"log",
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zstd"
|
||||
version = "0.13.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a"
|
||||
dependencies = [
|
||||
"zstd-safe",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zstd-safe"
|
||||
version = "7.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d"
|
||||
dependencies = [
|
||||
"zstd-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zstd-sys"
|
||||
version = "2.0.16+zstd.1.5.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zvariant"
|
||||
version = "5.10.0"
|
||||
|
||||
@ -58,6 +58,15 @@ tracing-appender = "0.2"
|
||||
|
||||
scraper = "0.20"
|
||||
|
||||
notify = "7"
|
||||
notify-debouncer-full = "0.4"
|
||||
axum = "0.7"
|
||||
tower = "0.5"
|
||||
tower-http = { version = "0.6", features = ["cors", "fs"] }
|
||||
self_update = { version = "0.41", default-features = false, features = ["rustls"] }
|
||||
semver = "1"
|
||||
zip = "2"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows = { version = "0.58", features = ["Win32_Foundation", "Win32_Security_Cryptography", "Win32_System_Memory"] }
|
||||
|
||||
|
||||
@ -1,23 +1,32 @@
|
||||
//! Tauri IPC command handlers.
|
||||
//!
|
||||
//! All functions are `async` so they don't block the main thread. Errors are
|
||||
//! serialized via `AppError`'s `Serialize` impl.
|
||||
//! Every method the v1 renderer calls on `window.api.*` has a command here so
|
||||
//! the copy-pasted v1 renderer keeps working unchanged. Features that aren't
|
||||
//! portable-in-a-session (folder monitor, remote server, auto-updater, drop
|
||||
//! target window) return sensible defaults as stubs instead of errors.
|
||||
|
||||
use crate::config::{Account, Config, ConfigStore, GlobalSettings, HosterSettings};
|
||||
use crate::error::{AppError, AppResult};
|
||||
use crate::secret;
|
||||
use crate::upload_manager::{Job, UploadManager};
|
||||
|
||||
use parking_lot::Mutex as PLMutex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{json, Value};
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tauri::State;
|
||||
use tauri::{AppHandle, Manager, State};
|
||||
|
||||
pub struct AppState {
|
||||
pub config: Arc<ConfigStore>,
|
||||
pub uploads: Arc<UploadManager>,
|
||||
pub folder_monitor: Arc<crate::folder_monitor::FolderMonitor>,
|
||||
pub remote_server: Arc<crate::remote_server::RemoteServer>,
|
||||
pub rot_log_path: Mutex<PathBuf>,
|
||||
pub upload_log_path: Mutex<PathBuf>,
|
||||
pub always_on_top: PLMutex<bool>,
|
||||
pub shutdown_mode: PLMutex<String>,
|
||||
}
|
||||
|
||||
// --- Config ---
|
||||
@ -33,7 +42,7 @@ pub async fn save_config(state: State<'_, AppState>, config: Config) -> AppResul
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_history(state: State<'_, AppState>) -> AppResult<Vec<serde_json::Value>> {
|
||||
pub async fn get_history(state: State<'_, AppState>) -> AppResult<Vec<Value>> {
|
||||
Ok(state.config.load()?.history)
|
||||
}
|
||||
|
||||
@ -42,64 +51,217 @@ pub async fn clear_history(state: State<'_, AppState>) -> AppResult<()> {
|
||||
state.config.clear_history().await
|
||||
}
|
||||
|
||||
// --- Backup ---
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn export_backup(
|
||||
state: State<'_, AppState>,
|
||||
target_path: String,
|
||||
) -> AppResult<String> {
|
||||
let cfg = state.config.load()?;
|
||||
let json = serde_json::to_vec(&cfg)?;
|
||||
let encrypted = secret::encrypt_backup(&json);
|
||||
tokio::fs::write(&target_path, &encrypted).await?;
|
||||
Ok(target_path)
|
||||
pub async fn export_history(_format: Option<String>) -> AppResult<Value> {
|
||||
// Trimmed implementation — renderer already falls back to save_text_file.
|
||||
Ok(json!({ "ok": false, "error": "use save_text_file from renderer" }))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn import_backup(
|
||||
state: State<'_, AppState>,
|
||||
source_path: String,
|
||||
legacy_password: Option<String>,
|
||||
) -> AppResult<Config> {
|
||||
let buf = tokio::fs::read(&source_path).await?;
|
||||
let plain = secret::decrypt_backup(&buf, legacy_password.as_deref().map(|s| s.as_bytes()))
|
||||
.map_err(|e| {
|
||||
if e == "needs-password" {
|
||||
AppError::Other("needs-password".into())
|
||||
} else {
|
||||
AppError::Other(e)
|
||||
}
|
||||
})?;
|
||||
let imported: Config = serde_json::from_slice(&plain)?;
|
||||
state.config.save(&imported).await?;
|
||||
Ok(imported)
|
||||
}
|
||||
|
||||
// --- Upload control ---
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct StartBatchPayload {
|
||||
pub jobs: Vec<Job>,
|
||||
#[serde(default)]
|
||||
pub hoster_settings: HashMap<String, HosterSettings>,
|
||||
#[serde(default)]
|
||||
pub global_settings: GlobalSettings,
|
||||
#[serde(default)]
|
||||
pub accounts: HashMap<String, Vec<Account>>,
|
||||
pub async fn get_hoster_settings(state: State<'_, AppState>) -> AppResult<HashMap<String, HosterSettings>> {
|
||||
Ok(state.config.load()?.hoster_settings)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn start_batch(
|
||||
pub async fn save_hoster_settings(
|
||||
state: State<'_, AppState>,
|
||||
payload: StartBatchPayload,
|
||||
settings: HashMap<String, HosterSettings>,
|
||||
) -> AppResult<()> {
|
||||
state.config.save_hoster_settings(settings).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_global_settings(state: State<'_, AppState>) -> AppResult<GlobalSettings> {
|
||||
Ok(state.config.load()?.global_settings)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn save_global_settings(
|
||||
state: State<'_, AppState>,
|
||||
settings: GlobalSettings,
|
||||
) -> AppResult<()> {
|
||||
state.config.save_global(settings).await
|
||||
}
|
||||
|
||||
// --- Window state ---
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn set_always_on_top(
|
||||
app: AppHandle,
|
||||
state: State<'_, AppState>,
|
||||
value: bool,
|
||||
) -> AppResult<()> {
|
||||
*state.always_on_top.lock() = value;
|
||||
if let Some(w) = app.get_webview_window("main") {
|
||||
let _ = w.set_always_on_top(value);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_always_on_top(state: State<'_, AppState>) -> AppResult<bool> {
|
||||
Ok(*state.always_on_top.lock())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn set_shutdown_after_finish(state: State<'_, AppState>, mode: String) -> AppResult<()> {
|
||||
*state.shutdown_mode.lock() = mode;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_shutdown_after_finish(state: State<'_, AppState>) -> AppResult<String> {
|
||||
Ok(state.shutdown_mode.lock().clone())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn cancel_shutdown(state: State<'_, AppState>) -> AppResult<()> {
|
||||
*state.shutdown_mode.lock() = "nothing".into();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// --- Folder walk / clipboard ---
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn resolve_folder_files(folder_path: String) -> AppResult<Vec<String>> {
|
||||
let mut out = Vec::new();
|
||||
fn walk(dir: &Path, out: &mut Vec<String>) {
|
||||
let Ok(rd) = std::fs::read_dir(dir) else { return };
|
||||
for entry in rd.flatten() {
|
||||
let p = entry.path();
|
||||
if p.is_dir() {
|
||||
walk(&p, out);
|
||||
} else if p.is_file() {
|
||||
if let Some(s) = p.to_str() { out.push(s.to_string()); }
|
||||
}
|
||||
}
|
||||
}
|
||||
walk(Path::new(&folder_path), &mut out);
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn copy_to_clipboard(app: AppHandle, text: String) -> AppResult<()> {
|
||||
// Tauri 2: use the window's webview to set clipboard via JS fallback if plugin
|
||||
// isn't present. We write to the OS clipboard through a small PowerShell call.
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
use std::process::Command;
|
||||
use std::io::Write;
|
||||
let mut child = Command::new("powershell")
|
||||
.args(["-NoProfile", "-Command", "Set-Clipboard -Value ([Console]::In.ReadToEnd())"])
|
||||
.stdin(std::process::Stdio::piped())
|
||||
.spawn()
|
||||
.map_err(|e| AppError::Other(format!("clip: {e}")))?;
|
||||
if let Some(mut stdin) = child.stdin.take() {
|
||||
let _ = stdin.write_all(text.as_bytes());
|
||||
}
|
||||
let _ = child.wait();
|
||||
let _ = app;
|
||||
return Ok(());
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{ let _ = (app, text); Ok(()) }
|
||||
}
|
||||
|
||||
// --- Uploads ---
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct StartUploadPayload {
|
||||
#[serde(default)]
|
||||
pub hosters: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub jobs: Vec<StartUploadJob>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct StartUploadJob {
|
||||
pub id: String,
|
||||
pub file: String,
|
||||
pub file_name: String,
|
||||
pub hoster: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct StartUploadResult {
|
||||
pub skipped_jobs: Vec<SkippedJob>,
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SkippedJob {
|
||||
pub job_id: String,
|
||||
pub hoster: String,
|
||||
pub reason: String,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn start_upload(
|
||||
state: State<'_, AppState>,
|
||||
payload: StartUploadPayload,
|
||||
) -> AppResult<StartUploadResult> {
|
||||
let cfg = state.config.load()?;
|
||||
let mut jobs = Vec::new();
|
||||
let mut skipped = Vec::new();
|
||||
for j in &payload.jobs {
|
||||
let accounts = cfg.hosters.get(&j.hoster).cloned().unwrap_or_default();
|
||||
let primary = accounts.iter().find(|a| a.enabled && has_creds(&j.hoster, a));
|
||||
match primary {
|
||||
Some(a) => {
|
||||
jobs.push(Job {
|
||||
id: j.id.clone(),
|
||||
upload_id: format!("u-{}", j.id),
|
||||
file: PathBuf::from(&j.file),
|
||||
file_name: j.file_name.clone(),
|
||||
hoster: j.hoster.clone(),
|
||||
account_id: a.id.clone(),
|
||||
username: a.username.clone(),
|
||||
password: a.password.clone(),
|
||||
api_key: a.api_key.clone(),
|
||||
max_attempts: cfg.hoster_settings.get(&j.hoster)
|
||||
.map(|s| s.retries).unwrap_or(3),
|
||||
});
|
||||
}
|
||||
None => {
|
||||
skipped.push(SkippedJob {
|
||||
job_id: j.id.clone(),
|
||||
hoster: j.hoster.clone(),
|
||||
reason: "Kein gültiger Account für diesen Hoster".into(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if jobs.is_empty() {
|
||||
return Ok(StartUploadResult {
|
||||
skipped_jobs: skipped,
|
||||
error: Some("Keine gültigen Zugangsdaten für die gewählten Hoster.".into()),
|
||||
});
|
||||
}
|
||||
|
||||
state.uploads.update_settings(
|
||||
payload.hoster_settings,
|
||||
payload.global_settings,
|
||||
payload.accounts,
|
||||
cfg.hoster_settings.clone(),
|
||||
cfg.global_settings.clone(),
|
||||
cfg.hosters.clone(),
|
||||
);
|
||||
state.uploads.clone().start_batch(payload.jobs).await
|
||||
|
||||
// Spawn the batch so the command returns immediately — the renderer
|
||||
// listens on upload-progress / upload-batch-done.
|
||||
let mgr = state.uploads.clone();
|
||||
tokio::spawn(async move { let _ = mgr.start_batch(jobs).await; });
|
||||
|
||||
Ok(StartUploadResult { skipped_jobs: skipped, error: None })
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn add_jobs_to_batch(
|
||||
_state: State<'_, AppState>,
|
||||
_payload: StartUploadPayload,
|
||||
) -> AppResult<Value> {
|
||||
Ok(json!({ "added": 0, "alreadyInBatchJobIds": [] }))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@ -110,39 +272,33 @@ pub async fn cancel_batch(state: State<'_, AppState>) -> AppResult<()> {
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn cancel_jobs(_state: State<'_, AppState>, _job_ids: Vec<String>) -> AppResult<()> {
|
||||
// Per-job cancel maps to the upload_manager's `cancel_jobs` helper. TODO:
|
||||
// wire individual AbortController-equivalents per job; for v2.0 POC we
|
||||
// offer batch-wide cancel only.
|
||||
// POC: full cancel only; per-job needs individual AbortControllers.
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn add_jobs(
|
||||
state: State<'_, AppState>,
|
||||
jobs: Vec<Job>,
|
||||
) -> AppResult<usize> {
|
||||
// TODO: live add during running batch (v1 upload-manager.js `addJobs`).
|
||||
// For POC, reject if running.
|
||||
if state.uploads.is_running() {
|
||||
return Err(AppError::Other("Laufendem Batch noch keine Jobs hinzuzufügen (2.0 POC)".into()));
|
||||
}
|
||||
Ok(jobs.len())
|
||||
pub async fn finish_after_active(state: State<'_, AppState>) -> AppResult<()> {
|
||||
state.uploads.finish_after_active();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// --- Health check ---
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct HealthCheckPayload {
|
||||
pub hosters: Vec<HealthCheckTarget>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct HealthCheckTarget {
|
||||
pub hoster: String,
|
||||
pub account_id: String,
|
||||
pub account_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct HealthCheckResult {
|
||||
pub account_id: String,
|
||||
pub hoster: String,
|
||||
@ -154,98 +310,275 @@ pub struct HealthCheckResult {
|
||||
pub async fn run_health_check(
|
||||
state: State<'_, AppState>,
|
||||
payload: HealthCheckPayload,
|
||||
) -> AppResult<Vec<HealthCheckResult>> {
|
||||
) -> AppResult<Value> {
|
||||
let cfg = state.config.load()?;
|
||||
let mut out = Vec::new();
|
||||
for target in payload.hosters {
|
||||
let accounts = cfg.hosters.get(&target.hoster);
|
||||
let account = accounts.and_then(|v| v.iter().find(|a| a.id == target.account_id));
|
||||
let (status, message) = match account {
|
||||
None => ("error", "Account nicht gefunden".into()),
|
||||
Some(a) => match check_account_live(&target.hoster, a).await {
|
||||
Ok(msg) => ("ok", msg),
|
||||
Err(e) => ("error", e.user_message()),
|
||||
let mut results = Vec::new();
|
||||
for target in &payload.hosters {
|
||||
let accounts = cfg.hosters.get(&target.hoster).cloned().unwrap_or_default();
|
||||
let matching: Vec<_> = accounts.into_iter()
|
||||
.filter(|a| target.account_id.as_ref()
|
||||
.map(|id| a.id == *id).unwrap_or(true))
|
||||
.collect();
|
||||
for a in matching {
|
||||
let (status, message) = match check_account_live(&target.hoster, &a).await {
|
||||
Ok(m) => ("ok", m),
|
||||
Err(e) => if e.is_account_specific() {
|
||||
("error", e.user_message())
|
||||
} else {
|
||||
("warn", e.user_message())
|
||||
},
|
||||
};
|
||||
out.push(HealthCheckResult {
|
||||
account_id: target.account_id,
|
||||
hoster: target.hoster,
|
||||
results.push(HealthCheckResult {
|
||||
account_id: a.id,
|
||||
hoster: target.hoster.clone(),
|
||||
status: status.into(),
|
||||
message: message.to_string(),
|
||||
});
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
Ok(json!({ "results": results }))
|
||||
}
|
||||
|
||||
async fn check_account_live(hoster: &str, a: &Account) -> AppResult<String> {
|
||||
match hoster {
|
||||
"clouddrop.cc" => {
|
||||
if a.api_key.is_empty() { return Err(AppError::BadCredentials); }
|
||||
// GET /api/cloud/files/?limit=1 with bearer token
|
||||
let c = reqwest::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(15))
|
||||
.build()?;
|
||||
let resp = c.get("https://clouddrop.cc/api/cloud/files/?limit=1")
|
||||
.bearer_auth(&a.api_key)
|
||||
.header("Accept", "application/json")
|
||||
.send().await?;
|
||||
if resp.status().is_success() {
|
||||
Ok("API Key gültig".into())
|
||||
} else {
|
||||
Err(AppError::HosterError("Clouddrop".into(),
|
||||
format!("HTTP {}", resp.status().as_u16())))
|
||||
}
|
||||
let c = reqwest::Client::builder().timeout(std::time::Duration::from_secs(15)).build()?;
|
||||
let r = c.get("https://clouddrop.cc/api/cloud/files/?limit=1").bearer_auth(&a.api_key).send().await?;
|
||||
if r.status().is_success() { Ok("API Key gültig".into()) } else { Err(AppError::HosterError("Clouddrop".into(), format!("HTTP {}", r.status().as_u16()))) }
|
||||
}
|
||||
"byse.sx" => {
|
||||
if a.api_key.is_empty() { return Err(AppError::BadCredentials); }
|
||||
let c = reqwest::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(15))
|
||||
.build()?;
|
||||
let resp = c.get(format!("https://api.byse.sx/api/account/info?key={}",
|
||||
urlencoding::encode(&a.api_key)))
|
||||
.header("Accept", "application/json")
|
||||
.send().await?;
|
||||
if resp.status().is_success() {
|
||||
Ok("API Key gültig".into())
|
||||
let c = reqwest::Client::builder().timeout(std::time::Duration::from_secs(15)).build()?;
|
||||
let r = c.get(format!("https://api.byse.sx/api/account/info?key={}", urlencoding::encode(&a.api_key))).send().await?;
|
||||
if r.status().is_success() { Ok("API Key gültig".into()) } else { Err(AppError::HosterError("Byse".into(), format!("HTTP {}", r.status().as_u16()))) }
|
||||
}
|
||||
_ => {
|
||||
if a.auth_type == "login" {
|
||||
if a.username.is_empty() || a.password.is_empty() { return Err(AppError::BadCredentials); }
|
||||
Ok("Login hinterlegt".into())
|
||||
} else if a.auth_type == "api" {
|
||||
if a.api_key.is_empty() { return Err(AppError::BadCredentials); }
|
||||
Ok("API Key hinterlegt".into())
|
||||
} else {
|
||||
Err(AppError::HosterError("Byse".into(),
|
||||
format!("HTTP {}", resp.status().as_u16())))
|
||||
Ok("Nicht geprüft".into())
|
||||
}
|
||||
}
|
||||
"vidmoly.me" => {
|
||||
if a.username.is_empty() || a.password.is_empty() {
|
||||
return Err(AppError::BadCredentials);
|
||||
}
|
||||
// POC: assume creds valid unless we actually want to hit login here.
|
||||
Ok("Login hinterlegt (nicht geprüft)".into())
|
||||
}
|
||||
_ => Ok("Nicht implementiert".into()),
|
||||
}
|
||||
}
|
||||
|
||||
// --- Log folder ---
|
||||
// --- Backup ---
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn export_backup(state: State<'_, AppState>, target_path: String) -> AppResult<String> {
|
||||
let cfg = state.config.load()?;
|
||||
let json = serde_json::to_vec(&cfg)?;
|
||||
let encrypted = secret::encrypt_backup(&json);
|
||||
tokio::fs::write(&target_path, &encrypted).await?;
|
||||
Ok(target_path)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn import_backup(state: State<'_, AppState>, source_path: String) -> AppResult<Config> {
|
||||
let buf = tokio::fs::read(&source_path).await?;
|
||||
*LAST_IMPORT_PATH.lock().unwrap() = Some(source_path);
|
||||
let plain = secret::decrypt_backup(&buf, None).map_err(AppError::Other)?;
|
||||
let imported: Config = serde_json::from_slice(&plain)?;
|
||||
state.config.save(&imported).await?;
|
||||
Ok(imported)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn import_backup_saved(
|
||||
state: State<'_, AppState>,
|
||||
legacy_password: String,
|
||||
) -> AppResult<Config> {
|
||||
let path = LAST_IMPORT_PATH.lock().unwrap().clone()
|
||||
.ok_or_else(|| AppError::Other("Kein Pfad gemerkt".into()))?;
|
||||
let buf = tokio::fs::read(&path).await?;
|
||||
let plain = secret::decrypt_backup(&buf, Some(legacy_password.as_bytes())).map_err(AppError::Other)?;
|
||||
let imported: Config = serde_json::from_slice(&plain)?;
|
||||
state.config.save(&imported).await?;
|
||||
*LAST_IMPORT_PATH.lock().unwrap() = None;
|
||||
Ok(imported)
|
||||
}
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
static LAST_IMPORT_PATH: Lazy<Mutex<Option<String>>> = Lazy::new(|| Mutex::new(None));
|
||||
|
||||
// --- Log / files ---
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn read_own_upload_log(state: State<'_, AppState>) -> AppResult<Value> {
|
||||
let path = state.upload_log_path.lock().unwrap().clone();
|
||||
let content = tokio::fs::read_to_string(&path).await.unwrap_or_default();
|
||||
let mut entries = Vec::new();
|
||||
for line in content.lines() {
|
||||
let parts: Vec<&str> = line.split('|').collect();
|
||||
if parts.len() >= 5 {
|
||||
entries.push(json!({
|
||||
"timestamp": parts[0],
|
||||
"hoster": parts[1],
|
||||
"link": parts[2],
|
||||
"fileName": parts[4],
|
||||
}));
|
||||
}
|
||||
}
|
||||
Ok(json!({ "entries": entries, "path": path.display().to_string() }))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn import_upload_log() -> AppResult<Value> {
|
||||
// Renderer can use dialog.open to pick a file, then save_text_file to process.
|
||||
Ok(json!({ "ok": false, "todo": true }))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn save_text_file(path: String, content: String) -> AppResult<()> {
|
||||
if let Some(parent) = Path::new(&path).parent() { let _ = std::fs::create_dir_all(parent); }
|
||||
tokio::fs::write(&path, content).await.map_err(AppError::from)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn open_log_folder(state: State<'_, AppState>) -> AppResult<()> {
|
||||
let path = state.upload_log_path.lock().unwrap().clone();
|
||||
let parent = path.parent().unwrap_or_else(|| std::path::Path::new(".")).to_path_buf();
|
||||
let parent = path.parent().unwrap_or_else(|| Path::new(".")).to_path_buf();
|
||||
let _ = std::fs::create_dir_all(&parent);
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
std::process::Command::new("explorer").arg(&parent).spawn().ok();
|
||||
}
|
||||
{ std::process::Command::new("explorer").arg(&parent).spawn().ok(); }
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
std::process::Command::new("xdg-open").arg(&parent).spawn().ok();
|
||||
}
|
||||
{ std::process::Command::new("xdg-open").arg(&parent).spawn().ok(); }
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn read_rotation_log(state: State<'_, AppState>) -> AppResult<String> {
|
||||
let path = state.rot_log_path.lock().unwrap().clone();
|
||||
match tokio::fs::read_to_string(&path).await {
|
||||
Ok(s) => Ok(s),
|
||||
Err(_) => Ok(String::new()),
|
||||
Ok(tokio::fs::read_to_string(&path).await.unwrap_or_default())
|
||||
}
|
||||
|
||||
// --- Update / version ---
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_version() -> AppResult<String> {
|
||||
Ok(env!("CARGO_PKG_VERSION").to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn check_for_update() -> AppResult<Value> {
|
||||
let c = crate::updater::check().await;
|
||||
Ok(serde_json::to_value(&c)?)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn install_update() -> AppResult<()> {
|
||||
// MVP: open the release page in the OS browser — user downloads installer
|
||||
// and runs it manually. True in-app update with signing cert is follow-up.
|
||||
let c = crate::updater::check().await;
|
||||
if let Some(url) = c.download_url {
|
||||
#[cfg(target_os = "windows")]
|
||||
{ std::process::Command::new("cmd").args(["/c", "start", "", &url]).spawn().ok(); }
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{ std::process::Command::new("xdg-open").arg(&url).spawn().ok(); }
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// --- Folder monitor ---
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn folder_monitor_start(
|
||||
state: State<'_, AppState>,
|
||||
settings: Value,
|
||||
) -> AppResult<()> {
|
||||
let s: crate::folder_monitor::FolderMonitorSettings = serde_json::from_value(settings)
|
||||
.map_err(|e| AppError::BadConfig(e.to_string()))?;
|
||||
state.folder_monitor.start(s).await.map_err(AppError::Other)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn folder_monitor_stop(state: State<'_, AppState>) -> AppResult<()> {
|
||||
state.folder_monitor.stop().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn folder_monitor_status(state: State<'_, AppState>) -> AppResult<Value> {
|
||||
let running = state.folder_monitor.is_running();
|
||||
let current = state.folder_monitor.current();
|
||||
Ok(serde_json::to_value(serde_json::json!({
|
||||
"running": running,
|
||||
"settings": current,
|
||||
}))?)
|
||||
}
|
||||
|
||||
// --- Remote control ---
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn remote_get_settings(state: State<'_, AppState>) -> AppResult<Value> {
|
||||
let cfg = state.config.load()?;
|
||||
Ok(serde_json::to_value(&cfg.global_settings.remote)?)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn remote_save_settings(
|
||||
state: State<'_, AppState>,
|
||||
settings: Value,
|
||||
) -> AppResult<()> {
|
||||
let mut cfg = state.config.load()?;
|
||||
let remote: crate::config::RemoteSettings = serde_json::from_value(settings)
|
||||
.map_err(|e| AppError::BadConfig(e.to_string()))?;
|
||||
cfg.global_settings.remote = remote.clone();
|
||||
state.config.save_global(cfg.global_settings).await?;
|
||||
if remote.enabled && !remote.token.is_empty() {
|
||||
state.remote_server.start(remote.port, remote.token).await
|
||||
.map_err(AppError::Other)?;
|
||||
} else {
|
||||
state.remote_server.stop().await;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn remote_generate_token() -> AppResult<String> {
|
||||
use rand::RngCore;
|
||||
let mut buf = [0u8; 16];
|
||||
rand::thread_rng().fill_bytes(&mut buf);
|
||||
Ok(buf.iter().map(|b| format!("{b:02x}")).collect())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn remote_status(state: State<'_, AppState>) -> AppResult<Value> {
|
||||
let cfg = state.config.load()?;
|
||||
Ok(serde_json::json!({
|
||||
"running": state.remote_server.is_running(),
|
||||
"port": cfg.global_settings.remote.port,
|
||||
"enabled": cfg.global_settings.remote.enabled,
|
||||
}))
|
||||
}
|
||||
|
||||
// --- Drop target window (stub) ---
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn show_drop_target() -> AppResult<()> { Ok(()) }
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn hide_drop_target() -> AppResult<()> { Ok(()) }
|
||||
|
||||
// --- Debug ---
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn debug_log(msg: String) -> AppResult<()> {
|
||||
tracing::info!(target: "renderer", "{msg}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// --- Helpers ---
|
||||
|
||||
fn has_creds(_hoster: &str, a: &Account) -> bool {
|
||||
match a.auth_type.as_str() {
|
||||
"api" => !a.api_key.is_empty(),
|
||||
"login" => !a.username.is_empty() && !a.password.is_empty(),
|
||||
_ => !a.api_key.is_empty() || (!a.username.is_empty() && !a.password.is_empty()),
|
||||
}
|
||||
}
|
||||
|
||||
181
src-tauri/src/folder_monitor.rs
Normal file
181
src-tauri/src/folder_monitor.rs
Normal file
@ -0,0 +1,181 @@
|
||||
//! Folder monitor — watches a directory for new files and emits
|
||||
//! `folder-monitor-new-files` with absolute paths to the renderer.
|
||||
//!
|
||||
//! Reflects the v1 `lib/folder-monitor.js` design: debounced notify events,
|
||||
//! extension include/exclude filter, skip-duplicates against a seen-set,
|
||||
//! optional recursive watch, initial scan on start.
|
||||
|
||||
use notify::{EventKind, RecursiveMode, Watcher};
|
||||
use notify_debouncer_full::{new_debouncer, DebouncedEvent, DebounceEventResult};
|
||||
use parking_lot::Mutex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashSet;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tauri::{AppHandle, Emitter};
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct FolderMonitorSettings {
|
||||
pub enabled: bool,
|
||||
pub folder_path: String,
|
||||
#[serde(default)]
|
||||
pub recursive: bool,
|
||||
#[serde(default = "default_filter_mode")]
|
||||
pub filter_mode: String,
|
||||
#[serde(default)]
|
||||
pub extensions: String,
|
||||
#[serde(default = "truthy")]
|
||||
pub skip_duplicates: bool,
|
||||
#[serde(default = "default_delay")]
|
||||
pub delay_sec: u64,
|
||||
}
|
||||
fn default_filter_mode() -> String { "include".into() }
|
||||
fn truthy() -> bool { true }
|
||||
fn default_delay() -> u64 { 3 }
|
||||
|
||||
pub struct FolderMonitor {
|
||||
app: AppHandle,
|
||||
stop_tx: Mutex<Option<mpsc::Sender<()>>>,
|
||||
current: Mutex<Option<FolderMonitorSettings>>,
|
||||
seen: Arc<Mutex<HashSet<PathBuf>>>,
|
||||
}
|
||||
|
||||
impl FolderMonitor {
|
||||
pub fn new(app: AppHandle) -> Self {
|
||||
Self {
|
||||
app,
|
||||
stop_tx: Mutex::new(None),
|
||||
current: Mutex::new(None),
|
||||
seen: Arc::new(Mutex::new(HashSet::new())),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_running(&self) -> bool {
|
||||
self.stop_tx.lock().is_some()
|
||||
}
|
||||
|
||||
pub fn current(&self) -> Option<FolderMonitorSettings> {
|
||||
self.current.lock().clone()
|
||||
}
|
||||
|
||||
pub async fn start(&self, settings: FolderMonitorSettings) -> Result<(), String> {
|
||||
self.stop().await;
|
||||
if !settings.enabled || settings.folder_path.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
let root = PathBuf::from(&settings.folder_path);
|
||||
if !root.exists() {
|
||||
return Err(format!("Ordner existiert nicht: {}", root.display()));
|
||||
}
|
||||
let recursive = if settings.recursive { RecursiveMode::Recursive } else { RecursiveMode::NonRecursive };
|
||||
let extensions = parse_extensions(&settings.extensions);
|
||||
let filter_include = settings.filter_mode == "include";
|
||||
let skip_dup = settings.skip_duplicates;
|
||||
let delay = Duration::from_secs(settings.delay_sec.max(1));
|
||||
|
||||
let seen = self.seen.clone();
|
||||
|
||||
// Seed baseline so existing files don't get queued on startup.
|
||||
if skip_dup {
|
||||
let mut s = seen.lock();
|
||||
s.clear();
|
||||
walk_collect(&root, settings.recursive, &mut s);
|
||||
}
|
||||
|
||||
let (stop_tx, mut stop_rx) = mpsc::channel::<()>(1);
|
||||
let (event_tx, mut event_rx) = mpsc::channel::<Vec<PathBuf>>(32);
|
||||
|
||||
let app = self.app.clone();
|
||||
tokio::spawn(async move {
|
||||
while let Some(paths) = event_rx.recv().await {
|
||||
if paths.is_empty() { continue; }
|
||||
let filtered: Vec<String> = paths.into_iter()
|
||||
.filter(|p| path_matches(p, &extensions, filter_include))
|
||||
.filter_map(|p| p.to_str().map(|s| s.to_string()))
|
||||
.collect();
|
||||
if filtered.is_empty() { continue; }
|
||||
let _ = app.emit("folder-monitor-new-files", filtered);
|
||||
}
|
||||
});
|
||||
|
||||
let event_tx_cloned = event_tx.clone();
|
||||
let seen_cloned = seen.clone();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let (debounce_tx, debounce_rx) = std::sync::mpsc::channel::<DebounceEventResult>();
|
||||
let mut debouncer = match new_debouncer(
|
||||
delay,
|
||||
None,
|
||||
move |res: DebounceEventResult| { let _ = debounce_tx.send(res); },
|
||||
) {
|
||||
Ok(d) => d,
|
||||
Err(e) => { tracing::error!("folder-monitor debouncer: {e}"); return; }
|
||||
};
|
||||
if let Err(e) = debouncer.watch(&root, recursive) {
|
||||
tracing::error!("folder-monitor watch: {e}");
|
||||
return;
|
||||
}
|
||||
loop {
|
||||
match debounce_rx.recv_timeout(Duration::from_millis(250)) {
|
||||
Ok(Ok(events)) => {
|
||||
let mut new_paths = Vec::new();
|
||||
for ev in events {
|
||||
if matches!(ev.event.kind, EventKind::Create(_) | EventKind::Modify(_)) {
|
||||
for p in &ev.paths {
|
||||
if p.is_file() {
|
||||
let mut s = seen_cloned.lock();
|
||||
if skip_dup && !s.insert(p.clone()) { continue; }
|
||||
new_paths.push(p.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !new_paths.is_empty() {
|
||||
let _ = event_tx_cloned.blocking_send(new_paths);
|
||||
}
|
||||
}
|
||||
Ok(Err(_)) => {}
|
||||
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
|
||||
if stop_rx.try_recv().is_ok() { break; }
|
||||
}
|
||||
Err(_) => break,
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
*self.stop_tx.lock() = Some(stop_tx);
|
||||
*self.current.lock() = Some(settings);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn stop(&self) {
|
||||
let tx = self.stop_tx.lock().take();
|
||||
if let Some(tx) = tx { let _ = tx.send(()).await; }
|
||||
*self.current.lock() = None;
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_extensions(s: &str) -> Vec<String> {
|
||||
s.split(',')
|
||||
.map(|x| x.trim().trim_start_matches('.').to_lowercase())
|
||||
.filter(|x| !x.is_empty())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn path_matches(p: &Path, extensions: &[String], include: bool) -> bool {
|
||||
if extensions.is_empty() { return true; }
|
||||
let ext = p.extension().and_then(|e| e.to_str()).map(|e| e.to_lowercase()).unwrap_or_default();
|
||||
let listed = extensions.iter().any(|e| *e == ext);
|
||||
if include { listed } else { !listed }
|
||||
}
|
||||
|
||||
fn walk_collect(dir: &Path, recursive: bool, out: &mut HashSet<PathBuf>) {
|
||||
let Ok(rd) = std::fs::read_dir(dir) else { return };
|
||||
for entry in rd.flatten() {
|
||||
let p = entry.path();
|
||||
if p.is_file() { out.insert(p); }
|
||||
else if p.is_dir() && recursive { walk_collect(&p, recursive, out); }
|
||||
}
|
||||
}
|
||||
@ -1,15 +1,4 @@
|
||||
// Multi-Hoster-Upload 2.0 — Tauri 2 / Rust port of the Electron original.
|
||||
//
|
||||
// Module layout
|
||||
// =============
|
||||
// - `error` Unified error type used across the backend.
|
||||
// - `secret` AES-GCM credential encryption (compatible with v1 .mhu backup format).
|
||||
// - `config` Persistent config with atomic writes + write queue.
|
||||
// - `throttle` Token-bucket bandwidth limiter.
|
||||
// - `events` Shared DTOs emitted to the frontend (progress, rot-log, batch-done, ...).
|
||||
// - `hosters` One module per hoster + shared XFS helpers.
|
||||
// - `upload_manager` Orchestrates batches, concurrency, rotation, retries.
|
||||
// - `commands` Tauri #[command] handlers — the IPC bridge to the frontend.
|
||||
|
||||
pub mod error;
|
||||
pub mod events;
|
||||
@ -18,14 +7,17 @@ pub mod config;
|
||||
pub mod throttle;
|
||||
pub mod hosters;
|
||||
pub mod upload_manager;
|
||||
pub mod folder_monitor;
|
||||
pub mod remote_server;
|
||||
pub mod updater;
|
||||
pub mod upload_log;
|
||||
pub mod commands;
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tauri::Manager;
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
// Logging → stdout in dev, file in release (written under app_log_dir).
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(
|
||||
tracing_subscriber::EnvFilter::try_from_default_env()
|
||||
@ -41,7 +33,6 @@ pub fn run() {
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.plugin(tauri_plugin_fs::init())
|
||||
.setup(|app| {
|
||||
// Resolve app data dir + load config; inject shared state.
|
||||
let data_dir = app.path().app_data_dir()
|
||||
.expect("app_data_dir not available")
|
||||
.to_path_buf();
|
||||
@ -51,31 +42,93 @@ pub fn run() {
|
||||
config::ConfigStore::new(data_dir.join("config.json"))
|
||||
.expect("config store init failed"),
|
||||
);
|
||||
|
||||
let manager = Arc::new(upload_manager::UploadManager::new(app.handle().clone()));
|
||||
let fm = Arc::new(folder_monitor::FolderMonitor::new(app.handle().clone()));
|
||||
let remote = Arc::new(remote_server::RemoteServer::new(manager.clone()));
|
||||
let log_writer = upload_log::UploadLogWriter::new(app.handle().clone());
|
||||
*upload_log::WRITER.lock() = Some(log_writer.clone());
|
||||
manager.set_upload_log_writer(log_writer);
|
||||
|
||||
app.manage(commands::AppState {
|
||||
config: store,
|
||||
config: store.clone(),
|
||||
uploads: manager,
|
||||
rot_log_path: std::sync::Mutex::new(data_dir.join("account-rotation.log")),
|
||||
upload_log_path: std::sync::Mutex::new(data_dir.join("fileuploader.log")),
|
||||
folder_monitor: fm.clone(),
|
||||
remote_server: remote.clone(),
|
||||
rot_log_path: Mutex::new(data_dir.join("account-rotation.log")),
|
||||
upload_log_path: Mutex::new(data_dir.join("fileuploader.log")),
|
||||
always_on_top: parking_lot::Mutex::new(false),
|
||||
shutdown_mode: parking_lot::Mutex::new("nothing".into()),
|
||||
});
|
||||
|
||||
// Restore folder-monitor + remote-server if configured.
|
||||
if let Ok(cfg) = store.load() {
|
||||
let gs = cfg.global_settings.clone();
|
||||
if gs.folder_monitor.enabled && !gs.folder_monitor.folder_path.is_empty() {
|
||||
let settings = folder_monitor::FolderMonitorSettings {
|
||||
enabled: true,
|
||||
folder_path: gs.folder_monitor.folder_path.clone(),
|
||||
recursive: gs.folder_monitor.recursive,
|
||||
filter_mode: gs.folder_monitor.filter_mode.clone(),
|
||||
extensions: gs.folder_monitor.extensions.clone(),
|
||||
skip_duplicates: gs.folder_monitor.skip_duplicates,
|
||||
delay_sec: gs.folder_monitor.delay_sec,
|
||||
};
|
||||
let fm_clone = fm.clone();
|
||||
tokio::spawn(async move { let _ = fm_clone.start(settings).await; });
|
||||
}
|
||||
if gs.remote.enabled && !gs.remote.token.is_empty() {
|
||||
let rs = remote.clone();
|
||||
let port = gs.remote.port;
|
||||
let tok = gs.remote.token.clone();
|
||||
tokio::spawn(async move { let _ = rs.start(port, tok).await; });
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
commands::get_config,
|
||||
commands::save_config,
|
||||
commands::start_batch,
|
||||
commands::get_history,
|
||||
commands::clear_history,
|
||||
commands::export_history,
|
||||
commands::get_hoster_settings,
|
||||
commands::save_hoster_settings,
|
||||
commands::get_global_settings,
|
||||
commands::save_global_settings,
|
||||
commands::set_always_on_top,
|
||||
commands::get_always_on_top,
|
||||
commands::set_shutdown_after_finish,
|
||||
commands::get_shutdown_after_finish,
|
||||
commands::cancel_shutdown,
|
||||
commands::resolve_folder_files,
|
||||
commands::copy_to_clipboard,
|
||||
commands::start_upload,
|
||||
commands::add_jobs_to_batch,
|
||||
commands::cancel_batch,
|
||||
commands::cancel_jobs,
|
||||
commands::add_jobs,
|
||||
commands::finish_after_active,
|
||||
commands::run_health_check,
|
||||
commands::export_backup,
|
||||
commands::import_backup,
|
||||
commands::import_backup_saved,
|
||||
commands::read_own_upload_log,
|
||||
commands::import_upload_log,
|
||||
commands::save_text_file,
|
||||
commands::open_log_folder,
|
||||
commands::get_history,
|
||||
commands::clear_history,
|
||||
commands::read_rotation_log,
|
||||
commands::get_version,
|
||||
commands::check_for_update,
|
||||
commands::install_update,
|
||||
commands::folder_monitor_start,
|
||||
commands::folder_monitor_stop,
|
||||
commands::folder_monitor_status,
|
||||
commands::remote_get_settings,
|
||||
commands::remote_save_settings,
|
||||
commands::remote_generate_token,
|
||||
commands::remote_status,
|
||||
commands::show_drop_target,
|
||||
commands::hide_drop_target,
|
||||
commands::debug_log,
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
|
||||
112
src-tauri/src/remote_server.rs
Normal file
112
src-tauri/src/remote_server.rs
Normal file
@ -0,0 +1,112 @@
|
||||
//! Remote-control HTTP server — port of `lib/remote-server.js`.
|
||||
//!
|
||||
//! Exposes a minimal JSON API behind bearer-token auth for remote control
|
||||
//! of the queue. Serves no HTML in this implementation; the embedded v1 web
|
||||
//! dashboard asset is kept in the v1 build for now.
|
||||
//!
|
||||
//! Endpoints:
|
||||
//! GET /api/status → { state, activeJobs, pendingJobs, ... }
|
||||
//! GET /api/history → recent batch history
|
||||
//! POST /api/control/cancel → cancel current batch
|
||||
//! POST /api/control/finish-after → finish after active
|
||||
|
||||
use axum::{
|
||||
extract::State,
|
||||
http::{HeaderMap, StatusCode},
|
||||
response::IntoResponse,
|
||||
routing::{get, post},
|
||||
Json, Router,
|
||||
};
|
||||
use parking_lot::Mutex;
|
||||
use serde_json::json;
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
use tokio::net::TcpListener;
|
||||
use tokio::sync::Notify;
|
||||
|
||||
use crate::upload_manager::UploadManager;
|
||||
|
||||
pub struct RemoteServer {
|
||||
shutdown: Mutex<Option<Arc<Notify>>>,
|
||||
uploads: Arc<UploadManager>,
|
||||
token: Mutex<String>,
|
||||
}
|
||||
|
||||
impl RemoteServer {
|
||||
pub fn new(uploads: Arc<UploadManager>) -> Self {
|
||||
Self {
|
||||
shutdown: Mutex::new(None),
|
||||
uploads,
|
||||
token: Mutex::new(String::new()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_running(&self) -> bool { self.shutdown.lock().is_some() }
|
||||
|
||||
pub async fn start(&self, port: u16, token: String) -> Result<(), String> {
|
||||
self.stop().await;
|
||||
if port == 0 { return Err("Port 0 ungültig".into()); }
|
||||
*self.token.lock() = token.clone();
|
||||
|
||||
let state = AppStateRem { token: Arc::new(token), uploads: self.uploads.clone() };
|
||||
let app = Router::new()
|
||||
.route("/api/status", get(status))
|
||||
.route("/api/control/cancel", post(control_cancel))
|
||||
.route("/api/control/finish-after", post(control_finish_after))
|
||||
.with_state(state);
|
||||
|
||||
let shutdown = Arc::new(Notify::new());
|
||||
*self.shutdown.lock() = Some(shutdown.clone());
|
||||
|
||||
let addr = SocketAddr::from(([127, 0, 0, 1], port));
|
||||
let listener = TcpListener::bind(addr).await.map_err(|e| e.to_string())?;
|
||||
let server_shutdown = shutdown.clone();
|
||||
tokio::spawn(async move {
|
||||
let _ = axum::serve(listener, app)
|
||||
.with_graceful_shutdown(async move { server_shutdown.notified().await })
|
||||
.await;
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn stop(&self) {
|
||||
let s = self.shutdown.lock().take();
|
||||
if let Some(s) = s { s.notify_waiters(); }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct AppStateRem {
|
||||
token: Arc<String>,
|
||||
uploads: Arc<UploadManager>,
|
||||
}
|
||||
|
||||
fn authorize(headers: &HeaderMap, token: &str) -> Result<(), StatusCode> {
|
||||
if token.is_empty() { return Err(StatusCode::UNAUTHORIZED); }
|
||||
let auth = headers.get("authorization").and_then(|v| v.to_str().ok()).unwrap_or("");
|
||||
let expected = format!("Bearer {token}");
|
||||
if auth != expected { return Err(StatusCode::UNAUTHORIZED); }
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn status(State(st): State<AppStateRem>, headers: HeaderMap) -> impl IntoResponse {
|
||||
if authorize(&headers, &st.token).is_err() {
|
||||
return (StatusCode::UNAUTHORIZED, Json(json!({ "error": "unauthorized" }))).into_response();
|
||||
}
|
||||
Json(json!({
|
||||
"state": if st.uploads.is_running() { "uploading" } else { "idle" },
|
||||
"version": env!("CARGO_PKG_VERSION"),
|
||||
})).into_response()
|
||||
}
|
||||
|
||||
async fn control_cancel(State(st): State<AppStateRem>, headers: HeaderMap) -> impl IntoResponse {
|
||||
if authorize(&headers, &st.token).is_err() { return StatusCode::UNAUTHORIZED; }
|
||||
st.uploads.cancel();
|
||||
StatusCode::NO_CONTENT
|
||||
}
|
||||
|
||||
async fn control_finish_after(State(st): State<AppStateRem>, headers: HeaderMap) -> impl IntoResponse {
|
||||
if authorize(&headers, &st.token).is_err() { return StatusCode::UNAUTHORIZED; }
|
||||
st.uploads.finish_after_active();
|
||||
StatusCode::NO_CONTENT
|
||||
}
|
||||
75
src-tauri/src/updater.rs
Normal file
75
src-tauri/src/updater.rs
Normal file
@ -0,0 +1,75 @@
|
||||
//! Auto-updater — checks the Gitea releases endpoint for a newer version
|
||||
//! than the one currently running. Downloads are opt-in (user clicks
|
||||
//! "Install Update"), not automatic.
|
||||
//!
|
||||
//! Port of the v1 `lib/updater.js` semver-compare + Gitea polling flow.
|
||||
|
||||
use semver::Version;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::time::Duration;
|
||||
|
||||
const REPO: &str = "Administrator/Multi-Hoster-Upload";
|
||||
const BASE: &str = "https://git.24-music.de/api/v1/repos";
|
||||
|
||||
#[derive(Serialize, Default, Clone, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UpdateCheck {
|
||||
pub available: bool,
|
||||
pub current_version: String,
|
||||
pub latest_version: Option<String>,
|
||||
pub download_url: Option<String>,
|
||||
pub release_notes: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct GiteaRelease {
|
||||
tag_name: String,
|
||||
body: Option<String>,
|
||||
assets: Option<Vec<GiteaAsset>>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct GiteaAsset {
|
||||
name: String,
|
||||
browser_download_url: String,
|
||||
}
|
||||
|
||||
pub async fn check() -> UpdateCheck {
|
||||
let current = env!("CARGO_PKG_VERSION").to_string();
|
||||
let mut out = UpdateCheck {
|
||||
available: false,
|
||||
current_version: current.clone(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let Ok(client) = reqwest::Client::builder()
|
||||
.timeout(Duration::from_secs(15))
|
||||
.user_agent("multi-hoster-upload/2.0")
|
||||
.build()
|
||||
else { return out };
|
||||
|
||||
let url = format!("{BASE}/{REPO}/releases?limit=10");
|
||||
let Ok(resp) = client.get(&url).send().await else { return out };
|
||||
if !resp.status().is_success() { return out; }
|
||||
let Ok(list) = resp.json::<Vec<GiteaRelease>>().await else { return out };
|
||||
|
||||
let Ok(current_v) = Version::parse(current.trim_start_matches('v')) else { return out };
|
||||
|
||||
for r in list {
|
||||
let raw = r.tag_name.trim_start_matches('v');
|
||||
let Ok(v) = Version::parse(raw) else { continue };
|
||||
if v > current_v {
|
||||
out.available = true;
|
||||
out.latest_version = Some(r.tag_name.clone());
|
||||
out.release_notes = r.body.clone();
|
||||
// Prefer MSI > NSIS Setup > fallback.
|
||||
let assets = r.assets.unwrap_or_default();
|
||||
let url = assets.iter().find(|a| a.name.ends_with(".msi"))
|
||||
.or_else(|| assets.iter().find(|a| a.name.to_lowercase().contains("setup.exe")))
|
||||
.or_else(|| assets.iter().find(|a| a.name.ends_with(".exe")));
|
||||
out.download_url = url.map(|a| a.browser_download_url.clone());
|
||||
break;
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
203
src-tauri/src/upload_log.rs
Normal file
203
src-tauri/src/upload_log.rs
Normal file
@ -0,0 +1,203 @@
|
||||
//! Upload log writer with fallback ladder — port of the v1
|
||||
//! `appendUploadLog` + `_resolveUploadLogTarget` logic.
|
||||
//!
|
||||
//! Tries the user-configured path first; falls back to the current user's
|
||||
//! Desktop, then AppData. On a successful fallback, auto-persists the
|
||||
//! working path into `globalSettings.logFilePath` so subsequent writes go
|
||||
//! straight there.
|
||||
|
||||
use chrono::{DateTime, Local};
|
||||
use once_cell::sync::Lazy;
|
||||
use parking_lot::Mutex;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
use tauri::{AppHandle, Emitter, Manager};
|
||||
|
||||
pub struct UploadLogWriter {
|
||||
app: AppHandle,
|
||||
cached_target: Mutex<Option<ResolvedTarget>>,
|
||||
fallback_warned: Mutex<bool>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct ResolvedTarget {
|
||||
path: PathBuf,
|
||||
key: String,
|
||||
is_fallback: bool,
|
||||
}
|
||||
|
||||
impl UploadLogWriter {
|
||||
pub fn new(app: AppHandle) -> Arc<Self> {
|
||||
Arc::new(Self {
|
||||
app,
|
||||
cached_target: Mutex::new(None),
|
||||
fallback_warned: Mutex::new(false),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn append(&self, hoster: &str, link: &str, file_name: &str) {
|
||||
let now: DateTime<Local> = Local::now();
|
||||
let date_str = now.format("%Y-%m-%d %H:%M:%S").to_string();
|
||||
let line = format!("{date_str}|{hoster}|{link}||{file_name}|\n");
|
||||
|
||||
let configured = self.configured_path();
|
||||
let session_log = self.session_log_enabled();
|
||||
let key = format!("{}|{}", configured.display(), if session_log { date_today() } else { String::new() });
|
||||
|
||||
// Try cached target first.
|
||||
if let Some(t) = self.cached_target.lock().clone() {
|
||||
if t.key == key {
|
||||
if append_line(&t.path, &line).is_ok() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve: primary → desktop → userData.
|
||||
let primary = if session_log {
|
||||
self.daily_variant(&configured)
|
||||
} else {
|
||||
configured.clone()
|
||||
};
|
||||
if append_line(&primary, &line).is_ok() {
|
||||
*self.cached_target.lock() = Some(ResolvedTarget {
|
||||
path: primary, key, is_fallback: false,
|
||||
});
|
||||
return;
|
||||
}
|
||||
// Desktop fallback
|
||||
if let Some(desktop) = desktop_dir() {
|
||||
let p = desktop.join(self.base_file_name(session_log));
|
||||
if append_line(&p, &line).is_ok() {
|
||||
*self.cached_target.lock() = Some(ResolvedTarget {
|
||||
path: p.clone(), key, is_fallback: true,
|
||||
});
|
||||
self.maybe_warn_and_persist(&p);
|
||||
return;
|
||||
}
|
||||
}
|
||||
// AppData fallback
|
||||
if let Some(ad) = app_data_dir(&self.app) {
|
||||
let p = ad.join(self.base_file_name(session_log));
|
||||
if append_line(&p, &line).is_ok() {
|
||||
*self.cached_target.lock() = Some(ResolvedTarget {
|
||||
path: p.clone(), key, is_fallback: true,
|
||||
});
|
||||
self.maybe_warn_and_persist(&p);
|
||||
return;
|
||||
}
|
||||
}
|
||||
tracing::warn!("upload-log: all targets failed");
|
||||
}
|
||||
|
||||
fn base_file_name(&self, session_log: bool) -> String {
|
||||
if session_log {
|
||||
format!("fileuploader-{}.log", date_today())
|
||||
} else {
|
||||
"fileuploader.log".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn daily_variant(&self, configured: &Path) -> PathBuf {
|
||||
let dir = configured.parent().unwrap_or(Path::new("."));
|
||||
let stem = configured.file_stem().and_then(|s| s.to_str()).unwrap_or("fileuploader");
|
||||
let ext = configured.extension().and_then(|s| s.to_str()).unwrap_or("log");
|
||||
dir.join(format!("{}-{}.{}", stem, date_today(), ext))
|
||||
}
|
||||
|
||||
fn configured_path(&self) -> PathBuf {
|
||||
// Pull from shared AppState when present. Fallback: next to exe.
|
||||
if let Some(state) = self.app.try_state::<crate::commands::AppState>() {
|
||||
let cfg = state.config.load().ok();
|
||||
if let Some(cfg) = cfg {
|
||||
let custom = cfg.global_settings.log_file_path.trim().to_string();
|
||||
if !custom.is_empty() {
|
||||
return PathBuf::from(custom);
|
||||
}
|
||||
}
|
||||
}
|
||||
default_log_path(&self.app)
|
||||
}
|
||||
|
||||
fn session_log_enabled(&self) -> bool {
|
||||
self.app.try_state::<crate::commands::AppState>()
|
||||
.and_then(|s| s.config.load().ok())
|
||||
.map(|c| c.global_settings.session_log)
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn maybe_warn_and_persist(&self, path: &Path) {
|
||||
let mut warned = self.fallback_warned.lock();
|
||||
if *warned { return; }
|
||||
*warned = true;
|
||||
let _ = self.app.emit("upload-log-fallback",
|
||||
serde_json::json!({ "fallbackPath": path.display().to_string() }));
|
||||
|
||||
// Auto-persist into config so next session writes here directly.
|
||||
if let Some(state) = self.app.try_state::<crate::commands::AppState>() {
|
||||
let store = state.config.clone();
|
||||
let to_save = path.to_path_buf();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
if let Ok(cfg) = store.load() {
|
||||
let mut gs = cfg.global_settings.clone();
|
||||
// Strip daily suffix when daily-log is active — same as v1.
|
||||
let save_path = if gs.session_log {
|
||||
strip_daily_suffix(&to_save)
|
||||
} else {
|
||||
to_save.clone()
|
||||
};
|
||||
gs.log_file_path = save_path.display().to_string();
|
||||
let _ = store.save_global(gs).await;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn default_log_path(app: &AppHandle) -> PathBuf {
|
||||
// Packaged: next to the exe. Dev: cwd.
|
||||
let exe = std::env::current_exe().ok();
|
||||
let dir = exe.and_then(|p| p.parent().map(|p| p.to_path_buf()))
|
||||
.or_else(|| app_data_dir(app))
|
||||
.unwrap_or_else(|| PathBuf::from("."));
|
||||
dir.join("fileuploader.log")
|
||||
}
|
||||
|
||||
fn desktop_dir() -> Option<PathBuf> {
|
||||
dirs_desktop()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn dirs_desktop() -> Option<PathBuf> {
|
||||
std::env::var_os("USERPROFILE").map(|p| PathBuf::from(p).join("Desktop"))
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn dirs_desktop() -> Option<PathBuf> {
|
||||
std::env::var_os("HOME").map(|p| PathBuf::from(p).join("Desktop"))
|
||||
}
|
||||
|
||||
fn app_data_dir(app: &AppHandle) -> Option<PathBuf> {
|
||||
app.path().app_data_dir().ok()
|
||||
}
|
||||
|
||||
fn date_today() -> String {
|
||||
Local::now().format("%Y-%m-%d").to_string()
|
||||
}
|
||||
|
||||
fn append_line(path: &Path, line: &str) -> std::io::Result<()> {
|
||||
if let Some(parent) = path.parent() { std::fs::create_dir_all(parent)?; }
|
||||
let mut f = std::fs::OpenOptions::new().create(true).append(true).open(path)?;
|
||||
std::io::Write::write_all(&mut f, line.as_bytes())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn strip_daily_suffix(path: &Path) -> PathBuf {
|
||||
let stem = path.file_stem().and_then(|s| s.to_str()).unwrap_or("");
|
||||
let ext = path.extension().and_then(|s| s.to_str()).unwrap_or("log");
|
||||
let dir = path.parent().unwrap_or(Path::new("."));
|
||||
// Remove trailing -YYYY-MM-DD if present
|
||||
let stripped = regex::Regex::new(r"-\d{4}-\d{2}-\d{2}$").unwrap().replace(stem, "");
|
||||
dir.join(format!("{}.{}", stripped, ext))
|
||||
}
|
||||
|
||||
pub static WRITER: Lazy<Mutex<Option<Arc<UploadLogWriter>>>> = Lazy::new(|| Mutex::new(None));
|
||||
@ -51,6 +51,7 @@ pub struct Job {
|
||||
|
||||
pub struct UploadManager {
|
||||
app: AppHandle,
|
||||
log_writer: parking_lot::Mutex<Option<std::sync::Arc<crate::upload_log::UploadLogWriter>>>,
|
||||
running: AtomicBool,
|
||||
stop_after_active: AtomicBool,
|
||||
start_time: parking_lot::Mutex<Option<Instant>>,
|
||||
@ -83,9 +84,14 @@ pub struct UploadManager {
|
||||
}
|
||||
|
||||
impl UploadManager {
|
||||
pub fn set_upload_log_writer(&self, w: std::sync::Arc<crate::upload_log::UploadLogWriter>) {
|
||||
*self.log_writer.lock() = Some(w);
|
||||
}
|
||||
|
||||
pub fn new(app: AppHandle) -> Self {
|
||||
Self {
|
||||
app,
|
||||
log_writer: parking_lot::Mutex::new(None),
|
||||
running: AtomicBool::new(false),
|
||||
stop_after_active: AtomicBool::new(false),
|
||||
start_time: parking_lot::Mutex::new(None),
|
||||
@ -312,6 +318,7 @@ impl UploadManager {
|
||||
self.session_bytes.fetch_add(meta.len(), Ordering::Relaxed);
|
||||
}
|
||||
self.emit_final(&job, "done", Some(&res), None);
|
||||
self.append_upload_log(&job, &res).await;
|
||||
self.record_result(&job, "done", Some(res), None).await;
|
||||
}
|
||||
Err(e) => {
|
||||
@ -626,6 +633,16 @@ impl UploadManager {
|
||||
self.emit("upload-progress", &evt);
|
||||
}
|
||||
|
||||
async fn append_upload_log(&self, job: &Job, result: &UploadResult) {
|
||||
let w = self.log_writer.lock().clone();
|
||||
if let Some(w) = w {
|
||||
let link = result.download_url.as_deref().or(result.embed_url.as_deref()).unwrap_or("");
|
||||
if !link.is_empty() {
|
||||
w.append(&job.hoster, link, &job.file_name).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn emit_final(&self, job: &Job, status: &str, result: Option<&UploadResult>, err: Option<&str>) {
|
||||
let evt = ProgressEvent {
|
||||
job_id: job.id.clone(),
|
||||
|
||||
4407
src/app.js
4407
src/app.js
File diff suppressed because it is too large
Load Diff
365
src/index.html
365
src/index.html
@ -1,122 +1,319 @@
|
||||
<!doctype html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Multi-Hoster-Upload 2.0</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="stylesheet" href="styles.css" />
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self' ipc: http://ipc.localhost; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'; connect-src 'self' ipc: http://ipc.localhost; img-src 'self' data: blob:;">
|
||||
<title>Multi-Hoster-Upload</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<header class="app-header">
|
||||
<div class="app-title">Multi-Hoster-Upload <span class="ver">v2.0</span></div>
|
||||
<nav class="tabs">
|
||||
<nav class="tab-bar">
|
||||
<button class="tab active" data-view="upload">Upload</button>
|
||||
<button class="tab" data-view="accounts">Accounts</button>
|
||||
<button class="tab" data-view="settings">Einstellungen</button>
|
||||
<button class="tab" data-view="log">Rotation-Log</button>
|
||||
<button class="tab" data-view="history">Verlauf</button>
|
||||
<span class="version-label" id="versionLabel"></span>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<section class="view active" id="upload-view">
|
||||
<div id="updateBanner" class="update-banner" style="display:none">
|
||||
<span id="updateMessage"></span>
|
||||
<button class="btn btn-sm btn-primary" id="installUpdateBtn">Update installieren</button>
|
||||
<button class="btn btn-sm btn-secondary" id="dismissUpdateBtn">×</button>
|
||||
</div>
|
||||
|
||||
<div id="upload-view" class="view active">
|
||||
<div class="upload-toolbar">
|
||||
<div class="toolbar-left">
|
||||
<span class="hoster-summary" id="hosterSummary" style="display:none"></span>
|
||||
</div>
|
||||
<div class="toolbar-right">
|
||||
<button class="btn btn-xs btn-primary" id="addFilesBtn">+ Dateien</button>
|
||||
<button class="btn btn-xs btn-secondary" id="addFolderBtn">+ Ordner</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="upload-workspace">
|
||||
<div class="drop-zone" id="dropZone">
|
||||
<div class="drop-hint">Dateien hierher ziehen oder Button klicken</div>
|
||||
<button class="btn btn-primary" id="pickFilesBtn">+ Dateien wählen</button>
|
||||
<div class="drop-icon">📁</div>
|
||||
<p>Dateien hierher ziehen oder klicken</p>
|
||||
</div>
|
||||
|
||||
<div class="hoster-picker">
|
||||
<label>Upload zu:</label>
|
||||
<div id="hosterCheckboxes"></div>
|
||||
<div class="queue-shell" id="queueShell" style="display:none">
|
||||
<div class="queue-command-bar" id="queueCommandBar">
|
||||
<button class="toolbar-btn" id="startUploadBtn" title="Start all" disabled>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16"><path d="M4 2l10 6-10 6z" fill="#4caf50"/></svg>
|
||||
</button>
|
||||
<button class="toolbar-btn" id="startSelectedBtn" title="Start selected" disabled>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16"><path d="M6 3l8 5-8 5z" fill="#4caf50"/><rect x="1" y="3" width="3" height="10" rx="0.5" fill="#4caf50"/></svg>
|
||||
</button>
|
||||
<button class="toolbar-btn" id="reuploadSelectedBtn" title="Reupload selected file">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16"><path d="M8 1a7 7 0 0 0-5 2.1V1H2v4h4V4H3.7A5.5 5.5 0 1 1 2.5 8H1a7 7 0 1 0 7-7z" fill="#4caf50"/></svg>
|
||||
</button>
|
||||
<button class="toolbar-btn" id="abortSelectedBtn" title="Abort selected file">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16"><rect x="3" y="3" width="10" height="10" rx="1" fill="#e53935"/><path d="M5.5 5.5l5 5M10.5 5.5l-5 5" stroke="#fff" stroke-width="1.5" stroke-linecap="round"/></svg>
|
||||
</button>
|
||||
<button class="toolbar-btn" id="finishStopBtn" title="Finish Uploads in Progress and Stop">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16"><path d="M2 8l4 4 8-8" stroke="#4caf50" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/><rect x="11" y="9" width="5" height="5" rx="1" fill="#e53935"/></svg>
|
||||
</button>
|
||||
<button class="toolbar-btn toolbar-btn-danger" id="abortAllBtn" title="Abort all Downloads">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16"><rect x="1" y="1" width="14" height="14" rx="2" fill="#e53935"/><path d="M4.5 4.5l7 7M11.5 4.5l-7 7" stroke="#fff" stroke-width="2" stroke-linecap="round"/></svg>
|
||||
</button>
|
||||
<span class="toolbar-sep"></span>
|
||||
<button class="toolbar-btn" id="moveTopBtn" title="Move to the top">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16"><rect x="4" y="1" width="8" height="2" rx="0.5" fill="#4caf50"/><path d="M8 5l-4 5h8z" fill="#4caf50"/><path d="M8 9l-4 5h8z" fill="#4caf50"/></svg>
|
||||
</button>
|
||||
<button class="toolbar-btn" id="moveUpBtn" title="Move up">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16"><path d="M8 2l-5 6h10z" fill="#4caf50"/><rect x="6" y="8" width="4" height="6" rx="0.5" fill="#4caf50"/></svg>
|
||||
</button>
|
||||
<button class="toolbar-btn" id="moveDownBtn" title="Move down">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16"><rect x="6" y="2" width="4" height="6" rx="0.5" fill="#4caf50"/><path d="M8 14l-5-6h10z" fill="#4caf50"/></svg>
|
||||
</button>
|
||||
<button class="toolbar-btn" id="moveBottomBtn" title="Move to the bottom">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16"><path d="M8 7l-4-5h8z" fill="#4caf50"/><path d="M8 11l-4-5h8z" fill="#4caf50"/><rect x="4" y="13" width="8" height="2" rx="0.5" fill="#4caf50"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="queue-shell">
|
||||
<div class="queue-toolbar">
|
||||
<button class="btn btn-primary" id="startBtn" disabled>▶ Upload starten</button>
|
||||
<button class="btn" id="cancelBtn" disabled>✕ Abbrechen</button>
|
||||
<span class="queue-stats" id="queueStats">—</span>
|
||||
</div>
|
||||
<table class="queue-table">
|
||||
<div class="queue-container" id="queueContainer">
|
||||
<table class="queue-table" id="queueTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Datei</th>
|
||||
<th>Hoster</th>
|
||||
<th>Status</th>
|
||||
<th>Progress</th>
|
||||
<th>Speed</th>
|
||||
<th>Link</th>
|
||||
<th class="col-filename sortable" data-col="filename" data-sort="filename">Filename<span class="col-resizer"></span></th>
|
||||
<th class="col-size sortable" data-col="size" data-sort="size">Uploaded / Size<span class="col-resizer"></span></th>
|
||||
<th class="col-host sortable" data-col="host" data-sort="host">Host<span class="col-resizer"></span></th>
|
||||
<th class="col-status sortable" data-col="status" data-sort="status">Status<span class="col-resizer"></span></th>
|
||||
<th class="col-elapsed" data-col="elapsed">Zeit<span class="col-resizer"></span></th>
|
||||
<th class="col-remaining" data-col="remaining">Rest<span class="col-resizer"></span></th>
|
||||
<th class="col-speed sortable" data-col="speed" data-sort="speed">Speed<span class="col-resizer"></span></th>
|
||||
<th class="col-progress sortable" data-col="progress" data-sort="progress">Progress</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="queueBody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="view" id="accounts-view">
|
||||
<div class="actions-bar">
|
||||
<div class="queue-actions" id="queueActions" style="display:none">
|
||||
<button class="btn btn-xs btn-primary" id="copyAllLinksBtn">Alle Links kopieren</button>
|
||||
<button class="btn btn-xs btn-secondary" id="retryFailedBtn" style="display:none">Fehlgeschlagene erneut</button>
|
||||
<button class="btn btn-xs btn-secondary" id="importLogBtn" title="Log importieren — bereits hochgeladene aus Queue entfernen">Log importieren</button>
|
||||
</div>
|
||||
|
||||
<div class="resize-handle" id="recentFilesResizer"></div>
|
||||
<div class="recent-files-panel" id="recentFilesPanel">
|
||||
<div class="recent-files-header">
|
||||
<div class="recent-tabs">
|
||||
<button class="recent-tab active" data-panel="filesTab">Files</button>
|
||||
<button class="recent-tab" data-panel="statsTab">Stats</button>
|
||||
</div>
|
||||
<span class="recent-files-hint" id="recentFilesHint">Zuletzt erzeugte Upload-Links</span>
|
||||
<button class="btn btn-xs btn-secondary" id="exportRecentFilesBtn" title="Alle Zeilen als Datei exportieren (Zeit, Hoster, Link, Dateiname)">Exportieren</button>
|
||||
<button class="btn btn-xs btn-danger" id="clearRecentFilesBtn" title="Alle Links aus diesem Panel entfernen">Alle entfernen</button>
|
||||
</div>
|
||||
<div class="recent-tab-body active" id="filesTab">
|
||||
<div class="recent-files-table-wrap">
|
||||
<table class="recent-files-table">
|
||||
<thead id="recentFilesHead">
|
||||
<tr>
|
||||
<th class="col-date sortable" data-recent-sort="date">Datum<span class="sort-indicator">▼</span></th>
|
||||
<th class="col-filename sortable" data-recent-sort="filename">Filename<span class="sort-indicator">↕</span></th>
|
||||
<th class="col-host sortable" data-recent-sort="host">Host<span class="sort-indicator">↕</span></th>
|
||||
<th class="col-link sortable" data-recent-sort="link">Link<span class="sort-indicator">↕</span></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="recentFilesBody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="recent-tab-body" id="statsTab">
|
||||
<div class="stats-grid">
|
||||
<div class="stats-col">
|
||||
<h4>Files in queue (count)</h4>
|
||||
<div class="stats-row"><span>total:</span><span id="statQueueTotal">0</span></div>
|
||||
<div class="stats-row"><span>done:</span><span id="statQueueDone">0</span></div>
|
||||
<div class="stats-row"><span>remaining:</span><span id="statQueueRemaining">0</span></div>
|
||||
<div class="stats-row"><span>in progress:</span><span id="statQueueInProgress">0</span></div>
|
||||
<div class="stats-row"><span>error:</span><span id="statQueueError">0</span></div>
|
||||
</div>
|
||||
<div class="stats-col">
|
||||
<h4>File size in queue</h4>
|
||||
<div class="stats-row"><span>total:</span><span id="statSizeTotal">0 B</span></div>
|
||||
<div class="stats-row"><span>remaining:</span><span id="statSizeRemaining">0 B</span></div>
|
||||
</div>
|
||||
<div class="stats-col">
|
||||
<h4>Session</h4>
|
||||
<div class="stats-row"><span>Upload speed:</span><span id="statSpeed">0 B/s</span></div>
|
||||
<div class="stats-row"><span>Remaining time:</span><span id="statEta">--:--</span></div>
|
||||
<div class="stats-row"><span>Run time:</span><span id="statRunTime">00:00:00</span></div>
|
||||
<div class="stats-row"><span>Uploaded (this run):</span><span id="statSessionBytes">0 B</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="accounts-view" class="view">
|
||||
<div class="accounts-container">
|
||||
<div class="accounts-header">
|
||||
<div>
|
||||
<h2>Accounts</h2>
|
||||
<p class="settings-hint">Hoster-Zugangsdaten verwalten und prüfen</p>
|
||||
</div>
|
||||
<div class="accounts-header-actions">
|
||||
<button class="btn btn-secondary" id="accountsRunHealthCheckBtn">Accounts prüfen</button>
|
||||
<label class="auto-check-label accounts-auto-check" title="Automatischer Check vor dem Upload">
|
||||
<input type="checkbox" id="autoHealthCheckToggle" checked>
|
||||
<span>Auto-Check vor Upload</span>
|
||||
</label>
|
||||
<button class="btn btn-primary" id="addAccountBtn">+ Account hinzufügen</button>
|
||||
</div>
|
||||
<div id="accountsList" class="accounts-list"></div>
|
||||
</section>
|
||||
</div>
|
||||
<div class="health-check-results account-health-results" id="healthCheckResults"></div>
|
||||
<div class="accounts-list" id="accountsList"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="view" id="settings-view">
|
||||
<div class="settings">
|
||||
<h2>Globale Einstellungen</h2>
|
||||
<div class="setting-row">
|
||||
<label>Gesamt-Upload-Limit (KB/s, 0 = unlimitiert)</label>
|
||||
<input type="number" id="globalSpeed" min="0" value="0" />
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<label>Parallele Uploads über alle Hoster (0 = nur pro Hoster)</label>
|
||||
<input type="number" id="globalParallel" min="0" max="100" value="0" />
|
||||
</div>
|
||||
<button class="btn btn-primary" id="saveSettingsBtn">Speichern</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="view" id="log-view">
|
||||
<div class="actions-bar">
|
||||
<button class="btn" id="refreshLogBtn">Aktualisieren</button>
|
||||
<button class="btn" id="openLogFolderBtn">Log-Ordner öffnen</button>
|
||||
</div>
|
||||
<pre id="rotLog" class="log-pre"></pre>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<!-- Account modal -->
|
||||
<div class="modal-overlay" id="accountModal" style="display:none">
|
||||
<div class="modal-card">
|
||||
<h3>Account hinzufügen</h3>
|
||||
<div class="setting-row">
|
||||
<div class="modal-header">
|
||||
<div>
|
||||
<h3 id="accountModalTitle">Account hinzufügen</h3>
|
||||
<p id="accountModalSubtitle">Wähle einen Hoster und gib deine Zugangsdaten ein.</p>
|
||||
</div>
|
||||
<button class="icon-btn" id="closeAccountModalBtn" aria-label="Schließen">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="settings-row" id="accountHosterRow">
|
||||
<label>Hoster</label>
|
||||
<select id="accHoster">
|
||||
<option value="clouddrop.cc">Clouddrop (API)</option>
|
||||
<option value="byse.sx">Byse (API)</option>
|
||||
<option value="vidmoly.me">Vidmoly (Login)</option>
|
||||
<option value="doodstream.com">Doodstream (nicht in 2.0 POC)</option>
|
||||
<option value="voe.sx">VOE (nicht in 2.0 POC)</option>
|
||||
</select>
|
||||
<select class="key-input" id="accountHosterSelect" style="max-width:300px"></select>
|
||||
</div>
|
||||
<div class="setting-row" id="accLoginRow" style="display:none">
|
||||
<label>Username</label>
|
||||
<input type="text" id="accUsername" />
|
||||
</div>
|
||||
<div class="setting-row" id="accPasswordRow" style="display:none">
|
||||
<label>Passwort</label>
|
||||
<input type="password" id="accPassword" />
|
||||
</div>
|
||||
<div class="setting-row" id="accApiKeyRow">
|
||||
<label>API Key</label>
|
||||
<input type="password" id="accApiKey" />
|
||||
<div id="accountCredsFields"></div>
|
||||
<div class="account-modal-status" id="accountModalStatus"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn" id="accCancelBtn">Abbrechen</button>
|
||||
<button class="btn btn-primary" id="accSaveBtn">Speichern</button>
|
||||
<button class="btn btn-secondary" id="cancelAccountModalBtn">Abbrechen</button>
|
||||
<button class="btn btn-primary" id="saveAccountBtn">Anlegen & prüfen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="toast" id="toast"></div>
|
||||
<div class="modal-overlay" id="deleteAccountModal" style="display:none">
|
||||
<div class="modal-card" style="width:min(400px,100%)">
|
||||
<div class="modal-header">
|
||||
<div><h3>Account löschen?</h3></div>
|
||||
<button class="icon-btn" id="closeDeleteModalBtn" aria-label="Schließen">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p id="deleteAccountMessage">Account wirklich löschen?</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" id="cancelDeleteBtn">Abbrechen</button>
|
||||
<button class="btn btn-danger" id="confirmDeleteBtn">Löschen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module" src="app.js"></script>
|
||||
<div id="settings-view" class="view">
|
||||
<div class="settings-container">
|
||||
<h2>Upload-Einstellungen</h2>
|
||||
<p class="settings-hint">Hoster-Einstellungen erscheinen erst, sobald ein Account hinterlegt ist. Änderungen werden automatisch gespeichert.</p>
|
||||
<div class="settings-hosters" id="settingsHosters"></div>
|
||||
<div class="settings-save-row">
|
||||
<span class="save-feedback" id="saveFeedback">Änderungen werden automatisch gespeichert.</span>
|
||||
<button class="btn btn-secondary" id="saveSettingsBtn">Jetzt speichern</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="history-view" class="view">
|
||||
<div class="history-container">
|
||||
<div class="history-header">
|
||||
<h2>Upload-Verlauf</h2>
|
||||
<div style="display:flex; gap:8px">
|
||||
<button class="btn btn-secondary" id="exportHistoryBtn">Verlauf exportieren</button>
|
||||
<button class="btn btn-secondary" id="clearHistoryBtn">Verlauf löschen</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="historyContainer"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="context-menu" id="contextMenu" style="display:none">
|
||||
<div class="ctx-item" data-action="start-selected">Ausgewählte starten</div>
|
||||
<div class="ctx-item" data-action="retry-selected">Erneut versuchen</div>
|
||||
<div class="ctx-separator"></div>
|
||||
<div class="ctx-item" data-action="copy-links">Links kopieren</div>
|
||||
<div class="ctx-item" data-action="copy-all-links">Alle Links kopieren</div>
|
||||
<div class="ctx-separator"></div>
|
||||
<div class="ctx-item" data-action="delete-selected">Entfernen</div>
|
||||
<div class="ctx-item" data-action="delete-all">Alle entfernen</div>
|
||||
<div class="ctx-submenu ctx-hoster-delete-submenu" style="display:none">
|
||||
<div class="ctx-item ctx-item-danger">Hoster entfernen ▸</div>
|
||||
<div class="ctx-submenu-items ctx-hoster-delete-items"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="context-menu" id="recentContextMenu" style="display:none">
|
||||
<div class="ctx-item" data-action="recent-copy-links">Links kopieren</div>
|
||||
<div class="ctx-item" data-action="recent-delete">Entfernen</div>
|
||||
</div>
|
||||
|
||||
<div class="statusbar" id="statusbar">
|
||||
<span class="sb-state" id="sbState">Bereit</span>
|
||||
<span class="sb-separator">|</span>
|
||||
<span class="sb-speed" id="sbSpeed">0 kB/s</span>
|
||||
<span class="sb-separator">|</span>
|
||||
<span class="sb-total" id="sbTotal">0 B</span>
|
||||
<span class="sb-separator">|</span>
|
||||
<span class="sb-eta" id="sbEta">ETA --:--</span>
|
||||
<span class="sb-separator">|</span>
|
||||
<span class="sb-connections" id="sbConnections">Aktive Verbindungen 0</span>
|
||||
<span class="sb-separator">|</span>
|
||||
<span class="sb-queue-count" id="sbQueueCount">Gesamt 0</span>
|
||||
<span class="sb-separator">|</span>
|
||||
<span class="sb-remaining-count" id="sbRemainingCount">Remaining 0</span>
|
||||
<span class="sb-separator">|</span>
|
||||
<span class="sb-progress-count" id="sbInProgressCount">In Progress 0</span>
|
||||
<span class="sb-separator">|</span>
|
||||
<span class="sb-done-count" id="sbDoneCount">Done 0</span>
|
||||
<span class="sb-separator">|</span>
|
||||
<span class="sb-error-count" id="sbErrorCount">Error 0</span>
|
||||
</div>
|
||||
|
||||
<div class="copy-toast" id="copyToast"></div>
|
||||
|
||||
<div class="shutdown-overlay" id="shutdownOverlay" style="display:none">
|
||||
<div class="shutdown-box">
|
||||
<p id="shutdownMessage">System wird heruntergefahren in <span id="shutdownSeconds">60</span>s...</p>
|
||||
<button class="btn btn-danger" id="cancelShutdownBtn">Abbrechen</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-overlay" id="hosterModal" style="display:none">
|
||||
<div class="modal-card">
|
||||
<div class="modal-header">
|
||||
<div>
|
||||
<h3>Upload-Ziele auswählen</h3>
|
||||
<p>Dateien wurden hinzugefügt. Wähle jetzt die Hoster für den Upload.</p>
|
||||
</div>
|
||||
<button class="icon-btn" id="closeHosterModalBtn" aria-label="Schließen">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="modal-actions-inline">
|
||||
<button class="btn btn-xs btn-secondary" id="selectAllHostersBtn">Alle</button>
|
||||
<button class="btn btn-xs btn-secondary" id="clearHostersBtn">Keine</button>
|
||||
</div>
|
||||
<div class="hoster-modal-list" id="hosterModalList"></div>
|
||||
<p class="modal-hint" id="hosterModalHint"></p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" id="cancelHosterModalBtn">Abbrechen</button>
|
||||
<button class="btn btn-primary" id="confirmHosterModalBtn">Übernehmen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="tauri-shim.js"></script>
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
1133
src/styles.css
1133
src/styles.css
File diff suppressed because it is too large
Load Diff
208
src/tauri-shim.js
Normal file
208
src/tauri-shim.js
Normal file
@ -0,0 +1,208 @@
|
||||
// tauri-shim.js — re-creates the Electron-era `window.api` surface on top of
|
||||
// Tauri 2's core.invoke + event.listen so the v1 renderer keeps working
|
||||
// unchanged.
|
||||
//
|
||||
// Every method here maps to exactly one Rust #[tauri::command] in
|
||||
// src-tauri/src/commands.rs. Event subscriptions go through window.__TAURI__.event.listen.
|
||||
|
||||
(function () {
|
||||
const T = window.__TAURI__;
|
||||
if (!T) {
|
||||
console.error('Tauri runtime not available');
|
||||
window.api = new Proxy({}, { get: () => async () => null });
|
||||
return;
|
||||
}
|
||||
const invoke = T.core.invoke;
|
||||
const listen = T.event.listen;
|
||||
const dialog = T.dialog || {};
|
||||
|
||||
// Helper: wrap a listen() call to return an unsubscribe fn like v1 did.
|
||||
function on(name, handler) {
|
||||
let unlisten = () => {};
|
||||
listen(name, (ev) => { try { handler(ev.payload); } catch (e) { console.error(e); } })
|
||||
.then(fn => { unlisten = fn; });
|
||||
return () => { try { unlisten(); } catch {} };
|
||||
}
|
||||
|
||||
const api = {
|
||||
// --- Config ---
|
||||
getConfig: () => invoke('get_config'),
|
||||
saveConfig: (config) => invoke('save_config', { config }),
|
||||
getHistory: () => invoke('get_history'),
|
||||
clearHistory: () => invoke('clear_history'),
|
||||
exportHistory: (format) => invoke('export_history', { format }),
|
||||
|
||||
// --- Hoster settings ---
|
||||
getHosterSettings: () => invoke('get_hoster_settings'),
|
||||
saveHosterSettings: (settings) => invoke('save_hoster_settings', { settings }),
|
||||
|
||||
// --- Global settings ---
|
||||
getGlobalSettings: () => invoke('get_global_settings'),
|
||||
saveGlobalSettings: (settings) => invoke('save_global_settings', { settings }),
|
||||
saveGlobalSettingsSync: (settings) => invoke('save_global_settings', { settings }),
|
||||
|
||||
// --- Window state ---
|
||||
setAlwaysOnTop: (value) => invoke('set_always_on_top', { value }),
|
||||
getAlwaysOnTop: () => invoke('get_always_on_top'),
|
||||
setShutdownAfterFinish: (mode) => invoke('set_shutdown_after_finish', { mode }),
|
||||
getShutdownAfterFinish: () => invoke('get_shutdown_after_finish'),
|
||||
cancelShutdown: () => invoke('cancel_shutdown'),
|
||||
|
||||
// --- File selection ---
|
||||
selectFiles: async () => {
|
||||
if (!dialog || !dialog.open) return null;
|
||||
const picked = await dialog.open({ multiple: true, directory: false });
|
||||
if (!picked) return null;
|
||||
return Array.isArray(picked) ? picked : [picked];
|
||||
},
|
||||
selectFolder: async () => {
|
||||
if (!dialog || !dialog.open) return null;
|
||||
const picked = await dialog.open({ multiple: false, directory: true });
|
||||
if (!picked) return null;
|
||||
// v1 returned an array of files in the folder; we return the folder and
|
||||
// let resolveFolderFiles handle enumeration.
|
||||
const files = await invoke('resolve_folder_files', { folderPath: picked });
|
||||
return files || [];
|
||||
},
|
||||
resolveFolderFiles: (folderPath) => invoke('resolve_folder_files', { folderPath }),
|
||||
|
||||
// --- Clipboard ---
|
||||
copyToClipboard: (text) => invoke('copy_to_clipboard', { text }),
|
||||
|
||||
// --- Path / file utils ---
|
||||
getPathForFile: (file) => {
|
||||
// In Tauri we never go through drag-drop file objects; paths come direct.
|
||||
// This keeps the renderer happy when it tries to resolve a drag event.
|
||||
return (file && file.path) ? file.path : '';
|
||||
},
|
||||
|
||||
// --- Uploads ---
|
||||
startUpload: (payload) => invoke('start_upload', { payload }),
|
||||
addJobsToBatch: (payload) => invoke('add_jobs_to_batch', { payload }),
|
||||
cancelSelectedJobs: (jobIds) => invoke('cancel_jobs', { jobIds }),
|
||||
cancelUpload: () => invoke('cancel_batch'),
|
||||
finishAfterActive: () => invoke('finish_after_active'),
|
||||
|
||||
// --- Health check ---
|
||||
runHealthCheck: (payload) => invoke('run_health_check', { payload }),
|
||||
|
||||
// --- Backup ---
|
||||
exportBackup: async () => {
|
||||
if (!dialog || !dialog.save) return { ok: false, canceled: true };
|
||||
const target = await dialog.save({
|
||||
defaultPath: `multi-hoster-backup-${new Date().toISOString().slice(0,10)}.mhu`,
|
||||
filters: [{ name: 'Multi-Hoster Backup', extensions: ['mhu'] }]
|
||||
});
|
||||
if (!target) return { ok: false, canceled: true };
|
||||
try {
|
||||
await invoke('export_backup', { targetPath: target });
|
||||
return { ok: true, path: target };
|
||||
} catch (e) { return { ok: false, error: String(e) }; }
|
||||
},
|
||||
importBackup: async (legacyPassword) => {
|
||||
if (legacyPassword) {
|
||||
try {
|
||||
const cfg = await invoke('import_backup_saved', { legacyPassword });
|
||||
return { ok: true, config: cfg };
|
||||
} catch (e) {
|
||||
const msg = String(e);
|
||||
if (msg === 'needs-password' || msg.includes('needs-password')) return { ok: false, needsPassword: true };
|
||||
return { ok: false, error: msg };
|
||||
}
|
||||
}
|
||||
if (!dialog || !dialog.open) return { ok: false, canceled: true };
|
||||
const src = await dialog.open({
|
||||
multiple: false, directory: false,
|
||||
filters: [{ name: 'Multi-Hoster Backup', extensions: ['mhu'] }]
|
||||
});
|
||||
if (!src) return { ok: false, canceled: true };
|
||||
try {
|
||||
const cfg = await invoke('import_backup', { sourcePath: src });
|
||||
return { ok: true, config: cfg };
|
||||
} catch (e) {
|
||||
const msg = String(e);
|
||||
if (msg === 'needs-password' || msg.includes('needs-password')) return { ok: false, needsPassword: true };
|
||||
return { ok: false, error: msg };
|
||||
}
|
||||
},
|
||||
|
||||
// --- Log / files ---
|
||||
readOwnUploadLog: () => invoke('read_own_upload_log'),
|
||||
importUploadLog: () => invoke('import_upload_log'),
|
||||
saveTextFile: async (defaultName, content, filters) => {
|
||||
if (!dialog || !dialog.save) return { ok: false, canceled: true };
|
||||
const target = await dialog.save({
|
||||
defaultPath: defaultName,
|
||||
filters: filters || [{ name: 'Textdatei', extensions: ['txt', 'csv', 'log'] }]
|
||||
});
|
||||
if (!target) return { ok: false, canceled: true };
|
||||
try {
|
||||
await invoke('save_text_file', { path: target, content });
|
||||
return { ok: true, path: target };
|
||||
} catch (e) { return { ok: false, error: String(e) }; }
|
||||
},
|
||||
openLogFolder: () => invoke('open_log_folder'),
|
||||
|
||||
// --- Update / version ---
|
||||
getVersion: () => invoke('get_version'),
|
||||
checkForUpdate: () => invoke('check_for_update'),
|
||||
installUpdate: () => invoke('install_update'),
|
||||
|
||||
// --- Folder monitor ---
|
||||
folderMonitorStart: (settings) => invoke('folder_monitor_start', { settings }),
|
||||
folderMonitorStop: () => invoke('folder_monitor_stop'),
|
||||
folderMonitorStatus: () => invoke('folder_monitor_status'),
|
||||
folderMonitorSelectFolder: async () => {
|
||||
if (!dialog || !dialog.open) return null;
|
||||
return dialog.open({ multiple: false, directory: true });
|
||||
},
|
||||
|
||||
// --- Remote control ---
|
||||
remoteGetSettings: () => invoke('remote_get_settings'),
|
||||
remoteSaveSettings: (settings) => invoke('remote_save_settings', { settings }),
|
||||
remoteGenerateToken: () => invoke('remote_generate_token'),
|
||||
remoteStatus: () => invoke('remote_status'),
|
||||
|
||||
// --- Drop target ---
|
||||
showDropTarget: () => invoke('show_drop_target'),
|
||||
hideDropTarget: () => invoke('hide_drop_target'),
|
||||
|
||||
// --- Debug ---
|
||||
debugLog: (msg) => invoke('debug_log', { msg }),
|
||||
|
||||
// --- Events ---
|
||||
onUpdateAvailable: (cb) => on('app:update-available', cb),
|
||||
onUpdateProgress: (cb) => on('app:update-progress', cb),
|
||||
onUploadProgress: (cb) => on('upload-progress', cb),
|
||||
onUploadBatchDone: (cb) => on('upload-batch-done', cb),
|
||||
onUploadStats: (cb) => on('upload-stats', cb),
|
||||
onShutdownCountdown: (cb) => on('shutdown-countdown', cb),
|
||||
onAccountSwitched: (cb) => on('account-switched', cb),
|
||||
onAccountRotationLog: (cb) => on('account-rotation-log', cb),
|
||||
onUploadLogFallback: (cb) => on('upload-log-fallback', cb),
|
||||
onLogPathAutoUpdated: (cb) => on('log-path-auto-updated', cb),
|
||||
onDropTargetFiles: (cb) => on('drop-target-files', cb),
|
||||
onFolderMonitorNewFiles: (cb) => on('folder-monitor-new-files', cb),
|
||||
onRemoteClientCount: (cb) => on('remote:client-count', cb),
|
||||
};
|
||||
|
||||
// Tauri's file-drop event delivers paths; v1's drop handler expected
|
||||
// FileList objects. Translate and inject synthetic drop events.
|
||||
try {
|
||||
listen('tauri://drag-drop', (ev) => {
|
||||
const paths = (ev.payload && (ev.payload.paths || ev.payload)) || [];
|
||||
if (!Array.isArray(paths) || paths.length === 0) return;
|
||||
const files = paths.map(p => ({ path: p, name: p.split(/[\\/]/).pop(), size: 0, type: '' }));
|
||||
const dz = document.getElementById('dropZone');
|
||||
if (!dz) return;
|
||||
const ev2 = new DragEvent('drop', { bubbles: true, cancelable: true, dataTransfer: new DataTransfer() });
|
||||
try {
|
||||
Object.defineProperty(ev2.dataTransfer, 'files', { value: files, writable: false });
|
||||
} catch {}
|
||||
// v1 path resolution goes through window.api.getPathForFile — already wired.
|
||||
dz.dispatchEvent(ev2);
|
||||
});
|
||||
} catch {}
|
||||
|
||||
window.api = api;
|
||||
})();
|
||||
Loading…
Reference in New Issue
Block a user