From 7b064b8255b34cde174a591e93c7c67604997f2c Mon Sep 17 00:00:00 2001 From: CodeMaster4711 Date: Sun, 4 Jan 2026 17:42:35 +0100 Subject: [PATCH] feat: updater for programm --- backend/Cargo.lock | 435 ++++++++++++++++-- backend/Cargo.toml | 1 + backend/src/routes/mod.rs | 2 + backend/src/routes/updates.rs | 246 ++++++++++ docs/features/AUTO_UPDATER.md | 151 ++++++ .../lib/components/UpdateNotification.svelte | 151 ++++++ .../lib/components/navbar/app-sidebar.svelte | 2 + .../components/settings/UpdateSettings.svelte | 205 +++++++++ frontend/src/lib/services/updates.ts | 80 ++++ frontend/src/lib/stores/updates.ts | 103 +++++ frontend/src/routes/settings/+page.svelte | 24 +- scripts/update.sh | 122 +++++ 12 files changed, 1475 insertions(+), 47 deletions(-) create mode 100644 backend/src/routes/updates.rs create mode 100644 docs/features/AUTO_UPDATER.md create mode 100644 frontend/src/lib/components/UpdateNotification.svelte create mode 100644 frontend/src/lib/components/settings/UpdateSettings.svelte create mode 100644 frontend/src/lib/services/updates.ts create mode 100644 frontend/src/lib/stores/updates.ts create mode 100755 scripts/update.sh diff --git a/backend/Cargo.lock b/backend/Cargo.lock index f0efdd08..8a14f6b6 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -387,10 +387,10 @@ dependencies = [ "axum-core", "bytes", "futures-util", - "http", - "http-body", + "http 1.3.1", + "http-body 1.0.1", "http-body-util", - "hyper", + "hyper 1.7.0", "hyper-util", "itoa", "matchit", @@ -403,7 +403,7 @@ dependencies = [ "serde_json", "serde_path_to_error", "serde_urlencoded", - "sync_wrapper", + "sync_wrapper 1.0.2", "tokio", "tower", "tower-layer", @@ -420,13 +420,13 @@ dependencies = [ "async-trait", "bytes", "futures-util", - "http", - "http-body", + "http 1.3.1", + "http-body 1.0.1", "http-body-util", "mime", "pin-project-lite", "rustversion", - "sync_wrapper", + "sync_wrapper 1.0.2", "tower-layer", "tower-service", "tracing", @@ -434,12 +434,12 @@ dependencies = [ [[package]] name = "backend" -version = "0.0.6" +version = "0.0.7" dependencies = [ "anyhow", "async-trait", "axum", - "base64", + "base64 0.22.1", "bcrypt", "bollard", "chrono", @@ -447,14 +447,15 @@ dependencies = [ "dotenvy", "entity", "futures-util", - "http", - "hyper", + "http 1.3.1", + "hyper 1.7.0", "hyper-util", "image", "jsonwebtoken", "migration", "qrcode", "rand 0.8.5", + "reqwest", "rsa", "sea-orm", "serde", @@ -479,6 +480,12 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "022dfe9eb35f19ebbcb51e0b40a5ab759f46ad60cadf7297e0bd085afb50e076" +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + [[package]] name = "base64" version = "0.22.1" @@ -497,7 +504,7 @@ version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e65938ed058ef47d92cf8b346cc76ef48984572ade631927e9937b5ffc7662c7" dependencies = [ - "base64", + "base64 0.22.1", "blowfish", "getrandom 0.2.16", "subtle", @@ -524,6 +531,12 @@ version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.10.0" @@ -592,15 +605,15 @@ version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d41711ad46fda47cd701f6908e59d1bd6b9a2b7464c0d0aeab95c6d37096ff8a" dependencies = [ - "base64", + "base64 0.22.1", "bollard-stubs", "bytes", "futures-core", "futures-util", "hex", - "http", + "http 1.3.1", "http-body-util", - "hyper", + "hyper 1.7.0", "hyper-named-pipe", "hyper-util", "hyperlocal", @@ -843,6 +856,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -1061,6 +1084,15 @@ dependencies = [ "serde", ] +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + [[package]] name = "entity" version = "0.1.0" @@ -1236,6 +1268,21 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -1425,6 +1472,25 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "h2" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.12", + "indexmap 2.12.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "h2" version = "0.4.12" @@ -1436,7 +1502,7 @@ dependencies = [ "fnv", "futures-core", "futures-sink", - "http", + "http 1.3.1", "indexmap 2.12.0", "slab", "tokio", @@ -1541,6 +1607,17 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + [[package]] name = "http" version = "1.3.1" @@ -1552,6 +1629,17 @@ dependencies = [ "itoa", ] +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http 0.2.12", + "pin-project-lite", +] + [[package]] name = "http-body" version = "1.0.1" @@ -1559,7 +1647,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http", + "http 1.3.1", ] [[package]] @@ -1570,8 +1658,8 @@ checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", "futures-core", - "http", - "http-body", + "http 1.3.1", + "http-body 1.0.1", "pin-project-lite", ] @@ -1587,6 +1675,30 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "hyper" +version = "0.14.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2 0.3.27", + "http 0.2.12", + "http-body 0.4.6", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2 0.5.10", + "tokio", + "tower-service", + "tracing", + "want", +] + [[package]] name = "hyper" version = "1.7.0" @@ -1597,9 +1709,9 @@ dependencies = [ "bytes", "futures-channel", "futures-core", - "h2", - "http", - "http-body", + "h2 0.4.12", + "http 1.3.1", + "http-body 1.0.1", "httparse", "httpdate", "itoa", @@ -1617,7 +1729,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73b7d8abf35697b81a825e386fc151e0d503e8cb5fcb93cc8669c376dfd6f278" dependencies = [ "hex", - "hyper", + "hyper 1.7.0", "hyper-util", "pin-project-lite", "tokio", @@ -1625,6 +1737,19 @@ dependencies = [ "winapi", ] +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper 0.14.32", + "native-tls", + "tokio", + "tokio-native-tls", +] + [[package]] name = "hyper-util" version = "0.1.17" @@ -1635,12 +1760,12 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "http", - "http-body", - "hyper", + "http 1.3.1", + "http-body 1.0.1", + "hyper 1.7.0", "libc", "pin-project-lite", - "socket2", + "socket2 0.6.1", "tokio", "tower-service", "tracing", @@ -1654,7 +1779,7 @@ checksum = "986c5ce3b994526b3cd75578e62554abd09f0899d6206de48b3e96ab34ccc8c7" dependencies = [ "hex", "http-body-util", - "hyper", + "hyper 1.7.0", "hyper-util", "pin-project-lite", "tokio", @@ -1887,6 +2012,12 @@ dependencies = [ "syn 2.0.108", ] +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + [[package]] name = "is_terminal_polyfill" version = "1.70.2" @@ -1934,7 +2065,7 @@ version = "9.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde" dependencies = [ - "base64", + "base64 0.22.1", "js-sys", "pem", "ring", @@ -1995,7 +2126,7 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" dependencies = [ - "bitflags", + "bitflags 2.10.0", "libc", "redox_syscall", ] @@ -2146,6 +2277,23 @@ dependencies = [ "pxfm", ] +[[package]] +name = "native-tls" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "new_debug_unreachable" version = "1.0.6" @@ -2281,6 +2429,50 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "openssl" +version = "0.10.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" +dependencies = [ + "bitflags 2.10.0", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-sys" +version = "0.9.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "ordered-float" version = "4.6.0" @@ -2361,7 +2553,7 @@ version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" dependencies = [ - "base64", + "base64 0.22.1", "serde_core", ] @@ -2445,7 +2637,7 @@ version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0" dependencies = [ - "bitflags", + "bitflags 2.10.0", "crc32fast", "fdeflate", "flate2", @@ -2645,7 +2837,7 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "221b7eace1aef8c95d65dbe09fb7a1a43d006045394a89afba6997721fcb7708" dependencies = [ - "base64", + "base64 0.22.1", "image", "qrcodegen", ] @@ -2812,7 +3004,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags", + "bitflags 2.10.0", ] [[package]] @@ -2873,6 +3065,46 @@ dependencies = [ "bytecheck", ] +[[package]] +name = "reqwest" +version = "0.11.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" +dependencies = [ + "base64 0.21.7", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2 0.3.27", + "http 0.2.12", + "http-body 0.4.6", + "hyper 0.14.32", + "hyper-tls", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper 0.1.2", + "system-configuration", + "tokio", + "tokio-native-tls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + [[package]] name = "rgb" version = "0.8.52" @@ -2999,7 +3231,7 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ - "bitflags", + "bitflags 2.10.0", "errno", "libc", "linux-raw-sys", @@ -3020,6 +3252,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64 0.21.7", +] + [[package]] name = "rustls-pki-types" version = "1.13.0" @@ -3061,6 +3302,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "schemars" version = "0.9.0" @@ -3261,6 +3511,29 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.10.0", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "serde" version = "1.0.228" @@ -3344,7 +3617,7 @@ version = "3.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fa237f2807440d238e0364a218270b98f767a00d3dada77b1c53ae88940e2e7" dependencies = [ - "base64", + "base64 0.22.1", "chrono", "hex", "indexmap 1.9.3", @@ -3460,6 +3733,16 @@ dependencies = [ "serde", ] +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "socket2" version = "0.6.1" @@ -3508,7 +3791,7 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" dependencies = [ - "base64", + "base64 0.22.1", "bigdecimal", "bytes", "chrono", @@ -3588,9 +3871,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" dependencies = [ "atoi", - "base64", + "base64 0.22.1", "bigdecimal", - "bitflags", + "bitflags 2.10.0", "byteorder", "bytes", "chrono", @@ -3635,9 +3918,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" dependencies = [ "atoi", - "base64", + "base64 0.22.1", "bigdecimal", - "bitflags", + "bitflags 2.10.0", "byteorder", "chrono", "crc", @@ -3761,6 +4044,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + [[package]] name = "sync_wrapper" version = "1.0.2" @@ -3792,12 +4081,46 @@ dependencies = [ "windows", ] +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "tap" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" +[[package]] +name = "tempfile" +version = "3.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +dependencies = [ + "fastrand", + "getrandom 0.3.4", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -3929,7 +4252,7 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2", + "socket2 0.6.1", "tokio-macros", "windows-sys 0.61.2", ] @@ -3945,6 +4268,16 @@ dependencies = [ "syn 2.0.108", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + [[package]] name = "tokio-stream" version = "0.1.17" @@ -4024,7 +4357,7 @@ dependencies = [ "futures-core", "futures-util", "pin-project-lite", - "sync_wrapper", + "sync_wrapper 1.0.2", "tokio", "tower-layer", "tower-service", @@ -4037,10 +4370,10 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" dependencies = [ - "bitflags", + "bitflags 2.10.0", "bytes", - "http", - "http-body", + "http 1.3.1", + "http-body 1.0.1", "http-body-util", "pin-project-lite", "tower-layer", @@ -4814,6 +5147,16 @@ dependencies = [ "memchr", ] +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + [[package]] name = "wit-bindgen" version = "0.46.0" diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 357b1e49..72e87779 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -39,6 +39,7 @@ sysinfo = "0.32" anyhow = "1.0" bollard = "0.17" futures-util = "0.3" +reqwest = { version = "0.11", features = ["json"] } [features] default = [] diff --git a/backend/src/routes/mod.rs b/backend/src/routes/mod.rs index d8e06e42..4b3026cb 100644 --- a/backend/src/routes/mod.rs +++ b/backend/src/routes/mod.rs @@ -18,6 +18,7 @@ pub mod resource_groups; pub mod resources; pub mod subscriptions; pub mod system; +pub mod updates; pub mod users; /// Creates the main application router and logs all registered routes. @@ -62,6 +63,7 @@ pub fn create_router() -> Router { .merge(resources::resources_routes()) .merge(subscriptions::subscriptions_routes()) .merge(system::routes()) + .merge(updates::router()) .merge(users::users_routes()); Router::new() diff --git a/backend/src/routes/updates.rs b/backend/src/routes/updates.rs new file mode 100644 index 00000000..36e0f955 --- /dev/null +++ b/backend/src/routes/updates.rs @@ -0,0 +1,246 @@ +use axum::{ + extract::State, + http::StatusCode, + response::{IntoResponse, Response}, + routing::{get, post}, + Json, Router, +}; +use serde::{Deserialize, Serialize}; +use std::process::Command; + +use crate::AppState; + +#[derive(Debug, Serialize, Deserialize)] +pub struct VersionInfo { + pub current_version: String, + pub latest_version: String, + pub update_available: bool, + pub changelog: Option, + pub release_url: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct UpdateRequest { + pub version: String, +} + +#[derive(Debug, Serialize)] +pub struct UpdateResponse { + pub success: bool, + pub message: String, +} + +/// Get current version and check for updates +#[utoipa::path( + get, + path = "/api/updates/check", + responses( + (status = 200, description = "Version information retrieved successfully", body = VersionInfo), + (status = 500, description = "Failed to check for updates") + ), + tag = "Updates" +)] +pub async fn check_updates(State(_state): State) -> Result, AppError> { + let current_version = env!("CARGO_PKG_VERSION").to_string(); + + // Fetch latest release from GitHub + let client = reqwest::Client::builder() + .user_agent("CSF-Core-Updater") + .build() + .map_err(|e| AppError::InternalError(format!("Failed to create HTTP client: {}", e)))?; + + let response = client + .get("https://api.github.com/repos/CS-Foundry/CSF-Core/releases/latest") + .send() + .await + .map_err(|e| AppError::InternalError(format!("Failed to fetch releases: {}", e)))?; + + if !response.status().is_success() { + return Err(AppError::InternalError(format!( + "GitHub API returned status: {}", + response.status() + ))); + } + + let release: GitHubRelease = response + .json() + .await + .map_err(|e| AppError::InternalError(format!("Failed to parse release data: {}", e)))?; + + let latest_version = release.tag_name.trim_start_matches('v').to_string(); + let update_available = version_compare(¤t_version, &latest_version); + + Ok(Json(VersionInfo { + current_version, + latest_version: latest_version.clone(), + update_available, + changelog: Some(release.body), + release_url: release.html_url, + })) +} + +/// Trigger update installation +#[utoipa::path( + post, + path = "/api/updates/install", + request_body = UpdateRequest, + responses( + (status = 200, description = "Update initiated successfully", body = UpdateResponse), + (status = 400, description = "Invalid request"), + (status = 500, description = "Failed to start update") + ), + tag = "Updates" +)] +pub async fn install_update( + State(_state): State, + Json(payload): Json, +) -> Result, AppError> { + let current_version = env!("CARGO_PKG_VERSION").to_string(); + + // Safety check: don't downgrade + if !version_compare(¤t_version, &payload.version) { + return Ok(Json(UpdateResponse { + success: false, + message: "Cannot install an older or same version".to_string(), + })); + } + + // Run update script in background + let script_path = std::env::current_dir() + .map_err(|e| AppError::InternalError(format!("Failed to get current dir: {}", e)))? + .join("scripts") + .join("update.sh"); + + if !script_path.exists() { + return Err(AppError::InternalError( + "Update script not found".to_string(), + )); + } + + // Clone version for use in response message + let version_for_message = payload.version.clone(); + + // Start update process in background + tokio::spawn(async move { + let _ = Command::new("sh") + .arg(&script_path) + .arg(&payload.version) + .spawn(); + }); + + Ok(Json(UpdateResponse { + success: true, + message: format!( + "Update to version {} initiated. The application will restart shortly.", + version_for_message + ), + })) +} + +/// Get changelog for a specific version +#[utoipa::path( + get, + path = "/api/updates/changelog/{version}", + params( + ("version" = String, Path, description = "Version to get changelog for") + ), + responses( + (status = 200, description = "Changelog retrieved successfully"), + (status = 404, description = "Version not found"), + (status = 500, description = "Failed to fetch changelog") + ), + tag = "Updates" +)] +pub async fn get_changelog( + State(_state): State, + axum::extract::Path(version): axum::extract::Path, +) -> Result, AppError> { + let client = reqwest::Client::builder() + .user_agent("CSF-Core-Updater") + .build() + .map_err(|e| AppError::InternalError(format!("Failed to create HTTP client: {}", e)))?; + + let url = format!( + "https://api.github.com/repos/CS-Foundry/CSF-Core/releases/tags/v{}", + version.trim_start_matches('v') + ); + + let response = client + .get(&url) + .send() + .await + .map_err(|e| AppError::InternalError(format!("Failed to fetch release: {}", e)))?; + + if response.status() == 404 { + return Err(AppError::NotFound(format!("Version {} not found", version))); + } + + if !response.status().is_success() { + return Err(AppError::InternalError(format!( + "GitHub API returned status: {}", + response.status() + ))); + } + + let release: GitHubRelease = response + .json() + .await + .map_err(|e| AppError::InternalError(format!("Failed to parse release data: {}", e)))?; + + Ok(Json(release.body)) +} + +// Helper structs for GitHub API +#[derive(Debug, Deserialize)] +struct GitHubRelease { + tag_name: String, + body: String, + html_url: String, +} + +// Compare versions (returns true if v2 is newer than v1) +fn version_compare(v1: &str, v2: &str) -> bool { + let parse_version = + |v: &str| -> Vec { v.split('.').filter_map(|s| s.parse::().ok()).collect() }; + + let v1_parts = parse_version(v1); + let v2_parts = parse_version(v2); + + for i in 0..v1_parts.len().max(v2_parts.len()) { + let p1 = v1_parts.get(i).copied().unwrap_or(0); + let p2 = v2_parts.get(i).copied().unwrap_or(0); + + if p2 > p1 { + return true; + } else if p2 < p1 { + return false; + } + } + + false +} + +// Error handling +#[derive(Debug)] +pub enum AppError { + InternalError(String), + NotFound(String), +} + +impl IntoResponse for AppError { + fn into_response(self) -> Response { + let (status, message) = match self { + AppError::InternalError(msg) => (StatusCode::INTERNAL_SERVER_ERROR, msg), + AppError::NotFound(msg) => (StatusCode::NOT_FOUND, msg), + }; + + (status, Json(serde_json::json!({ "error": message }))).into_response() + } +} + +pub fn router() -> Router { + Router::new() + .route("/updates/check", get(check_updates)) + .route("/updates/install", post(install_update)) + .route("/updates/changelog/:version", get(get_changelog)) +} diff --git a/docs/features/AUTO_UPDATER.md b/docs/features/AUTO_UPDATER.md new file mode 100644 index 00000000..63c9b2ad --- /dev/null +++ b/docs/features/AUTO_UPDATER.md @@ -0,0 +1,151 @@ +# Auto-Updater Feature + +## Übersicht + +Das Auto-Updater-Feature ermöglicht es CSF-Core, automatisch nach Updates zu suchen und diese mit einem Klick zu installieren. + +## Features + +### 1. Automatische Update-Prüfung + +- Prüft automatisch stündlich auf neue Versionen +- Nutzt GitHub Releases API +- Keine manuelle Aktion erforderlich + +### 2. Sidebar-Benachrichtigung + +- Zeigt eine Benachrichtigung in der Sidebar, wenn ein Update verfügbar ist +- Zeigt die aktuelle und neue Version an +- Link zum Changelog-Dialog + +### 3. Changelog-Anzeige + +- Vollständiger Changelog für neue Versionen +- Markdown-Formatierung wird unterstützt +- Link zu den vollständigen Release-Notes auf GitHub + +### 4. Ein-Klick-Installation + +- Update-Installation per Klick +- Automatischer Download und Installation +- Neustart der Anwendung nach der Installation + +### 5. Einstellungsseite + +- Manuelle Update-Prüfung +- Detaillierte Versionsinformationen +- Vollständige Changelog-Ansicht + +## Technische Details + +### Backend (Rust) + +#### API-Endpunkte + +1. **GET** `/api/updates/check` + + - Prüft die neueste Version von GitHub Releases + - Gibt Versionsinformationen und Changelog zurück + +2. **POST** `/api/updates/install` + + - Startet die Update-Installation + - Akzeptiert die Zielversion als Parameter + +3. **GET** `/api/updates/changelog/:version` + - Ruft den Changelog für eine bestimmte Version ab + +#### Implementierung + +- Datei: `backend/src/routes/updates.rs` +- Nutzt `reqwest` für GitHub API-Aufrufe +- Asynchrone Update-Installation mit tokio + +### Frontend (Svelte) + +#### Komponenten + +1. **UpdateNotification.svelte** + + - Sidebar-Benachrichtigung + - Changelog-Dialog + - Installation-Button + +2. **UpdateSettings.svelte** + - Einstellungsseite für Updates + - Manuelle Update-Prüfung + - Detaillierte Informationen + +#### Services + +- `lib/services/updates.ts`: API-Client für Update-Endpunkte +- `lib/stores/updates.ts`: Svelte-Store für Update-Status + +### Update-Script + +Das Update-Script (`scripts/update.sh`) führt folgende Schritte aus: + +1. Backup der aktuellen Installation erstellen +2. CSF-Core-Service stoppen +3. Neue Version von GitHub herunterladen +4. Installation extrahieren und kopieren +5. Service neu starten +6. Erfolg verifizieren + +## Verwendung + +### Als Benutzer + +1. **Automatische Benachrichtigung**: Eine Benachrichtigung erscheint in der Sidebar, wenn ein Update verfügbar ist +2. **Changelog ansehen**: Klicken Sie auf "Changelog anzeigen & Installieren" +3. **Installieren**: Klicken Sie auf "Jetzt installieren" im Dialog +4. **Warten**: Die Anwendung startet automatisch neu + +### Manuelle Prüfung + +1. Gehen Sie zu **Einstellungen** → **Updates** +2. Klicken Sie auf "Nach Updates suchen" +3. Wenn verfügbar, klicken Sie auf "Jetzt installieren" + +## Sicherheit + +- Updates werden nur von GitHub Releases heruntergeladen +- Versionsnummern werden validiert (keine Downgrades) +- Backup wird vor jedem Update erstellt +- Rollback-Anweisungen werden nach der Installation angezeigt + +## Fehlerbehandlung + +- Netzwerkfehler werden dem Benutzer angezeigt +- Bei Installationsfehlern bleibt die alte Version aktiv +- Backup ermöglicht manuelle Wiederherstellung + +## Entwicklung + +### Testen + +Um das Update-Feature zu testen: + +1. Erstellen Sie ein neues Release auf GitHub +2. Warten Sie auf die automatische Prüfung (oder manuell prüfen) +3. Installieren Sie das Update + +### Anpassung + +- **Prüfintervall ändern**: Passen Sie `60 * 60 * 1000` in `updates.ts` an +- **GitHub-Repository ändern**: Aktualisieren Sie `CS-Foundry/CSF-Core` in `updates.rs` +- **Update-Script anpassen**: Bearbeiten Sie `scripts/update.sh` + +## Bekannte Einschränkungen + +- Update-Installation erfordert Root-Rechte oder sudo +- Während des Updates ist die Anwendung nicht verfügbar +- Nur für Linux-Systeme mit systemd getestet + +## Zukünftige Verbesserungen + +- [ ] Beta/Alpha-Kanal-Unterstützung +- [ ] Automatische Installation planen +- [ ] Rollback-Funktion in der UI +- [ ] Update-Historie +- [ ] Differenzielle Updates (nur geänderte Dateien) diff --git a/frontend/src/lib/components/UpdateNotification.svelte b/frontend/src/lib/components/UpdateNotification.svelte new file mode 100644 index 00000000..11068ed1 --- /dev/null +++ b/frontend/src/lib/components/UpdateNotification.svelte @@ -0,0 +1,151 @@ + + +{#if $updateAvailable && $updateStore.versionInfo} +
+ + +
+
+ + Update verfügbar +
+ + v{$updateStore.versionInfo.latest_version} + +
+
+ +

+ Eine neue Version ist verfügbar. Aktuelle Version: v{$updateStore.versionInfo + .current_version} +

+
+ +
+
+
+
+{/if} + + + + + Update auf v{$updateStore.versionInfo?.latest_version} + Neue Features und Verbesserungen in dieser Version + + +
+ {#if message} + + {message} + + {/if} + +
+ {@html parseChangelog(changelog)} +
+
+ + + + {#if $updateStore.versionInfo} + + {/if} + + +
+
diff --git a/frontend/src/lib/components/navbar/app-sidebar.svelte b/frontend/src/lib/components/navbar/app-sidebar.svelte index 634e9a72..cc25893f 100644 --- a/frontend/src/lib/components/navbar/app-sidebar.svelte +++ b/frontend/src/lib/components/navbar/app-sidebar.svelte @@ -105,6 +105,7 @@ import NavProjects from './nav-projects.svelte'; import NavUser from './nav-user.svelte'; import TeamSwitcher from './team-switcher.svelte'; + import UpdateNotification from '$lib/components/UpdateNotification.svelte'; import * as Sidebar from '$lib/components/ui/sidebar/index.js'; import type { ComponentProps } from 'svelte'; @@ -120,6 +121,7 @@ + diff --git a/frontend/src/lib/components/settings/UpdateSettings.svelte b/frontend/src/lib/components/settings/UpdateSettings.svelte new file mode 100644 index 00000000..5e174af1 --- /dev/null +++ b/frontend/src/lib/components/settings/UpdateSettings.svelte @@ -0,0 +1,205 @@ + + +
+ + + Software-Updates + Überprüfen und installieren Sie CSF-Core Updates + + + +
+
+

Aktuelle Version

+

+ {#if $updateStore.versionInfo} + v{$updateStore.versionInfo.current_version} + {:else} + + {/if} +

+
+ +
+ + + {#if message} + + {message} + + {/if} + + + {#if $updateStore.loading} +
+ + +
+ {:else if $updateStore.error} + + {$updateStore.error} + + {:else if $updateStore.versionInfo} + {#if $updateStore.versionInfo.update_available} + + + +
+
+

Neue Version verfügbar!

+

+ Version v{$updateStore.versionInfo.latest_version} ist jetzt verfügbar. +

+
+ +
+
+
+ + + {#if $updateStore.versionInfo.changelog} + + + Was ist neu? + + +
+ {@html parseChangelog($updateStore.versionInfo.changelog)} +
+
+ + + +
+ {/if} + {:else} + + + +

Sie verwenden die neueste Version!

+

+ CSF-Core ist auf dem neuesten Stand (v{$updateStore.versionInfo.current_version}). +

+
+
+ {/if} + + + {#if $updateStore.lastChecked} +

+ Zuletzt geprüft: {new Intl.DateTimeFormat('de-DE', { + dateStyle: 'short', + timeStyle: 'short', + }).format($updateStore.lastChecked)} +

+ {/if} + {/if} + + +
+

Automatische Update-Prüfung

+

+ CSF-Core prüft automatisch stündlich auf neue Updates. Updates werden nur angezeigt, wenn + sie verfügbar sind. +

+ + + + Hinweis: Bei der Installation eines Updates wird die Anwendung neu gestartet. + Stellen Sie sicher, dass alle Änderungen gespeichert sind. + + +
+
+
+
diff --git a/frontend/src/lib/services/updates.ts b/frontend/src/lib/services/updates.ts new file mode 100644 index 00000000..a60a1fd4 --- /dev/null +++ b/frontend/src/lib/services/updates.ts @@ -0,0 +1,80 @@ +export interface VersionInfo { + current_version: string; + latest_version: string; + update_available: boolean; + changelog: string | null; + release_url: string; +} + +export interface UpdateResponse { + success: boolean; + message: string; +} + +export class UpdateService { + private apiUrl: string; + + constructor() { + this.apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:8080'; + } + + /** + * Check for available updates + */ + async checkForUpdates(): Promise { + const response = await fetch(`${this.apiUrl}/api/updates/check`, { + method: 'GET', + credentials: 'include', + headers: { + 'Content-Type': 'application/json' + } + }); + + if (!response.ok) { + throw new Error(`Failed to check for updates: ${response.statusText}`); + } + + return await response.json(); + } + + /** + * Install a specific version + */ + async installUpdate(version: string): Promise { + const response = await fetch(`${this.apiUrl}/api/updates/install`, { + method: 'POST', + credentials: 'include', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ version }) + }); + + if (!response.ok) { + throw new Error(`Failed to install update: ${response.statusText}`); + } + + return await response.json(); + } + + /** + * Get changelog for a specific version + */ + async getChangelog(version: string): Promise { + const response = await fetch(`${this.apiUrl}/api/updates/changelog/${version}`, { + method: 'GET', + credentials: 'include', + headers: { + 'Content-Type': 'application/json' + } + }); + + if (!response.ok) { + throw new Error(`Failed to get changelog: ${response.statusText}`); + } + + return await response.json(); + } +} + +export const updateService = new UpdateService(); diff --git a/frontend/src/lib/stores/updates.ts b/frontend/src/lib/stores/updates.ts new file mode 100644 index 00000000..dd3b554c --- /dev/null +++ b/frontend/src/lib/stores/updates.ts @@ -0,0 +1,103 @@ +import { writable, derived } from 'svelte/store'; +import { updateService, type VersionInfo } from '$lib/services/updates'; + +interface UpdateState { + versionInfo: VersionInfo | null; + loading: boolean; + error: string | null; + lastChecked: Date | null; + isInstalling: boolean; +} + +const initialState: UpdateState = { + versionInfo: null, + loading: false, + error: null, + lastChecked: null, + isInstalling: false +}; + +function createUpdateStore() { + const { subscribe, set, update } = writable(initialState); + + // Check for updates every hour + let intervalId: number | null = null; + + return { + subscribe, + + async checkForUpdates() { + update((state) => ({ ...state, loading: true, error: null })); + + try { + const versionInfo = await updateService.checkForUpdates(); + update((state) => ({ + ...state, + versionInfo, + loading: false, + lastChecked: new Date() + })); + } catch (error) { + update((state) => ({ + ...state, + loading: false, + error: error instanceof Error ? error.message : 'Failed to check for updates' + })); + } + }, + + async installUpdate(version: string) { + update((state) => ({ ...state, isInstalling: true, error: null })); + + try { + const response = await updateService.installUpdate(version); + if (response.success) { + update((state) => ({ ...state, isInstalling: false })); + return response; + } else { + throw new Error(response.message); + } + } catch (error) { + update((state) => ({ + ...state, + isInstalling: false, + error: error instanceof Error ? error.message : 'Failed to install update' + })); + throw error; + } + }, + + startAutoCheck() { + // Check immediately + this.checkForUpdates(); + + // Then check every hour + if (intervalId === null) { + intervalId = window.setInterval( + () => { + this.checkForUpdates(); + }, + 60 * 60 * 1000 + ); // 1 hour + } + }, + + stopAutoCheck() { + if (intervalId !== null) { + clearInterval(intervalId); + intervalId = null; + } + }, + + reset() { + set(initialState); + } + }; +} + +export const updateStore = createUpdateStore(); + +// Derived store for easy access to update availability +export const updateAvailable = derived(updateStore, ($updateStore) => { + return $updateStore.versionInfo?.update_available ?? false; +}); diff --git a/frontend/src/routes/settings/+page.svelte b/frontend/src/routes/settings/+page.svelte index e8fe6524..2b723963 100644 --- a/frontend/src/routes/settings/+page.svelte +++ b/frontend/src/routes/settings/+page.svelte @@ -20,6 +20,7 @@ } from '@lucide/svelte'; import { onMount } from 'svelte'; import OrganizationSettings from '$lib/components/settings/OrganizationSettings.svelte'; + import UpdateSettings from '$lib/components/settings/UpdateSettings.svelte'; let authState = $derived($authStore); let email = $state(''); @@ -179,7 +180,7 @@ - + Profil @@ -188,6 +189,23 @@ Sicherheit + + + + + Updates + Organisation @@ -433,6 +451,10 @@ + + + + diff --git a/scripts/update.sh b/scripts/update.sh new file mode 100755 index 00000000..0973087c --- /dev/null +++ b/scripts/update.sh @@ -0,0 +1,122 @@ +#!/bin/bash + +set -e + +# CSF-Core Update Script +# This script downloads and installs updates for CSF-Core + +VERSION="${1}" +REPO="CS-Foundry/CSF-Core" +INSTALL_DIR="/opt/csf-core" +BACKUP_DIR="/tmp/csf-core-backup-$(date +%s)" + +log() { + echo "[$(date +'%Y-%m-%d %H:%M:%S')] $1" +} + +error() { + echo "[$(date +'%Y-%m-%d %H:%M:%S')] ERROR: $1" >&2 + exit 1 +} + +# Check if version is provided +if [ -z "$VERSION" ]; then + error "No version specified. Usage: $0 " +fi + +log "Starting CSF-Core update to version ${VERSION}..." + +# Check if running as root or with sudo +if [ "$EUID" -eq 0 ]; then + SUDO="" +else + SUDO="sudo" +fi + +# Create backup directory +log "Creating backup at ${BACKUP_DIR}..." +$SUDO mkdir -p "${BACKUP_DIR}" + +# Backup current installation +if [ -d "${INSTALL_DIR}" ]; then + log "Backing up current installation..." + $SUDO cp -r "${INSTALL_DIR}" "${BACKUP_DIR}/" +fi + +# Stop the service if running +if systemctl is-active --quiet csf-core.service 2>/dev/null; then + log "Stopping CSF-Core service..." + $SUDO systemctl stop csf-core.service || log "Service stop failed or not installed" +fi + +# Download the new version +DOWNLOAD_URL="https://github.com/${REPO}/archive/refs/tags/v${VERSION}.tar.gz" +TMP_DIR=$(mktemp -d) +log "Downloading version ${VERSION} from ${DOWNLOAD_URL}..." + +if ! curl -L -o "${TMP_DIR}/csf-core.tar.gz" "${DOWNLOAD_URL}"; then + error "Failed to download update from ${DOWNLOAD_URL}" +fi + +# Extract the archive +log "Extracting archive..." +cd "${TMP_DIR}" +tar -xzf csf-core.tar.gz || error "Failed to extract archive" + +# Find the extracted directory (should be CSF-Core-) +EXTRACTED_DIR=$(find "${TMP_DIR}" -maxdepth 1 -type d -name "CSF-Core-*" | head -1) +if [ -z "$EXTRACTED_DIR" ]; then + error "Could not find extracted directory" +fi + +log "Extracted to ${EXTRACTED_DIR}" + +# Install the update +log "Installing update..." +cd "${EXTRACTED_DIR}" + +# Run installation script if it exists +if [ -f "scripts/install.sh" ]; then + log "Running installation script..." + $SUDO bash scripts/install.sh || error "Installation script failed" +else + # Manual installation fallback + log "No installation script found, performing manual update..." + + # Copy files to installation directory + $SUDO mkdir -p "${INSTALL_DIR}" + $SUDO cp -r ./* "${INSTALL_DIR}/" + + # Set permissions + $SUDO chown -R root:root "${INSTALL_DIR}" + $SUDO chmod +x "${INSTALL_DIR}/scripts/"*.sh 2>/dev/null || true + + # Restart service if systemd service exists + if [ -f "${INSTALL_DIR}/csf-core.service" ]; then + log "Reloading systemd and starting service..." + $SUDO systemctl daemon-reload + $SUDO systemctl start csf-core.service + $SUDO systemctl enable csf-core.service + fi +fi + +# Cleanup +log "Cleaning up temporary files..." +rm -rf "${TMP_DIR}" + +# Verify installation +if systemctl is-active --quiet csf-core.service 2>/dev/null; then + log "✓ CSF-Core service is running" +else + log "⚠ CSF-Core service is not running. Manual start may be required." +fi + +log "Update to version ${VERSION} completed successfully!" +log "Backup saved at: ${BACKUP_DIR}" +log "" +log "If you encounter any issues, you can restore from the backup:" +log " sudo rm -rf ${INSTALL_DIR}" +log " sudo mv ${BACKUP_DIR}/csf-core ${INSTALL_DIR}" +log " sudo systemctl restart csf-core.service" + +exit 0