Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1048253497 | ||
|
|
aa65d96434 | ||
|
|
3f3f8f2730 | ||
|
|
650cfc7cd4 | ||
|
|
9d1a4d3ad7 | ||
|
|
aa25719c0d | ||
|
|
34378998c4 | ||
|
|
4ef0c414fa | ||
|
|
1f3afb1e52 | ||
|
|
5f2040f8bf | ||
|
|
da2ddbd6ab | ||
|
|
46f976b353 |
+49
-4
@@ -5,6 +5,55 @@ 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.5.0] - 2026-09-16
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **`sanctum sync` - Native rsync-ähnliche Synchronisation**:
|
||||||
|
- Bidirektionale Synchronisation (`Push`: Host -> Tresor, `Pull`: Tresor -> Host) direkt auf SQLite- und VFS-Ebene unter vollständiger Umgehung des WebDAV-Layers.
|
||||||
|
- **Beseitigung des 4-GB-Dateilimits**: Löst das fundamentale Problem des Windows WebClient-Dienstes (`mrxdav.sys`), welcher systembedingt auf 32-Bit Dateigrößen (~4 GB - 1 Byte / Fehler `0x800700DF`) beschränkt ist. Sanctum streamt nun Dateien beliebiger Größe (5 GB, 50 GB, 500 GB+) in 1 MB Chunks mit AES-256-GCM direkt in/aus den Tresor.
|
||||||
|
- **Dry-Run-Simulation (`--dry-run` / `-n`)**: Ermöglicht die risikolose Vorschau aller auszuführenden Aktionen (Hinzufügen, Aktualisieren, Löschen, Überspringen) samt Datenmengen-Berechnung, ohne tatsächliche Änderungen am Ziel durchzuführen.
|
||||||
|
- **Spiegelung & Verwaiste Dateien bereinigen (`--delete`)**: Unterstützt das automatische Löschen von Dateien im Ziel, die in der Quelle nicht mehr existieren, für exakte Ordnerspiegelungen.
|
||||||
|
- **Inhaltsbasierte Hash-Prüfung (`--checksum` / `-c`)**: Optionaler byteweiser SHA-256 Inhaltsvergleich anstelle des schnellen Größen-/mtime-Prüfverfahrens.
|
||||||
|
- **Ausschlussmuster (`--exclude <PATTERN>`)**: Unterstützung für flexible Glob-Muster zum Ignorieren temporärer Dateien, Downloads oder System-Metadaten (z. B. `*.crdownload`, `*.tmp`, `Thumbs.db`).
|
||||||
|
- **Quiet-Modus (`--quiet` / `-q`)**: Ermöglicht lautlose Ausführung für Skripte und geplante Aufgaben mit knapper Zusammenfassungsstatistik.
|
||||||
|
- **Metadaten-Konservierung**: Erhaltung und Übertragung präziser Datei-Modifikationszeitstempel (`mtime`) in beide Richtungen.
|
||||||
|
- **Integrationstests**:
|
||||||
|
- `tests/sync_test.rs`: Umfassende automatisierte Testabdeckung für Push/Pull, Multi-Chunk Großdateien, Fast-Delta-Check, Dry-Run-Invarianz, `--delete` und `--exclude`.
|
||||||
|
|
||||||
|
## [0.4.1] - 2026-09-14
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **Linux Mount-Stabilität & Signal-Handling**:
|
||||||
|
- Behebung eines sofortigen automatischen Aushängens direkt nach erfolgreichem Mount unter Linux: Windows-spezifische Kontrollkanäle (Console-Close, Session-Lock, System-Tray) wurden auf Nicht-Windows-Systemen sofort geschlossen, wodurch `tokio::select!` über `None` vorzeitig ausgelöst wurde.
|
||||||
|
- Plattform-spezifische Aufteilung der `tokio::select!`-Ereignisschleife: Unter Linux/Unix wird nun sauber auf POSIX-Terminierungssignale (`SIGINT`, `SIGTERM`, `SIGHUP`) gelauscht.
|
||||||
|
- Defensives Pattern-Matching mit `Some(()) = rx.recv()` stellt sicher, dass geschlossene Kanäle keine unberechtigten Shutdowns mehr auslösen können.
|
||||||
|
- Automatisches Öffnen via `xdg-open` auf Linux vorerst unterdrückt.
|
||||||
|
|
||||||
|
## [0.4.0] - 2026-09-10
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **Distribution & Package Ecosystem**:
|
||||||
|
- Offizielles Scoop-Manifest (`packages/scoop/sanctum.json`) für One-Click CLI-Installation und nahtlose Upgrades unter Windows.
|
||||||
|
- Windows Package Manager (Winget) Manifest (`packages/winget/HaraldPansi.Sanctum.yaml`) zur systemweiten Paketverwaltung.
|
||||||
|
- Vollständiger Installations-Leitfaden (`INSTALL.md`) für Scoop, Winget, Standalone-Binaries und `cargo install`.
|
||||||
|
- **Cross-Platform Architektur (Linux & macOS Vorbereitung)**:
|
||||||
|
- Neues Plattform-Abstraktionsmodul `src/platform/mod.rs` für OS-unabhängiges Speicherschutz- und Mount-Handling.
|
||||||
|
- POSIX-Memory-Hardening mit `mlock` und `munlock` Systemaufrufen für Unix-Systeme.
|
||||||
|
- Plattformübergreifendes Datei- und Ordneröffnen (`explorer.exe` unter Windows, `xdg-open` unter Linux, `open` unter macOS).
|
||||||
|
- Native WebDAV-VFS-Einbindung für Linux GNOME/KDE via `gio mount`.
|
||||||
|
- **Chaos Engineering & Live-Crash-Resilienz**:
|
||||||
|
- `tests/live_crash_resilience_test.rs`: Umfassende automatisierte Stresstests mit Multi-Worker Schreibabbrüchen, asynchronen Panic-Simulationen und Validierung der SQLite WAL-Konsistenz (Null Korruption).
|
||||||
|
- **Legal, Compliance & Software Bill of Materials (SBOM)**:
|
||||||
|
- `LEGAL.md`: Rechtliche Leitlinien, US EAR § 742.15(b) & EU Dual-Use Exportkontroll-Klassifizierung, DSGVO Art. 25 Zero-Data-Erklärung und Haftungsausschluss nach § 521 BGB.
|
||||||
|
- `THIRD_PARTY_LICENSES.md`: Lückenloses Lizenzaudit aller 231 Abhängigkeiten (100% permissive Lizenzen, 0% Copyleft) zur strikten Compliance mit Apache-2.0 Section 4.
|
||||||
|
- Aufnahme von `LEGAL.md` und `THIRD_PARTY_LICENSES.md` in das offizielle Release-Distributionsarchiv (`package-release.ps1`).
|
||||||
|
- **Statisches Linux Single-Binary (`x86_64-unknown-linux-musl`)**:
|
||||||
|
- 100% statisch gelinktes Linux-Binary ohne GLIBC-Abhängigkeiten via Zig-Cross-Toolchain.
|
||||||
|
- Neues Linux-Distributionsarchiv `sanctum-v0.4.0-linux-x86_64.tar.gz` inklusive Dokumentation und Prüfsummen.
|
||||||
|
- Native Freedesktop `.desktop`-Datei und MIME-Type-Integration (`application/x-sanctum`) via `sanctum register`.
|
||||||
|
- **OpSec & Disaster Recovery UX**:
|
||||||
|
- `QUICKSTART.md`: Druckbare 24-Wörter BIP-39 Notfallkarte zur sicheren analogen Verwahrung des Notfallschlüssels (Air-Gapped Vault Paper Backup).
|
||||||
|
|
||||||
## [0.3.1] - 2026-09-10
|
## [0.3.1] - 2026-09-10
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
@@ -20,10 +69,6 @@ und dieses Projekt folgt den Richtlinien von [Semantic Versioning](https://semve
|
|||||||
- **DFIR & Windows Host-Memory Hardening**:
|
- **DFIR & Windows Host-Memory Hardening**:
|
||||||
- Win32 `VirtualLock` Speicherschutz: Physische Verriegelung des Root-DEK im RAM zur Unterbindung von Auslagerungen in `pagefile.sys` oder `swapfile.sys`.
|
- Win32 `VirtualLock` Speicherschutz: Physische Verriegelung des Root-DEK im RAM zur Unterbindung von Auslagerungen in `pagefile.sys` oder `swapfile.sys`.
|
||||||
- Windows Defender Controlled Folder Access (CFA / Ransomware-Schutz) Diagnose mit gezielter Hilfestellung.
|
- Windows Defender Controlled Folder Access (CFA / Ransomware-Schutz) Diagnose mit gezielter Hilfestellung.
|
||||||
- **Legal, Compliance & Export-Control (LEGAL-01 bis LEGAL-03)**:
|
|
||||||
- `LEGAL.md`: Umfassende rechtliche Dokumentation inkl. Exportkontroll-Klassifizierung (US EAR § 742.15(b) & EU Dual-Use GSN), Aufklärung über Plausible Deniability bei Key-Disclosure-Gesetzen, EU/DACH-Haftungsbeschränkung gem. § 521 BGB (Schenkungsrecht) sowie DSGVO Art. 25 Zero-Data-Erklärung.
|
|
||||||
- `THIRD_PARTY_LICENSES.md`: Lückenlose Attribution aller ~230 statisch eingebundenen Crates (Apache-2.0, MIT, BSD, SQLite Public Domain) zur strikten Erfüllung von Section 4 der Apache-2.0-Lizenz.
|
|
||||||
- Automatisierte Aufnahme von `LEGAL.md` und `THIRD_PARTY_LICENSES.md` in das Release-Packaging (`package-release.ps1`).
|
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- **Visuelle Parität & Plausible Deniability beim Mounten (HF-01)**:
|
- **Visuelle Parität & Plausible Deniability beim Mounten (HF-01)**:
|
||||||
|
|||||||
Generated
+1
-1
@@ -1377,7 +1377,7 @@ checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sanctum"
|
name = "sanctum"
|
||||||
version = "0.3.1"
|
version = "0.5.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes-gcm",
|
"aes-gcm",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
|||||||
+3
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "sanctum"
|
name = "sanctum"
|
||||||
version = "0.3.1"
|
version = "0.5.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,8 @@ 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"
|
||||||
|
|
||||||
|
[target.'cfg(windows)'.dependencies]
|
||||||
tray-item = "0.10"
|
tray-item = "0.10"
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
|
|||||||
+94
@@ -0,0 +1,94 @@
|
|||||||
|
# Installation & Paketmanager-Guide für Sanctum 🛡️
|
||||||
|
|
||||||
|
Sanctum kann auf verschiedene Arten unter Windows 10/11 installiert und verwaltet werden. Es sind **keine Administratorrechte** erforderlich.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 1. Installation via Scoop (Empfohlen für Entwickler)
|
||||||
|
|
||||||
|
Wenn du den Windows-Paketmanager [Scoop](https://scoop.sh/) nutzt, kannst du Sanctum direkt über das bereitgestellte Manifest installieren:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Direkt über die URL des Manifests installieren:
|
||||||
|
scoop install https://gitea.pansi.eu/harald/sanctum/raw/branch/main/packages/scoop/sanctum.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Oder falls du ein lokales Repository geklont hast:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
scoop install .\packages\scoop\sanctum.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Nach der Installation ist der Befehl `sanctum` sofort global in deiner PowerShell verfügbar.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🪟 2. Installation via Windows Package Manager (Winget)
|
||||||
|
|
||||||
|
Sanctum verfügt über ein standardkonformes Winget-Manifest (Schema 1.6.0).
|
||||||
|
|
||||||
|
### Lokale Installation via Manifest:
|
||||||
|
```powershell
|
||||||
|
winget install --manifest .\packages\winget\HaraldPansi.Sanctum.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
Winget entpackt das portable Binary automatisch in das Benutzer-Anwendungsverzeichnis und verknüpft das Alias `sanctum`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📦 3. Manuelle Installation (Standalone Portable)
|
||||||
|
|
||||||
|
1. Lade das aktuelle Release-Paket herunter:
|
||||||
|
* **URL:** [https://gitea.pansi.eu/harald/sanctum/releases/tag/v0.4.0](https://gitea.pansi.eu/harald/sanctum/releases/tag/v0.4.0)
|
||||||
|
* **Datei:** `sanctum-v0.4.0-windows-x86_64.zip`
|
||||||
|
2. Entpacke das Archiv in ein Verzeichnis deiner Wahl (z. B. `C:\Tools\Sanctum\`).
|
||||||
|
3. *(Optional)* Registriere das Windows Explorer Rechtsklick-Kontextmenü:
|
||||||
|
```powershell
|
||||||
|
.\sanctum.exe register
|
||||||
|
```
|
||||||
|
*(Erlaubt das Einhängen per Rechtsklick "In Sanctum öffnen" ohne Adminrechte).*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🐧 4. Installation unter Linux (x86_64)
|
||||||
|
|
||||||
|
Sanctum wird für Linux als **100 % statisches Single-Binary** (`x86_64-unknown-linux-musl`) ohne jegliche GLIBC- oder Bibliotheksabhängigkeiten bereitgestellt. Es läuft direkt auf Ubuntu, Debian, Alpine, Arch, Fedora und weiteren Distributionen:
|
||||||
|
|
||||||
|
1. Lade das Linux-Release herunter und installiere es:
|
||||||
|
```bash
|
||||||
|
curl -LO https://gitea.pansi.eu/harald/sanctum/releases/download/v0.4.0/sanctum-v0.4.0-linux-x86_64.tar.gz
|
||||||
|
tar -xzf sanctum-v0.4.0-linux-x86_64.tar.gz
|
||||||
|
chmod +x sanctum
|
||||||
|
sudo install -m 755 sanctum /usr/local/bin/sanctum
|
||||||
|
```
|
||||||
|
2. *(Optional)* Freedesktop-Integration einrichten (Doppelklick in GNOME / KDE / Thunar):
|
||||||
|
```bash
|
||||||
|
sanctum register
|
||||||
|
```
|
||||||
|
*Richtet die `.desktop`-Datei und den MIME-Typ `application/x-sanctum` im Userland (`~/.local/share`) ein.*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 Updates
|
||||||
|
|
||||||
|
* **Scoop:**
|
||||||
|
```powershell
|
||||||
|
scoop update sanctum
|
||||||
|
```
|
||||||
|
* **Winget:**
|
||||||
|
```powershell
|
||||||
|
winget upgrade --manifest .\packages\winget\HaraldPansi.Sanctum.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🗑️ Deinstallation
|
||||||
|
|
||||||
|
* **Scoop:**
|
||||||
|
```powershell
|
||||||
|
scoop uninstall sanctum
|
||||||
|
```
|
||||||
|
* **Explorer-Kontextmenü rückstandslos bereinigen:**
|
||||||
|
```powershell
|
||||||
|
sanctum.exe unregister
|
||||||
|
```
|
||||||
+113
@@ -0,0 +1,113 @@
|
|||||||
|
# Sanctum 🛡️ — Schnellstartanleitung & Notfallhandbuch
|
||||||
|
|
||||||
|
Willkommen bei **Sanctum**! Diese Anleitung führt dich in wenigen Minuten durch die grundlegenden Funktionen zur sicheren Verwaltung deiner verschlüsselten Container unter Windows 10/11 – **100% Userland, ohne Administratorrechte und ohne Treiber**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚡ In 3 Minuten startklar
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# 1. Neuen Tresor mit Notfallkarte anlegen:
|
||||||
|
sanctum.exe init --path "C:\Users\DeinName\Dokumente\mein_tresor.sanctum"
|
||||||
|
|
||||||
|
# 2. Tresor einbinden (wählt automatisch den nächsten freien Buchstaben, z. B. Z:):
|
||||||
|
sanctum.exe mount --path "C:\Users\DeinName\Dokumente\mein_tresor.sanctum"
|
||||||
|
|
||||||
|
# 3. Sicher trennen:
|
||||||
|
# Entweder Ctrl+C im Konsolenfenster ODER Rechtsklick auf das Schild-Icon im Infobereich (Systray) -> "Aushängen & Beenden".
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📂 Die Kernfunktionen im Überblick
|
||||||
|
|
||||||
|
### 1. Tresor initialisieren (`init`)
|
||||||
|
|
||||||
|
Beim Erstellen eines Tresors wählst du zwischen zwei Sicherheitsstufen:
|
||||||
|
|
||||||
|
* **Standard-Tresor (Single-Vault)**:
|
||||||
|
```powershell
|
||||||
|
sanctum.exe init --path "D:\Tresor\daten.sanctum"
|
||||||
|
```
|
||||||
|
* **Plausible Deniability Tresor (Dual-Vault mit Alibi-Carrier)**:
|
||||||
|
```powershell
|
||||||
|
sanctum.exe init --path "D:\Tresor\daten.sanctum" --with-hidden
|
||||||
|
```
|
||||||
|
* Hier legst du **zwei verschiedene Passwörter** fest:
|
||||||
|
1. **Decoy-Passwort**: Öffnet den äußeren Safe (enthält eine scheinbare Backup-Datei `system_backup.dat`).
|
||||||
|
2. **Hidden-Passwort**: Öffnet den geheimen, unnachweisbaren Safe.
|
||||||
|
* **Wichtig**: Notiere dir die ausgegebenen **24 Wörter des Notfallschlüssels (BIP-39)** auf der untenstehenden Notfallkarte!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Tresor einbinden (`mount`)
|
||||||
|
|
||||||
|
Sanctum erkennt automatisch anhand des eingegebenen Passworts, ob der Decoy- oder Hidden-Safe geöffnet werden soll.
|
||||||
|
|
||||||
|
| Befehl | Zweck |
|
||||||
|
| :--- | :--- |
|
||||||
|
| `sanctum mount --path <DATEI>` | Standard-Mount. Öffnet das Laufwerk automatisch im Explorer. |
|
||||||
|
| `sanctum mount --path <DATEI> --drive S` | Bindet den Tresor fest an den Buchstaben `S:` (statt automatischer Wahl). |
|
||||||
|
| `sanctum mount --path <DATEI> --idle-timeout 300` | Trennt das Laufwerk automatisch nach 5 Minuten (300 Sek.) Inaktivität. |
|
||||||
|
| `sanctum mount --path <DATEI> --no-open` | Verhindert das automatische Öffnen des Windows Explorers (Schutz vor ShellBag-Spuren). |
|
||||||
|
| `sanctum mount --path <DATEI> --stealth` | **Lautloser Stealth-Modus**: Keine Terminal-Ausgaben, keine URLs, kein Explorer-Start. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Notfallrettung & Wartung
|
||||||
|
|
||||||
|
* **Integritätsprüfung (FSCK)**:
|
||||||
|
```powershell
|
||||||
|
sanctum.exe verify --path "D:\Tresor\daten.sanctum"
|
||||||
|
```
|
||||||
|
Überprüft die B-Tree-Struktur der Datenbank und testet sämtliche Chunks gegen ihre kryptografischen AEAD-Authentifizierungs-Tags.
|
||||||
|
|
||||||
|
* **Online-Backup im laufenden Betrieb**:
|
||||||
|
```powershell
|
||||||
|
sanctum.exe backup --path "D:\Tresor\daten.sanctum" --output "E:\Backup\daten_backup.sanctum"
|
||||||
|
```
|
||||||
|
Erzeugt über die SQLite Online Backup API eine konsistente Kopie – selbst während Dateien geöffnet sind.
|
||||||
|
|
||||||
|
* **Passwort vergessen? Wiederherstellung via BIP-39 Notfallschlüssel**:
|
||||||
|
```powershell
|
||||||
|
sanctum.exe passwd --path "D:\Tresor\daten.sanctum" --recovery-key
|
||||||
|
```
|
||||||
|
*(Liest die 24 Wörter maskiert ein, ohne Spuren in der PowerShell-Historie zu hinterlassen, und vergibt ein neues Passwort).*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🖨️ Druckvorlage: BIP-39 Notfallkarte
|
||||||
|
|
||||||
|
Drucke diesen Abschnitt aus oder übertrage die Wörter handschriftlich auf ein Blatt Papier. Bewahre diese Karte physisch getrennt von deinem Computer an einem sicheren Ort (z. B. Tresor, Dokumentenmappe) auf.
|
||||||
|
|
||||||
|
```text
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ SANCTUM — KRYPTOGRAFISCHE NOTFALL-WIEDERHERSTELLUNGSKARTE (BIP-39) │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
Container: __________________________________________________________________
|
||||||
|
Erstelldatum: ____.___.202__ Safe: [ ] Standard [ ] Hidden Vault
|
||||||
|
Hinweis: Alle Wörter sind in Kleinbuchstaben aus der offiziellen BIP-39 Liste.
|
||||||
|
|
||||||
|
┌────┬────────────────────────────┬────┬────────────────────────────┐
|
||||||
|
│ # │ WORT │ # │ WORT │
|
||||||
|
├────┼────────────────────────────┼────┼────────────────────────────┤
|
||||||
|
│ 01 │ __________________________ │ 13 │ __________________________ │
|
||||||
|
│ 02 │ __________________________ │ 14 │ __________________________ │
|
||||||
|
│ 03 │ __________________________ │ 15 │ __________________________ │
|
||||||
|
│ 04 │ __________________________ │ 16 │ __________________________ │
|
||||||
|
│ 05 │ __________________________ │ 17 │ __________________________ │
|
||||||
|
│ 06 │ __________________________ │ 18 │ __________________________ │
|
||||||
|
│ 07 │ __________________________ │ 19 │ __________________________ │
|
||||||
|
│ 08 │ __________________________ │ 20 │ __________________________ │
|
||||||
|
│ 09 │ __________________________ │ 21 │ __________________________ │
|
||||||
|
│ 10 │ __________________________ │ 22 │ __________________________ │
|
||||||
|
│ 11 │ __________________________ │ 23 │ __________________________ │
|
||||||
|
│ 12 │ __________________________ │ 24 │ __________________________ │
|
||||||
|
└────┴────────────────────────────┴────┴────────────────────────────┘
|
||||||
|
|
||||||
|
⚠️ SICHERHEITSHINWEISE:
|
||||||
|
1. Wer im Besitz dieser 24 Wörter ist, kann den Tresor ohne Passwort entschlüsseln!
|
||||||
|
2. Niemals abfotografieren, in Cloud-Notizen speichern oder unverschlüsselt versenden.
|
||||||
|
3. Bei Verlust beider Passwörter und dieser Karte sind die Daten unwiederbringlich verloren.
|
||||||
|
```
|
||||||
@@ -17,6 +17,8 @@ Sanctum ist eine eigenständige, speichersichere und hochperformante CLI-Anwendu
|
|||||||
- **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**: `sanctum.exe` (~5.3 MB) ohne externe DLL-Abhängigkeiten.
|
||||||
|
|
||||||
|
> 💡 **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).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🔐 Kryptografie & Sicherheitsarchitektur
|
## 🔐 Kryptografie & Sicherheitsarchitektur
|
||||||
@@ -209,7 +211,8 @@ powershell -ExecutionPolicy Bypass -File .\scripts\package-release.ps1
|
|||||||
```
|
```
|
||||||
|
|
||||||
Erzeugt:
|
Erzeugt:
|
||||||
- `dist/sanctum-v0.3.1-windows-x86_64.zip`
|
- `dist/sanctum-v0.4.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/SHA256SUMS.txt`
|
- `dist/SHA256SUMS.txt`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"version": "0.5.0",
|
||||||
|
"description": "Verschlüsselter Ein-Datei-Container unter Windows im reinen Userland via WebDAV",
|
||||||
|
"homepage": "https://gitea.pansi.eu/harald/sanctum",
|
||||||
|
"license": "MIT",
|
||||||
|
"architecture": {
|
||||||
|
"64bit": {
|
||||||
|
"url": "https://gitea.pansi.eu/harald/sanctum/releases/download/v0.5.0/sanctum-v0.5.0-windows-x86_64.zip",
|
||||||
|
"hash": "2f069a8cc41f9fa6a2559e2952e5669523c21c175df15f6d80f0473bbe2fa60e",
|
||||||
|
"bin": "sanctum.exe"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"checkver": {
|
||||||
|
"url": "https://gitea.pansi.eu/api/v1/repos/harald/sanctum/releases/latest",
|
||||||
|
"jsonpath": "$.tag_name",
|
||||||
|
"regex": "v([\\d.]+)"
|
||||||
|
},
|
||||||
|
"autoupdate": {
|
||||||
|
"architecture": {
|
||||||
|
"64bit": {
|
||||||
|
"url": "https://gitea.pansi.eu/harald/sanctum/releases/download/v$version/sanctum-v$version-windows-x86_64.zip"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# yaml-language-server: $schema=https://aka.ms/winget-manifest.singleton.1.6.0.schema.json
|
||||||
|
PackageIdentifier: HaraldPansi.Sanctum
|
||||||
|
PackageVersion: 0.5.0
|
||||||
|
PackageName: Sanctum
|
||||||
|
Publisher: Harald Pansi
|
||||||
|
PublisherUrl: https://gitea.pansi.eu/harald
|
||||||
|
PublisherSupportUrl: https://gitea.pansi.eu/harald/sanctum/issues
|
||||||
|
Author: Harald Pansi
|
||||||
|
License: MIT
|
||||||
|
LicenseUrl: https://gitea.pansi.eu/harald/sanctum/src/branch/main/LICENSE
|
||||||
|
ShortDescription: Verschlüsselter Ein-Datei-Container unter Windows im reinen Userland via WebDAV
|
||||||
|
Description: Sanctum ist eine eigenständige, speichersichere CLI-Anwendung in Rust, die verschlüsselte Ein-Datei-Container (.sanctum) unter Windows 10/11 im reinen Userland verwaltet. 100% Userland, keine Adminrechte, keine Treiber.
|
||||||
|
Moniker: sanctum
|
||||||
|
Tags:
|
||||||
|
- encryption
|
||||||
|
- security
|
||||||
|
- privacy
|
||||||
|
- webdav
|
||||||
|
- container
|
||||||
|
- plausible-deniability
|
||||||
|
ReleaseNotesUrl: https://gitea.pansi.eu/harald/sanctum/releases/tag/v0.5.0
|
||||||
|
Installers:
|
||||||
|
- Architecture: x64
|
||||||
|
InstallerType: zip
|
||||||
|
NestedInstallerType: portable
|
||||||
|
NestedInstallerFiles:
|
||||||
|
- RelativeFilePath: sanctum.exe
|
||||||
|
PortableCommandAlias: sanctum
|
||||||
|
InstallerUrl: https://gitea.pansi.eu/harald/sanctum/releases/download/v0.5.0/sanctum-v0.5.0-windows-x86_64.zip
|
||||||
|
InstallerSha256: 2f069a8cc41f9fa6a2559e2952e5669523c21c175df15f6d80f0473bbe2fa60e
|
||||||
|
ManifestType: singleton
|
||||||
|
ManifestVersion: 1.6.0
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
param (
|
||||||
|
[switch]$SkipBuild = $false
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
# Repository-Wurzelverzeichnis ermitteln
|
||||||
|
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||||
|
$ProjectRoot = Split-Path -Parent $ScriptDir
|
||||||
|
Set-Location $ProjectRoot
|
||||||
|
|
||||||
|
# Toolchain-Pfade sicherstellen (Zig + Cargo Bin)
|
||||||
|
$ZigDir = "C:\Users\pansih\AppData\Local\Microsoft\WinGet\Packages\zig.zig_Microsoft.Winget.Source_8wekyb3d8bbwe\zig-x86_64-windows-0.16.0"
|
||||||
|
$env:PATH = "$ZigDir;C:\Users\pansih\.cargo\bin;C:\Windows\System32;" + $env:PATH
|
||||||
|
|
||||||
|
# Version aus Cargo.toml auslesen
|
||||||
|
$CargoToml = Get-Content (Join-Path $ProjectRoot "Cargo.toml") -Raw
|
||||||
|
if ($CargoToml -match 'version\s*=\s*"([^"]+)"') {
|
||||||
|
$Version = $matches[1]
|
||||||
|
} else {
|
||||||
|
Write-Error "Konnte Versionsnummer nicht aus Cargo.toml ermitteln."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "============================================================" -ForegroundColor Cyan
|
||||||
|
Write-Host " Sanctum Linux Release Packaging v$Version (x86_64 musl)" -ForegroundColor Cyan
|
||||||
|
Write-Host "============================================================" -ForegroundColor Cyan
|
||||||
|
|
||||||
|
# 1. Linux Release-Binary bauen
|
||||||
|
$Target = "x86_64-unknown-linux-musl"
|
||||||
|
$BinaryPath = Join-Path $ProjectRoot "target\$Target\release\sanctum"
|
||||||
|
|
||||||
|
if (-not $SkipBuild -or -not (Test-Path $BinaryPath)) {
|
||||||
|
Write-Host "`n[1/3] Kompiliere statisches Linux-Binary via cargo-zigbuild..." -ForegroundColor Yellow
|
||||||
|
cargo-zigbuild.exe zigbuild --target $Target --release
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
Write-Error "Linux-Build fehlgeschlagen!"
|
||||||
|
exit $LASTEXITCODE
|
||||||
|
}
|
||||||
|
Write-Host "[OK] Linux-Binary erfolgreich gebaut." -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host "`n[1/3] Build uebersprungen (-SkipBuild)." -ForegroundColor DarkYellow
|
||||||
|
}
|
||||||
|
|
||||||
|
# 2. Release-Verzeichnisstruktur vorbereiten
|
||||||
|
Write-Host "`n[2/3] Erstelle Linux-Distributionspaket..." -ForegroundColor Yellow
|
||||||
|
|
||||||
|
$DistDir = Join-Path $ProjectRoot "dist"
|
||||||
|
if (-not (Test-Path $DistDir)) {
|
||||||
|
New-Item -ItemType Directory -Path $DistDir -Force | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
$PackageName = "sanctum-v$Version-linux-x86_64"
|
||||||
|
$StagingDir = Join-Path $DistDir $PackageName
|
||||||
|
$TarGzFile = Join-Path $DistDir "$PackageName.tar.gz"
|
||||||
|
|
||||||
|
if (Test-Path $StagingDir) {
|
||||||
|
Remove-Item $StagingDir -Recurse -Force
|
||||||
|
}
|
||||||
|
New-Item -ItemType Directory -Path $StagingDir -Force | Out-Null
|
||||||
|
|
||||||
|
# Dateien kopieren
|
||||||
|
Copy-Item $BinaryPath -Destination (Join-Path $StagingDir "sanctum") -Force
|
||||||
|
Copy-Item (Join-Path $ProjectRoot "README.md") -Destination $StagingDir -Force
|
||||||
|
Copy-Item (Join-Path $ProjectRoot "LICENSE") -Destination $StagingDir -Force
|
||||||
|
Copy-Item (Join-Path $ProjectRoot "CHANGELOG.md") -Destination $StagingDir -Force
|
||||||
|
Copy-Item (Join-Path $ProjectRoot "INSTALL.md") -Destination $StagingDir -Force
|
||||||
|
Copy-Item (Join-Path $ProjectRoot "QUICKSTART.md") -Destination $StagingDir -Force
|
||||||
|
Copy-Item (Join-Path $ProjectRoot "LEGAL.md") -Destination $StagingDir -Force
|
||||||
|
Copy-Item (Join-Path $ProjectRoot "THIRD_PARTY_LICENSES.md") -Destination $StagingDir -Force
|
||||||
|
|
||||||
|
# .tar.gz Archiv erstellen mit Windows nativem bsdtar
|
||||||
|
if (Test-Path $TarGzFile) {
|
||||||
|
Remove-Item $TarGzFile -Force
|
||||||
|
}
|
||||||
|
|
||||||
|
tar.exe -czf $TarGzFile -C $DistDir $PackageName
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
Write-Error "Fehler beim Erstellen des tar.gz-Archivs!"
|
||||||
|
exit $LASTEXITCODE
|
||||||
|
}
|
||||||
|
|
||||||
|
# Staging-Verzeichnis bereinigen
|
||||||
|
Remove-Item $StagingDir -Recurse -Force
|
||||||
|
|
||||||
|
Write-Host "[OK] Archiv erstellt: $TarGzFile" -ForegroundColor Green
|
||||||
|
|
||||||
|
# 3. SHA-256 Pruefsummen berechnen und aktualisieren
|
||||||
|
Write-Host "`n[3/3] Aktualisiere SHA-256 Pruefsummen..." -ForegroundColor Yellow
|
||||||
|
|
||||||
|
$ChecksumFile = Join-Path $DistDir "SHA256SUMS.txt"
|
||||||
|
$TarHash = (Get-FileHash -Path $TarGzFile -Algorithm SHA256).Hash.ToLower()
|
||||||
|
$ElfHash = (Get-FileHash -Path $BinaryPath -Algorithm SHA256).Hash.ToLower()
|
||||||
|
|
||||||
|
# Existierende Pruefsummen lesen und um Linux-Assets ergaenzen/aktualisieren
|
||||||
|
$ExistingLines = if (Test-Path $ChecksumFile) {
|
||||||
|
Get-Content $ChecksumFile | Where-Object { $_ -notmatch "$PackageName\.tar\.gz" -and $_ -notmatch "\s+sanctum$" }
|
||||||
|
} else {
|
||||||
|
@()
|
||||||
|
}
|
||||||
|
|
||||||
|
$AllLines = @()
|
||||||
|
$AllLines += $ExistingLines
|
||||||
|
$AllLines += "$TarHash $PackageName.tar.gz"
|
||||||
|
$AllLines += "$ElfHash sanctum"
|
||||||
|
|
||||||
|
$AllLines | Set-Content -Path $ChecksumFile -Encoding utf8
|
||||||
|
|
||||||
|
$TarSizeMB = [math]::Round((Get-Item $TarGzFile).Length / 1MB, 2)
|
||||||
|
$ElfSizeMB = [math]::Round((Get-Item $BinaryPath).Length / 1MB, 2)
|
||||||
|
|
||||||
|
Write-Host "`n============================================================" -ForegroundColor Green
|
||||||
|
Write-Host " Sanctum Linux Release v$Version erfolgreich gepackt!" -ForegroundColor Green
|
||||||
|
Write-Host "============================================================" -ForegroundColor Green
|
||||||
|
Write-Host " Archiv: $TarGzFile ($TarSizeMB MB)"
|
||||||
|
Write-Host " TAR SHA-256: $TarHash"
|
||||||
|
Write-Host " ELF SHA-256: $ElfHash"
|
||||||
|
Write-Host " Checksum-File: $ChecksumFile"
|
||||||
|
Write-Host "`nBereit fuer Gitea Release."
|
||||||
@@ -55,14 +55,19 @@ $PackageName = "sanctum-v$Version-windows-x86_64"
|
|||||||
$StagingDir = Join-Path $DistDir $PackageName
|
$StagingDir = Join-Path $DistDir $PackageName
|
||||||
$ZipFile = Join-Path $DistDir "$PackageName.zip"
|
$ZipFile = Join-Path $DistDir "$PackageName.zip"
|
||||||
|
|
||||||
if (Test-Path $DistDir) {
|
if (-not (Test-Path $DistDir)) {
|
||||||
Remove-Item $DistDir -Recurse -Force
|
New-Item -ItemType Directory -Path $DistDir -Force | Out-Null
|
||||||
|
}
|
||||||
|
if (Test-Path $StagingDir) {
|
||||||
|
Remove-Item $StagingDir -Recurse -Force
|
||||||
}
|
}
|
||||||
New-Item -ItemType Directory -Path $StagingDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $StagingDir -Force | Out-Null
|
||||||
|
|
||||||
$ExeSource = Join-Path $ProjectRoot "target\release\sanctum.exe"
|
$ExeSource = Join-Path $ProjectRoot "target\release\sanctum.exe"
|
||||||
Copy-Item $ExeSource (Join-Path $StagingDir "sanctum.exe")
|
Copy-Item $ExeSource (Join-Path $StagingDir "sanctum.exe")
|
||||||
Copy-Item (Join-Path $ProjectRoot "README.md") (Join-Path $StagingDir "README.md")
|
Copy-Item (Join-Path $ProjectRoot "README.md") (Join-Path $StagingDir "README.md")
|
||||||
|
Copy-Item (Join-Path $ProjectRoot "QUICKSTART.md") (Join-Path $StagingDir "QUICKSTART.md")
|
||||||
|
Copy-Item (Join-Path $ProjectRoot "INSTALL.md") (Join-Path $StagingDir "INSTALL.md")
|
||||||
Copy-Item (Join-Path $ProjectRoot "LICENSE") (Join-Path $StagingDir "LICENSE")
|
Copy-Item (Join-Path $ProjectRoot "LICENSE") (Join-Path $StagingDir "LICENSE")
|
||||||
Copy-Item (Join-Path $ProjectRoot "LEGAL.md") (Join-Path $StagingDir "LEGAL.md")
|
Copy-Item (Join-Path $ProjectRoot "LEGAL.md") (Join-Path $StagingDir "LEGAL.md")
|
||||||
Copy-Item (Join-Path $ProjectRoot "THIRD_PARTY_LICENSES.md") (Join-Path $StagingDir "THIRD_PARTY_LICENSES.md")
|
Copy-Item (Join-Path $ProjectRoot "THIRD_PARTY_LICENSES.md") (Join-Path $StagingDir "THIRD_PARTY_LICENSES.md")
|
||||||
@@ -82,7 +87,17 @@ $ZipHash = (Get-FileHash -Path $ZipFile -Algorithm SHA256).Hash.ToLower()
|
|||||||
$ExeHash = (Get-FileHash -Path $ExeSource -Algorithm SHA256).Hash.ToLower()
|
$ExeHash = (Get-FileHash -Path $ExeSource -Algorithm SHA256).Hash.ToLower()
|
||||||
|
|
||||||
$ChecksumFile = Join-Path $DistDir "SHA256SUMS.txt"
|
$ChecksumFile = Join-Path $DistDir "SHA256SUMS.txt"
|
||||||
@("$ZipHash $PackageName.zip", "$ExeHash sanctum.exe") | Set-Content -Path $ChecksumFile -Encoding utf8
|
$ExistingLines = if (Test-Path $ChecksumFile) {
|
||||||
|
Get-Content $ChecksumFile | Where-Object { $_ -notmatch "$PackageName\.zip" -and $_ -notmatch "\s+sanctum\.exe$" }
|
||||||
|
} else {
|
||||||
|
@()
|
||||||
|
}
|
||||||
|
|
||||||
|
$AllLines = @()
|
||||||
|
$AllLines += "$ZipHash $PackageName.zip"
|
||||||
|
$AllLines += "$ExeHash sanctum.exe"
|
||||||
|
$AllLines += $ExistingLines
|
||||||
|
$AllLines | Set-Content -Path $ChecksumFile -Encoding utf8
|
||||||
|
|
||||||
Write-Host "[OK] Pruefsummen in SHA256SUMS.txt gespeichert." -ForegroundColor Green
|
Write-Host "[OK] Pruefsummen in SHA256SUMS.txt gespeichert." -ForegroundColor Green
|
||||||
|
|
||||||
|
|||||||
+47
-21
@@ -108,29 +108,55 @@ if (-not $Release) {
|
|||||||
$UploadUrl = "$GiteaUrl/api/v1/repos/$Owner/$Repo/releases/$($Release.id)/assets"
|
$UploadUrl = "$GiteaUrl/api/v1/repos/$Owner/$Repo/releases/$($Release.id)/assets"
|
||||||
$AuthHeader = "Authorization: token $Token"
|
$AuthHeader = "Authorization: token $Token"
|
||||||
|
|
||||||
# ZIP Asset hochladen
|
# Hilfsfunktion zum sauberen Hochladen/Ersetzen von Assets
|
||||||
Write-Host "Lade sanctum-$TagName-windows-x86_64.zip hoch..." -ForegroundColor Cyan
|
function Upload-ReleaseAsset {
|
||||||
$ZipPathNorm = $ZipFile.Replace('\', '/')
|
param (
|
||||||
$ZipUrl = "$UploadUrl?name=sanctum-$TagName-windows-x86_64.zip"
|
[string]$FilePath,
|
||||||
$ZipResult = & curl.exe -s -X POST $ZipUrl -H $AuthHeader -F "attachment=@$ZipPathNorm"
|
[string]$AssetName
|
||||||
Write-Host "[OK] sanctum-$TagName-windows-x86_64.zip hochgeladen." -ForegroundColor Green
|
)
|
||||||
|
|
||||||
|
if (-not (Test-Path $FilePath)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Lade $AssetName hoch..." -ForegroundColor Cyan
|
||||||
|
|
||||||
|
# Vorhandenes Asset mit gleichem Namen ermitteln und ggf. loeschen
|
||||||
|
$ExistingAssets = try {
|
||||||
|
Invoke-RestMethod -Uri $UploadUrl -Headers $Headers -Method Get
|
||||||
|
} catch {
|
||||||
|
@()
|
||||||
|
}
|
||||||
|
$Duplicate = $ExistingAssets | Where-Object { $_.name -eq $AssetName }
|
||||||
|
if ($Duplicate) {
|
||||||
|
Write-Host " [i] Ersetze existierendes Asset '$AssetName' (ID: $($Duplicate.id))..." -ForegroundColor DarkYellow
|
||||||
|
$DeleteUrl = "$GiteaUrl/api/v1/repos/$Owner/$Repo/releases/$($Release.id)/assets/$($Duplicate.id)"
|
||||||
|
Invoke-RestMethod -Uri $DeleteUrl -Headers $Headers -Method Delete | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
$PathNorm = $FilePath.Replace('\', '/')
|
||||||
|
$TargetUrl = "${UploadUrl}?name=$AssetName"
|
||||||
|
$Result = & curl.exe -sS -X POST $TargetUrl -H $AuthHeader -F "attachment=@$PathNorm"
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
Write-Error "Upload von $AssetName fehlgeschlagen: $Result"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
Write-Host "[OK] $AssetName erfolgreich hochgeladen." -ForegroundColor Green
|
||||||
|
}
|
||||||
|
|
||||||
|
# Windows-Assets hochladen
|
||||||
|
Upload-ReleaseAsset -FilePath $ZipFile -AssetName "sanctum-$TagName-windows-x86_64.zip"
|
||||||
|
$ExeFile = Join-Path $ProjectRoot "target\release\sanctum.exe"
|
||||||
|
Upload-ReleaseAsset -FilePath $ExeFile -AssetName "sanctum.exe"
|
||||||
|
|
||||||
|
# Linux-Assets hochladen
|
||||||
|
$LinuxTar = Join-Path $DistDir "sanctum-$TagName-linux-x86_64.tar.gz"
|
||||||
|
Upload-ReleaseAsset -FilePath $LinuxTar -AssetName "sanctum-$TagName-linux-x86_64.tar.gz"
|
||||||
|
$LinuxElf = Join-Path $ProjectRoot "target\x86_64-unknown-linux-musl\release\sanctum"
|
||||||
|
Upload-ReleaseAsset -FilePath $LinuxElf -AssetName "sanctum"
|
||||||
|
|
||||||
# SHA256SUMS.txt hochladen
|
# SHA256SUMS.txt hochladen
|
||||||
Write-Host "Lade SHA256SUMS.txt hoch..." -ForegroundColor Cyan
|
Upload-ReleaseAsset -FilePath $ChecksumFile -AssetName "SHA256SUMS.txt"
|
||||||
$SumPathNorm = $ChecksumFile.Replace('\', '/')
|
|
||||||
$SumUrl = "$UploadUrl?name=SHA256SUMS.txt"
|
|
||||||
$SumResult = & curl.exe -s -X POST $SumUrl -H $AuthHeader -F "attachment=@$SumPathNorm"
|
|
||||||
Write-Host "[OK] SHA256SUMS.txt hochgeladen." -ForegroundColor Green
|
|
||||||
|
|
||||||
# Standalone EXE hochladen
|
|
||||||
$ExeFile = Join-Path $ProjectRoot "target\release\sanctum.exe"
|
|
||||||
if (Test-Path $ExeFile) {
|
|
||||||
Write-Host "Lade sanctum.exe hoch..." -ForegroundColor Cyan
|
|
||||||
$ExePathNorm = $ExeFile.Replace('\', '/')
|
|
||||||
$ExeUrl = "$UploadUrl?name=sanctum.exe"
|
|
||||||
$ExeResult = & curl.exe -s -X POST $ExeUrl -H $AuthHeader -F "attachment=@$ExePathNorm"
|
|
||||||
Write-Host "[OK] sanctum.exe hochgeladen." -ForegroundColor Green
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
Write-Host "`n============================================================" -ForegroundColor Green
|
Write-Host "`n============================================================" -ForegroundColor Green
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
pub mod carrier;
|
pub mod carrier;
|
||||||
pub mod crypto;
|
pub mod crypto;
|
||||||
pub mod mount;
|
pub mod mount;
|
||||||
|
pub mod platform;
|
||||||
pub mod recovery;
|
pub mod recovery;
|
||||||
pub mod storage;
|
pub mod storage;
|
||||||
|
pub mod sync;
|
||||||
pub mod ui;
|
pub mod ui;
|
||||||
pub mod verify;
|
pub mod verify;
|
||||||
pub mod vfs;
|
pub mod vfs;
|
||||||
|
|||||||
+234
-1
@@ -68,10 +68,14 @@ enum Commands {
|
|||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
|
|
||||||
/// Laufwerksbuchstabe (z. B. 'S' oder 'S:', optional; wählt standardmäßig automatisch das nächste freie Laufwerk)
|
/// Laufwerksbuchstabe (z. B. 'S' oder 'S:', optional; wählt unter Windows standardmäßig das nächste freie Laufwerk)
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
drive: Option<String>,
|
drive: Option<String>,
|
||||||
|
|
||||||
|
/// Optionaler lokaler Mount-Pfad unter Linux/macOS (z. B. '/mnt/sanctum' oder '~/vault')
|
||||||
|
#[arg(short = 'm', long = "mount-point", value_name = "DIR")]
|
||||||
|
mount_point: Option<PathBuf>,
|
||||||
|
|
||||||
/// Optionaler TCP-Port für den lokalen WebDAV-Server (Standard: 8443)
|
/// Optionaler TCP-Port für den lokalen WebDAV-Server (Standard: 8443)
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
port: Option<u16>,
|
port: Option<u16>,
|
||||||
@@ -197,6 +201,48 @@ enum Commands {
|
|||||||
full: bool,
|
full: bool,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// Synchronisiert Dateien und Ordner zwischen Host und Container (rsync-artig, ohne Größenbeschränkung)
|
||||||
|
Sync {
|
||||||
|
/// Pfad zur .sanctum Containerdatei
|
||||||
|
#[arg(short, long)]
|
||||||
|
path: PathBuf,
|
||||||
|
|
||||||
|
/// Quellpfad (lokales Verzeichnis/Datei oder Vault-Pfad bei --pull)
|
||||||
|
source: String,
|
||||||
|
|
||||||
|
/// Zielpfad (Vault-Pfad wie z. B. '/Downloads' oder lokaler Pfad bei --pull; Standard: '/')
|
||||||
|
#[arg(default_value = "/")]
|
||||||
|
target: String,
|
||||||
|
|
||||||
|
/// Kehrt die Richtung um: Lädt Daten aus dem Tresor auf die lokale Festplatte (Pull)
|
||||||
|
#[arg(long, default_value_t = false)]
|
||||||
|
pull: bool,
|
||||||
|
|
||||||
|
/// Löscht Dateien im Ziel, die an der Quelle nicht mehr existieren (Spiegelung)
|
||||||
|
#[arg(long, default_value_t = false)]
|
||||||
|
delete: bool,
|
||||||
|
|
||||||
|
/// Führt eine Simulation aus: Zeigt Änderungen an, ohne Dateien zu schreiben oder zu löschen
|
||||||
|
#[arg(short = 'n', long, default_value_t = false)]
|
||||||
|
dry_run: bool,
|
||||||
|
|
||||||
|
/// Vergleicht Dateien anhand kryptografischer Hashes statt nur Zeitstempel und Dateigröße
|
||||||
|
#[arg(short = 'c', long, default_value_t = false)]
|
||||||
|
checksum: bool,
|
||||||
|
|
||||||
|
/// Schließt Dateien oder Ordner anhand von Mustern aus (z. B. '*.tmp', '*.crdownload')
|
||||||
|
#[arg(long)]
|
||||||
|
exclude: Vec<String>,
|
||||||
|
|
||||||
|
/// Unterdrückt Pro-Datei-Ausgaben
|
||||||
|
#[arg(short, long, default_value_t = false)]
|
||||||
|
quiet: bool,
|
||||||
|
|
||||||
|
/// Optionaler 24-Wort Notfallschlüssel (umgeht Passwortabfrage)
|
||||||
|
#[arg(long, num_args = 0..=1, default_missing_value = "")]
|
||||||
|
recovery_key: Option<String>,
|
||||||
|
},
|
||||||
|
|
||||||
/// Registriert .sanctum Containerdateien im Windows Explorer (Doppelklick & Kontextmenü, keine Adminrechte)
|
/// Registriert .sanctum Containerdateien im Windows Explorer (Doppelklick & Kontextmenü, keine Adminrechte)
|
||||||
Register,
|
Register,
|
||||||
|
|
||||||
@@ -880,6 +926,159 @@ fn handle_verify(container_path: &Path, full: bool) -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn handle_sync(
|
||||||
|
container_path: &Path,
|
||||||
|
source: &str,
|
||||||
|
target: &str,
|
||||||
|
pull: bool,
|
||||||
|
delete: bool,
|
||||||
|
dry_run: bool,
|
||||||
|
checksum: bool,
|
||||||
|
exclude: Vec<String>,
|
||||||
|
quiet: bool,
|
||||||
|
recovery_key: Option<&str>,
|
||||||
|
) -> Result<()> {
|
||||||
|
if !container_path.exists() {
|
||||||
|
bail!("Containerdatei '{}' existiert nicht.", container_path.display());
|
||||||
|
}
|
||||||
|
|
||||||
|
let auth = if let Some(key_arg) = recovery_key {
|
||||||
|
let phrase_str = if key_arg.trim().is_empty() {
|
||||||
|
println!("┌─────────────────────────────────────────────────────────────┐");
|
||||||
|
println!("│ 🔑 Maskierte interaktive Notfallschlüssel-Eingabe │");
|
||||||
|
println!("└─────────────────────────────────────────────────────────────┘");
|
||||||
|
println!();
|
||||||
|
rpassword::prompt_password("24-Wort Notfall-Wiederherstellungsschlüssel: ")
|
||||||
|
.context("Fehler beim Einlesen des Schlüssels")?
|
||||||
|
} else {
|
||||||
|
key_arg.to_string()
|
||||||
|
};
|
||||||
|
ContainerAuth::RecoveryKey(Zeroizing::new(phrase_str))
|
||||||
|
} else {
|
||||||
|
println!("┌─────────────────────────────────────────────────────────────┐");
|
||||||
|
println!("│ Sanctum — Ordner- & Dateisynchronisation (Sync) │");
|
||||||
|
println!("└─────────────────────────────────────────────────────────────┘");
|
||||||
|
println!(" Container: {}", container_path.display());
|
||||||
|
println!();
|
||||||
|
let pass = rpassword::prompt_password("Master-Passwort: ")
|
||||||
|
.context("Fehler beim Einlesen des Passworts")?;
|
||||||
|
ContainerAuth::Password(Zeroizing::new(pass))
|
||||||
|
};
|
||||||
|
|
||||||
|
let db = Database::open(container_path)
|
||||||
|
.context("Konnte Container-Datenbank nicht öffnen")?;
|
||||||
|
let meta = db.read_meta().context("Konnte Container-Header nicht lesen")?;
|
||||||
|
|
||||||
|
let (dek, version, vault_id) = match auth {
|
||||||
|
ContainerAuth::Password(ref password) => {
|
||||||
|
let keys = meta
|
||||||
|
.authenticate(password)
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("Ungültiges Master-Passwort!"))?;
|
||||||
|
(keys.dek().clone(), meta.version, keys.slot_id())
|
||||||
|
}
|
||||||
|
ContainerAuth::RecoveryKey(ref phrase) => {
|
||||||
|
let dek = mnemonic_to_dek(phrase)
|
||||||
|
.context("Ungültiger 24-Wort Notfallschlüssel")?;
|
||||||
|
let is_hidden = {
|
||||||
|
let children = db.list_children_in_vault(2, 1, &dek).unwrap_or_default();
|
||||||
|
!children.is_empty()
|
||||||
|
};
|
||||||
|
let v_id = if is_hidden { 1 } else { 0 };
|
||||||
|
(dek, meta.version, v_id)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Richtung ermitteln
|
||||||
|
let direction = if pull || source.starts_with('/') {
|
||||||
|
sanctum::sync::SyncDirection::Pull
|
||||||
|
} else {
|
||||||
|
sanctum::sync::SyncDirection::Push
|
||||||
|
};
|
||||||
|
|
||||||
|
let options = sanctum::sync::SyncOptions {
|
||||||
|
direction,
|
||||||
|
delete,
|
||||||
|
dry_run,
|
||||||
|
checksum,
|
||||||
|
exclude_patterns: exclude,
|
||||||
|
quiet,
|
||||||
|
};
|
||||||
|
|
||||||
|
if !quiet {
|
||||||
|
let dir_str = match direction {
|
||||||
|
sanctum::sync::SyncDirection::Push => "Push (Host -> Container)",
|
||||||
|
sanctum::sync::SyncDirection::Pull => "Pull (Container -> Host)",
|
||||||
|
};
|
||||||
|
|
||||||
|
println!();
|
||||||
|
if dry_run {
|
||||||
|
println!(" {} Ausführung im SIMULATIONS-MODUS (-n / --dry-run)!", ui::yellow("[!]"));
|
||||||
|
}
|
||||||
|
println!(" • Richtung: {}", ui::cyan(dir_str));
|
||||||
|
println!(" • Quelle: {}", source);
|
||||||
|
println!(" • Ziel: {}", target);
|
||||||
|
if delete {
|
||||||
|
println!(" • Spiegelung: {} (Dateien im Ziel ohne Entsprechung werden gelöscht)", ui::red("--delete aktiv"));
|
||||||
|
}
|
||||||
|
if checksum {
|
||||||
|
println!(" • Modus: Kryptografischer Inhaltsabgleich (--checksum aktiv)");
|
||||||
|
}
|
||||||
|
if !options.exclude_patterns.is_empty() {
|
||||||
|
println!(" • Filter: {} Ausschlussmuster", options.exclude_patterns.len());
|
||||||
|
}
|
||||||
|
println!();
|
||||||
|
println!(" {} Starte Synchronisation ...", ui::dim("[-]"));
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
|
||||||
|
let stats = sanctum::sync::run_sync(
|
||||||
|
&db,
|
||||||
|
vault_id,
|
||||||
|
&dek,
|
||||||
|
version,
|
||||||
|
source,
|
||||||
|
target,
|
||||||
|
&options,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
if !dry_run {
|
||||||
|
let _ = db.checkpoint();
|
||||||
|
}
|
||||||
|
|
||||||
|
if !quiet {
|
||||||
|
println!();
|
||||||
|
if dry_run {
|
||||||
|
println!("┌─────────────────────────────────────────────────────────────┐");
|
||||||
|
println!("│ ℹ Dry-Run Simulation erfolgreich abgeschlossen │");
|
||||||
|
println!("└─────────────────────────────────────────────────────────────┘");
|
||||||
|
println!(" • Gescannt: {} Dateien/Objekte", stats.files_scanned);
|
||||||
|
println!(" • Zu übertragen: {} Dateien ({})", stats.files_transferred, ui::format_bytes(stats.bytes_transferred));
|
||||||
|
println!(" • Bereits aktuell: {} Dateien (übersprungen)", stats.files_skipped);
|
||||||
|
if delete {
|
||||||
|
println!(" • Zu löschen: {} Dateien/Ordner", stats.files_deleted);
|
||||||
|
}
|
||||||
|
println!(" • Hinweis: {} Es wurden keine Änderungen vorgenommen.", ui::yellow("✔"));
|
||||||
|
} else {
|
||||||
|
println!("┌─────────────────────────────────────────────────────────────┐");
|
||||||
|
println!("│ ✔ Synchronisation erfolgreich abgeschlossen │");
|
||||||
|
println!("└─────────────────────────────────────────────────────────────┘");
|
||||||
|
println!(" • Gescannt: {} Dateien/Objekte", stats.files_scanned);
|
||||||
|
println!(" • Übertragen: {} Dateien ({})", stats.files_transferred, ui::format_bytes(stats.bytes_transferred));
|
||||||
|
println!(" • Übersprungen: {} Dateien (bereits aktuell)", stats.files_skipped);
|
||||||
|
if delete {
|
||||||
|
println!(" • Gelöscht: {} verwaiste Dateien/Ordner", stats.files_deleted);
|
||||||
|
}
|
||||||
|
let secs = stats.elapsed.as_secs_f64();
|
||||||
|
let mb = (stats.bytes_transferred as f64) / (1024.0 * 1024.0);
|
||||||
|
let speed = if secs > 0.05 { mb / secs } else { 0.0 };
|
||||||
|
println!(" • Dauer: {:.2}s (Durchschnitt: {:.2} MB/s)", secs, speed);
|
||||||
|
}
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
async fn run() -> Result<()> {
|
async fn run() -> Result<()> {
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
|
|
||||||
@@ -904,6 +1103,7 @@ async fn run() -> Result<()> {
|
|||||||
Commands::Mount {
|
Commands::Mount {
|
||||||
path,
|
path,
|
||||||
drive,
|
drive,
|
||||||
|
mount_point,
|
||||||
port,
|
port,
|
||||||
recovery_key,
|
recovery_key,
|
||||||
no_open,
|
no_open,
|
||||||
@@ -916,6 +1116,8 @@ async fn run() -> Result<()> {
|
|||||||
let drive_char = match drive {
|
let drive_char = match drive {
|
||||||
Some(ref d) => parse_drive_letter(d)?,
|
Some(ref d) => parse_drive_letter(d)?,
|
||||||
None => {
|
None => {
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
let auto_drive = sanctum::windows::find_next_available_drive()?;
|
let auto_drive = sanctum::windows::find_next_available_drive()?;
|
||||||
if !stealth {
|
if !stealth {
|
||||||
println!(
|
println!(
|
||||||
@@ -926,6 +1128,11 @@ async fn run() -> Result<()> {
|
|||||||
}
|
}
|
||||||
auto_drive
|
auto_drive
|
||||||
}
|
}
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
{
|
||||||
|
'Z'
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
let auth = if let Some(key) = recovery_key {
|
let auth = if let Some(key) = recovery_key {
|
||||||
let actual_key = if key.trim().is_empty() {
|
let actual_key = if key.trim().is_empty() {
|
||||||
@@ -953,6 +1160,7 @@ async fn run() -> Result<()> {
|
|||||||
mount_container(
|
mount_container(
|
||||||
&path,
|
&path,
|
||||||
drive_char,
|
drive_char,
|
||||||
|
mount_point.as_deref(),
|
||||||
port,
|
port,
|
||||||
auth,
|
auth,
|
||||||
open_explorer,
|
open_explorer,
|
||||||
@@ -993,6 +1201,31 @@ async fn run() -> Result<()> {
|
|||||||
Commands::Verify { path, full } => {
|
Commands::Verify { path, full } => {
|
||||||
handle_verify(&path, full)?;
|
handle_verify(&path, full)?;
|
||||||
}
|
}
|
||||||
|
Commands::Sync {
|
||||||
|
path,
|
||||||
|
source,
|
||||||
|
target,
|
||||||
|
pull,
|
||||||
|
delete,
|
||||||
|
dry_run,
|
||||||
|
checksum,
|
||||||
|
exclude,
|
||||||
|
quiet,
|
||||||
|
recovery_key,
|
||||||
|
} => {
|
||||||
|
handle_sync(
|
||||||
|
&path,
|
||||||
|
&source,
|
||||||
|
&target,
|
||||||
|
pull,
|
||||||
|
delete,
|
||||||
|
dry_run,
|
||||||
|
checksum,
|
||||||
|
exclude,
|
||||||
|
quiet,
|
||||||
|
recovery_key.as_deref(),
|
||||||
|
)?;
|
||||||
|
}
|
||||||
Commands::Register => {
|
Commands::Register => {
|
||||||
println!("┌─────────────────────────────────────────────────────────────┐");
|
println!("┌─────────────────────────────────────────────────────────────┐");
|
||||||
println!("│ Sanctum — Windows Explorer Integration einrichten │");
|
println!("│ Sanctum — Windows Explorer Integration einrichten │");
|
||||||
|
|||||||
+170
-15
@@ -32,8 +32,10 @@ pub fn format_drive(drive_letter: char) -> String {
|
|||||||
format!("{}:", drive_letter.to_ascii_uppercase())
|
format!("{}:", drive_letter.to_ascii_uppercase())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Trennt ein Windows-Netzlaufwerk via `net use <DRIVE>: /delete /y`.
|
/// Trennt ein Windows-Netzlaufwerk via `net use <DRIVE>: /delete /y` bzw. Unix-Mountpoint via `gio mount -u`.
|
||||||
pub fn unmount_drive(drive_letter: char) -> Result<()> {
|
pub fn unmount_drive(drive_letter: char) -> Result<()> {
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
let drive_str = format_drive(drive_letter);
|
let drive_str = format_drive(drive_letter);
|
||||||
|
|
||||||
let output = Command::new("net")
|
let output = Command::new("net")
|
||||||
@@ -54,9 +56,23 @@ pub fn unmount_drive(drive_letter: char) -> Result<()> {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
let _ = drive_letter;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
#[cfg(not(any(windows, unix)))]
|
||||||
|
{
|
||||||
|
let _ = drive_letter;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Bindet ein Windows-Netzlaufwerk via `net use <DRIVE>: http://127.0.0.1:<PORT>/<TOKEN>/ /persistent:no` ein.
|
/// Bindet ein Windows-Netzlaufwerk via `net use <DRIVE>: http://127.0.0.1:<PORT>/<TOKEN>/ /persistent:no` ein
|
||||||
fn run_net_use_mount(drive_str: &str, port: u16, session_token: &str) -> Result<()> {
|
/// bzw. unter Unix via `gio mount dav://127.0.0.1:<PORT>/<TOKEN>/`.
|
||||||
|
fn run_mount_command(drive_str: &str, port: u16, session_token: &str) -> Result<()> {
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
let url = format!("http://127.0.0.1:{}/{}/", port, session_token);
|
let url = format!("http://127.0.0.1:{}/{}/", port, session_token);
|
||||||
|
|
||||||
let mut output = Command::new("net")
|
let mut output = Command::new("net")
|
||||||
@@ -108,11 +124,27 @@ fn run_net_use_mount(drive_str: &str, port: u16, session_token: &str) -> Result<
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
let dav_url = format!("dav://127.0.0.1:{}/{}/", port, session_token);
|
||||||
|
let _ = Command::new("gio")
|
||||||
|
.args(["mount", &dav_url])
|
||||||
|
.output();
|
||||||
|
let _ = drive_str;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
#[cfg(not(any(windows, unix)))]
|
||||||
|
{
|
||||||
|
let _ = (drive_str, port, session_token);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Startet den WebDAV-Server für den Sanctum-Container und bindet ihn als Netzlaufwerk ein.
|
/// Startet den WebDAV-Server für den Sanctum-Container und bindet ihn als Netzlaufwerk (Windows) bzw. VFS (Linux) ein.
|
||||||
pub async fn mount_container(
|
pub async fn mount_container(
|
||||||
container_path: &Path,
|
container_path: &Path,
|
||||||
drive_letter: char,
|
drive_letter: char,
|
||||||
|
mount_point: Option<&Path>,
|
||||||
requested_port: Option<u16>,
|
requested_port: Option<u16>,
|
||||||
auth: ContainerAuth,
|
auth: ContainerAuth,
|
||||||
open_explorer: bool,
|
open_explorer: bool,
|
||||||
@@ -123,6 +155,10 @@ pub async fn mount_container(
|
|||||||
stealth: bool,
|
stealth: bool,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let drive_str = format_drive(drive_letter);
|
let drive_str = format_drive(drive_letter);
|
||||||
|
let _ = mount_point;
|
||||||
|
let _ = open_explorer;
|
||||||
|
let _ = enable_tray;
|
||||||
|
let _ = lock_on_screen_lock;
|
||||||
|
|
||||||
if !container_path.exists() {
|
if !container_path.exists() {
|
||||||
bail!(
|
bail!(
|
||||||
@@ -224,6 +260,7 @@ pub async fn mount_container(
|
|||||||
let bound_port = bound_addr.port();
|
let bound_port = bound_addr.port();
|
||||||
|
|
||||||
if !stealth {
|
if !stealth {
|
||||||
|
#[cfg(windows)]
|
||||||
ui::step(
|
ui::step(
|
||||||
4,
|
4,
|
||||||
4,
|
4,
|
||||||
@@ -233,6 +270,16 @@ pub async fn mount_container(
|
|||||||
bound_port, drive_str
|
bound_port, drive_str
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
ui::step(
|
||||||
|
4,
|
||||||
|
4,
|
||||||
|
"🌐",
|
||||||
|
&format!(
|
||||||
|
"Starte WebDAV-Server auf Port {} & initialisiere VFS...",
|
||||||
|
bound_port
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let (shutdown_tx, shutdown_rx) = watch::channel(false);
|
let (shutdown_tx, shutdown_rx) = watch::channel(false);
|
||||||
@@ -245,20 +292,22 @@ pub async fn mount_container(
|
|||||||
shutdown_rx,
|
shutdown_rx,
|
||||||
));
|
));
|
||||||
|
|
||||||
// Netzlaufwerk einbinden
|
// Netzlaufwerk bzw. Verzeichnis einbinden
|
||||||
if let Err(e) = run_net_use_mount(&drive_str, bound_port, &session_token) {
|
if let Err(e) = run_mount_command(&drive_str, bound_port, &session_token) {
|
||||||
let _ = shutdown_tx.send(true);
|
let _ = shutdown_tx.send(true);
|
||||||
let _ = server_handle.await;
|
let _ = server_handle.await;
|
||||||
return Err(e);
|
return Err(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Optional automatisch im Windows Explorer öffnen (visuelle Parität für Decoy und Hidden Vault)
|
// Optional automatisch im Windows Explorer öffnen (visuelle Parität für Decoy und Hidden Vault)
|
||||||
|
#[cfg(windows)]
|
||||||
if open_explorer {
|
if open_explorer {
|
||||||
let _ = crate::windows::open_in_explorer(drive_letter);
|
let _ = crate::windows::open_in_explorer(drive_letter);
|
||||||
}
|
}
|
||||||
|
|
||||||
// System-Tray Initialisierung
|
// System-Tray Initialisierung (nur Windows)
|
||||||
let (tray_shutdown_tx, mut tray_shutdown_rx) = tokio::sync::mpsc::channel::<()>(1);
|
#[cfg(windows)]
|
||||||
|
let (_tray_shutdown_tx, mut tray_shutdown_rx) = tokio::sync::mpsc::channel::<()>(1);
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
let _tray = if enable_tray {
|
let _tray = if enable_tray {
|
||||||
let icon_source = crate::windows::get_default_system_icon()
|
let icon_source = crate::windows::get_default_system_icon()
|
||||||
@@ -276,7 +325,7 @@ pub async fn mount_container(
|
|||||||
let _ = tray.add_menu_item("Im Explorer öffnen", move || {
|
let _ = tray.add_menu_item("Im Explorer öffnen", move || {
|
||||||
let _ = crate::windows::open_in_explorer(dl);
|
let _ = crate::windows::open_in_explorer(dl);
|
||||||
});
|
});
|
||||||
let s_tx = tray_shutdown_tx.clone();
|
let s_tx = _tray_shutdown_tx.clone();
|
||||||
let _ = tray.add_menu_item("Trennen & Beenden", move || {
|
let _ = tray.add_menu_item("Trennen & Beenden", move || {
|
||||||
let _ = s_tx.blocking_send(());
|
let _ = s_tx.blocking_send(());
|
||||||
});
|
});
|
||||||
@@ -315,8 +364,10 @@ pub async fn mount_container(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Windows-Sitzungssperre (Win + L Auto-Lock)
|
// Windows-Sitzungssperre (Win + L Auto-Lock, nur Windows)
|
||||||
|
#[cfg(windows)]
|
||||||
let (session_lock_tx, mut session_lock_rx) = tokio::sync::mpsc::channel::<()>(1);
|
let (session_lock_tx, mut session_lock_rx) = tokio::sync::mpsc::channel::<()>(1);
|
||||||
|
#[cfg(windows)]
|
||||||
let _session_monitor = if lock_on_screen_lock {
|
let _session_monitor = if lock_on_screen_lock {
|
||||||
match crate::windows::start_session_lock_monitor(session_lock_tx) {
|
match crate::windows::start_session_lock_monitor(session_lock_tx) {
|
||||||
Ok(guard) => Some(guard),
|
Ok(guard) => Some(guard),
|
||||||
@@ -330,7 +381,10 @@ pub async fn mount_container(
|
|||||||
};
|
};
|
||||||
|
|
||||||
if stealth {
|
if stealth {
|
||||||
|
#[cfg(windows)]
|
||||||
println!("Sanctum: Netzlaufwerk {} bereit.", drive_str);
|
println!("Sanctum: Netzlaufwerk {} bereit.", drive_str);
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
println!("Sanctum: WebDAV-Server auf Port {} bereit.", bound_port);
|
||||||
} else {
|
} else {
|
||||||
println!();
|
println!();
|
||||||
println!("┌─────────────────────────────────────────────────────────────┐");
|
println!("┌─────────────────────────────────────────────────────────────┐");
|
||||||
@@ -338,30 +392,63 @@ pub async fn mount_container(
|
|||||||
println!("└─────────────────────────────────────────────────────────────┘");
|
println!("└─────────────────────────────────────────────────────────────┘");
|
||||||
println!();
|
println!();
|
||||||
println!(" • Container: {}", container_path.display());
|
println!(" • Container: {}", container_path.display());
|
||||||
|
#[cfg(windows)]
|
||||||
println!(" • Netzlaufwerk: {} (im Windows Explorer bereit)", ui::cyan(&drive_str));
|
println!(" • Netzlaufwerk: {} (im Windows Explorer bereit)", ui::cyan(&drive_str));
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
{
|
||||||
|
if let Some(mp) = mount_point {
|
||||||
|
println!(" • Mountpoint: {}", ui::cyan(&mp.display().to_string()));
|
||||||
|
} else {
|
||||||
|
println!(" • Modus: WebDAV Userland-VFS");
|
||||||
|
}
|
||||||
|
}
|
||||||
println!(" • WebDAV-URL: http://127.0.0.1:{}/{}/ (Session-Token geschützt)", bound_port, session_token);
|
println!(" • WebDAV-URL: http://127.0.0.1:{}/{}/ (Session-Token geschützt)", bound_port, session_token);
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
{
|
||||||
|
println!(" • gio Befehl: gio mount dav://127.0.0.1:{}/{}/", bound_port, session_token);
|
||||||
|
if let Some(mp) = mount_point {
|
||||||
|
println!(" • davfs2: mount -t davfs http://127.0.0.1:{}/{}/ {}", bound_port, session_token, mp.display());
|
||||||
|
}
|
||||||
|
}
|
||||||
if let Some(secs) = idle_timeout {
|
if let Some(secs) = idle_timeout {
|
||||||
println!(" • Auto-Lock: Inaktivität nach {}s", secs);
|
println!(" • Auto-Lock: Inaktivität nach {}s", secs);
|
||||||
}
|
}
|
||||||
|
#[cfg(windows)]
|
||||||
if lock_on_screen_lock {
|
if lock_on_screen_lock {
|
||||||
println!(" • Sitzung: Automatisches Sperren bei Win + L aktiv");
|
println!(" • Sitzung: Automatisches Sperren bei Win + L aktiv");
|
||||||
}
|
}
|
||||||
if anti_leak {
|
if anti_leak {
|
||||||
|
#[cfg(windows)]
|
||||||
println!(" • Anti-Leak: Explorer-Metadatenfilter aktiv (Thumbs.db, desktop.ini blockiert)");
|
println!(" • Anti-Leak: Explorer-Metadatenfilter aktiv (Thumbs.db, desktop.ini blockiert)");
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
println!(" • Anti-Leak: VFS-Metadatenfilter aktiv (.directory, .Trash, desktop.ini blockiert)");
|
||||||
}
|
}
|
||||||
|
#[cfg(windows)]
|
||||||
if enable_tray {
|
if enable_tray {
|
||||||
println!(" • System-Tray: Icon aktiv (Rechtsklick für Explorer/Trennen)");
|
println!(" • System-Tray: Icon aktiv (Rechtsklick für Explorer/Trennen)");
|
||||||
}
|
}
|
||||||
println!();
|
println!();
|
||||||
|
#[cfg(windows)]
|
||||||
println!(" [{}] Drücke [Ctrl+C] oder nutze das Tray-Icon zum Beenden.", ui::yellow("Tipp"));
|
println!(" [{}] Drücke [Ctrl+C] oder nutze das Tray-Icon zum Beenden.", ui::yellow("Tipp"));
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
println!(" [{}] Drücke [Ctrl+C] zum sicheren Beenden.", ui::yellow("Tipp"));
|
||||||
println!();
|
println!();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Windows Console Close Monitor (CTRL_CLOSE_EVENT / CTRL_SHUTDOWN_EVENT)
|
// Windows Console Close Monitor (CTRL_CLOSE_EVENT / CTRL_SHUTDOWN_EVENT, nur Windows)
|
||||||
|
#[cfg(windows)]
|
||||||
let (console_close_tx, mut console_close_rx) = tokio::sync::mpsc::channel::<()>(1);
|
let (console_close_tx, mut console_close_rx) = tokio::sync::mpsc::channel::<()>(1);
|
||||||
|
#[cfg(windows)]
|
||||||
let _console_guard = crate::windows::start_console_ctrl_monitor(console_close_tx, drive_letter).ok();
|
let _console_guard = crate::windows::start_console_ctrl_monitor(console_close_tx, drive_letter).ok();
|
||||||
|
|
||||||
|
// Unix Signale (SIGTERM, SIGHUP)
|
||||||
|
#[cfg(unix)]
|
||||||
|
let mut sigterm = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()).ok();
|
||||||
|
#[cfg(unix)]
|
||||||
|
let mut sighup = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::hangup()).ok();
|
||||||
|
|
||||||
// Warten auf Beendigungssignal (Ctrl+C, Tray-Klick, Inaktivität, Win+L, Konsolenfenster-Schließen)
|
// Warten auf Beendigungssignal (Ctrl+C, Tray-Klick, Inaktivität, Win+L, Konsolenfenster-Schließen)
|
||||||
|
#[cfg(windows)]
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
res = tokio::signal::ctrl_c() => {
|
res = tokio::signal::ctrl_c() => {
|
||||||
let _ = res;
|
let _ = res;
|
||||||
@@ -370,25 +457,81 @@ pub async fn mount_container(
|
|||||||
println!(" {} Beendigungssignal (Ctrl+C) empfangen.", ui::yellow("[!]"));
|
println!(" {} Beendigungssignal (Ctrl+C) empfangen.", ui::yellow("[!]"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ = console_close_rx.recv() => {
|
Some(()) = console_close_rx.recv() => {
|
||||||
if !stealth {
|
if !stealth {
|
||||||
println!();
|
println!();
|
||||||
println!(" {} Konsolenfenster wird geschlossen — sichere Trennung ausgeführt!", ui::yellow("[!]"));
|
println!(" {} Konsolenfenster wird geschlossen — sichere Trennung ausgeführt!", ui::yellow("[!]"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ = tray_shutdown_rx.recv() => {
|
Some(()) = tray_shutdown_rx.recv() => {
|
||||||
if !stealth {
|
if !stealth {
|
||||||
println!();
|
println!();
|
||||||
println!(" {} Beendigungssignal aus System-Tray empfangen.", ui::yellow("[!]"));
|
println!(" {} Beendigungssignal aus System-Tray empfangen.", ui::yellow("[!]"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ = session_lock_rx.recv() => {
|
Some(()) = session_lock_rx.recv() => {
|
||||||
if !stealth {
|
if !stealth {
|
||||||
println!();
|
println!();
|
||||||
println!(" {} Windows-Sitzung gesperrt (Win + L) — Auto-Lock ausgelöst!", ui::yellow("[!]"));
|
println!(" {} Windows-Sitzung gesperrt (Win + L) — Auto-Lock ausgelöst!", ui::yellow("[!]"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ = idle_shutdown_rx.recv() => {
|
Some(()) = idle_shutdown_rx.recv() => {
|
||||||
|
if !stealth {
|
||||||
|
println!();
|
||||||
|
println!(" {} Inaktivitäts-Timeout erreicht — Auto-Lock ausgelöst!", ui::yellow("[!]"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
tokio::select! {
|
||||||
|
res = tokio::signal::ctrl_c() => {
|
||||||
|
let _ = res;
|
||||||
|
if !stealth {
|
||||||
|
println!();
|
||||||
|
println!(" {} Beendigungssignal (Ctrl+C) empfangen.", ui::yellow("[!]"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ = async {
|
||||||
|
match sigterm.as_mut() {
|
||||||
|
Some(s) => { s.recv().await; }
|
||||||
|
None => { std::future::pending::<()>().await; }
|
||||||
|
}
|
||||||
|
} => {
|
||||||
|
if !stealth {
|
||||||
|
println!();
|
||||||
|
println!(" {} Beendigungssignal (SIGTERM) empfangen.", ui::yellow("[!]"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ = async {
|
||||||
|
match sighup.as_mut() {
|
||||||
|
Some(s) => { s.recv().await; }
|
||||||
|
None => { std::future::pending::<()>().await; }
|
||||||
|
}
|
||||||
|
} => {
|
||||||
|
if !stealth {
|
||||||
|
println!();
|
||||||
|
println!(" {} Beendigungssignal (SIGHUP) empfangen.", ui::yellow("[!]"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(()) = idle_shutdown_rx.recv() => {
|
||||||
|
if !stealth {
|
||||||
|
println!();
|
||||||
|
println!(" {} Inaktivitäts-Timeout erreicht — Auto-Lock ausgelöst!", ui::yellow("[!]"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(any(windows, unix)))]
|
||||||
|
tokio::select! {
|
||||||
|
res = tokio::signal::ctrl_c() => {
|
||||||
|
let _ = res;
|
||||||
|
if !stealth {
|
||||||
|
println!();
|
||||||
|
println!(" {} Beendigungssignal (Ctrl+C) empfangen.", ui::yellow("[!]"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(()) = idle_shutdown_rx.recv() => {
|
||||||
if !stealth {
|
if !stealth {
|
||||||
println!();
|
println!();
|
||||||
println!(" {} Inaktivitäts-Timeout erreicht — Auto-Lock ausgelöst!", ui::yellow("[!]"));
|
println!(" {} Inaktivitäts-Timeout erreicht — Auto-Lock ausgelöst!", ui::yellow("[!]"));
|
||||||
@@ -406,8 +549,13 @@ pub async fn mount_container(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
let _ = db.checkpoint();
|
let _ = db.checkpoint();
|
||||||
|
#[cfg(windows)]
|
||||||
println!("Sanctum: Laufwerk {} getrennt und geschlossen.", drive_str);
|
println!("Sanctum: Laufwerk {} getrennt und geschlossen.", drive_str);
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
println!("Sanctum: Container geschlossen und WebDAV-Server beendet.");
|
||||||
} else {
|
} else {
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
print!(" {} Trenne Windows-Netzlaufwerk {} ... ", ui::dim("[-]"), drive_str);
|
print!(" {} Trenne Windows-Netzlaufwerk {} ... ", ui::dim("[-]"), drive_str);
|
||||||
let _ = std::io::Write::flush(&mut std::io::stdout());
|
let _ = std::io::Write::flush(&mut std::io::stdout());
|
||||||
|
|
||||||
@@ -417,6 +565,13 @@ pub async fn mount_container(
|
|||||||
} else {
|
} else {
|
||||||
println!("{}", ui::green("OK"));
|
println!("{}", ui::green("OK"));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
{
|
||||||
|
print!(" {} Beende WebDAV-Server ... ", ui::dim("[-]"));
|
||||||
|
let _ = std::io::Write::flush(&mut std::io::stdout());
|
||||||
|
println!("{}", ui::green("OK"));
|
||||||
|
}
|
||||||
|
|
||||||
// HTTP Server beenden
|
// HTTP Server beenden
|
||||||
let _ = shutdown_tx.send(true);
|
let _ = shutdown_tx.send(true);
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
//! Plattform-Abstraktionsschicht für Sanctum.
|
||||||
|
//!
|
||||||
|
//! Dieses Modul bündelt alle betriebssystemspezifischen Funktionen
|
||||||
|
//! (Speichersperren, Dateimanager-Aufrufe, Signal-Monitoring, Shell-Integration)
|
||||||
|
//! für Windows, Linux und macOS unter einer einheitlichen, speichersicheren Schnittstelle.
|
||||||
|
|
||||||
|
pub use crate::windows::*;
|
||||||
+850
@@ -0,0 +1,850 @@
|
|||||||
|
use std::collections::HashSet;
|
||||||
|
use std::fs::{self, File};
|
||||||
|
use std::io::{Read, Write};
|
||||||
|
use std::path::Path;
|
||||||
|
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
|
use anyhow::{bail, Context, Result};
|
||||||
|
|
||||||
|
use crate::crypto::{decrypt_chunk, encrypt_chunk, CHUNK_SIZE};
|
||||||
|
use crate::storage::{Database, NodeRecord};
|
||||||
|
use crate::ui;
|
||||||
|
use crate::vfs::is_leak_file;
|
||||||
|
|
||||||
|
/// Synchronisationsrichtung
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum SyncDirection {
|
||||||
|
/// Host-Dateisystem -> Sanctum Container (Upload / Backup)
|
||||||
|
Push,
|
||||||
|
/// Sanctum Container -> Host-Dateisystem (Download / Restore)
|
||||||
|
Pull,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Optionen für den Synchronisationslauf
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct SyncOptions {
|
||||||
|
pub direction: SyncDirection,
|
||||||
|
pub delete: bool,
|
||||||
|
pub dry_run: bool,
|
||||||
|
pub checksum: bool,
|
||||||
|
pub exclude_patterns: Vec<String>,
|
||||||
|
pub quiet: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for SyncOptions {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
direction: SyncDirection::Push,
|
||||||
|
delete: false,
|
||||||
|
dry_run: false,
|
||||||
|
checksum: false,
|
||||||
|
exclude_patterns: Vec::new(),
|
||||||
|
quiet: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Statistiken über den Synchronisationslauf
|
||||||
|
#[derive(Debug, Default, Clone)]
|
||||||
|
pub struct SyncStats {
|
||||||
|
pub files_scanned: usize,
|
||||||
|
pub files_transferred: usize,
|
||||||
|
pub files_skipped: usize,
|
||||||
|
pub files_deleted: usize,
|
||||||
|
pub bytes_transferred: u64,
|
||||||
|
pub elapsed: Duration,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ergebnis einer einzelnen Dateiübertragung
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum FileTransferResult {
|
||||||
|
Transferred { size: u64 },
|
||||||
|
Skipped { size: u64 },
|
||||||
|
DryRunTransferred { size: u64 },
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Prüft, ob ein Dateiname oder Pfad einem der Ausschlussmuster entspricht.
|
||||||
|
pub fn is_excluded(name: &str, rel_path: &str, patterns: &[String]) -> bool {
|
||||||
|
// 1. Anti-Leak Shield: Typische Explorer- und OS-Metadaten immer ausschließen
|
||||||
|
if is_leak_file(name) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let norm_rel = rel_path.replace('\\', "/");
|
||||||
|
let norm_name = name.to_ascii_lowercase();
|
||||||
|
|
||||||
|
for pat in patterns {
|
||||||
|
let p = pat.trim();
|
||||||
|
if p.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wildcard-Muster: *.ext
|
||||||
|
if p.starts_with("*.") {
|
||||||
|
let ext = &p[1..].to_ascii_lowercase();
|
||||||
|
if norm_name.ends_with(ext) || norm_rel.to_ascii_lowercase().ends_with(ext) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Wildcard-Muster: prefix*
|
||||||
|
else if p.ends_with('*') && !p[..p.len() - 1].contains('*') {
|
||||||
|
let prefix = p[..p.len() - 1].to_ascii_lowercase();
|
||||||
|
if norm_name.starts_with(&prefix) || norm_rel.to_ascii_lowercase().starts_with(&prefix) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Exakter Name oder Teilpfad
|
||||||
|
else {
|
||||||
|
let p_lower = p.to_ascii_lowercase();
|
||||||
|
if norm_name == p_lower {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if norm_rel.trim_start_matches('/').to_ascii_lowercase() == p_lower.trim_start_matches('/') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Liest bis zu `buf.len()` Bytes aus einer Datei (verlässliche Chunk-Lesung).
|
||||||
|
fn read_chunk_buffer(file: &mut File, buf: &mut [u8]) -> std::io::Result<usize> {
|
||||||
|
let mut total = 0;
|
||||||
|
while total < buf.len() {
|
||||||
|
match file.read(&mut buf[total..]) {
|
||||||
|
Ok(0) => break,
|
||||||
|
Ok(n) => total += n,
|
||||||
|
Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
|
||||||
|
Err(e) => return Err(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(total)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Setzt den Modifikationszeitstempel einer lokalen Datei via std::fs::FileTimes.
|
||||||
|
fn set_local_file_mtime(file: &File, mtime_secs: u64) {
|
||||||
|
let times = std::fs::FileTimes::new()
|
||||||
|
.set_modified(UNIX_EPOCH + Duration::from_secs(mtime_secs));
|
||||||
|
let _ = file.set_times(times);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stellt sicher, dass ein Verzeichnispfad im Tresor existiert und gibt den Ziel-Knoten zurück.
|
||||||
|
pub fn ensure_vault_dir_tree(
|
||||||
|
db: &Database,
|
||||||
|
vault_id: u32,
|
||||||
|
dek: &[u8; 32],
|
||||||
|
vault_dir_path: &str,
|
||||||
|
) -> Result<NodeRecord> {
|
||||||
|
let normalized = vault_dir_path.trim_matches('/');
|
||||||
|
let root_id = Database::get_root_node_id_for_vault(vault_id);
|
||||||
|
let root_node = db
|
||||||
|
.get_node_by_id_in_vault(root_id, vault_id, dek)?
|
||||||
|
.context("Wurzelknoten im Container nicht gefunden")?;
|
||||||
|
|
||||||
|
if normalized.is_empty() {
|
||||||
|
return Ok(root_node);
|
||||||
|
}
|
||||||
|
|
||||||
|
let segments: Vec<&str> = normalized.split('/').filter(|s| !s.is_empty()).collect();
|
||||||
|
let mut current_id = root_id;
|
||||||
|
let mut current_node = root_node;
|
||||||
|
|
||||||
|
for segment in segments {
|
||||||
|
let children = db.list_children_in_vault(current_id, vault_id, dek)?;
|
||||||
|
if let Some(existing) = children.into_iter().find(|c| c.name == segment) {
|
||||||
|
if !existing.is_dir {
|
||||||
|
bail!("Pfad-Konflikt: '{}' existiert im Container bereits als Datei", segment);
|
||||||
|
}
|
||||||
|
current_id = existing.id;
|
||||||
|
current_node = existing;
|
||||||
|
} else {
|
||||||
|
let new_dir = db.create_node_in_vault(vault_id, current_id, segment, true, dek)?;
|
||||||
|
current_id = new_dir.id;
|
||||||
|
current_node = new_dir;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(current_node)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Synchronisiert eine einzelne Host-Datei in den Tresor (Push).
|
||||||
|
pub fn sync_single_file_to_vault(
|
||||||
|
db: &Database,
|
||||||
|
vault_id: u32,
|
||||||
|
dek: &[u8; 32],
|
||||||
|
version: u32,
|
||||||
|
local_path: &Path,
|
||||||
|
parent_node_id: i64,
|
||||||
|
file_name: &str,
|
||||||
|
checksum: bool,
|
||||||
|
dry_run: bool,
|
||||||
|
) -> Result<FileTransferResult> {
|
||||||
|
let meta = fs::metadata(local_path)
|
||||||
|
.with_context(|| format!("Konnte Metadaten für '{}' nicht lesen", local_path.display()))?;
|
||||||
|
let local_size = meta.len();
|
||||||
|
let local_mtime = meta
|
||||||
|
.modified()
|
||||||
|
.unwrap_or(SystemTime::now())
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.as_secs();
|
||||||
|
|
||||||
|
let children = db.list_children_in_vault(parent_node_id, vault_id, dek)?;
|
||||||
|
let existing_node = children.into_iter().find(|c| c.name == file_name);
|
||||||
|
|
||||||
|
if let Some(ref node) = existing_node {
|
||||||
|
if node.is_dir {
|
||||||
|
bail!("Pfad-Konflikt: '{}' existiert im Tresor als Ordner", file_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fast Check: Wenn Größe und mtime identisch sind, überspringen
|
||||||
|
if !checksum && node.size == local_size && node.modified_at == local_mtime {
|
||||||
|
return Ok(FileTransferResult::Skipped { size: local_size });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if dry_run {
|
||||||
|
return Ok(FileTransferResult::DryRunTransferred { size: local_size });
|
||||||
|
}
|
||||||
|
|
||||||
|
let node_id = match existing_node {
|
||||||
|
Some(n) => n.id,
|
||||||
|
None => {
|
||||||
|
let new_node = db.create_node_in_vault(vault_id, parent_node_id, file_name, false, dek)?;
|
||||||
|
new_node.id
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 1-MB-Chunk Streaming direkt in SQLite
|
||||||
|
let mut file = File::open(local_path)
|
||||||
|
.with_context(|| format!("Konnte '{}' nicht zum Lesen öffnen", local_path.display()))?;
|
||||||
|
let mut buffer = vec![0u8; CHUNK_SIZE];
|
||||||
|
let mut chunk_idx = 0u32;
|
||||||
|
let mut bytes_written = 0u64;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let n = read_chunk_buffer(&mut file, &mut buffer)?;
|
||||||
|
if n == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let chunk_data = &buffer[..n];
|
||||||
|
let (ciphertext, nonce, tag) = encrypt_chunk(dek, node_id, chunk_idx, chunk_data, version)?;
|
||||||
|
bytes_written += n as u64;
|
||||||
|
|
||||||
|
db.write_chunk_and_update_size(
|
||||||
|
node_id,
|
||||||
|
chunk_idx,
|
||||||
|
&nonce,
|
||||||
|
&tag,
|
||||||
|
&ciphertext,
|
||||||
|
bytes_written,
|
||||||
|
local_mtime,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
chunk_idx += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bei Überschreiben einer ehemals größeren Datei überzählige alte Chunks entfernen
|
||||||
|
db.truncate_chunks_after(node_id, chunk_idx.saturating_sub(1))?;
|
||||||
|
db.update_node_size_and_time(node_id, local_size, local_mtime)?;
|
||||||
|
|
||||||
|
Ok(FileTransferResult::Transferred { size: local_size })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Synchronisiert eine Datei aus dem Tresor auf die lokale Festplatte (Pull).
|
||||||
|
pub fn sync_single_file_to_host(
|
||||||
|
db: &Database,
|
||||||
|
_vault_id: u32,
|
||||||
|
dek: &[u8; 32],
|
||||||
|
version: u32,
|
||||||
|
node: &NodeRecord,
|
||||||
|
local_path: &Path,
|
||||||
|
checksum: bool,
|
||||||
|
dry_run: bool,
|
||||||
|
) -> Result<FileTransferResult> {
|
||||||
|
if local_path.exists() {
|
||||||
|
if let Ok(meta) = fs::metadata(local_path) {
|
||||||
|
let local_size = meta.len();
|
||||||
|
let local_mtime = meta
|
||||||
|
.modified()
|
||||||
|
.unwrap_or(SystemTime::now())
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.as_secs();
|
||||||
|
|
||||||
|
if !checksum && local_size == node.size && local_mtime == node.modified_at {
|
||||||
|
return Ok(FileTransferResult::Skipped { size: node.size });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if dry_run {
|
||||||
|
return Ok(FileTransferResult::DryRunTransferred { size: node.size });
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(parent) = local_path.parent() {
|
||||||
|
fs::create_dir_all(parent)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut out_file = File::create(local_path)
|
||||||
|
.with_context(|| format!("Konnte Zieldatei '{}' nicht erstellen", local_path.display()))?;
|
||||||
|
|
||||||
|
let total_chunks = if node.size == 0 {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
((node.size - 1) / CHUNK_SIZE as u64 + 1) as u32
|
||||||
|
};
|
||||||
|
|
||||||
|
for idx in 0..total_chunks {
|
||||||
|
if let Some(record) = db.read_chunk(node.id, idx)? {
|
||||||
|
let plaintext = decrypt_chunk(dek, node.id, idx, &record.ciphertext, &record.nonce, &record.tag, version)?;
|
||||||
|
out_file.write_all(&plaintext)?;
|
||||||
|
} else {
|
||||||
|
bail!("Beschädigte Datei im Tresor: Chunk #{} für Knoten '{}' fehlt", idx, node.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
out_file.flush()?;
|
||||||
|
set_local_file_mtime(&out_file, node.modified_at);
|
||||||
|
|
||||||
|
Ok(FileTransferResult::Transferred { size: node.size })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Führt die vollständige Synchronisation zwischen Host und Container aus.
|
||||||
|
pub fn run_sync(
|
||||||
|
db: &Database,
|
||||||
|
vault_id: u32,
|
||||||
|
dek: &[u8; 32],
|
||||||
|
version: u32,
|
||||||
|
source_arg: &str,
|
||||||
|
target_arg: &str,
|
||||||
|
options: &SyncOptions,
|
||||||
|
) -> Result<SyncStats> {
|
||||||
|
let start_time = Instant::now();
|
||||||
|
let mut stats = SyncStats::default();
|
||||||
|
|
||||||
|
match options.direction {
|
||||||
|
SyncDirection::Push => {
|
||||||
|
sync_push(db, vault_id, dek, version, source_arg, target_arg, options, &mut stats)?;
|
||||||
|
}
|
||||||
|
SyncDirection::Pull => {
|
||||||
|
sync_pull(db, vault_id, dek, version, source_arg, target_arg, options, &mut stats)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stats.elapsed = start_time.elapsed();
|
||||||
|
Ok(stats)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Push: Host -> Tresor
|
||||||
|
fn sync_push(
|
||||||
|
db: &Database,
|
||||||
|
vault_id: u32,
|
||||||
|
dek: &[u8; 32],
|
||||||
|
version: u32,
|
||||||
|
source_str: &str,
|
||||||
|
target_str: &str,
|
||||||
|
options: &SyncOptions,
|
||||||
|
stats: &mut SyncStats,
|
||||||
|
) -> Result<()> {
|
||||||
|
let local_source = Path::new(source_str);
|
||||||
|
if !local_source.exists() {
|
||||||
|
bail!("Lokale Quelle '{}' existiert nicht.", source_str);
|
||||||
|
}
|
||||||
|
|
||||||
|
let target_vault_dir = if target_str.is_empty() { "/" } else { target_str };
|
||||||
|
|
||||||
|
if local_source.is_file() {
|
||||||
|
let file_name = local_source
|
||||||
|
.file_name()
|
||||||
|
.context("Ungültiger Dateiname")?
|
||||||
|
.to_string_lossy();
|
||||||
|
|
||||||
|
if is_excluded(&file_name, &file_name, &options.exclude_patterns) {
|
||||||
|
if !options.quiet {
|
||||||
|
println!(" {} Ausgeschlossen: {}", ui::dim("[-]"), file_name);
|
||||||
|
}
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let parent_node = ensure_vault_dir_tree(db, vault_id, dek, target_vault_dir)?;
|
||||||
|
stats.files_scanned += 1;
|
||||||
|
|
||||||
|
match sync_single_file_to_vault(
|
||||||
|
db,
|
||||||
|
vault_id,
|
||||||
|
dek,
|
||||||
|
version,
|
||||||
|
local_source,
|
||||||
|
parent_node.id,
|
||||||
|
&file_name,
|
||||||
|
options.checksum,
|
||||||
|
options.dry_run,
|
||||||
|
)? {
|
||||||
|
FileTransferResult::Transferred { size } => {
|
||||||
|
stats.files_transferred += 1;
|
||||||
|
stats.bytes_transferred += size;
|
||||||
|
if !options.quiet {
|
||||||
|
println!(" {} Übertragen: {} ({})", ui::green("[+]"), file_name, ui::format_bytes(size));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FileTransferResult::DryRunTransferred { size } => {
|
||||||
|
stats.files_transferred += 1;
|
||||||
|
stats.bytes_transferred += size;
|
||||||
|
if !options.quiet {
|
||||||
|
println!(" {} [DRY-RUN] Würde übertragen: {} ({})", ui::yellow("[~]"), file_name, ui::format_bytes(size));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FileTransferResult::Skipped { .. } => {
|
||||||
|
stats.files_skipped += 1;
|
||||||
|
if !options.quiet {
|
||||||
|
println!(" {} Aktuell (übersprungen): {}", ui::dim("[=]"), file_name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if local_source.is_dir() {
|
||||||
|
let parent_node = ensure_vault_dir_tree(db, vault_id, dek, target_vault_dir)?;
|
||||||
|
let mut local_relative_paths = HashSet::new();
|
||||||
|
|
||||||
|
// Rekursiv alle lokalen Dateien und Ordner erfassen
|
||||||
|
collect_and_push_dir(
|
||||||
|
db,
|
||||||
|
vault_id,
|
||||||
|
dek,
|
||||||
|
version,
|
||||||
|
local_source,
|
||||||
|
local_source,
|
||||||
|
parent_node.id,
|
||||||
|
options,
|
||||||
|
stats,
|
||||||
|
&mut local_relative_paths,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// Spiegelung mit --delete: Im Tresor verwaiste Dateien entfernen
|
||||||
|
if options.delete {
|
||||||
|
delete_orphans_in_vault(
|
||||||
|
db,
|
||||||
|
vault_id,
|
||||||
|
dek,
|
||||||
|
parent_node.id,
|
||||||
|
"",
|
||||||
|
&local_relative_paths,
|
||||||
|
options.dry_run,
|
||||||
|
options.quiet,
|
||||||
|
stats,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collect_and_push_dir(
|
||||||
|
db: &Database,
|
||||||
|
vault_id: u32,
|
||||||
|
dek: &[u8; 32],
|
||||||
|
version: u32,
|
||||||
|
base_dir: &Path,
|
||||||
|
current_dir: &Path,
|
||||||
|
current_vault_parent_id: i64,
|
||||||
|
options: &SyncOptions,
|
||||||
|
stats: &mut SyncStats,
|
||||||
|
local_relative_paths: &mut HashSet<String>,
|
||||||
|
) -> Result<()> {
|
||||||
|
for entry in fs::read_dir(current_dir)? {
|
||||||
|
let entry = entry?;
|
||||||
|
let path = entry.path();
|
||||||
|
let file_name = entry.file_name().to_string_lossy().to_string();
|
||||||
|
|
||||||
|
let rel_path = path
|
||||||
|
.strip_prefix(base_dir)
|
||||||
|
.unwrap_or(&path)
|
||||||
|
.to_string_lossy()
|
||||||
|
.replace('\\', "/");
|
||||||
|
|
||||||
|
if is_excluded(&file_name, &rel_path, &options.exclude_patterns) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
local_relative_paths.insert(rel_path.clone());
|
||||||
|
|
||||||
|
if path.is_dir() {
|
||||||
|
// Ordner im Tresor anlegen falls nötig
|
||||||
|
let children = db.list_children_in_vault(current_vault_parent_id, vault_id, dek)?;
|
||||||
|
let sub_dir_node = match children.into_iter().find(|c| c.name == file_name && c.is_dir) {
|
||||||
|
Some(n) => n,
|
||||||
|
None => {
|
||||||
|
if !options.dry_run {
|
||||||
|
db.create_node_in_vault(vault_id, current_vault_parent_id, &file_name, true, dek)?
|
||||||
|
} else {
|
||||||
|
// Dummy für dry-run
|
||||||
|
NodeRecord {
|
||||||
|
id: -1,
|
||||||
|
parent_id: Some(current_vault_parent_id),
|
||||||
|
name: file_name.clone(),
|
||||||
|
is_dir: true,
|
||||||
|
size: 0,
|
||||||
|
created_at: 0,
|
||||||
|
modified_at: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
collect_and_push_dir(
|
||||||
|
db,
|
||||||
|
vault_id,
|
||||||
|
dek,
|
||||||
|
version,
|
||||||
|
base_dir,
|
||||||
|
&path,
|
||||||
|
sub_dir_node.id,
|
||||||
|
options,
|
||||||
|
stats,
|
||||||
|
local_relative_paths,
|
||||||
|
)?;
|
||||||
|
} else if path.is_file() {
|
||||||
|
stats.files_scanned += 1;
|
||||||
|
match sync_single_file_to_vault(
|
||||||
|
db,
|
||||||
|
vault_id,
|
||||||
|
dek,
|
||||||
|
version,
|
||||||
|
&path,
|
||||||
|
current_vault_parent_id,
|
||||||
|
&file_name,
|
||||||
|
options.checksum,
|
||||||
|
options.dry_run,
|
||||||
|
)? {
|
||||||
|
FileTransferResult::Transferred { size } => {
|
||||||
|
stats.files_transferred += 1;
|
||||||
|
stats.bytes_transferred += size;
|
||||||
|
if !options.quiet {
|
||||||
|
println!(" {} Übertragen: {} ({})", ui::green("[+]"), rel_path, ui::format_bytes(size));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FileTransferResult::DryRunTransferred { size } => {
|
||||||
|
stats.files_transferred += 1;
|
||||||
|
stats.bytes_transferred += size;
|
||||||
|
if !options.quiet {
|
||||||
|
println!(" {} [DRY-RUN] Würde übertragen: {} ({})", ui::yellow("[~]"), rel_path, ui::format_bytes(size));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FileTransferResult::Skipped { .. } => {
|
||||||
|
stats.files_skipped += 1;
|
||||||
|
if !options.quiet {
|
||||||
|
println!(" {} Aktuell (übersprungen): {}", ui::dim("[=]"), rel_path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn delete_orphans_in_vault(
|
||||||
|
db: &Database,
|
||||||
|
vault_id: u32,
|
||||||
|
dek: &[u8; 32],
|
||||||
|
current_vault_id: i64,
|
||||||
|
prefix_rel: &str,
|
||||||
|
local_relative_paths: &HashSet<String>,
|
||||||
|
dry_run: bool,
|
||||||
|
quiet: bool,
|
||||||
|
stats: &mut SyncStats,
|
||||||
|
) -> Result<()> {
|
||||||
|
let children = db.list_children_in_vault(current_vault_id, vault_id, dek)?;
|
||||||
|
|
||||||
|
for child in children {
|
||||||
|
let child_rel = if prefix_rel.is_empty() {
|
||||||
|
child.name.clone()
|
||||||
|
} else {
|
||||||
|
format!("{}/{}", prefix_rel, child.name)
|
||||||
|
};
|
||||||
|
|
||||||
|
if !local_relative_paths.contains(&child_rel) {
|
||||||
|
stats.files_deleted += 1;
|
||||||
|
if dry_run {
|
||||||
|
if !quiet {
|
||||||
|
println!(" {} [DRY-RUN] Würde aus Tresor löschen: {}", ui::red("[-]"), child_rel);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
db.delete_node(child.id)?;
|
||||||
|
if !quiet {
|
||||||
|
println!(" {} Gelöscht aus Tresor: {}", ui::red("[-]"), child_rel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if child.is_dir {
|
||||||
|
delete_orphans_in_vault(
|
||||||
|
db,
|
||||||
|
vault_id,
|
||||||
|
dek,
|
||||||
|
child.id,
|
||||||
|
&child_rel,
|
||||||
|
local_relative_paths,
|
||||||
|
dry_run,
|
||||||
|
quiet,
|
||||||
|
stats,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pull: Tresor -> Host
|
||||||
|
fn sync_pull(
|
||||||
|
db: &Database,
|
||||||
|
vault_id: u32,
|
||||||
|
dek: &[u8; 32],
|
||||||
|
version: u32,
|
||||||
|
source_vault_str: &str,
|
||||||
|
target_host_str: &str,
|
||||||
|
options: &SyncOptions,
|
||||||
|
stats: &mut SyncStats,
|
||||||
|
) -> Result<()> {
|
||||||
|
let vault_source_node = db
|
||||||
|
.resolve_path_in_vault(source_vault_str, vault_id, dek)?
|
||||||
|
.with_context(|| format!("Quelle '{}' im Tresor nicht gefunden", source_vault_str))?;
|
||||||
|
|
||||||
|
let local_target_dir = Path::new(target_host_str);
|
||||||
|
|
||||||
|
if !vault_source_node.is_dir {
|
||||||
|
// Einzelne Datei aus Tresor laden
|
||||||
|
let local_file_path = if local_target_dir.is_dir() {
|
||||||
|
local_target_dir.join(&vault_source_node.name)
|
||||||
|
} else {
|
||||||
|
local_target_dir.to_path_buf()
|
||||||
|
};
|
||||||
|
|
||||||
|
stats.files_scanned += 1;
|
||||||
|
match sync_single_file_to_host(
|
||||||
|
db,
|
||||||
|
vault_id,
|
||||||
|
dek,
|
||||||
|
version,
|
||||||
|
&vault_source_node,
|
||||||
|
&local_file_path,
|
||||||
|
options.checksum,
|
||||||
|
options.dry_run,
|
||||||
|
)? {
|
||||||
|
FileTransferResult::Transferred { size } => {
|
||||||
|
stats.files_transferred += 1;
|
||||||
|
stats.bytes_transferred += size;
|
||||||
|
if !options.quiet {
|
||||||
|
println!(" {} Wiederhergestellt: {} ({})", ui::green("[+]"), local_file_path.display(), ui::format_bytes(size));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FileTransferResult::DryRunTransferred { size } => {
|
||||||
|
stats.files_transferred += 1;
|
||||||
|
stats.bytes_transferred += size;
|
||||||
|
if !options.quiet {
|
||||||
|
println!(" {} [DRY-RUN] Würde wiederherstellen: {} ({})", ui::yellow("[~]"), local_file_path.display(), ui::format_bytes(size));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FileTransferResult::Skipped { .. } => {
|
||||||
|
stats.files_skipped += 1;
|
||||||
|
if !options.quiet {
|
||||||
|
println!(" {} Aktuell (übersprungen): {}", ui::dim("[=]"), local_file_path.display());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Ganzes Verzeichnis aus Tresor laden
|
||||||
|
if !options.dry_run {
|
||||||
|
fs::create_dir_all(local_target_dir)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut vault_relative_paths = HashSet::new();
|
||||||
|
|
||||||
|
collect_and_pull_dir(
|
||||||
|
db,
|
||||||
|
vault_id,
|
||||||
|
dek,
|
||||||
|
version,
|
||||||
|
vault_source_node.id,
|
||||||
|
"",
|
||||||
|
local_target_dir,
|
||||||
|
options,
|
||||||
|
stats,
|
||||||
|
&mut vault_relative_paths,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// Spiegelung mit --delete: Lokale verwaiste Dateien entfernen
|
||||||
|
if options.delete && local_target_dir.exists() {
|
||||||
|
delete_orphans_on_host(
|
||||||
|
local_target_dir,
|
||||||
|
local_target_dir,
|
||||||
|
&vault_relative_paths,
|
||||||
|
options.dry_run,
|
||||||
|
options.quiet,
|
||||||
|
stats,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collect_and_pull_dir(
|
||||||
|
db: &Database,
|
||||||
|
vault_id: u32,
|
||||||
|
dek: &[u8; 32],
|
||||||
|
version: u32,
|
||||||
|
vault_node_id: i64,
|
||||||
|
rel_prefix: &str,
|
||||||
|
local_target_base: &Path,
|
||||||
|
options: &SyncOptions,
|
||||||
|
stats: &mut SyncStats,
|
||||||
|
vault_relative_paths: &mut HashSet<String>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let children = db.list_children_in_vault(vault_node_id, vault_id, dek)?;
|
||||||
|
|
||||||
|
for child in children {
|
||||||
|
let child_rel = if rel_prefix.is_empty() {
|
||||||
|
child.name.clone()
|
||||||
|
} else {
|
||||||
|
format!("{}/{}", rel_prefix, child.name)
|
||||||
|
};
|
||||||
|
|
||||||
|
if is_excluded(&child.name, &child_rel, &options.exclude_patterns) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
vault_relative_paths.insert(child_rel.clone());
|
||||||
|
let local_child_path = local_target_base.join(&child_rel.replace('/', "\\"));
|
||||||
|
|
||||||
|
if child.is_dir {
|
||||||
|
if !options.dry_run {
|
||||||
|
fs::create_dir_all(&local_child_path)?;
|
||||||
|
}
|
||||||
|
collect_and_pull_dir(
|
||||||
|
db,
|
||||||
|
vault_id,
|
||||||
|
dek,
|
||||||
|
version,
|
||||||
|
child.id,
|
||||||
|
&child_rel,
|
||||||
|
local_target_base,
|
||||||
|
options,
|
||||||
|
stats,
|
||||||
|
vault_relative_paths,
|
||||||
|
)?;
|
||||||
|
} else {
|
||||||
|
stats.files_scanned += 1;
|
||||||
|
match sync_single_file_to_host(
|
||||||
|
db,
|
||||||
|
vault_id,
|
||||||
|
dek,
|
||||||
|
version,
|
||||||
|
&child,
|
||||||
|
&local_child_path,
|
||||||
|
options.checksum,
|
||||||
|
options.dry_run,
|
||||||
|
)? {
|
||||||
|
FileTransferResult::Transferred { size } => {
|
||||||
|
stats.files_transferred += 1;
|
||||||
|
stats.bytes_transferred += size;
|
||||||
|
if !options.quiet {
|
||||||
|
println!(" {} Wiederhergestellt: {} ({})", ui::green("[+]"), child_rel, ui::format_bytes(size));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FileTransferResult::DryRunTransferred { size } => {
|
||||||
|
stats.files_transferred += 1;
|
||||||
|
stats.bytes_transferred += size;
|
||||||
|
if !options.quiet {
|
||||||
|
println!(" {} [DRY-RUN] Würde wiederherstellen: {} ({})", ui::yellow("[~]"), child_rel, ui::format_bytes(size));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FileTransferResult::Skipped { .. } => {
|
||||||
|
stats.files_skipped += 1;
|
||||||
|
if !options.quiet {
|
||||||
|
println!(" {} Aktuell (übersprungen): {}", ui::dim("[=]"), child_rel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn delete_orphans_on_host(
|
||||||
|
base_dir: &Path,
|
||||||
|
current_dir: &Path,
|
||||||
|
vault_relative_paths: &HashSet<String>,
|
||||||
|
dry_run: bool,
|
||||||
|
quiet: bool,
|
||||||
|
stats: &mut SyncStats,
|
||||||
|
) -> Result<()> {
|
||||||
|
if !current_dir.exists() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
for entry in fs::read_dir(current_dir)? {
|
||||||
|
let entry = entry?;
|
||||||
|
let path = entry.path();
|
||||||
|
let rel_path = path
|
||||||
|
.strip_prefix(base_dir)
|
||||||
|
.unwrap_or(&path)
|
||||||
|
.to_string_lossy()
|
||||||
|
.replace('\\', "/");
|
||||||
|
|
||||||
|
if !vault_relative_paths.contains(&rel_path) {
|
||||||
|
stats.files_deleted += 1;
|
||||||
|
if path.is_dir() {
|
||||||
|
if dry_run {
|
||||||
|
if !quiet {
|
||||||
|
println!(" {} [DRY-RUN] Würde lokalen Ordner löschen: {}", ui::red("[-]"), rel_path);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fs::remove_dir_all(&path)?;
|
||||||
|
if !quiet {
|
||||||
|
println!(" {} Lokalen Ordner gelöscht: {}", ui::red("[-]"), rel_path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if dry_run {
|
||||||
|
if !quiet {
|
||||||
|
println!(" {} [DRY-RUN] Würde lokale Datei löschen: {}", ui::red("[-]"), rel_path);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fs::remove_file(&path)?;
|
||||||
|
if !quiet {
|
||||||
|
println!(" {} Lokale Datei gelöscht: {}", ui::red("[-]"), rel_path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if path.is_dir() {
|
||||||
|
delete_orphans_on_host(base_dir, &path, vault_relative_paths, dry_run, quiet, stats)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_is_excluded_patterns() {
|
||||||
|
let patterns = vec![
|
||||||
|
"*.tmp".to_string(),
|
||||||
|
"*.crdownload".to_string(),
|
||||||
|
"Thumbs.db".to_string(),
|
||||||
|
"backup_*".to_string(),
|
||||||
|
"/logs".to_string(),
|
||||||
|
];
|
||||||
|
|
||||||
|
assert!(is_excluded("file.tmp", "sub/file.tmp", &patterns));
|
||||||
|
assert!(is_excluded("download.crdownload", "download.crdownload", &patterns));
|
||||||
|
assert!(is_excluded("Thumbs.db", "Thumbs.db", &patterns));
|
||||||
|
assert!(is_excluded("backup_2026.tar", "backup_2026.tar", &patterns));
|
||||||
|
assert!(!is_excluded("important.doc", "sub/important.doc", &patterns));
|
||||||
|
assert!(!is_excluded("video.mp4", "video.mp4", &patterns));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -100,6 +100,27 @@ pub fn magenta(s: &str) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Formatiert eine Byte-Anzahl lesbar in B, KB, MB, GB oder TB.
|
||||||
|
pub fn format_bytes(bytes: u64) -> String {
|
||||||
|
const KB: f64 = 1024.0;
|
||||||
|
const MB: f64 = KB * 1024.0;
|
||||||
|
const GB: f64 = MB * 1024.0;
|
||||||
|
const TB: f64 = GB * 1024.0;
|
||||||
|
|
||||||
|
let b = bytes as f64;
|
||||||
|
if b >= TB {
|
||||||
|
format!("{:.2} TB", b / TB)
|
||||||
|
} else if b >= GB {
|
||||||
|
format!("{:.2} GB", b / GB)
|
||||||
|
} else if b >= MB {
|
||||||
|
format!("{:.2} MB", b / MB)
|
||||||
|
} else if b >= KB {
|
||||||
|
format!("{:.2} KB", b / KB)
|
||||||
|
} else {
|
||||||
|
format!("{} B", bytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Gibt einen formatierten Fortschrittsschritt aus: ` [1/4] 📦 Schrittbeschreibung...`
|
/// Gibt einen formatierten Fortschrittsschritt aus: ` [1/4] 📦 Schrittbeschreibung...`
|
||||||
pub fn step(num: u8, total: u8, icon: &str, msg: &str) {
|
pub fn step(num: u8, total: u8, icon: &str, msg: &str) {
|
||||||
let tag = cyan(&format!("[{}/{}]", num, total));
|
let tag = cyan(&format!("[{}/{}]", num, total));
|
||||||
|
|||||||
+133
-17
@@ -1,5 +1,6 @@
|
|||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
|
#[allow(unused_imports)]
|
||||||
use anyhow::{bail, Context, Result};
|
use anyhow::{bail, Context, Result};
|
||||||
|
|
||||||
/// Ermittelt den nächsten verfügbaren Windows-Laufwerksbuchstaben (von 'Z' rückwärts bis 'D').
|
/// Ermittelt den nächsten verfügbaren Windows-Laufwerksbuchstaben (von 'Z' rückwärts bis 'D').
|
||||||
@@ -32,8 +33,11 @@ pub fn find_next_available_drive() -> Result<char> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Öffnet das eingebundene Netzlaufwerk direkt im Windows Explorer.
|
/// Öffnet das eingebundene Netzlaufwerk oder Verzeichnis direkt im systemeigenen Dateimanager
|
||||||
|
/// (Windows: Explorer, macOS: open, Linux: xdg-open).
|
||||||
pub fn open_in_explorer(drive_char: char) -> Result<()> {
|
pub fn open_in_explorer(drive_char: char) -> Result<()> {
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
let drive_path = format!("{}:\\", drive_char.to_ascii_uppercase());
|
let drive_path = format!("{}:\\", drive_char.to_ascii_uppercase());
|
||||||
Command::new("explorer.exe")
|
Command::new("explorer.exe")
|
||||||
.arg(&drive_path)
|
.arg(&drive_path)
|
||||||
@@ -41,6 +45,57 @@ pub fn open_in_explorer(drive_char: char) -> Result<()> {
|
|||||||
.with_context(|| format!("Konnte Windows Explorer für '{}' nicht öffnen", drive_path))?;
|
.with_context(|| format!("Konnte Windows Explorer für '{}' nicht öffnen", drive_path))?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
let _ = drive_char;
|
||||||
|
let _ = Command::new("open").arg(".").spawn();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
#[cfg(all(unix, not(target_os = "macos")))]
|
||||||
|
{
|
||||||
|
let _ = drive_char;
|
||||||
|
let _ = Command::new("xdg-open").arg(".").spawn();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
#[cfg(not(any(windows, unix)))]
|
||||||
|
{
|
||||||
|
let _ = drive_char;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Öffnet einen beliebigen Pfad im nativen Dateimanager der Plattform.
|
||||||
|
pub fn open_in_file_manager(path: &std::path::Path) -> Result<()> {
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
Command::new("explorer.exe")
|
||||||
|
.arg(path)
|
||||||
|
.spawn()
|
||||||
|
.with_context(|| format!("Konnte Windows Explorer für '{}' nicht öffnen", path.display()))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
Command::new("open")
|
||||||
|
.arg(path)
|
||||||
|
.spawn()
|
||||||
|
.with_context(|| format!("Konnte macOS Finder für '{}' nicht öffnen", path.display()))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
#[cfg(all(unix, not(target_os = "macos")))]
|
||||||
|
{
|
||||||
|
Command::new("xdg-open")
|
||||||
|
.arg(path)
|
||||||
|
.spawn()
|
||||||
|
.with_context(|| format!("Konnte Dateimanager via xdg-open für '{}' nicht öffnen", path.display()))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
#[cfg(not(any(windows, unix)))]
|
||||||
|
{
|
||||||
|
let _ = path;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Benachrichtigt die Windows-Shell (Explorer) über geänderte Dateiverknüpfungen (SHCNE_ASSOCCHANGED).
|
/// Benachrichtigt die Windows-Shell (Explorer) über geänderte Dateiverknüpfungen (SHCNE_ASSOCCHANGED).
|
||||||
pub fn notify_shell_associations_changed() {
|
pub fn notify_shell_associations_changed() {
|
||||||
@@ -67,10 +122,19 @@ pub fn notify_shell_associations_changed() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#[cfg(all(unix, not(target_os = "macos")))]
|
||||||
|
{
|
||||||
|
let _ = Command::new("update-desktop-database").spawn();
|
||||||
|
}
|
||||||
|
#[cfg(not(any(windows, all(unix, not(target_os = "macos")))))]
|
||||||
|
{
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Registriert `.sanctum`-Containerdateien im Windows Explorer für den aktuellen Benutzer (HKCU, 100% Userland, keine Adminrechte).
|
/// Registriert `.sanctum`-Containerdateien im Windows Explorer für den aktuellen Benutzer bzw. unter Linux via Freedesktop.
|
||||||
pub fn register_explorer_integration() -> Result<()> {
|
pub fn register_explorer_integration() -> Result<()> {
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
let current_exe = std::env::current_exe()
|
let current_exe = std::env::current_exe()
|
||||||
.context("Konnte den Pfad zur aktuellen sanctum.exe nicht ermitteln")?;
|
.context("Konnte den Pfad zur aktuellen sanctum.exe nicht ermitteln")?;
|
||||||
let exe_str = current_exe.display().to_string();
|
let exe_str = current_exe.display().to_string();
|
||||||
@@ -98,14 +162,14 @@ pub fn register_explorer_integration() -> Result<()> {
|
|||||||
(
|
(
|
||||||
r"HKCU\Software\Classes\Sanctum.Container\shell\open",
|
r"HKCU\Software\Classes\Sanctum.Container\shell\open",
|
||||||
"",
|
"",
|
||||||
"Als Laufwerk einbinden",
|
"In Sanctum öffnen",
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
r"HKCU\Software\Classes\Sanctum.Container\shell\open\command",
|
r"HKCU\Software\Classes\Sanctum.Container\shell\open\command",
|
||||||
"",
|
"",
|
||||||
&format!("\"{exe_str}\" mount --path \"%1\""),
|
&format!("\"{exe_str}\" mount \"%1\""),
|
||||||
),
|
),
|
||||||
// 5. Kontextmenü-Aktion: Integritätsprüfung
|
// 5. Kontextmenü-Aktion: Verify / FSCK
|
||||||
(
|
(
|
||||||
r"HKCU\Software\Classes\Sanctum.Container\shell\verify",
|
r"HKCU\Software\Classes\Sanctum.Container\shell\verify",
|
||||||
"",
|
"",
|
||||||
@@ -114,18 +178,18 @@ pub fn register_explorer_integration() -> Result<()> {
|
|||||||
(
|
(
|
||||||
r"HKCU\Software\Classes\Sanctum.Container\shell\verify\command",
|
r"HKCU\Software\Classes\Sanctum.Container\shell\verify\command",
|
||||||
"",
|
"",
|
||||||
&format!("\"{exe_str}\" verify --path \"%1\""),
|
&format!("cmd /k \"\"{exe_str}\" verify \"%1\"\""),
|
||||||
),
|
),
|
||||||
// 6. Kontextmenü-Aktion: Header sichern
|
// 6. Kontextmenü-Aktion: Header-Backup
|
||||||
(
|
(
|
||||||
r"HKCU\Software\Classes\Sanctum.Container\shell\backup_header",
|
r"HKCU\Software\Classes\Sanctum.Container\shell\backup",
|
||||||
"",
|
"",
|
||||||
"Header sichern",
|
"Header sichern (Disaster Recovery)",
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
r"HKCU\Software\Classes\Sanctum.Container\shell\backup_header\command",
|
r"HKCU\Software\Classes\Sanctum.Container\shell\backup\command",
|
||||||
"",
|
"",
|
||||||
&format!("\"{exe_str}\" backup-header --path \"%1\""),
|
&format!("cmd /k \"\"{exe_str}\" backup-header \"%1\"\""),
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -149,9 +213,31 @@ pub fn register_explorer_integration() -> Result<()> {
|
|||||||
notify_shell_associations_changed();
|
notify_shell_associations_changed();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
{
|
||||||
|
if let Some(home) = std::env::var_os("HOME") {
|
||||||
|
let home_path = std::path::PathBuf::from(home);
|
||||||
|
let apps_dir = home_path.join(".local/share/applications");
|
||||||
|
let mime_dir = home_path.join(".local/share/mime/packages");
|
||||||
|
let _ = std::fs::create_dir_all(&apps_dir);
|
||||||
|
let _ = std::fs::create_dir_all(&mime_dir);
|
||||||
|
|
||||||
/// Entfernt die Windows-Explorer-Verknüpfungen aus der Benutzer-Registry (HKCU).
|
let desktop_content = "[Desktop Entry]\nType=Application\nName=Sanctum\nComment=Verschlüsselter Ein-Datei-Container\nExec=sanctum mount %f\nIcon=security-high\nTerminal=true\nMimeType=application/x-sanctum;\nCategories=Utility;Security;\n";
|
||||||
|
let _ = std::fs::write(apps_dir.join("sanctum.desktop"), desktop_content);
|
||||||
|
|
||||||
|
let mime_content = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<mime-info xmlns=\"http://www.freedesktop.org/standards/shared-mime-info\">\n <mime-type type=\"application/x-sanctum\">\n <comment>Sanctum Verschlüsselter Container</comment>\n <glob pattern=\"*.sanctum\"/>\n </mime-type>\n</mime-info>\n";
|
||||||
|
let _ = std::fs::write(mime_dir.join("application-x-sanctum.xml"), mime_content);
|
||||||
|
|
||||||
|
notify_shell_associations_changed();
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Entfernt die Windows-Explorer-Verknüpfungen aus der Benutzer-Registry (HKCU) bzw. unter Linux aus Freedesktop.
|
||||||
pub fn unregister_explorer_integration() -> Result<()> {
|
pub fn unregister_explorer_integration() -> Result<()> {
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
let keys_to_delete = [
|
let keys_to_delete = [
|
||||||
r"HKCU\Software\Classes\.sanctum",
|
r"HKCU\Software\Classes\.sanctum",
|
||||||
r"HKCU\Software\Classes\Sanctum.Container",
|
r"HKCU\Software\Classes\Sanctum.Container",
|
||||||
@@ -166,6 +252,17 @@ pub fn unregister_explorer_integration() -> Result<()> {
|
|||||||
notify_shell_associations_changed();
|
notify_shell_associations_changed();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
{
|
||||||
|
if let Some(home) = std::env::var_os("HOME") {
|
||||||
|
let home_path = std::path::PathBuf::from(home);
|
||||||
|
let _ = std::fs::remove_file(home_path.join(".local/share/applications/sanctum.desktop"));
|
||||||
|
let _ = std::fs::remove_file(home_path.join(".local/share/mime/packages/application-x-sanctum.xml"));
|
||||||
|
notify_shell_associations_changed();
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Lädt das Windows-Sicherheitsschild-Icon (IDI_SHIELD) oder Anwendungs-Icon für den System-Tray.
|
/// Lädt das Windows-Sicherheitsschild-Icon (IDI_SHIELD) oder Anwendungs-Icon für den System-Tray.
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
@@ -198,10 +295,9 @@ pub fn get_default_system_icon() -> Option<tray_item::IconSource> {
|
|||||||
|
|
||||||
/// Guard zur Verwaltung des Hintergrundthreads für die Windows-Sitzungssperre.
|
/// Guard zur Verwaltung des Hintergrundthreads für die Windows-Sitzungssperre.
|
||||||
/// Beim Droppen wird das Win32-Nachrichtenfenster geschlossen und der Thread sauber beendet.
|
/// Beim Droppen wird das Win32-Nachrichtenfenster geschlossen und der Thread sauber beendet.
|
||||||
|
#[cfg(windows)]
|
||||||
pub struct SessionLockGuard {
|
pub struct SessionLockGuard {
|
||||||
#[cfg(windows)]
|
|
||||||
hwnd: isize,
|
hwnd: isize,
|
||||||
#[cfg(windows)]
|
|
||||||
join_handle: Option<std::thread::JoinHandle<()>>,
|
join_handle: Option<std::thread::JoinHandle<()>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -527,7 +623,7 @@ pub fn start_console_ctrl_monitor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Verriegelt einen Speicherbereich im physischen RAM (verhindert Paging in pagefile.sys / swapfile.sys).
|
/// Verriegelt einen Speicherbereich im physischen RAM (verhindert Paging in pagefile.sys / swapfile.sys unter Windows bzw. Swap unter Linux/macOS).
|
||||||
pub fn lock_memory(ptr: *const u8, len: usize) -> bool {
|
pub fn lock_memory(ptr: *const u8, len: usize) -> bool {
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
{
|
{
|
||||||
@@ -539,7 +635,17 @@ pub fn lock_memory(ptr: *const u8, len: usize) -> bool {
|
|||||||
}
|
}
|
||||||
unsafe { VirtualLock(ptr as *const std::ffi::c_void, len) != 0 }
|
unsafe { VirtualLock(ptr as *const std::ffi::c_void, len) != 0 }
|
||||||
}
|
}
|
||||||
#[cfg(not(windows))]
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
extern "C" {
|
||||||
|
fn mlock(addr: *const std::ffi::c_void, len: usize) -> std::ffi::c_int;
|
||||||
|
}
|
||||||
|
if ptr.is_null() || len == 0 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
unsafe { mlock(ptr as *const std::ffi::c_void, len) == 0 }
|
||||||
|
}
|
||||||
|
#[cfg(not(any(windows, unix)))]
|
||||||
{
|
{
|
||||||
let _ = (ptr, len);
|
let _ = (ptr, len);
|
||||||
false
|
false
|
||||||
@@ -558,7 +664,17 @@ pub fn unlock_memory(ptr: *const u8, len: usize) -> bool {
|
|||||||
}
|
}
|
||||||
unsafe { VirtualUnlock(ptr as *const std::ffi::c_void, len) != 0 }
|
unsafe { VirtualUnlock(ptr as *const std::ffi::c_void, len) != 0 }
|
||||||
}
|
}
|
||||||
#[cfg(not(windows))]
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
extern "C" {
|
||||||
|
fn munlock(addr: *const std::ffi::c_void, len: usize) -> std::ffi::c_int;
|
||||||
|
}
|
||||||
|
if ptr.is_null() || len == 0 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
unsafe { munlock(ptr as *const std::ffi::c_void, len) == 0 }
|
||||||
|
}
|
||||||
|
#[cfg(not(any(windows, unix)))]
|
||||||
{
|
{
|
||||||
let _ = (ptr, len);
|
let _ = (ptr, len);
|
||||||
false
|
false
|
||||||
|
|||||||
@@ -0,0 +1,152 @@
|
|||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use bytes::Bytes;
|
||||||
|
use dav_server::{
|
||||||
|
davpath::DavPath,
|
||||||
|
fs::{DavFileSystem, OpenOptions},
|
||||||
|
};
|
||||||
|
use rand::RngCore;
|
||||||
|
use sanctum::{
|
||||||
|
crypto::{derive_kek, generate_dek, generate_salt, wrap_dek, KdfParams, FORMAT_VERSION},
|
||||||
|
storage::Database,
|
||||||
|
verify::verify_container,
|
||||||
|
vfs::SanctumFs,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Live-Crash- und Stresstest: Simuliert harten Verbindungsabbruch und Power-Cut
|
||||||
|
/// während intensiver paralleler Schreibvorgänge im VFS.
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_live_crash_and_recovery_stress() {
|
||||||
|
let temp_dir = std::env::temp_dir();
|
||||||
|
let container_path: PathBuf = temp_dir.join(format!("sanctum_live_stress_{}.sanctum", std::process::id()));
|
||||||
|
if container_path.exists() {
|
||||||
|
let _ = std::fs::remove_file(&container_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
let password = "LiveStressPassword2026!";
|
||||||
|
let salt = generate_salt();
|
||||||
|
let kdf_params = KdfParams {
|
||||||
|
memory_cost: 1024,
|
||||||
|
time_cost: 1,
|
||||||
|
parallelism: 1,
|
||||||
|
};
|
||||||
|
let kek = derive_kek(password, &salt, &kdf_params).expect("KEK derivation");
|
||||||
|
let dek = generate_dek();
|
||||||
|
let (wrapped_dek, header_nonce, header_tag) = wrap_dek(&kek, &dek).expect("DEK wrapping");
|
||||||
|
|
||||||
|
// 1. Initialisierung des Containers
|
||||||
|
{
|
||||||
|
let db = Database::open(&container_path).expect("Open database");
|
||||||
|
db.init_schema(&salt, &kdf_params, &wrapped_dek, &header_nonce, &header_tag)
|
||||||
|
.expect("Init schema");
|
||||||
|
db.checkpoint().expect("Initial Checkpoint");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Parallele Schreiblast mit SanctumFs erzeugen
|
||||||
|
let stop_signal = Arc::new(AtomicBool::new(false));
|
||||||
|
let db = Database::open(&container_path).expect("Open database for VFS");
|
||||||
|
let fs = SanctumFs::new(db, dek.clone(), FORMAT_VERSION);
|
||||||
|
|
||||||
|
let mut handles = Vec::new();
|
||||||
|
|
||||||
|
// Spawn 4 parallele Schreiber
|
||||||
|
for worker_id in 0..4 {
|
||||||
|
let fs_clone = fs.clone();
|
||||||
|
let stop_clone = stop_signal.clone();
|
||||||
|
|
||||||
|
let handle = tokio::spawn(async move {
|
||||||
|
let mut file_idx = 0;
|
||||||
|
while !stop_clone.load(Ordering::Relaxed) && file_idx < 10 {
|
||||||
|
let file_path_str = format!("/worker_{}_file_{}.dat", worker_id, file_idx);
|
||||||
|
let dav_path = DavPath::new(&file_path_str).unwrap();
|
||||||
|
|
||||||
|
let mut opts = OpenOptions::default();
|
||||||
|
opts.write = true;
|
||||||
|
opts.create = true;
|
||||||
|
opts.truncate = true;
|
||||||
|
|
||||||
|
// Datei anlegen
|
||||||
|
let mut file = match fs_clone.open(&dav_path, opts).await {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(_) => break,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mehrere 256-KB Blöcke schreiben (über mehrere Chunks hinweg)
|
||||||
|
let mut payload = vec![0u8; 256 * 1024];
|
||||||
|
rand::thread_rng().fill_bytes(&mut payload);
|
||||||
|
|
||||||
|
for _ in 0..6 {
|
||||||
|
if stop_clone.load(Ordering::Relaxed) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let _ = file.write_bytes(Bytes::copy_from_slice(&payload)).await;
|
||||||
|
}
|
||||||
|
let _ = file.flush().await;
|
||||||
|
file_idx += 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
handles.push(handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lass die Worker 500ms unter Volllast schreiben
|
||||||
|
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
|
||||||
|
|
||||||
|
// 3. Simuliere abrupten Prozessabbruch (Hard Kill / Power Cut)
|
||||||
|
// Wir brechen die Worker hart ab (Cancel) und verwerfen das FS-Handle ohne sauberen Unmount
|
||||||
|
stop_signal.store(true, Ordering::SeqCst);
|
||||||
|
for h in handles {
|
||||||
|
h.abort(); // Simuliert Kill
|
||||||
|
}
|
||||||
|
|
||||||
|
// FS ohne Checkpoint/Drop-Finalisierung freigeben
|
||||||
|
drop(fs);
|
||||||
|
|
||||||
|
// 4. Recovery & Integritätsprüfung nach Crash
|
||||||
|
// Das System muss die SQLite WAL-Datei automatisch erkennen und verarbeiten
|
||||||
|
let verify_result = verify_container(&container_path, Some(&dek), false).expect("Verify post-crash");
|
||||||
|
assert!(
|
||||||
|
verify_result.is_healthy(),
|
||||||
|
"Container muss nach Crash vollkommen konsistent sein! Fehler: {:?}",
|
||||||
|
verify_result.errors
|
||||||
|
);
|
||||||
|
assert_eq!(verify_result.corrupted_chunks, 0, "Keine korrupten Chunks erlaubt");
|
||||||
|
|
||||||
|
// 5. Konsistentes Weiterarbeiten nach dem Absturz
|
||||||
|
let db_recovered = Database::open(&container_path).expect("Open database after crash");
|
||||||
|
let fs_recovered = SanctumFs::new(db_recovered, dek.clone(), FORMAT_VERSION);
|
||||||
|
|
||||||
|
// Neue Datei im wiederhergestellten Dateisystem anlegen und lesen
|
||||||
|
let recovery_test_path = DavPath::new("/post_crash_verification.txt").unwrap();
|
||||||
|
{
|
||||||
|
let mut opts = OpenOptions::default();
|
||||||
|
opts.write = true;
|
||||||
|
opts.create = true;
|
||||||
|
opts.truncate = true;
|
||||||
|
let mut file = fs_recovered
|
||||||
|
.open(&recovery_test_path, opts)
|
||||||
|
.await
|
||||||
|
.expect("Create post-crash file");
|
||||||
|
file.write_bytes(Bytes::from_static(b"Sanctum Crash Consistency Verified!"))
|
||||||
|
.await
|
||||||
|
.expect("Write post crash file");
|
||||||
|
file.flush().await.expect("Flush post crash file");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Datei wieder einlesen
|
||||||
|
{
|
||||||
|
let mut opts = OpenOptions::default();
|
||||||
|
opts.read = true;
|
||||||
|
let mut file = fs_recovered
|
||||||
|
.open(&recovery_test_path, opts)
|
||||||
|
.await
|
||||||
|
.expect("Read post-crash file");
|
||||||
|
let bytes = file.read_bytes(1024).await.expect("Read bytes");
|
||||||
|
assert_eq!(&bytes[..], b"Sanctum Crash Consistency Verified!");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sauberes Aufräumen der Testdatei
|
||||||
|
drop(fs_recovered);
|
||||||
|
let _ = std::fs::remove_file(&container_path);
|
||||||
|
}
|
||||||
@@ -0,0 +1,241 @@
|
|||||||
|
use std::fs::{self, File};
|
||||||
|
use std::io::Write;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use sanctum::crypto::{
|
||||||
|
derive_kek, generate_dek, generate_salt, wrap_dek, KdfParams, CHUNK_SIZE, FORMAT_VERSION,
|
||||||
|
};
|
||||||
|
use sanctum::storage::Database;
|
||||||
|
use sanctum::sync::{run_sync, SyncDirection, SyncOptions};
|
||||||
|
|
||||||
|
fn create_test_container(path: &Path) -> (Database, [u8; 32]) {
|
||||||
|
if path.exists() {
|
||||||
|
let _ = fs::remove_file(path);
|
||||||
|
}
|
||||||
|
let db = Database::open(path).expect("Open database");
|
||||||
|
let salt = generate_salt();
|
||||||
|
let kdf_params = KdfParams {
|
||||||
|
memory_cost: 1024,
|
||||||
|
time_cost: 1,
|
||||||
|
parallelism: 1,
|
||||||
|
};
|
||||||
|
let kek = derive_kek("sync_password", &salt, &kdf_params).unwrap();
|
||||||
|
let dek = generate_dek();
|
||||||
|
let (wrapped_dek, header_nonce, header_tag) = wrap_dek(&kek, &dek).unwrap();
|
||||||
|
db.init_schema(&salt, &kdf_params, &wrapped_dek, &header_nonce, &header_tag).unwrap();
|
||||||
|
(db, *dek)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sync_push_and_pull_basic() {
|
||||||
|
let temp_root = std::env::temp_dir().join(format!("sanctum_sync_test_{}", rand::random::<u64>()));
|
||||||
|
let container_path = temp_root.join("test.sanctum");
|
||||||
|
let source_dir = temp_root.join("source");
|
||||||
|
let target_dir = temp_root.join("restored");
|
||||||
|
|
||||||
|
fs::create_dir_all(&source_dir).unwrap();
|
||||||
|
fs::create_dir_all(source_dir.join("sub")).unwrap();
|
||||||
|
|
||||||
|
// Testdateien anlegen
|
||||||
|
fs::write(source_dir.join("file1.txt"), b"Hello Sanctum Sync!").unwrap();
|
||||||
|
fs::write(source_dir.join("sub").join("file2.bin"), vec![0x42u8; 100_000]).unwrap();
|
||||||
|
|
||||||
|
let (db, dek) = create_test_container(&container_path);
|
||||||
|
|
||||||
|
// 1. Push
|
||||||
|
let mut opts = SyncOptions::default();
|
||||||
|
opts.direction = SyncDirection::Push;
|
||||||
|
opts.quiet = true;
|
||||||
|
|
||||||
|
let stats = run_sync(
|
||||||
|
&db,
|
||||||
|
0,
|
||||||
|
&dek,
|
||||||
|
FORMAT_VERSION,
|
||||||
|
source_dir.to_str().unwrap(),
|
||||||
|
"/Backup",
|
||||||
|
&opts,
|
||||||
|
).expect("Sync push");
|
||||||
|
|
||||||
|
assert_eq!(stats.files_scanned, 2);
|
||||||
|
assert_eq!(stats.files_transferred, 2);
|
||||||
|
assert_eq!(stats.files_skipped, 0);
|
||||||
|
|
||||||
|
// 2. Erneuter Push (Fast check / Delta): Muss 0 übertragen, 2 überspringen
|
||||||
|
let stats_delta = run_sync(
|
||||||
|
&db,
|
||||||
|
0,
|
||||||
|
&dek,
|
||||||
|
FORMAT_VERSION,
|
||||||
|
source_dir.to_str().unwrap(),
|
||||||
|
"/Backup",
|
||||||
|
&opts,
|
||||||
|
).expect("Sync push delta");
|
||||||
|
|
||||||
|
assert_eq!(stats_delta.files_transferred, 0);
|
||||||
|
assert_eq!(stats_delta.files_skipped, 2);
|
||||||
|
|
||||||
|
// 3. Dry-Run mit neuer Datei
|
||||||
|
fs::write(source_dir.join("new_file.txt"), b"Brand new file").unwrap();
|
||||||
|
let mut dry_opts = opts.clone();
|
||||||
|
dry_opts.dry_run = true;
|
||||||
|
|
||||||
|
let stats_dry = run_sync(
|
||||||
|
&db,
|
||||||
|
0,
|
||||||
|
&dek,
|
||||||
|
FORMAT_VERSION,
|
||||||
|
source_dir.to_str().unwrap(),
|
||||||
|
"/Backup",
|
||||||
|
&dry_opts,
|
||||||
|
).expect("Sync push dry-run");
|
||||||
|
|
||||||
|
assert_eq!(stats_dry.files_transferred, 1);
|
||||||
|
assert_eq!(stats_dry.files_skipped, 2);
|
||||||
|
|
||||||
|
// Verifizieren, dass new_file.txt im Tresor tatsächlich NICHT existiert
|
||||||
|
let node = db.resolve_path_in_vault("/Backup/new_file.txt", 0, &dek).unwrap();
|
||||||
|
assert!(node.is_none());
|
||||||
|
|
||||||
|
// 4. Pull
|
||||||
|
let mut pull_opts = SyncOptions::default();
|
||||||
|
pull_opts.direction = SyncDirection::Pull;
|
||||||
|
pull_opts.quiet = true;
|
||||||
|
|
||||||
|
let stats_pull = run_sync(
|
||||||
|
&db,
|
||||||
|
0,
|
||||||
|
&dek,
|
||||||
|
FORMAT_VERSION,
|
||||||
|
"/Backup",
|
||||||
|
target_dir.to_str().unwrap(),
|
||||||
|
&pull_opts,
|
||||||
|
).expect("Sync pull");
|
||||||
|
|
||||||
|
assert_eq!(stats_pull.files_transferred, 2);
|
||||||
|
|
||||||
|
// Inhalt vergleichen
|
||||||
|
let c1 = fs::read(target_dir.join("file1.txt")).unwrap();
|
||||||
|
assert_eq!(c1, b"Hello Sanctum Sync!");
|
||||||
|
let c2 = fs::read(target_dir.join("sub").join("file2.bin")).unwrap();
|
||||||
|
assert_eq!(c2, vec![0x42u8; 100_000]);
|
||||||
|
|
||||||
|
// Aufräumen
|
||||||
|
let _ = fs::remove_dir_all(&temp_root);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sync_multi_megabyte_large_file() {
|
||||||
|
let temp_root = std::env::temp_dir().join(format!("sanctum_sync_large_{}", rand::random::<u64>()));
|
||||||
|
let container_path = temp_root.join("test_large.sanctum");
|
||||||
|
let source_dir = temp_root.join("source");
|
||||||
|
let target_dir = temp_root.join("restored");
|
||||||
|
|
||||||
|
fs::create_dir_all(&source_dir).unwrap();
|
||||||
|
|
||||||
|
// 3.5 MB große Datei erzeugen (geht über 4 Chunks: 0, 1, 2, 3)
|
||||||
|
let large_file_path = source_dir.join("large_payload.bin");
|
||||||
|
let mut large_file = File::create(&large_file_path).unwrap();
|
||||||
|
let chunk_sample = vec![0xA5u8; CHUNK_SIZE];
|
||||||
|
for _ in 0..3 {
|
||||||
|
large_file.write_all(&chunk_sample).unwrap();
|
||||||
|
}
|
||||||
|
large_file.write_all(&vec![0x5Au8; 512 * 1024]).unwrap(); // 3.5 MB
|
||||||
|
large_file.flush().unwrap();
|
||||||
|
drop(large_file);
|
||||||
|
|
||||||
|
let (db, dek) = create_test_container(&container_path);
|
||||||
|
|
||||||
|
// Push
|
||||||
|
let mut opts = SyncOptions::default();
|
||||||
|
opts.direction = SyncDirection::Push;
|
||||||
|
opts.quiet = true;
|
||||||
|
|
||||||
|
let stats = run_sync(
|
||||||
|
&db,
|
||||||
|
0,
|
||||||
|
&dek,
|
||||||
|
FORMAT_VERSION,
|
||||||
|
source_dir.to_str().unwrap(),
|
||||||
|
"/LargeTest",
|
||||||
|
&opts,
|
||||||
|
).expect("Sync push large");
|
||||||
|
|
||||||
|
assert_eq!(stats.files_transferred, 1);
|
||||||
|
assert_eq!(stats.bytes_transferred, 3 * (CHUNK_SIZE as u64) + 512 * 1024);
|
||||||
|
|
||||||
|
// Pull
|
||||||
|
let mut pull_opts = SyncOptions::default();
|
||||||
|
pull_opts.direction = SyncDirection::Pull;
|
||||||
|
pull_opts.quiet = true;
|
||||||
|
|
||||||
|
run_sync(
|
||||||
|
&db,
|
||||||
|
0,
|
||||||
|
&dek,
|
||||||
|
FORMAT_VERSION,
|
||||||
|
"/LargeTest",
|
||||||
|
target_dir.to_str().unwrap(),
|
||||||
|
&pull_opts,
|
||||||
|
).expect("Sync pull large");
|
||||||
|
|
||||||
|
let restored = fs::read(target_dir.join("large_payload.bin")).unwrap();
|
||||||
|
let original = fs::read(&large_file_path).unwrap();
|
||||||
|
assert_eq!(restored.len(), original.len());
|
||||||
|
assert_eq!(restored, original);
|
||||||
|
|
||||||
|
let _ = fs::remove_dir_all(&temp_root);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sync_delete_and_exclude_flags() {
|
||||||
|
let temp_root = std::env::temp_dir().join(format!("sanctum_sync_flags_{}", rand::random::<u64>()));
|
||||||
|
let container_path = temp_root.join("test_flags.sanctum");
|
||||||
|
let source_dir = temp_root.join("source");
|
||||||
|
|
||||||
|
fs::create_dir_all(&source_dir).unwrap();
|
||||||
|
fs::write(source_dir.join("keep.txt"), b"Keep this").unwrap();
|
||||||
|
fs::write(source_dir.join("remove_me.txt"), b"Will be deleted later").unwrap();
|
||||||
|
fs::write(source_dir.join("ignore.tmp"), b"Temporary file").unwrap();
|
||||||
|
|
||||||
|
let (db, dek) = create_test_container(&container_path);
|
||||||
|
|
||||||
|
let mut opts = SyncOptions::default();
|
||||||
|
opts.direction = SyncDirection::Push;
|
||||||
|
opts.exclude_patterns = vec!["*.tmp".to_string()];
|
||||||
|
opts.quiet = true;
|
||||||
|
|
||||||
|
// 1. Initialer Push mit Exclude
|
||||||
|
let stats = run_sync(
|
||||||
|
&db,
|
||||||
|
0,
|
||||||
|
&dek,
|
||||||
|
FORMAT_VERSION,
|
||||||
|
source_dir.to_str().unwrap(),
|
||||||
|
"/Files",
|
||||||
|
&opts,
|
||||||
|
).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(stats.files_transferred, 2); // keep.txt und remove_me.txt
|
||||||
|
assert!(db.resolve_path_in_vault("/Files/ignore.tmp", 0, &dek).unwrap().is_none());
|
||||||
|
|
||||||
|
// 2. Lokale Datei löschen und Sync mit --delete ausführen
|
||||||
|
fs::remove_file(source_dir.join("remove_me.txt")).unwrap();
|
||||||
|
opts.delete = true;
|
||||||
|
|
||||||
|
let stats_del = run_sync(
|
||||||
|
&db,
|
||||||
|
0,
|
||||||
|
&dek,
|
||||||
|
FORMAT_VERSION,
|
||||||
|
source_dir.to_str().unwrap(),
|
||||||
|
"/Files",
|
||||||
|
&opts,
|
||||||
|
).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(stats_del.files_deleted, 1);
|
||||||
|
assert!(db.resolve_path_in_vault("/Files/remove_me.txt", 0, &dek).unwrap().is_none());
|
||||||
|
assert!(db.resolve_path_in_vault("/Files/keep.txt", 0, &dek).unwrap().is_some());
|
||||||
|
|
||||||
|
let _ = fs::remove_dir_all(&temp_root);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user