Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
80a3bd1911 | ||
|
|
2da1ea55a9 | ||
|
|
2e4cf1cef0 |
@@ -5,6 +5,20 @@ Alle nennenswerten Änderungen an diesem Projekt werden in dieser Datei dokument
|
|||||||
Das Format basiert auf [Keep a Changelog](https://keepachangelog.com/de/1.1.0/)
|
Das Format basiert auf [Keep a Changelog](https://keepachangelog.com/de/1.1.0/)
|
||||||
und dieses Projekt folgt den Richtlinien von [Semantic Versioning](https://semver.org/lang/de/).
|
und dieses Projekt folgt den Richtlinien von [Semantic Versioning](https://semver.org/lang/de/).
|
||||||
|
|
||||||
|
## [0.6.0] - 2026-09-16
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **`sanctum upgrade` (Alias: `sanctum update`) - In-Place Self-Upgrade Engine**:
|
||||||
|
- Ermöglicht das direkte Aktualisieren der laufenden Sanctum-Binary ohne externe Paketmanager oder manuelle Download-Schritte.
|
||||||
|
- **Plattformunabhängiger In-Place-Austausch**: Löst das Windows-Dateilock-Problem (`ERROR_ACCESS_DENIED`) via `self-replace` (Umbenennen der laufenden Executable und verzögerte Bereinigung). Auf Linux/Unix wird ein sauberer atomarer Tausch mit Ausführungsrechten (`0o755`) durchgeführt.
|
||||||
|
- **Kryptografische Integritätsprüfung**: Vor dem Austausch wird jedes heruntergeladene Binary zwingend gegen die offizielle `SHA256SUMS.txt` des Gitea-Releases via SHA-256 verifiziert. Bei Abweichungen bleibt die bestehende Installation vollständig unberührt.
|
||||||
|
- **Prüfmodus (`--check` / `-c`)**: Schneller Abgleich mit der Gitea-Releases-API zur Feststellung, ob ein Update vorliegt, ohne Downloads durchzuführen.
|
||||||
|
- **Automatisierungsmodus (`--yes` / `-y`)**: Führt das Upgrade ohne interaktive Bestätigung durch – ideal für geplante Aufgaben und Skripte.
|
||||||
|
- **Force-Option (`--force` / `-f`)**: Erlaubt das Erzwingen einer Neuinstallation / Aktualisierung auch bei gleicher Version oder erkannter Paketmanager-Umgebung.
|
||||||
|
- **Paketmanager-Erkennung**: Erkennt automatisch, ob Sanctum über Scoop oder Winget ausgeführt wird, und empfiehlt den entsprechenden Paketmanager-Befehl (`scoop update sanctum` bzw. `winget upgrade HaraldPansi.Sanctum`).
|
||||||
|
- **Testsuite**:
|
||||||
|
- `tests/upgrade_test.rs`: 5 neue Tests zur Verifikation von Checksum-Parsing, SemVer-Vergleich, Paketmanager-Erkennung, SHA-256 Hash-Validierung und Gitea Release API-JSON-Deserialisierung.
|
||||||
|
|
||||||
## [0.5.0] - 2026-09-16
|
## [0.5.0] - 2026-09-16
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
Generated
+161
-1
@@ -512,6 +512,12 @@ version = "0.1.9"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
|
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fastrand"
|
||||||
|
version = "2.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "find-msvc-tools"
|
name = "find-msvc-tools"
|
||||||
version = "0.1.12"
|
version = "0.1.12"
|
||||||
@@ -1020,6 +1026,12 @@ dependencies = [
|
|||||||
"vcpkg",
|
"vcpkg",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "linux-raw-sys"
|
||||||
|
version = "0.12.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "litemap"
|
name = "litemap"
|
||||||
version = "0.8.3"
|
version = "0.8.3"
|
||||||
@@ -1334,6 +1346,20 @@ version = "0.8.11"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
|
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ring"
|
||||||
|
version = "0.17.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"cfg-if",
|
||||||
|
"getrandom 0.2.17",
|
||||||
|
"libc",
|
||||||
|
"untrusted",
|
||||||
|
"windows-sys 0.52.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rpassword"
|
name = "rpassword"
|
||||||
version = "7.5.4"
|
version = "7.5.4"
|
||||||
@@ -1369,6 +1395,54 @@ dependencies = [
|
|||||||
"smallvec",
|
"smallvec",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustix"
|
||||||
|
version = "1.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.13.1",
|
||||||
|
"errno",
|
||||||
|
"libc",
|
||||||
|
"linux-raw-sys",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustls"
|
||||||
|
version = "0.23.45"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"once_cell",
|
||||||
|
"ring",
|
||||||
|
"rustls-pki-types",
|
||||||
|
"rustls-webpki",
|
||||||
|
"subtle",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustls-pki-types"
|
||||||
|
version = "1.15.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96"
|
||||||
|
dependencies = [
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustls-webpki"
|
||||||
|
version = "0.103.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2"
|
||||||
|
dependencies = [
|
||||||
|
"ring",
|
||||||
|
"rustls-pki-types",
|
||||||
|
"untrusted",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustversion"
|
name = "rustversion"
|
||||||
version = "1.0.23"
|
version = "1.0.23"
|
||||||
@@ -1377,7 +1451,7 @@ checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sanctum"
|
name = "sanctum"
|
||||||
version = "0.5.0"
|
version = "0.6.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes-gcm",
|
"aes-gcm",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
@@ -1396,13 +1470,17 @@ dependencies = [
|
|||||||
"rand",
|
"rand",
|
||||||
"rpassword",
|
"rpassword",
|
||||||
"rusqlite",
|
"rusqlite",
|
||||||
|
"self-replace",
|
||||||
|
"semver",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"sha2",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
"tray-item",
|
"tray-item",
|
||||||
|
"ureq",
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1412,6 +1490,23 @@ version = "1.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||||
|
|
||||||
|
[[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 = "semver"
|
||||||
|
version = "1.0.28"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.229"
|
version = "1.0.229"
|
||||||
@@ -1466,6 +1561,17 @@ dependencies = [
|
|||||||
"digest",
|
"digest",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sha2"
|
||||||
|
version = "0.10.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"cpufeatures",
|
||||||
|
"digest",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sharded-slab"
|
name = "sharded-slab"
|
||||||
version = "0.1.7"
|
version = "0.1.7"
|
||||||
@@ -1564,6 +1670,19 @@ dependencies = [
|
|||||||
"syn 2.0.119",
|
"syn 2.0.119",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tempfile"
|
||||||
|
version = "3.27.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
|
||||||
|
dependencies = [
|
||||||
|
"fastrand",
|
||||||
|
"getrandom 0.4.3",
|
||||||
|
"once_cell",
|
||||||
|
"rustix",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "2.0.20"
|
version = "2.0.20"
|
||||||
@@ -1780,6 +1899,29 @@ dependencies = [
|
|||||||
"subtle",
|
"subtle",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "untrusted"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ureq"
|
||||||
|
version = "2.12.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d"
|
||||||
|
dependencies = [
|
||||||
|
"base64",
|
||||||
|
"log",
|
||||||
|
"once_cell",
|
||||||
|
"rustls",
|
||||||
|
"rustls-pki-types",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"url",
|
||||||
|
"webpki-roots 0.26.11",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "url"
|
name = "url"
|
||||||
version = "2.5.8"
|
version = "2.5.8"
|
||||||
@@ -1884,6 +2026,24 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "webpki-roots"
|
||||||
|
version = "0.26.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9"
|
||||||
|
dependencies = [
|
||||||
|
"webpki-roots 1.0.9",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "webpki-roots"
|
||||||
|
version = "1.0.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a"
|
||||||
|
dependencies = [
|
||||||
|
"rustls-pki-types",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-core"
|
name = "windows-core"
|
||||||
version = "0.62.2"
|
version = "0.62.2"
|
||||||
|
|||||||
+5
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "sanctum"
|
name = "sanctum"
|
||||||
version = "0.5.0"
|
version = "0.6.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
authors = ["Harald Pansi <harald@pansi.eu>", "Sanctum Engineering Team"]
|
authors = ["Harald Pansi <harald@pansi.eu>", "Sanctum Engineering Team"]
|
||||||
description = "Verschlüsselter Ein-Datei-Container unter Windows im reinen Userland via WebDAV"
|
description = "Verschlüsselter Ein-Datei-Container unter Windows im reinen Userland via WebDAV"
|
||||||
@@ -36,6 +36,10 @@ dyn-clone = "1.0"
|
|||||||
lz4_flex = "0.11"
|
lz4_flex = "0.11"
|
||||||
bip39 = { version = "2.2", features = ["zeroize"] }
|
bip39 = { version = "2.2", features = ["zeroize"] }
|
||||||
hex = "0.4"
|
hex = "0.4"
|
||||||
|
self-replace = "1.5"
|
||||||
|
ureq = { version = "2.10", default-features = false, features = ["json", "tls"] }
|
||||||
|
sha2 = "0.10"
|
||||||
|
semver = "1.0"
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
tray-item = "0.10"
|
tray-item = "0.10"
|
||||||
|
|||||||
@@ -4,18 +4,20 @@
|
|||||||
|
|
||||||
# Sanctum 🛡️
|
# Sanctum 🛡️
|
||||||
|
|
||||||
**Verschlüsselter Ein-Datei-Container unter Windows 10/11 im reinen Userland**
|
**Verschlüsselter Ein-Datei-Container unter Windows & Linux im reinen Userland**
|
||||||
|
|
||||||
Sanctum ist eine eigenständige, speichersichere und hochperformante CLI-Anwendung in Rust, die verschlüsselte Ein-Datei-Container (`.sanctum`) unter Windows 10/11 im reinen Userland verwaltet.
|
Sanctum ist eine eigenständige, speichersichere und hochperformante CLI-Anwendung in Rust, die verschlüsselte Ein-Datei-Container (`.sanctum`) unter Windows 10/11 und Linux im reinen Userland verwaltet.
|
||||||
|
|
||||||
- **100% Userland**: Keine Administratorrechte erforderlich, keine Kernel-Treiber (weder WinFsp noch Dokan).
|
- **100% Userland**: Keine Administratorrechte erforderlich, keine Kernel-Treiber (weder WinFsp noch Dokan).
|
||||||
- **Windows Explorer-Integration**: Einhängen als nativer Netzlaufwerk-Buchstabe (Z: abwärts) via lokalem WebDAV (`127.0.0.1`), Explorer-Kontextmenü und System-Tray-Icon.
|
- **Windows Explorer- & Desktop-Integration**: Einhängen als nativer Netzlaufwerk-Buchstabe (Z: abwärts) via lokalem WebDAV (`127.0.0.1`), Explorer-Kontextmenü und System-Tray-Icon unter Windows sowie `gio mount` / `.desktop`-MIME unter Linux.
|
||||||
|
- **Native Synchronisation (`sanctum sync`)**: RSync-artiger bidirektionaler Dateiabgleich (`Push` & `Pull`) mit 1-MB-Chunk-Streaming direkt in SQLite. Umgeht das fundamentale 4-GB-Dateilimit des Windows WebClient-Dienstes (`0x800700DF`) für Dateien beliebiger Größe (5 GB, 50 GB, 500 GB+). Inklusive `--dry-run` Simulation, `--delete` Spiegelung und `--checksum` Hashprüfung.
|
||||||
|
- **In-Place Self-Upgrade (`sanctum upgrade`)**: Aktualisiert das laufende Binary per Knopfdruck direkt über Gitea Releases. Löst Windows File-Locking via `self-replace` und verifiziert jedes Release kryptografisch gegen `SHA256SUMS.txt`.
|
||||||
- **Transparente LZ4-Kompression**: Automatische Kompression von Chunks mit On-the-Fly-Dekompression und adaptivem Fallback.
|
- **Transparente LZ4-Kompression**: Automatische Kompression von Chunks mit On-the-Fly-Dekompression und adaptivem Fallback.
|
||||||
- **Anti-Forensik & Storage Compaction**: Inkrementelles Auto-Vacuum zur physikalischen Freigabe von Speicherplatz und kryptografisches Überschreiben (Shredding) von Datenblöcken mit CSPRNG-Rauschen vor dem Löschen.
|
- **Anti-Forensik & Storage Compaction**: Inkrementelles Auto-Vacuum zur physikalischen Freigabe von Speicherplatz und kryptografisches Überschreiben (Shredding) von Datenblöcken mit CSPRNG-Rauschen vor dem Löschen.
|
||||||
- **Plausible Deniability (Hidden Safe)**: Dual-Slot-Header. Ein unbenutzter Slot enthält uniformes CSPRNG-Rauschen, das sich nicht von einem echten Hidden Vault unterscheiden lässt. Volle Dateinamen-Verschlüsselung im Hidden Vault.
|
- **Plausible Deniability (Hidden Safe)**: Dual-Slot-Header. Ein unbenutzter Slot enthält uniformes CSPRNG-Rauschen, das sich nicht von einem echten Hidden Vault unterscheiden lässt. Volle Dateinamen-Verschlüsselung im Hidden Vault.
|
||||||
- **OpSec & Explorer Anti-Leak Shield**: Blockiert Explorer-Spuren (`Thumbs.db`, `desktop.ini`, `*.tmp`, `:Zone.Identifier`), automatischer Unmount bei Inaktivität oder Windows-Sitzungssperre (`Win + L`).
|
- **OpSec & Explorer Anti-Leak Shield**: Blockiert Explorer-Spuren (`Thumbs.db`, `desktop.ini`, `*.tmp`, `:Zone.Identifier`), automatischer Unmount bei Inaktivität oder Windows-Sitzungssperre (`Win + L`).
|
||||||
- **Disaster Recovery**: 24-Wort BIP-39 Mnemonic Seed Phrases, konsistente Online-Backups via SQLite Online Backup API und kryptografische Vollprüfung (`sanctum verify`).
|
- **Disaster Recovery**: 24-Wort BIP-39 Mnemonic Seed Phrases, konsistente Online-Backups via SQLite Online Backup API und kryptografische Vollprüfung (`sanctum verify`).
|
||||||
- **Statisches Single-Binary**: `sanctum.exe` (~5.3 MB) ohne externe DLL-Abhängigkeiten.
|
- **Statisches Single-Binary**: Standalone-Executables ohne externe DLL-Abhängigkeiten (`sanctum.exe` für Windows, statisches musl-ELF für Linux).
|
||||||
|
|
||||||
> 💡 **Neu bei Sanctum?** Eine kompakte Schritt-für-Schritt-Anleitung findest du in der [Schnellstartanleitung (QUICKSTART.md)](QUICKSTART.md). Hinweise zur Installation via Scoop oder Winget gibt es im [Installations-Guide (INSTALL.md)](INSTALL.md).
|
> 💡 **Neu bei Sanctum?** Eine kompakte Schritt-für-Schritt-Anleitung findest du in der [Schnellstartanleitung (QUICKSTART.md)](QUICKSTART.md). Hinweise zur Installation via Scoop oder Winget gibt es im [Installations-Guide (INSTALL.md)](INSTALL.md).
|
||||||
|
|
||||||
@@ -169,6 +171,59 @@ sanctum.exe unregister
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### 8. Native Synchronisation (`sanctum sync`)
|
||||||
|
|
||||||
|
Sanctum bietet eine native, rsync-ähnliche Synchronisations-Engine, die Dateien direkt auf Datenbank- und Chunk-Ebene überträgt. Dies löst das fundamentale Problem des Windows WebClient-Dienstes (`mrxdav.sys`), welcher beim Kopieren über gemountete WebDAV-Netzlaufwerke konstruktionsbedingt auf maximal 4 GB pro Datei beschränkt ist (`FileSizeLimitInBytes` / Fehler `0x800700DF`).
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# 1. Vorab-Simulation ausführen (Dry-Run / risikolos prüfen):
|
||||||
|
sanctum.exe sync --path "C:\Pfad\tresor.sanctum" "C:\Users\User\Downloads" /Downloads --dry-run
|
||||||
|
|
||||||
|
# 2. Ordner synchronisieren (unfertige Downloads und Temporärdateien ausschließen):
|
||||||
|
sanctum.exe sync --path "C:\Pfad\tresor.sanctum" "C:\Users\User\Downloads" /Downloads --exclude "*.crdownload" --exclude "*.tmp"
|
||||||
|
|
||||||
|
# 3. Exakte 1:1 Spiegelung (löscht verwaiste Dateien im Ziel):
|
||||||
|
sanctum.exe sync --path "C:\Pfad\tresor.sanctum" "C:\Users\User\Dokumente" /Dokumente --delete
|
||||||
|
|
||||||
|
# 4. Mit kryptografischer Inhaltsprüfung (SHA-256 Hashes vergleichen):
|
||||||
|
sanctum.exe sync --path "C:\Pfad\tresor.sanctum" "C:\Users\User\Projekte" /Projekte --checksum
|
||||||
|
|
||||||
|
# 5. Daten aus dem Tresor wiederherstellen (Pull / Container -> Host):
|
||||||
|
sanctum.exe sync --path "C:\Pfad\tresor.sanctum" /Downloads "D:\Wiederhergestellt\Downloads" --pull
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Dateigrößen**: Dateien beliebiger Größe (**5 GB, 50 GB, 500 GB+**) werden in 1-MB-Streaming-Chunks mit AES-256-GCM direkt übertragen.
|
||||||
|
- **Fast-Delta Check**: Dateien mit identischer Größe und Modifikationszeitstempel (`mtime`) werden sofort übersprungen.
|
||||||
|
- **Metadaten-Erhaltung**: Datei-Zeitstempel (`mtime`) werden in beide Richtungen exakt konserviert.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 9. In-Place Self-Upgrade (`sanctum upgrade`)
|
||||||
|
|
||||||
|
Aktualisiert die laufende Sanctum-Executable direkt über die offizielle Gitea-Releases-API auf die neueste Version, ohne manuelle Browser-Downloads oder separate Paketmanager:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Nur prüfen, ob ein Update verfügbar ist:
|
||||||
|
sanctum.exe upgrade --check
|
||||||
|
# oder mit dem Alias:
|
||||||
|
sanctum.exe update -c
|
||||||
|
|
||||||
|
# Interaktives Upgrade durchführen (zeigt Release-Details und fragt nach Bestätigung):
|
||||||
|
sanctum.exe upgrade
|
||||||
|
|
||||||
|
# Vollautomatisches Upgrade ohne Rückfragen (z. B. in Skripten):
|
||||||
|
sanctum.exe upgrade -y
|
||||||
|
|
||||||
|
# Upgrade erzwingen (Neuinstallation / Reinstall derselben Version):
|
||||||
|
sanctum.exe upgrade --force
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Plattformunabhängig**: Löst das Windows-Dateilock (`ERROR_ACCESS_DENIED`) via `self-replace` und unterstützt ebenso Linux (statisches musl ELF).
|
||||||
|
- **Kryptografische Integrität**: Das heruntergeladene Binary wird vor dem Ersetzen zwingend gegen die offizielle `SHA256SUMS.txt` via SHA-256 verifiziert.
|
||||||
|
- **Paketmanager-Harmonie**: Erkennt automatisch Installationen via Scoop (`scoop update sanctum`) oder Winget (`winget upgrade HaraldPansi.Sanctum`) und warnt den Benutzer.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 🛡️ OpSec & Explorer Anti-Leak Shield
|
## 🛡️ OpSec & Explorer Anti-Leak Shield
|
||||||
|
|
||||||
Sanctum schützt vertrauliche Daten vor unbeabsichtigten Windows-Spuren:
|
Sanctum schützt vertrauliche Daten vor unbeabsichtigten Windows-Spuren:
|
||||||
@@ -211,8 +266,8 @@ powershell -ExecutionPolicy Bypass -File .\scripts\package-release.ps1
|
|||||||
```
|
```
|
||||||
|
|
||||||
Erzeugt:
|
Erzeugt:
|
||||||
- `dist/sanctum-v0.4.0-windows-x86_64.zip` (Windows x86_64 ZIP)
|
- `dist/sanctum-v0.6.0-windows-x86_64.zip` (Windows x86_64 ZIP)
|
||||||
- `dist/sanctum-v0.4.0-linux-x86_64.tar.gz` (Linux x86_64 musl static TAR.GZ via `package-release-linux.ps1`)
|
- `dist/sanctum-v0.6.0-linux-x86_64.tar.gz` (Linux x86_64 musl static TAR.GZ via `package-release-linux.ps1`)
|
||||||
- `dist/SHA256SUMS.txt`
|
- `dist/SHA256SUMS.txt`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"version": "0.5.0",
|
"version": "0.6.0",
|
||||||
"description": "Verschlüsselter Ein-Datei-Container unter Windows im reinen Userland via WebDAV",
|
"description": "Verschlüsselter Ein-Datei-Container unter Windows im reinen Userland via WebDAV",
|
||||||
"homepage": "https://gitea.pansi.eu/harald/sanctum",
|
"homepage": "https://gitea.pansi.eu/harald/sanctum",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"architecture": {
|
"architecture": {
|
||||||
"64bit": {
|
"64bit": {
|
||||||
"url": "https://gitea.pansi.eu/harald/sanctum/releases/download/v0.5.0/sanctum-v0.5.0-windows-x86_64.zip",
|
"url": "https://gitea.pansi.eu/harald/sanctum/releases/download/v0.6.0/sanctum-v0.6.0-windows-x86_64.zip",
|
||||||
"hash": "2f069a8cc41f9fa6a2559e2952e5669523c21c175df15f6d80f0473bbe2fa60e",
|
"hash": "5f2a66bd5ff2ab322b92108fde243c2c5c8d331d55ea98b76f0c94cd8c536a69",
|
||||||
"bin": "sanctum.exe"
|
"bin": "sanctum.exe"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# yaml-language-server: $schema=https://aka.ms/winget-manifest.singleton.1.6.0.schema.json
|
# yaml-language-server: $schema=https://aka.ms/winget-manifest.singleton.1.6.0.schema.json
|
||||||
PackageIdentifier: HaraldPansi.Sanctum
|
PackageIdentifier: HaraldPansi.Sanctum
|
||||||
PackageVersion: 0.5.0
|
PackageVersion: 0.6.0
|
||||||
PackageName: Sanctum
|
PackageName: Sanctum
|
||||||
Publisher: Harald Pansi
|
Publisher: Harald Pansi
|
||||||
PublisherUrl: https://gitea.pansi.eu/harald
|
PublisherUrl: https://gitea.pansi.eu/harald
|
||||||
@@ -18,7 +18,7 @@ Tags:
|
|||||||
- webdav
|
- webdav
|
||||||
- container
|
- container
|
||||||
- plausible-deniability
|
- plausible-deniability
|
||||||
ReleaseNotesUrl: https://gitea.pansi.eu/harald/sanctum/releases/tag/v0.5.0
|
ReleaseNotesUrl: https://gitea.pansi.eu/harald/sanctum/releases/tag/v0.6.0
|
||||||
Installers:
|
Installers:
|
||||||
- Architecture: x64
|
- Architecture: x64
|
||||||
InstallerType: zip
|
InstallerType: zip
|
||||||
@@ -26,7 +26,7 @@ Installers:
|
|||||||
NestedInstallerFiles:
|
NestedInstallerFiles:
|
||||||
- RelativeFilePath: sanctum.exe
|
- RelativeFilePath: sanctum.exe
|
||||||
PortableCommandAlias: sanctum
|
PortableCommandAlias: sanctum
|
||||||
InstallerUrl: https://gitea.pansi.eu/harald/sanctum/releases/download/v0.5.0/sanctum-v0.5.0-windows-x86_64.zip
|
InstallerUrl: https://gitea.pansi.eu/harald/sanctum/releases/download/v0.6.0/sanctum-v0.6.0-windows-x86_64.zip
|
||||||
InstallerSha256: 2f069a8cc41f9fa6a2559e2952e5669523c21c175df15f6d80f0473bbe2fa60e
|
InstallerSha256: 5f2a66bd5ff2ab322b92108fde243c2c5c8d331d55ea98b76f0c94cd8c536a69
|
||||||
ManifestType: singleton
|
ManifestType: singleton
|
||||||
ManifestVersion: 1.6.0
|
ManifestVersion: 1.6.0
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ pub mod recovery;
|
|||||||
pub mod storage;
|
pub mod storage;
|
||||||
pub mod sync;
|
pub mod sync;
|
||||||
pub mod ui;
|
pub mod ui;
|
||||||
|
pub mod upgrade;
|
||||||
pub mod verify;
|
pub mod verify;
|
||||||
pub mod vfs;
|
pub mod vfs;
|
||||||
pub mod windows;
|
pub mod windows;
|
||||||
|
|||||||
+29
@@ -248,6 +248,26 @@ enum Commands {
|
|||||||
|
|
||||||
/// Entfernt die .sanctum Explorer-Integration aus der Benutzer-Registry
|
/// Entfernt die .sanctum Explorer-Integration aus der Benutzer-Registry
|
||||||
Unregister,
|
Unregister,
|
||||||
|
|
||||||
|
/// Aktualisiert Sanctum auf die neueste Version (In-Place Self-Upgrade)
|
||||||
|
#[command(alias = "update")]
|
||||||
|
Upgrade {
|
||||||
|
/// Prüft nur auf verfügbare Aktualisierungen, ohne Dateien herunterzuladen
|
||||||
|
#[arg(short = 'c', long, default_value_t = false)]
|
||||||
|
check: bool,
|
||||||
|
|
||||||
|
/// Führt die Aktualisierung ohne interaktive Bestätigung durch
|
||||||
|
#[arg(short = 'y', long, default_value_t = false)]
|
||||||
|
yes: bool,
|
||||||
|
|
||||||
|
/// Erzwingt die Aktualisierung (auch bei gleicher Version oder erkannter Paketmanager-Verwaltung)
|
||||||
|
#[arg(short = 'f', long, default_value_t = false)]
|
||||||
|
force: bool,
|
||||||
|
|
||||||
|
/// Benutzerdefinierte Gitea-Basis-URL (Standard: https://gitea.pansi.eu)
|
||||||
|
#[arg(long, default_value = "https://gitea.pansi.eu")]
|
||||||
|
url: String,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_drive_letter(s: &str) -> Result<char> {
|
fn parse_drive_letter(s: &str) -> Result<char> {
|
||||||
@@ -1246,6 +1266,15 @@ async fn run() -> Result<()> {
|
|||||||
println!(" {} .sanctum Dateiverknüpfungen wurden aus der Registry entfernt.", ui::green("✔"));
|
println!(" {} .sanctum Dateiverknüpfungen wurden aus der Registry entfernt.", ui::green("✔"));
|
||||||
println!();
|
println!();
|
||||||
}
|
}
|
||||||
|
Commands::Upgrade { check, yes, force, url } => {
|
||||||
|
let options = sanctum::upgrade::UpgradeOptions {
|
||||||
|
check_only: check,
|
||||||
|
yes,
|
||||||
|
force,
|
||||||
|
base_url: url,
|
||||||
|
};
|
||||||
|
sanctum::upgrade::run_upgrade(&options)?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
+454
@@ -0,0 +1,454 @@
|
|||||||
|
use std::fs::{self, File};
|
||||||
|
use std::io::{Read, Write};
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use anyhow::{bail, Context, Result};
|
||||||
|
use semver::Version;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
|
||||||
|
use crate::ui;
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
pub const TARGET_ASSET_NAME: &str = "sanctum.exe";
|
||||||
|
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
pub const TARGET_ASSET_NAME: &str = "sanctum";
|
||||||
|
|
||||||
|
pub const CHECKSUM_ASSET_NAME: &str = "SHA256SUMS.txt";
|
||||||
|
|
||||||
|
/// Repräsentiert ein Asset in einem Gitea-Release
|
||||||
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
|
pub struct ReleaseAsset {
|
||||||
|
pub id: u64,
|
||||||
|
pub name: String,
|
||||||
|
pub size: u64,
|
||||||
|
pub browser_download_url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Repräsentiert ein Release von der Gitea Releases-API
|
||||||
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
|
pub struct GiteaRelease {
|
||||||
|
pub id: u64,
|
||||||
|
pub tag_name: String,
|
||||||
|
pub name: String,
|
||||||
|
pub body: String,
|
||||||
|
pub html_url: String,
|
||||||
|
pub published_at: Option<String>,
|
||||||
|
pub assets: Vec<ReleaseAsset>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Optionen für das Upgrade
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct UpgradeOptions {
|
||||||
|
pub check_only: bool,
|
||||||
|
pub yes: bool,
|
||||||
|
pub force: bool,
|
||||||
|
pub base_url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for UpgradeOptions {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
check_only: false,
|
||||||
|
yes: false,
|
||||||
|
force: false,
|
||||||
|
base_url: "https://gitea.pansi.eu".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Erkannter Paketmanager
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum PackageManager {
|
||||||
|
Scoop,
|
||||||
|
Winget,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PackageManager {
|
||||||
|
pub fn name(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
PackageManager::Scoop => "Scoop",
|
||||||
|
PackageManager::Winget => "Winget",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn upgrade_command(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
PackageManager::Scoop => "scoop update sanctum",
|
||||||
|
PackageManager::Winget => "winget upgrade HaraldPansi.Sanctum",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Prüft, ob die laufende Binary durch einen Paketmanager (Scoop oder Winget) verwaltet wird.
|
||||||
|
pub fn detect_package_manager() -> Option<PackageManager> {
|
||||||
|
if let Ok(exe_path) = std::env::current_exe() {
|
||||||
|
detect_package_manager_from_path(&exe_path)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Hilfsfunktion für die Paketmanager-Erkennung anhand eines Pfads (für Tests isolierbar).
|
||||||
|
pub fn detect_package_manager_from_path(path: &Path) -> Option<PackageManager> {
|
||||||
|
let s = path.to_string_lossy().to_lowercase();
|
||||||
|
if s.contains("scoop\\apps\\sanctum")
|
||||||
|
|| s.contains("scoop/apps/sanctum")
|
||||||
|
|| s.contains("scoop\\shims")
|
||||||
|
|| s.contains("scoop/shims")
|
||||||
|
{
|
||||||
|
return Some(PackageManager::Scoop);
|
||||||
|
}
|
||||||
|
if s.contains("winget\\packages")
|
||||||
|
|| s.contains("winget/packages")
|
||||||
|
|| s.contains("winget\\links")
|
||||||
|
|| s.contains("winget/links")
|
||||||
|
{
|
||||||
|
return Some(PackageManager::Winget);
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ruft das neueste Release von der Gitea Releases-API ab.
|
||||||
|
pub fn fetch_latest_release(base_url: &str) -> Result<GiteaRelease> {
|
||||||
|
let base = base_url.trim_end_matches('/');
|
||||||
|
let api_url = format!("{}/api/v1/repos/harald/sanctum/releases/latest", base);
|
||||||
|
|
||||||
|
let resp = ureq::get(&api_url)
|
||||||
|
.set("User-Agent", &format!("sanctum/{}", env!("CARGO_PKG_VERSION")))
|
||||||
|
.set("Accept", "application/json")
|
||||||
|
.timeout(Duration::from_secs(15))
|
||||||
|
.call()
|
||||||
|
.with_context(|| format!("Netzwerkfehler beim Abrufen der Releases von '{}'", api_url))?;
|
||||||
|
|
||||||
|
let release: GiteaRelease = resp
|
||||||
|
.into_json()
|
||||||
|
.context("Ungültiges JSON-Format von der Gitea Releases-API empfangen")?;
|
||||||
|
|
||||||
|
Ok(release)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lädt eine Datei von einer URL als Text herunter.
|
||||||
|
pub fn fetch_text(url: &str) -> Result<String> {
|
||||||
|
let resp = ureq::get(url)
|
||||||
|
.set("User-Agent", &format!("sanctum/{}", env!("CARGO_PKG_VERSION")))
|
||||||
|
.timeout(Duration::from_secs(15))
|
||||||
|
.call()
|
||||||
|
.with_context(|| format!("Fehler beim Herunterladen von '{}'", url))?;
|
||||||
|
|
||||||
|
let text = resp
|
||||||
|
.into_string()
|
||||||
|
.with_context(|| format!("Fehler beim Lesen des Texts von '{}'", url))?;
|
||||||
|
|
||||||
|
Ok(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extrahiert die SHA-256 Prüfsumme für ein bestimmtes Asset aus dem Inhalt von `SHA256SUMS.txt`.
|
||||||
|
pub fn parse_checksum_for_asset(sha256sums_content: &str, target_asset: &str) -> Option<String> {
|
||||||
|
for line in sha256sums_content.lines() {
|
||||||
|
let trimmed = line.trim();
|
||||||
|
if trimmed.is_empty() || trimmed.starts_with('#') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let parts: Vec<&str> = trimmed.split_whitespace().collect();
|
||||||
|
if parts.len() >= 2 {
|
||||||
|
let hash = parts[0];
|
||||||
|
let filename = parts[1].trim_start_matches('*');
|
||||||
|
if filename.eq_ignore_ascii_case(target_asset) && hash.len() == 64 {
|
||||||
|
return Some(hash.to_lowercase());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bereinigt einen Versionsstring von führenden 'v' / 'V' Zeichen und parst ihn als SemVer.
|
||||||
|
pub fn parse_clean_version(ver_str: &str) -> Result<Version> {
|
||||||
|
let clean = ver_str.trim().trim_start_matches(|c| c == 'v' || c == 'V');
|
||||||
|
Version::parse(clean).with_context(|| format!("Ungültiges SemVer-Format: '{}'", ver_str))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Vergleicht die aktuelle Version mit der entfernten Version.
|
||||||
|
/// Gibt `true` zurück, wenn `remote > current`.
|
||||||
|
pub fn is_newer_version(current: &str, remote: &str) -> bool {
|
||||||
|
let cur = match parse_clean_version(current) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(_) => return false,
|
||||||
|
};
|
||||||
|
let rem = match parse_clean_version(remote) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(_) => return false,
|
||||||
|
};
|
||||||
|
rem > cur
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lädt das Ziel-Binary in eine temporäre Datei herunter und validiert dabei den SHA-256 Hash.
|
||||||
|
pub fn download_and_verify_binary(
|
||||||
|
download_url: &str,
|
||||||
|
expected_hash: &str,
|
||||||
|
expected_size: u64,
|
||||||
|
) -> Result<PathBuf> {
|
||||||
|
let resp = ureq::get(download_url)
|
||||||
|
.set("User-Agent", &format!("sanctum/{}", env!("CARGO_PKG_VERSION")))
|
||||||
|
.timeout(Duration::from_secs(120))
|
||||||
|
.call()
|
||||||
|
.with_context(|| format!("Fehler beim Starten des Downloads von '{}'", download_url))?;
|
||||||
|
|
||||||
|
let mut reader = resp.into_reader();
|
||||||
|
|
||||||
|
let temp_dir = std::env::temp_dir();
|
||||||
|
let temp_file_path = temp_dir.join(format!(
|
||||||
|
"sanctum_update_{}_{}",
|
||||||
|
std::process::id(),
|
||||||
|
TARGET_ASSET_NAME
|
||||||
|
));
|
||||||
|
|
||||||
|
let mut temp_file = File::create(&temp_file_path).with_context(|| {
|
||||||
|
format!(
|
||||||
|
"Konnte temporäre Update-Datei '{}' nicht erstellen",
|
||||||
|
temp_file_path.display()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let mut hasher = Sha256::new();
|
||||||
|
let mut buffer = [0u8; 64 * 1024]; // 64 KB Puffer
|
||||||
|
let mut downloaded_bytes = 0u64;
|
||||||
|
|
||||||
|
println!(" • Herunterladen von: {}", download_url);
|
||||||
|
print!(" • Fortschritt: ");
|
||||||
|
let _ = std::io::stdout().flush();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let n = reader
|
||||||
|
.read(&mut buffer)
|
||||||
|
.context("Fehler beim Lesen des Download-Datenstroms")?;
|
||||||
|
if n == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
temp_file
|
||||||
|
.write_all(&buffer[..n])
|
||||||
|
.context("Fehler beim Schreiben in die temporäre Datei")?;
|
||||||
|
hasher.update(&buffer[..n]);
|
||||||
|
|
||||||
|
downloaded_bytes += n as u64;
|
||||||
|
|
||||||
|
if expected_size > 0 {
|
||||||
|
let percent = (downloaded_bytes as f64 / expected_size as f64 * 100.0).min(100.0);
|
||||||
|
print!(
|
||||||
|
"\r • Fortschritt: {} / {} ({:.1}%)",
|
||||||
|
ui::format_bytes(downloaded_bytes),
|
||||||
|
ui::format_bytes(expected_size),
|
||||||
|
percent
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
print!(
|
||||||
|
"\r • Fortschritt: {}",
|
||||||
|
ui::format_bytes(downloaded_bytes)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let _ = std::io::stdout().flush();
|
||||||
|
}
|
||||||
|
println!();
|
||||||
|
|
||||||
|
temp_file
|
||||||
|
.flush()
|
||||||
|
.context("Fehler beim Flush der temporären Datei")?;
|
||||||
|
drop(temp_file);
|
||||||
|
|
||||||
|
// SHA-256 Integritätsprüfung
|
||||||
|
let calculated_hash = hex::encode(hasher.finalize()).to_lowercase();
|
||||||
|
let expected_lower = expected_hash.to_lowercase();
|
||||||
|
|
||||||
|
if calculated_hash != expected_lower {
|
||||||
|
let _ = fs::remove_file(&temp_file_path);
|
||||||
|
bail!(
|
||||||
|
"Kryptografische Prüfsumme ungültig!\n Erwartet: {}\n Berechnet: {}\nDas Update wurde aus Sicherheitsgründen verworfen.",
|
||||||
|
expected_lower,
|
||||||
|
calculated_hash
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
println!(" • SHA-256: {} (verifiziert)", ui::green(&calculated_hash[..16]));
|
||||||
|
|
||||||
|
Ok(temp_file_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Führt den In-Place-Austausch des laufenden Binaries durch.
|
||||||
|
pub fn apply_upgrade(temp_binary: &Path) -> Result<PathBuf> {
|
||||||
|
let current_exe = std::env::current_exe()
|
||||||
|
.context("Konnte den Pfad der aktuell ausgeführten Datei nicht ermitteln")?;
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
let mut perms = fs::metadata(temp_binary)
|
||||||
|
.context("Konnte Metadaten der Update-Datei nicht lesen")?
|
||||||
|
.permissions();
|
||||||
|
perms.set_mode(0o755);
|
||||||
|
fs::set_permissions(temp_binary, perms)
|
||||||
|
.context("Konnte Ausführungsrechte (chmod +x / 0755) nicht setzen")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
self_replace::self_replace(temp_binary)
|
||||||
|
.context("In-Place-Ersetzung der laufenden Anwendungsdatei fehlgeschlagen")?;
|
||||||
|
|
||||||
|
let _ = fs::remove_file(temp_binary);
|
||||||
|
|
||||||
|
Ok(current_exe)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Führt den gesamten Upgrade-Workflow aus.
|
||||||
|
pub fn run_upgrade(options: &UpgradeOptions) -> Result<()> {
|
||||||
|
let current_version = env!("CARGO_PKG_VERSION");
|
||||||
|
|
||||||
|
println!("┌─────────────────────────────────────────────────────────────┐");
|
||||||
|
println!("│ Sanctum — In-Place Upgrade & Aktualisierungsdienst │");
|
||||||
|
println!("└─────────────────────────────────────────────────────────────┘");
|
||||||
|
println!(" • Aktuelle Version: {}", ui::cyan(&format!("v{}", current_version)));
|
||||||
|
println!(" • Server: {}", options.base_url);
|
||||||
|
println!();
|
||||||
|
|
||||||
|
// 1. Paketmanager-Prüfung
|
||||||
|
if let Some(pm) = detect_package_manager() {
|
||||||
|
println!(
|
||||||
|
" {} Sanctum wird anscheinend über {} verwaltet!",
|
||||||
|
ui::yellow("[!]"),
|
||||||
|
pm.name()
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
" Empfohlener Aktualisierungsbefehl: {}",
|
||||||
|
ui::cyan(pm.upgrade_command())
|
||||||
|
);
|
||||||
|
println!();
|
||||||
|
|
||||||
|
if !options.force && !options.yes && !options.check_only {
|
||||||
|
print!(" Möchten Sie das direkte In-Place-Upgrade trotzdem fortsetzen? [j/N]: ");
|
||||||
|
let _ = std::io::stdout().flush();
|
||||||
|
let mut answer = String::new();
|
||||||
|
std::io::stdin()
|
||||||
|
.read_line(&mut answer)
|
||||||
|
.context("Fehler beim Einlesen der Benutzereingabe")?;
|
||||||
|
let trimmed = answer.trim().to_lowercase();
|
||||||
|
if trimmed != "j" && trimmed != "ja" && trimmed != "y" && trimmed != "yes" {
|
||||||
|
println!(" Upgrade abgebrochen.");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Neuestes Release abfragen
|
||||||
|
println!(" {} Suche nach neuesten Releases auf Gitea ...", ui::dim("[-]"));
|
||||||
|
let release = fetch_latest_release(&options.base_url)?;
|
||||||
|
let remote_tag = &release.tag_name;
|
||||||
|
let is_newer = is_newer_version(current_version, remote_tag);
|
||||||
|
|
||||||
|
println!(" • Neueste Version: {}", if is_newer { ui::green(remote_tag) } else { ui::cyan(remote_tag) });
|
||||||
|
println!(" • Release-Name: {}", release.name);
|
||||||
|
if let Some(ref pub_at) = release.published_at {
|
||||||
|
println!(" • Veröffentlicht: {}", pub_at);
|
||||||
|
}
|
||||||
|
println!(" • Release-URL: {}", release.html_url);
|
||||||
|
println!();
|
||||||
|
|
||||||
|
if !is_newer && !options.force {
|
||||||
|
println!(
|
||||||
|
" {} Sie verwenden bereits die neueste Version von Sanctum.",
|
||||||
|
ui::green("✔")
|
||||||
|
);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
if options.check_only {
|
||||||
|
if is_newer {
|
||||||
|
println!(
|
||||||
|
" {} Ein Update auf {} ist verfügbar! Führen Sie 'sanctum upgrade' aus.",
|
||||||
|
ui::yellow("[!]"),
|
||||||
|
remote_tag
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Ziel-Asset und Checksumme identifizieren
|
||||||
|
let target_asset = release
|
||||||
|
.assets
|
||||||
|
.iter()
|
||||||
|
.find(|a| a.name.eq_ignore_ascii_case(TARGET_ASSET_NAME))
|
||||||
|
.ok_or_else(|| {
|
||||||
|
anyhow::anyhow!(
|
||||||
|
"Im Release '{}' wurde kein passendes Binary für Ihr Betriebssystem ('{}') gefunden!",
|
||||||
|
remote_tag,
|
||||||
|
TARGET_ASSET_NAME
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let checksum_asset = release
|
||||||
|
.assets
|
||||||
|
.iter()
|
||||||
|
.find(|a| a.name.eq_ignore_ascii_case(CHECKSUM_ASSET_NAME))
|
||||||
|
.ok_or_else(|| {
|
||||||
|
anyhow::anyhow!(
|
||||||
|
"Im Release '{}' wurde keine Prüfsummendatei ('{}') gefunden!",
|
||||||
|
remote_tag,
|
||||||
|
CHECKSUM_ASSET_NAME
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// 4. Prüfsummen herunterladen und extrahieren
|
||||||
|
println!(" {} Lade Prüfsummen ({}) herunter ...", ui::dim("[-]"), CHECKSUM_ASSET_NAME);
|
||||||
|
let sums_content = fetch_text(&checksum_asset.browser_download_url)?;
|
||||||
|
let expected_hash = parse_checksum_for_asset(&sums_content, TARGET_ASSET_NAME).ok_or_else(|| {
|
||||||
|
anyhow::anyhow!(
|
||||||
|
"Prüfsummendatei enthält keinen SHA-256 Eintrag für '{}'!",
|
||||||
|
TARGET_ASSET_NAME
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// 5. Interaktive Bestätigung (sofern nicht -y / --yes)
|
||||||
|
if !options.yes {
|
||||||
|
println!(" • Ziel-Binary: {} ({})", target_asset.name, ui::format_bytes(target_asset.size));
|
||||||
|
println!(" • Erwarteter Hash: {}...", &expected_hash[..16]);
|
||||||
|
println!();
|
||||||
|
print!(
|
||||||
|
" Möchten Sie Sanctum jetzt von v{} auf {} aktualisieren? [J/n]: ",
|
||||||
|
current_version, remote_tag
|
||||||
|
);
|
||||||
|
let _ = std::io::stdout().flush();
|
||||||
|
let mut answer = String::new();
|
||||||
|
std::io::stdin()
|
||||||
|
.read_line(&mut answer)
|
||||||
|
.context("Fehler beim Einlesen der Benutzereingabe")?;
|
||||||
|
let trimmed = answer.trim().to_lowercase();
|
||||||
|
if trimmed == "n" || trimmed == "nein" || trimmed == "no" {
|
||||||
|
println!(" Upgrade abgebrochen.");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Download & Verifikation
|
||||||
|
println!(" {} Lade neues Binary herunter ...", ui::dim("[-]"));
|
||||||
|
let temp_file = download_and_verify_binary(
|
||||||
|
&target_asset.browser_download_url,
|
||||||
|
&expected_hash,
|
||||||
|
target_asset.size,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// 7. In-Place-Austausch
|
||||||
|
println!(" {} Führe In-Place-Austausch durch ...", ui::dim("[-]"));
|
||||||
|
let updated_exe = apply_upgrade(&temp_file)?;
|
||||||
|
|
||||||
|
println!();
|
||||||
|
println!("┌─────────────────────────────────────────────────────────────┐");
|
||||||
|
println!("│ ✔ Sanctum wurde erfolgreich auf {} aktualisiert! │", remote_tag);
|
||||||
|
println!("└─────────────────────────────────────────────────────────────┘");
|
||||||
|
println!(" • Pfad: {}", updated_exe.display());
|
||||||
|
println!(" • Neue Version: {}", ui::green(remote_tag));
|
||||||
|
println!();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
use std::fs::File;
|
||||||
|
use std::io::Write;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use sanctum::upgrade::{
|
||||||
|
detect_package_manager_from_path, is_newer_version, parse_checksum_for_asset,
|
||||||
|
parse_clean_version, GiteaRelease, PackageManager,
|
||||||
|
};
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_checksum_for_asset() {
|
||||||
|
let sample_checksums = r#"
|
||||||
|
# Offizielle SHA-256 Prüfsummen für Sanctum Release
|
||||||
|
2f069a8cc41f9fa6a2559e2952e5669523c21c175df15f6d80f0473bbe2fa60e sanctum-v0.5.0-windows-x86_64.zip
|
||||||
|
e0c64ecc12c7f49180487b638f1152eccaf5c90270a93880db52ca56eb1c1d63 sanctum.exe
|
||||||
|
557dd84f339336899fdd67e69d894dcd6e32f3e0bff2ae8a3057df596eddf4b4 sanctum-v0.5.0-linux-x86_64.tar.gz
|
||||||
|
2816f00cb15825d92f35f93fd667ed04d36501763a70b9399dd0ab9355c9448d *sanctum
|
||||||
|
"#;
|
||||||
|
|
||||||
|
// Windows Binary
|
||||||
|
let win_hash = parse_checksum_for_asset(sample_checksums, "sanctum.exe");
|
||||||
|
assert_eq!(
|
||||||
|
win_hash.as_deref(),
|
||||||
|
Some("e0c64ecc12c7f49180487b638f1152eccaf5c90270a93880db52ca56eb1c1d63")
|
||||||
|
);
|
||||||
|
|
||||||
|
// Linux Binary (mit führendem Asterisk im Format)
|
||||||
|
let linux_hash = parse_checksum_for_asset(sample_checksums, "sanctum");
|
||||||
|
assert_eq!(
|
||||||
|
linux_hash.as_deref(),
|
||||||
|
Some("2816f00cb15825d92f35f93fd667ed04d36501763a70b9399dd0ab9355c9448d")
|
||||||
|
);
|
||||||
|
|
||||||
|
// Nicht existentes Asset
|
||||||
|
let non_existent = parse_checksum_for_asset(sample_checksums, "nonexistent.exe");
|
||||||
|
assert_eq!(non_existent, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_semver_comparisons() {
|
||||||
|
// Normaler Versionssprung
|
||||||
|
assert!(is_newer_version("0.5.0", "0.6.0"));
|
||||||
|
assert!(is_newer_version("0.5.0", "v0.6.0"));
|
||||||
|
assert!(is_newer_version("v0.5.0", "V0.5.1"));
|
||||||
|
assert!(is_newer_version("0.5.0", "1.0.0"));
|
||||||
|
|
||||||
|
// Gleiche Version
|
||||||
|
assert!(!is_newer_version("0.6.0", "0.6.0"));
|
||||||
|
assert!(!is_newer_version("0.6.0", "v0.6.0"));
|
||||||
|
assert!(!is_newer_version("v0.6.0", "0.6.0"));
|
||||||
|
|
||||||
|
// Ältere Version
|
||||||
|
assert!(!is_newer_version("0.6.0", "0.5.0"));
|
||||||
|
assert!(!is_newer_version("0.6.0", "v0.4.1"));
|
||||||
|
|
||||||
|
// Parsing mit führenden 'v' / 'V'
|
||||||
|
let v1 = parse_clean_version("v0.6.0").expect("Parse v0.6.0");
|
||||||
|
let v2 = parse_clean_version("V1.2.3").expect("Parse V1.2.3");
|
||||||
|
assert_eq!(v1.major, 0);
|
||||||
|
assert_eq!(v1.minor, 6);
|
||||||
|
assert_eq!(v2.major, 1);
|
||||||
|
assert_eq!(v2.minor, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_package_manager_detection() {
|
||||||
|
// Scoop Pfade
|
||||||
|
let scoop_path = Path::new(r"C:\Users\harald\scoop\apps\sanctum\current\sanctum.exe");
|
||||||
|
assert_eq!(
|
||||||
|
detect_package_manager_from_path(scoop_path),
|
||||||
|
Some(PackageManager::Scoop)
|
||||||
|
);
|
||||||
|
|
||||||
|
let scoop_shim = Path::new(r"C:\Users\harald\scoop\shims\sanctum.exe");
|
||||||
|
assert_eq!(
|
||||||
|
detect_package_manager_from_path(scoop_shim),
|
||||||
|
Some(PackageManager::Scoop)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Winget Pfade
|
||||||
|
let winget_path = Path::new(
|
||||||
|
r"C:\Users\harald\AppData\Local\Microsoft\WinGet\Packages\HaraldPansi.Sanctum_Microsoft.Winget.Source_8wekyb3d8bbwe\sanctum.exe",
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
detect_package_manager_from_path(winget_path),
|
||||||
|
Some(PackageManager::Winget)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Standard Standalone Pfade (kein Paketmanager)
|
||||||
|
let standalone = Path::new(r"C:\Tools\sanctum\sanctum.exe");
|
||||||
|
assert_eq!(detect_package_manager_from_path(standalone), None);
|
||||||
|
|
||||||
|
let linux_usr = Path::new("/usr/local/bin/sanctum");
|
||||||
|
assert_eq!(detect_package_manager_from_path(linux_usr), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_hash_integrity_calculation() {
|
||||||
|
let test_file = std::env::temp_dir().join(format!("sanctum_test_{}.bin", std::process::id()));
|
||||||
|
|
||||||
|
let payload = b"Sanctum Standalone Binary Payload for Integrity Testing";
|
||||||
|
{
|
||||||
|
let mut f = File::create(&test_file).expect("create file");
|
||||||
|
f.write_all(payload).expect("write");
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut hasher = Sha256::new();
|
||||||
|
hasher.update(payload);
|
||||||
|
let expected = hex::encode(hasher.finalize());
|
||||||
|
|
||||||
|
// Datei erneut lesen und hashen
|
||||||
|
let mut reader = File::open(&test_file).expect("open");
|
||||||
|
let mut file_hasher = Sha256::new();
|
||||||
|
std::io::copy(&mut reader, &mut file_hasher).expect("copy");
|
||||||
|
let calculated = hex::encode(file_hasher.finalize());
|
||||||
|
|
||||||
|
assert_eq!(expected, calculated);
|
||||||
|
let _ = std::fs::remove_file(&test_file);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_json_deserialization_of_gitea_release() {
|
||||||
|
let gitea_json = r##"{
|
||||||
|
"id": 27,
|
||||||
|
"tag_name": "v0.5.0",
|
||||||
|
"name": "Sanctum v0.5.0 (Windows x86_64)",
|
||||||
|
"body": "Release v0.5.0 - sanctum sync",
|
||||||
|
"html_url": "https://gitea.pansi.eu/harald/sanctum/releases/tag/v0.5.0",
|
||||||
|
"published_at": "2026-09-16T10:44:18+02:00",
|
||||||
|
"assets": [
|
||||||
|
{
|
||||||
|
"id": 38,
|
||||||
|
"name": "sanctum.exe",
|
||||||
|
"size": 5467136,
|
||||||
|
"browser_download_url": "https://gitea.pansi.eu/harald/sanctum/releases/download/v0.5.0/sanctum.exe"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 40,
|
||||||
|
"name": "sanctum",
|
||||||
|
"size": 5033952,
|
||||||
|
"browser_download_url": "https://gitea.pansi.eu/harald/sanctum/releases/download/v0.5.0/sanctum"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 41,
|
||||||
|
"name": "SHA256SUMS.txt",
|
||||||
|
"size": 563,
|
||||||
|
"browser_download_url": "https://gitea.pansi.eu/harald/sanctum/releases/download/v0.5.0/SHA256SUMS.txt"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}"##;
|
||||||
|
|
||||||
|
let release: GiteaRelease = serde_json::from_str(gitea_json).expect("Deserialisierung erfolgreich");
|
||||||
|
assert_eq!(release.id, 27);
|
||||||
|
assert_eq!(release.tag_name, "v0.5.0");
|
||||||
|
assert_eq!(release.assets.len(), 3);
|
||||||
|
assert_eq!(release.assets[0].name, "sanctum.exe");
|
||||||
|
assert_eq!(release.assets[1].name, "sanctum");
|
||||||
|
assert_eq!(release.assets[2].name, "SHA256SUMS.txt");
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user