Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a5b48e560e
|
||
|
|
ce266787b6
|
||
|
|
121bffc289
|
||
|
|
fc68daddac | ||
|
|
3bf2ce0718
|
||
|
|
c4a415e3e4 | ||
|
|
18d84c94f6
|
||
|
|
f4220e34ae | ||
|
|
86d43761d9
|
||
|
|
abcc11f078
|
||
|
|
eff6dafa77
|
||
|
|
cc000a2bda
|
||
|
|
0ed470281b
|
@@ -24,16 +24,14 @@ jobs:
|
||||
components: clippy, rustfmt
|
||||
cache: false
|
||||
|
||||
# Ein rohes 'actions/cache' auf target/ liefert zwar einen technischen Cache-Hit (Dateien
|
||||
# werden wiederhergestellt), Cargo kompiliert die Abhängigkeiten aber oft trotzdem neu:
|
||||
# Der tar-basierte Restore-Vorgang setzt bei allen wiederhergestellten Dateien dieselbe
|
||||
# Mtime, wodurch Cargos Fingerprinting nicht mehr zuverlässig erkennen kann, was
|
||||
# älter/neuer als was ist, und sicherheitshalber alles neu baut. Swatinem/rust-cache ist
|
||||
# genau dafür gebaut (u. a. gezielte Mtime-Korrektur nach dem Restore).
|
||||
- name: Cache Cargo-Abhängigkeiten & Build-Artefakte
|
||||
uses: actions/cache@v6
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
key: cargo-${{ runner.os }}-${{ hashFiles('Cargo.lock') }}
|
||||
restore-keys: |
|
||||
cargo-${{ runner.os }}-
|
||||
uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Formatierung automatisch beheben
|
||||
run: cargo fmt
|
||||
|
||||
+15
-10
@@ -47,19 +47,19 @@ jobs:
|
||||
toolchain: stable
|
||||
cache: false
|
||||
|
||||
# Ein rohes 'actions/cache' auf target/ liefert zwar einen technischen Cache-Hit
|
||||
# (Dateien werden restauriert), Cargo kompiliert die Abhängigkeiten aber trotzdem neu:
|
||||
# Der tar-basierte Restore-Vorgang setzt bei allen wiederhergestellten Dateien
|
||||
# (Quelldateien wie kompilierte .rlib/.d-Dateien) dieselbe Mtime, wodurch Cargos
|
||||
# Fingerprinting nicht mehr zuverlässig erkennen kann, was tatsächlich älter/neuer als
|
||||
# was ist, und sicherheitshalber alles neu baut. Swatinem/rust-cache ist genau dafür
|
||||
# gebaut (u. a. gezielte Mtime-Korrektur nach dem Restore) und cached dabei automatisch
|
||||
# auch beide Cross-Compilation-Targets (x86_64 + aarch64) in diesem Job.
|
||||
- name: Cache Cargo-Abhängigkeiten & Build-Artefakte
|
||||
uses: actions/cache@v6
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
key: cargo-${{ runner.os }}-${{ hashFiles('Cargo.lock') }}
|
||||
restore-keys: |
|
||||
cargo-${{ runner.os }}-
|
||||
uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Alte Paketierungs-Ausgaben aus dem Cache entfernen
|
||||
run: rm -rf target/debian target/generate-rpm target/arch
|
||||
run: rm -rf target/debian target/generate-rpm target/arch target/completions
|
||||
|
||||
- name: Install Cross-Compilation Toolchains (apt)
|
||||
run: |
|
||||
@@ -111,6 +111,11 @@ jobs:
|
||||
cargo build --release --target x86_64-unknown-linux-gnu
|
||||
cargo build --release --target aarch64-unknown-linux-gnu
|
||||
|
||||
- name: Generate Shell Completions
|
||||
run: |
|
||||
cargo build --release --bin generate-completions
|
||||
./target/release/generate-completions target/completions
|
||||
|
||||
- name: Determine Build Number
|
||||
id: build_num
|
||||
env:
|
||||
|
||||
@@ -47,19 +47,19 @@ jobs:
|
||||
toolchain: stable
|
||||
cache: false
|
||||
|
||||
# Ein rohes 'actions/cache' auf target/ liefert zwar einen technischen Cache-Hit
|
||||
# (Dateien werden restauriert), Cargo kompiliert die Abhängigkeiten aber trotzdem neu:
|
||||
# Der tar-basierte Restore-Vorgang setzt bei allen wiederhergestellten Dateien
|
||||
# (Quelldateien wie kompilierte .rlib/.d-Dateien) dieselbe Mtime, wodurch Cargos
|
||||
# Fingerprinting nicht mehr zuverlässig erkennen kann, was tatsächlich älter/neuer als
|
||||
# was ist, und sicherheitshalber alles neu baut. Swatinem/rust-cache ist genau dafür
|
||||
# gebaut (u. a. gezielte Mtime-Korrektur nach dem Restore) und cached dabei automatisch
|
||||
# auch beide Cross-Compilation-Targets (x86_64 + aarch64) in diesem Job.
|
||||
- name: Cache Cargo-Abhängigkeiten & Build-Artefakte
|
||||
uses: actions/cache@v6
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
key: cargo-${{ runner.os }}-${{ hashFiles('Cargo.lock') }}
|
||||
restore-keys: |
|
||||
cargo-${{ runner.os }}-
|
||||
uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Alte Paketierungs-Ausgaben aus dem Cache entfernen
|
||||
run: rm -rf target/debian target/generate-rpm target/arch
|
||||
run: rm -rf target/debian target/generate-rpm target/arch target/completions
|
||||
|
||||
- name: Install Cross-Compilation Toolchains (apt)
|
||||
run: |
|
||||
@@ -111,6 +111,11 @@ jobs:
|
||||
cargo build --release --target x86_64-unknown-linux-gnu
|
||||
cargo build --release --target aarch64-unknown-linux-gnu
|
||||
|
||||
- name: Generate Shell Completions
|
||||
run: |
|
||||
cargo build --release --bin generate-completions
|
||||
./target/release/generate-completions target/completions
|
||||
|
||||
- name: Determine Build Number
|
||||
id: build_num
|
||||
env:
|
||||
|
||||
@@ -8,6 +8,7 @@ on:
|
||||
- reopened
|
||||
branches:
|
||||
- testing
|
||||
- dev
|
||||
|
||||
jobs:
|
||||
test:
|
||||
@@ -23,16 +24,11 @@ jobs:
|
||||
toolchain: stable
|
||||
cache: false
|
||||
|
||||
# Siehe main.yaml/testing.yaml: Swatinem/rust-cache statt eines rohen 'actions/cache' auf
|
||||
# target/, das trotz Cache-Hit wegen verschobener Mtimes nach dem Restore oft trotzdem
|
||||
# alles neu kompiliert.
|
||||
- name: Cache Cargo-Abhängigkeiten & Build-Artefakte
|
||||
uses: actions/cache@v6
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
key: cargo-${{ runner.os }}-${{ hashFiles('Cargo.lock') }}
|
||||
restore-keys: |
|
||||
cargo-${{ runner.os }}-
|
||||
uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Run Tests
|
||||
run: cargo test
|
||||
|
||||
Generated
+1
-1
@@ -2052,7 +2052,7 @@ checksum = "ba467056f1b547ed52077911161fc86985becbc60e8e1857c8a144dab0def891"
|
||||
|
||||
[[package]]
|
||||
name = "smart-mount"
|
||||
version = "2.0.1"
|
||||
version = "2.0.4"
|
||||
dependencies = [
|
||||
"aes-gcm 0.11.1",
|
||||
"anyhow",
|
||||
|
||||
+41
-8
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "smart-mount"
|
||||
version = "2.0.1"
|
||||
version = "2.0.4"
|
||||
edition = "2024"
|
||||
authors = ['DragonSlayer_14']
|
||||
readme = "README.md"
|
||||
@@ -68,6 +68,17 @@ assets = [
|
||||
["packaging/systemd/smart-mount-mount.service", "usr/lib/systemd/system/smart-mount-mount.service", "644"],
|
||||
["packaging/systemd/smart-mount-watch.service", "usr/lib/systemd/system/smart-mount-watch.service", "644"],
|
||||
["packaging/systemd/smart-mount-watch.timer", "usr/lib/systemd/system/smart-mount-watch.timer", "644"],
|
||||
# Von 'cargo build --bin generate-completions' erzeugt (siehe .gitea/workflows/main.yaml) -
|
||||
# kein 'smart-mount completions'-Subcommand mehr, Shell-Completion wird stattdessen wie jedes
|
||||
# andere Paket-Asset automatisch mit installiert/entfernt. Debian/Ubuntu laden Zsh-Completions
|
||||
# aus 'vendor-completions' (anders als Fedora/Arch, siehe [package.metadata.generate-rpm]).
|
||||
["target/completions/smart-mount.bash", "usr/share/bash-completion/completions/smart-mount", "644"],
|
||||
["target/completions/_smart-mount", "usr/share/zsh/vendor-completions/_smart-mount", "644"],
|
||||
["target/completions/smart-mount.fish", "usr/share/fish/vendor_completions.d/smart-mount.fish", "644"],
|
||||
# Kein 'smart-mount service'-Subcommand mehr - packaging/deb/postinst ruft dieses interne
|
||||
# Hilfsprogramm (siehe src/bin/setup-cron.rs) stattdessen automatisch auf und richtet damit
|
||||
# bei Bedarf (kein systemd, aber /etc/cron.d vorhanden) den Cron-Fallback ein.
|
||||
["target/release/setup-cron", "usr/lib/smart-mount/setup-cron", "755"],
|
||||
]
|
||||
|
||||
[package.metadata.generate-rpm]
|
||||
@@ -78,22 +89,44 @@ assets = [
|
||||
{ source = "packaging/systemd/smart-mount-mount.service", dest = "/usr/lib/systemd/system/smart-mount-mount.service", mode = "644" },
|
||||
{ source = "packaging/systemd/smart-mount-watch.service", dest = "/usr/lib/systemd/system/smart-mount-watch.service", mode = "644" },
|
||||
{ source = "packaging/systemd/smart-mount-watch.timer", dest = "/usr/lib/systemd/system/smart-mount-watch.timer", mode = "644" },
|
||||
# Fedora/RHEL (wie Arch, siehe scripts/package-arch.py) laden Zsh-Completions aus
|
||||
# 'site-functions' statt Debians 'vendor-completions' (siehe [package.metadata.deb]).
|
||||
{ source = "target/completions/smart-mount.bash", dest = "/usr/share/bash-completion/completions/smart-mount", mode = "644" },
|
||||
{ source = "target/completions/_smart-mount", dest = "/usr/share/zsh/site-functions/_smart-mount", mode = "644" },
|
||||
{ source = "target/completions/smart-mount.fish", dest = "/usr/share/fish/vendor_completions.d/smart-mount.fish", mode = "644" },
|
||||
# Kein 'smart-mount service'-Subcommand mehr - post_install_script ruft dieses interne
|
||||
# Hilfsprogramm (siehe src/bin/setup-cron.rs) stattdessen automatisch auf und richtet damit
|
||||
# bei Bedarf (kein systemd, aber /etc/cron.d vorhanden) den Cron-Fallback ein.
|
||||
{ source = "target/release/setup-cron", dest = "/usr/lib/smart-mount/setup-cron", mode = "755" },
|
||||
]
|
||||
requires = { "mac2ip" = "*" }
|
||||
suggests = { "davfs2" = "*", "cifs-utils" = "*", "nfs-utils" = "*" }
|
||||
# postinst/postrm-Äquivalente: richten den paketierten systemd-Dienst automatisch ein/entfernen
|
||||
# (Gegenstück zu packaging/deb/postinst+postrm). $1 in %postun: Anzahl verbleibender Versionen
|
||||
# nach diesem Schritt - 0 nur bei vollständiger Deinstallation, nicht bei einem Upgrade.
|
||||
# postinst/postrm-Äquivalente: richten den paketierten systemd-Dienst (bzw. ohne systemd den
|
||||
# Cron-Fallback über setup-cron) automatisch ein/entfernen (Gegenstück zu
|
||||
# packaging/deb/postinst+postrm). $1 in %postun: Anzahl verbleibender Versionen nach diesem
|
||||
# Schritt - 0 nur bei vollständiger Deinstallation, nicht bei einem Upgrade. Die Cron-Datei wird
|
||||
# in %postun direkt per "rm -f" entfernt statt über setup-cron selbst: zu diesem Zeitpunkt sind
|
||||
# die Paketdateien - und damit auch setup-cron selbst - bereits entfernt.
|
||||
post_install_script = """
|
||||
if command -v systemctl >/dev/null 2>&1; then
|
||||
systemctl daemon-reload || true
|
||||
systemctl enable --now smart-mount-mount.service smart-mount-watch.timer || true
|
||||
# smart-mount-mount.service mountet echte Netzlaufwerke (kann bei nicht erreichbaren
|
||||
# Servern pro Paar mehrere zehn Sekunden dauern) - "enable --now" wuerde synchron darauf
|
||||
# warten und damit die Paketinstallation blockieren, daher aktivieren + nur als
|
||||
# asynchroner Job starten ("--no-block", kehrt sofort zurueck).
|
||||
systemctl enable smart-mount-mount.service || true
|
||||
systemctl start --no-block smart-mount-mount.service || true
|
||||
systemctl enable --now smart-mount-watch.timer || true
|
||||
fi
|
||||
/usr/lib/smart-mount/setup-cron || true
|
||||
"""
|
||||
post_uninstall_script = """
|
||||
if [ "$1" = "0" ] && command -v systemctl >/dev/null 2>&1; then
|
||||
systemctl disable --now smart-mount-mount.service smart-mount-watch.timer smart-mount-watch.service || true
|
||||
systemctl daemon-reload || true
|
||||
if [ "$1" = "0" ]; then
|
||||
if command -v systemctl >/dev/null 2>&1; then
|
||||
systemctl disable --now smart-mount-mount.service smart-mount-watch.timer smart-mount-watch.service || true
|
||||
systemctl daemon-reload || true
|
||||
fi
|
||||
rm -f /etc/cron.d/smart-mount
|
||||
fi
|
||||
"""
|
||||
|
||||
|
||||
@@ -26,8 +26,9 @@ werden (z. B. über ein separates Sync-Tool).
|
||||
- `cifs-utils` für SMB/CIFS-Laufwerke
|
||||
- `nfs-common` (Debian/Ubuntu) bzw. `nfs-utils` (Fedora/Arch) für NFS-Laufwerke
|
||||
- `systemd`: smart-mount richtet sich beim Installieren des Pakets automatisch als
|
||||
System-Dienst ein (siehe "Automatischer Start beim Systemstart" unten). Ohne systemd bleibt
|
||||
`smart-mount service crontab` als manueller Fallback.
|
||||
System-Dienst ein (siehe "Automatischer Start beim Systemstart" unten). Ohne systemd, aber mit
|
||||
`/etc/cron.d`, wird beim Installieren automatisch ein Cron-Fallback eingerichtet - kein
|
||||
manueller Schritt nötig.
|
||||
|
||||
---
|
||||
|
||||
@@ -79,21 +80,16 @@ smart-mount status --json
|
||||
# Ein Reconcile-Durchlauf (lokal/Cloud-Umschaltung) - für systemd-Timer/Cron gedacht
|
||||
sudo smart-mount watch
|
||||
|
||||
# Cron-Fallback für Systeme ohne (genutzten) systemd - siehe "Automatischer Start beim
|
||||
# Systemstart" unten
|
||||
sudo smart-mount service crontab
|
||||
sudo smart-mount service crontab --remove
|
||||
|
||||
# Voraussetzungen prüfen (Binaries, Scheduler) - deckt gebündelt ab, was man sonst erst
|
||||
# einzeln beim Mount-Fehlschlag entdecken würde
|
||||
smart-mount doctor
|
||||
smart-mount doctor --json
|
||||
|
||||
# Shell-Completion-Skript ausgeben (bash/zsh/fish/elvish/powershell)
|
||||
smart-mount completions bash > /etc/bash_completion.d/smart-mount
|
||||
smart-mount completions zsh > "${fpath[1]}/_smart-mount"
|
||||
```
|
||||
|
||||
Shell-Completion (bash/zsh/fish) wird beim Installieren des .deb/.rpm/.pkg.tar.zst-Pakets
|
||||
automatisch mit installiert (und beim Entfernen des Pakets automatisch mit entfernt) - dafür ist
|
||||
kein eigenes `smart-mount`-Subcommand nötig.
|
||||
|
||||
Die Konfiguration liegt immer unter `/etc/smart-mount/config.toml`, unabhängig davon, ob
|
||||
`smart-mount` selbst mit oder ohne Root-Rechte aufgerufen wird (siehe oben). Die verschlüsselte
|
||||
Zugangsdaten-Datenbank (`smart-mount.db`) liegt im selben Verzeichnis.
|
||||
@@ -149,13 +145,19 @@ System-systemd-Dienst ein - kein manueller Schritt nötig. Die dafür paketierte
|
||||
`smart-mount watch` aufruft (Standardintervall: 120s).
|
||||
|
||||
Die postinst/postrm-Skripte des Pakets (bzw. das `.INSTALL`-Skriptlet bei Arch) aktivieren und
|
||||
starten beide Units beim Installieren/Upgraden (`systemctl enable --now ...`) und deaktivieren
|
||||
sie wieder beim Deinstallieren (`systemctl disable --now ...`) - siehe `packaging/deb/postinst`
|
||||
+ `packaging/deb/postrm` bzw. `[package.metadata.generate-rpm]`/`scripts/package-arch.py` in
|
||||
deaktivieren beide Units beim Installieren/Deinstallieren - siehe `packaging/deb/postinst` +
|
||||
`packaging/deb/postrm` bzw. `[package.metadata.generate-rpm]`/`scripts/package-arch.py` in
|
||||
`Cargo.toml`. Welche Laufwerkspaare dabei gemountet werden (und für welchen Nutzer, siehe
|
||||
`owner_user` unten) steuert ausschließlich `/etc/smart-mount/config.toml` - nicht die
|
||||
Unit-Dateien selbst.
|
||||
|
||||
`smart-mount-mount.service` mountet dabei echte Netzlaufwerke, was bei einem gerade nicht
|
||||
erreichbaren Server pro Paar mehrere zehn Sekunden dauern kann (begrenzt auf max. 10 Minuten
|
||||
insgesamt, siehe `TimeoutStartSec` in der Unit) - das Paket-Setup wird dadurch **nicht**
|
||||
blockiert: `systemctl start --no-block` stößt den ersten Mount-Versuch beim Installieren nur an,
|
||||
statt synchron auf ihn zu warten. `smart-mount-watch.timer` wird dagegen sofort synchron
|
||||
gestartet (das Timer-"Armen" selbst ist immer augenblicklich fertig).
|
||||
|
||||
**Anderes Watch-Intervall als der Standard (120s):** `settings.watch_interval_secs` in
|
||||
`config.toml` steuert nur den Cron-Fallback (s. u.) - der paketierte Timer hat ein fest
|
||||
eingebautes Intervall. Zum Anpassen:
|
||||
@@ -166,23 +168,36 @@ sudo systemctl edit smart-mount-watch.timer
|
||||
# OnUnitActiveSec=60s
|
||||
```
|
||||
|
||||
**Logs:** beide Units schreiben mit festem `SyslogIdentifier=smart-mount` ins systemd-Journal -
|
||||
`journalctl -u smart-mount-mount` / `-u smart-mount-watch` (oder gebündelt `journalctl -t
|
||||
smart-mount`) zeigt sie an. Zusätzlich schreibt smart-mount dieselben Meldungen dauerhaft nach
|
||||
`/var/log/smart-mount/log-<Datum>.log` (siehe `settings.log_level` für die Ausführlichkeit,
|
||||
Standard `info`) - unabhängig von der Journal-Rotation. `smart-mount-mount.service` bleibt nach
|
||||
einem erfolgreichen Lauf als "active (exited)" sichtbar (`RemainAfterExit=yes`), und ein
|
||||
Fehlschlag beim Mounten mindestens eines konfigurierten Laufwerkspaars lässt den Dienst jetzt
|
||||
auch tatsächlich als "failed" erscheinen, statt es stillschweigend zu ignorieren.
|
||||
|
||||
### Cron-Fallback (Systeme ohne systemd)
|
||||
|
||||
```bash
|
||||
sudo smart-mount service crontab # einrichten
|
||||
sudo smart-mount service crontab --remove # wieder entfernen
|
||||
```
|
||||
|
||||
Erfordert Root (wie `mount`/`watch` - Mounten läuft immer als root, siehe oben) und schreibt
|
||||
`/etc/cron.d/smart-mount` - das Intervall folgt `settings.watch_interval_secs`. Ist
|
||||
`/etc/cron.d` nicht vorhanden, werden stattdessen die beiden äquivalenten Zeilen zum manuellen
|
||||
Eintragen ausgegeben:
|
||||
Ist beim Installieren/Upgraden des Pakets kein `systemctl` gefunden, aber `/etc/cron.d`
|
||||
vorhanden, schreibt das Paket **automatisch** `/etc/cron.d/smart-mount` (kein manueller Schritt,
|
||||
kein eigenes `smart-mount`-Subcommand) - das Intervall folgt `settings.watch_interval_secs`
|
||||
(Standard 120s):
|
||||
|
||||
```cron
|
||||
@reboot smart-mount mount --all
|
||||
*/2 * * * * smart-mount watch
|
||||
@reboot root /usr/bin/smart-mount mount --all
|
||||
*/2 * * * * root /usr/bin/smart-mount watch
|
||||
```
|
||||
|
||||
Zuständig dafür ist `setup-cron` (`src/bin/setup-cron.rs`, installiert nach
|
||||
`/usr/lib/smart-mount/setup-cron`) - ein internes Hilfsprogramm, kein Teil der öffentlichen
|
||||
`smart-mount`-CLI, das die postinst-Skripte des Pakets (bzw. das `.INSTALL`-Skriptlet bei Arch)
|
||||
automatisch aufrufen; beim Deinstallieren wird `/etc/cron.d/smart-mount` ebenso automatisch
|
||||
wieder entfernt. Ist weder systemd noch `/etc/cron.d` vorhanden, meldet `setup-cron` das nur
|
||||
auf der Konsole (Paketinstallation schlägt dadurch nicht fehl) - die periodische Ausführung
|
||||
muss dann manuell eingerichtet werden, z. B. über eine eigene Crontab-Zeile analog zu obigem
|
||||
Beispiel.
|
||||
|
||||
---
|
||||
|
||||
## Architekturentscheidungen
|
||||
@@ -268,7 +283,9 @@ Eintragen ausgegeben:
|
||||
│ └── report-security-issue.py # Security-Scan-Ergebnisse als Gitea-Issue melden
|
||||
├── src/
|
||||
│ ├── main.rs # Dünner Einstiegspunkt (CLI-Parsing, Dispatch)
|
||||
│ ├── lib.rs # Bibliotheks-Wurzel
|
||||
│ ├── lib.rs # Bibliotheks-Wurzel (inkl. `pub mod cli`)
|
||||
│ ├── bin/ # Interne Hilfsprogramme (kein Teil der `smart-mount`-CLI):
|
||||
│ │ # generate-completions (Build-Zeit), setup-cron (Paket-Installation)
|
||||
│ ├── cli/ # `clap`-Subcommands
|
||||
│ ├── config/ # Konfigurationsschema + CRUD (config-ctdra)
|
||||
│ ├── crypto/ # Verschlüsselung + Master-Key-Auflösung
|
||||
|
||||
+18
-6
@@ -1,13 +1,25 @@
|
||||
#!/bin/sh
|
||||
# Richtet smart-mount beim Installieren/Upgraden des Pakets automatisch als
|
||||
# System-systemd-Dienst ein (Gegenstueck: postrm). Laeuft nur beim eigentlichen
|
||||
# "configure"-Schritt (siehe Debian Policy Manual, Abschnitt 6.5), nicht bei
|
||||
# "abort-upgrade"/"abort-remove" etc.
|
||||
# System-systemd-Dienst ein (Gegenstueck: postrm); ist kein systemd verfuegbar, richtet
|
||||
# stattdessen setup-cron (siehe src/bin/setup-cron.rs) automatisch den Cron-Fallback ein,
|
||||
# sofern /etc/cron.d existiert. Laeuft nur beim eigentlichen "configure"-Schritt (siehe
|
||||
# Debian Policy Manual, Abschnitt 6.5), nicht bei "abort-upgrade"/"abort-remove" etc.
|
||||
set -e
|
||||
|
||||
if [ "$1" = "configure" ] && command -v systemctl >/dev/null 2>&1; then
|
||||
systemctl daemon-reload || true
|
||||
systemctl enable --now smart-mount-mount.service smart-mount-watch.timer || true
|
||||
if [ "$1" = "configure" ]; then
|
||||
if command -v systemctl >/dev/null 2>&1; then
|
||||
systemctl daemon-reload || true
|
||||
# smart-mount-mount.service mountet echte Netzlaufwerke (kann pro konfiguriertem Paar
|
||||
# mehrere zehn Sekunden dauern, wenn ein Server gerade nicht erreichbar ist) - "enable
|
||||
# --now" wuerde synchron darauf warten und damit "apt"/"dpkg" fuer die gesamte Dauer
|
||||
# blockieren. Daher: aktivieren (fuer den naechsten Boot) und der eigentliche Mount-Lauf
|
||||
# nur als asynchroner Job ("--no-block", kehrt sofort zurueck) - das Paket-Setup selbst
|
||||
# wartet also nie auf Netzwerk-Timeouts.
|
||||
systemctl enable smart-mount-mount.service || true
|
||||
systemctl start --no-block smart-mount-mount.service || true
|
||||
systemctl enable --now smart-mount-watch.timer || true
|
||||
fi
|
||||
/usr/lib/smart-mount/setup-cron || true
|
||||
fi
|
||||
|
||||
exit 0
|
||||
|
||||
+13
-6
@@ -1,13 +1,20 @@
|
||||
#!/bin/sh
|
||||
# Entfernt den beim Installieren eingerichteten systemd-Dienst wieder (Gegenstueck:
|
||||
# Entfernt den beim Installieren eingerichteten systemd-Dienst sowie einen eventuell von
|
||||
# setup-cron (siehe src/bin/setup-cron.rs) angelegten Cron-Fallback wieder (Gegenstueck:
|
||||
# postinst). Nur bei tatsaechlicher Entfernung ("remove"/"purge"), nicht bei einem
|
||||
# Upgrade (dort ersetzt dpkg die Unit-Dateien einfach durch die neue Version, ohne den
|
||||
# laufenden Dienst zwischenzeitlich zu deaktivieren).
|
||||
# Upgrade (dort ersetzt dpkg die Unit-/Paket-Dateien einfach durch die neue Version, ohne
|
||||
# den laufenden Dienst/Cron-Eintrag zwischenzeitlich zu deaktivieren). Die Cron-Datei wird
|
||||
# hier direkt per "rm -f" entfernt statt ueber setup-cron selbst aufgerufen zu werden: zum
|
||||
# Zeitpunkt, zu dem "postrm remove" laeuft, hat dpkg die Paketdateien - und damit auch
|
||||
# setup-cron selbst - bereits entfernt (siehe Debian Policy Manual, Abschnitt 6.5).
|
||||
set -e
|
||||
|
||||
if { [ "$1" = "remove" ] || [ "$1" = "purge" ]; } && command -v systemctl >/dev/null 2>&1; then
|
||||
systemctl disable --now smart-mount-mount.service smart-mount-watch.timer smart-mount-watch.service || true
|
||||
systemctl daemon-reload || true
|
||||
if { [ "$1" = "remove" ] || [ "$1" = "purge" ]; }; then
|
||||
if command -v systemctl >/dev/null 2>&1; then
|
||||
systemctl disable --now smart-mount-mount.service smart-mount-watch.timer smart-mount-watch.service || true
|
||||
systemctl daemon-reload || true
|
||||
fi
|
||||
rm -f /etc/cron.d/smart-mount
|
||||
fi
|
||||
|
||||
exit 0
|
||||
|
||||
@@ -5,7 +5,26 @@ Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
# Bleibt nach einem erfolgreichen Lauf als "active (exited)" sichtbar (statt sofort wieder
|
||||
# "inactive") - macht in 'systemctl status' auf einen Blick erkennbar, dass der letzte Boot-Lauf
|
||||
# tatsächlich durchlief, statt nur "inactive" zu zeigen (das genauso gut "nie gelaufen" heißen
|
||||
# könnte).
|
||||
RemainAfterExit=yes
|
||||
ExecStart=/usr/bin/smart-mount mount --all
|
||||
# Harte Obergrenze statt des systemd-Standards (üblicherweise 90s): pro konfiguriertem Paar
|
||||
# werden Erreichbarkeit UND Mount-Versuch sequenziell geprüft (bis zu ~20s Erreichbarkeits-
|
||||
# check + 30s Mount-Timeout, siehe crate::mount::MOUNT_TIMEOUT_SECS), bei mehreren Paaren
|
||||
# summiert sich das - 90s würde bei zwei oder mehr gerade nicht erreichbaren Servern schon
|
||||
# reichen, um mitten im Mount-Versuch abgebrochen zu werden. 10 Minuten sind großzügig genug für
|
||||
# eine typische Anzahl Paare, ohne bei einem echten Hänger (z. B. einer verwaisten Dateisperre)
|
||||
# unbegrenzt zu warten.
|
||||
TimeoutStartSec=600
|
||||
# Explizit (statt sich auf den systemd-Standard zu verlassen): stdout/stderr gehen ins Journal,
|
||||
# unter einem festen, von der jeweiligen Unit unabhängigen Tag - 'journalctl -t smart-mount'
|
||||
# zeigt damit die Ausgaben aller smart-mount-Units gebündelt.
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
SyslogIdentifier=smart-mount
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
@@ -4,3 +4,6 @@ Description=smart-mount: check reachability and switch local/cloud if needed
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/bin/smart-mount watch
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
SyslogIdentifier=smart-mount
|
||||
|
||||
+35
-5
@@ -1,29 +1,59 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": ["config:recommended"],
|
||||
"extends": [
|
||||
"config:recommended"
|
||||
],
|
||||
"timezone": "Europe/Berlin",
|
||||
"schedule": ["before 6am on monday"],
|
||||
"schedule": [
|
||||
"before 6am"
|
||||
],
|
||||
"baseBranchPatterns": [
|
||||
"dev"
|
||||
],
|
||||
"packageRules": [
|
||||
{
|
||||
"matchFileNames": [".gitea/workflows/**"],
|
||||
"matchFileNames": [
|
||||
".gitea/workflows/**"
|
||||
],
|
||||
"groupName": "Gitea Actions",
|
||||
"separateMajorMinor": false,
|
||||
"separateMinorPatch": false
|
||||
},
|
||||
{
|
||||
"matchManagers": ["cargo"],
|
||||
"matchManagers": [
|
||||
"cargo"
|
||||
],
|
||||
"groupName": "Cargo Dependencies",
|
||||
"separateMajorMinor": false,
|
||||
"separateMinorPatch": false
|
||||
},
|
||||
{
|
||||
"matchManagers": ["dockerfile", "docker-compose"],
|
||||
"matchManagers": [
|
||||
"dockerfile",
|
||||
"docker-compose"
|
||||
],
|
||||
"groupName": "Docker-Images",
|
||||
"separateMajorMinor": false,
|
||||
"separateMinorPatch": false
|
||||
},
|
||||
{
|
||||
"description": "Patch-/Minor-/Digest-Updates automatisch mergen, sobald alle CI-Checks (inkl. Unit-Tests) erfolgreich sind - Major-Updates sind unten explizit ausgenommen (siehe nächste Regel).",
|
||||
"matchUpdateTypes": [
|
||||
"patch",
|
||||
"minor",
|
||||
"digest",
|
||||
"lockFileMaintenance"
|
||||
],
|
||||
"automerge": true,
|
||||
"automergeType": "pr",
|
||||
"platformAutomerge": true
|
||||
},
|
||||
{
|
||||
"description": "Major-Updates immer manuell prüfen, da potenziell brechende Änderungen.",
|
||||
"matchUpdateTypes": [
|
||||
"major"
|
||||
],
|
||||
"automerge": false
|
||||
}
|
||||
],
|
||||
"customManagers": [
|
||||
|
||||
+97
-22
@@ -61,30 +61,86 @@ def collect_systemd_units(systemd_src_dir="packaging/systemd"):
|
||||
return unit_files, installable
|
||||
|
||||
|
||||
def build_install_scriptlet(installable, all_units):
|
||||
def collect_completions(completions_dir="target/completions"):
|
||||
"""Findet die von 'cargo build --bin generate-completions' erzeugten Shell-Completion-
|
||||
Skripte (siehe src/bin/generate-completions.rs), falls vorhanden, und ordnet sie generisch -
|
||||
ohne den Anwendungsnamen zu kennen - allein anhand ihrer clap_complete-Namenskonvention
|
||||
(Bash: '<name>.bash', Zsh: '_<name>', Fish: '<name>.fish') dem jeweiligen
|
||||
System-Verzeichnis zu. Gibt eine Liste aus (Quellpfad, Zielverzeichnis, Zieldateiname) zurück."""
|
||||
if not os.path.isdir(completions_dir):
|
||||
return []
|
||||
|
||||
mapping = []
|
||||
for f in sorted(os.listdir(completions_dir)):
|
||||
src = os.path.join(completions_dir, f)
|
||||
if f.endswith(".bash"):
|
||||
mapping.append((src, "usr/share/bash-completion/completions", f[: -len(".bash")]))
|
||||
elif f.endswith(".fish"):
|
||||
mapping.append((src, "usr/share/fish/vendor_completions.d", f))
|
||||
elif f.startswith("_"):
|
||||
mapping.append((src, "usr/share/zsh/site-functions", f))
|
||||
return mapping
|
||||
|
||||
|
||||
def find_helper_binary(name, helper, target_triple=None):
|
||||
"""Sucht eine optionale interne Hilfs-Binary (eigenes Cargo-Binary-Target unter
|
||||
'src/bin/<helper>.rs', z. B. fuer einen Cron-Fallback) am selben kompilierten Ort wie die
|
||||
Haupt-Binary - generisch ueber den fest bekannten Hilfsprogramm-Namen, ohne den
|
||||
Anwendungsnamen selbst zu kennen. Gibt None zurueck, falls nicht gefunden (z. B. weil das
|
||||
abgeleitete Projekt kein solches Hilfsprogramm hat)."""
|
||||
candidates = []
|
||||
if target_triple:
|
||||
candidates.append(f"target/{target_triple}/release/{helper}")
|
||||
candidates.append(f"target/release/{helper}")
|
||||
for p in candidates:
|
||||
if os.path.exists(p):
|
||||
return p
|
||||
return None
|
||||
|
||||
|
||||
def build_install_scriptlet(name, installable, all_units, has_cron_helper):
|
||||
"""Erzeugt den Inhalt einer Arch-'.INSTALL'-Datei (siehe `man PKGBUILD`, Abschnitt
|
||||
'install'), die den paketierten systemd-Dienst beim Installieren aktiviert/startet und beim
|
||||
Entfernen wieder deaktiviert/stoppt - rein generisch anhand der tatsächlich gefundenen
|
||||
Unit-Dateien, ohne Anwendungsnamen hart zu codieren."""
|
||||
installable_str = " ".join(installable)
|
||||
'install'): aktiviert/startet paketierte systemd-Units beim Installieren/Upgraden und
|
||||
deaktiviert sie beim Entfernen, und ruft - sofern vorhanden - generisch das interne
|
||||
Cron-Fallback-Hilfsprogramm ('usr/lib/<name>/setup-cron', siehe src/bin/setup-cron.rs) beim
|
||||
Installieren/Upgraden auf bzw. entfernt dessen Cron-Datei beim Entfernen wieder. Rein anhand
|
||||
der tatsächlich gefundenen Unit-Dateien/Hilfsprogramme zusammengesetzt, ohne den
|
||||
Anwendungsnamen selbst hart zu codieren (der als `name`-Parameter hereinkommt).
|
||||
|
||||
'.service'-Units werden bewusst NICHT über 'enable --now' gestartet: Bei einem oneshot-
|
||||
Service (typischer Fall für ein Boot-Setup-Skript) würde das den ExecStart-Befehl synchron
|
||||
ausführen und damit 'pacman' für die gesamte Laufzeit des Skripts blockieren (siehe
|
||||
packaging/deb/postinst für den konkreten Fall, der das erst sichtbar gemacht hat: ein
|
||||
Mount-Versuch gegen ein gerade nicht erreichbares Netzlaufwerk). '.timer'-Units sind davon
|
||||
nicht betroffen - sie nur zu "armen" ist immer sofort fertig - und starten daher weiterhin
|
||||
synchron mit 'enable --now'."""
|
||||
timers = [u for u in installable if u.endswith(".timer")]
|
||||
services = [u for u in installable if not u.endswith(".timer")]
|
||||
all_units_str = " ".join(all_units)
|
||||
return f"""post_install() {{
|
||||
systemctl daemon-reload >/dev/null 2>&1 || true
|
||||
systemctl enable --now {installable_str} >/dev/null 2>&1 || true
|
||||
}}
|
||||
|
||||
post_upgrade() {{
|
||||
systemctl daemon-reload >/dev/null 2>&1 || true
|
||||
}}
|
||||
systemd_enable = " systemctl daemon-reload >/dev/null 2>&1 || true\n" if installable else ""
|
||||
if timers:
|
||||
systemd_enable += f" systemctl enable --now {' '.join(timers)} >/dev/null 2>&1 || true\n"
|
||||
for service in services:
|
||||
systemd_enable += f" systemctl enable {service} >/dev/null 2>&1 || true\n"
|
||||
systemd_enable += f" systemctl start --no-block {service} >/dev/null 2>&1 || true\n"
|
||||
|
||||
pre_remove() {{
|
||||
systemctl disable --now {all_units_str} >/dev/null 2>&1 || true
|
||||
}}
|
||||
systemd_reload = " systemctl daemon-reload >/dev/null 2>&1 || true\n" if all_units else ""
|
||||
systemd_disable = (
|
||||
f" systemctl disable --now {all_units_str} >/dev/null 2>&1 || true\n" if all_units else ""
|
||||
)
|
||||
# Ein zusaetzlicher Cron-Eintrag ist redundant, wenn systemd verfuegbar ist - das
|
||||
# Hilfsprogramm selbst prueft das (siehe src/bin/setup-cron.rs), hier wird es einfach
|
||||
# unconditional aufgerufen.
|
||||
cron_install = f" /usr/lib/{name}/setup-cron >/dev/null 2>&1 || true\n" if has_cron_helper else ""
|
||||
cron_remove = f" rm -f /etc/cron.d/{name}\n" if has_cron_helper else ""
|
||||
|
||||
post_remove() {{
|
||||
systemctl daemon-reload >/dev/null 2>&1 || true
|
||||
}}
|
||||
"""
|
||||
return (
|
||||
"post_install() {\n" + systemd_enable + cron_install + "}\n\n"
|
||||
"post_upgrade() {\n" + systemd_reload + cron_install + "}\n\n"
|
||||
"pre_remove() {\n" + systemd_disable + cron_remove + "}\n\n"
|
||||
"post_remove() {\n" + systemd_reload + "}\n"
|
||||
)
|
||||
|
||||
|
||||
def build_package(target_triple=None, target_arch=None, pkgrel=None):
|
||||
@@ -155,6 +211,24 @@ def build_package(target_triple=None, target_arch=None, pkgrel=None):
|
||||
check=True,
|
||||
)
|
||||
|
||||
for src, dest_dir, dest_name in collect_completions():
|
||||
target_dir = os.path.join(build_dir, dest_dir)
|
||||
os.makedirs(target_dir, exist_ok=True)
|
||||
subprocess.run(
|
||||
["install", "-m", "644", src, os.path.join(target_dir, dest_name)],
|
||||
check=True,
|
||||
)
|
||||
|
||||
setup_cron_path = find_helper_binary(name, "setup-cron", target_triple)
|
||||
has_cron_helper = setup_cron_path is not None
|
||||
if has_cron_helper:
|
||||
helper_dir = os.path.join(build_dir, f"usr/lib/{name}")
|
||||
os.makedirs(helper_dir, exist_ok=True)
|
||||
subprocess.run(
|
||||
["install", "-m", "755", setup_cron_path, os.path.join(helper_dir, "setup-cron")],
|
||||
check=True,
|
||||
)
|
||||
|
||||
installed_size = subprocess.check_output(["du", "-sb", build_dir]).decode().split()[0]
|
||||
builddate = str(int(time.time()))
|
||||
|
||||
@@ -174,7 +248,8 @@ def build_package(target_triple=None, target_arch=None, pkgrel=None):
|
||||
pkginfo_lines.append(f"depend = {dep}")
|
||||
for optdep in optdepends:
|
||||
pkginfo_lines.append(f"optdepend = {optdep}")
|
||||
if all_units:
|
||||
needs_install_scriptlet = bool(all_units) or has_cron_helper
|
||||
if needs_install_scriptlet:
|
||||
pkginfo_lines.append(f"install = {name}.install")
|
||||
pkginfo_lines.append("makepkgopt = strip\n")
|
||||
|
||||
@@ -182,10 +257,10 @@ def build_package(target_triple=None, target_arch=None, pkgrel=None):
|
||||
f.write("\n".join(pkginfo_lines))
|
||||
|
||||
tar_members = [".PKGINFO", "usr"]
|
||||
if all_units:
|
||||
if needs_install_scriptlet:
|
||||
install_script_name = f"{name}.install"
|
||||
with open(os.path.join(build_dir, install_script_name), "w") as f:
|
||||
f.write(build_install_scriptlet(installable_units, all_units))
|
||||
f.write(build_install_scriptlet(name, installable_units, all_units, has_cron_helper))
|
||||
tar_members.append(install_script_name)
|
||||
|
||||
os.makedirs("target/arch", exist_ok=True)
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
//! Erzeugt die Shell-Completion-Skripte (bash/zsh/fish) für `smart-mount` als Dateien in einem
|
||||
//! Ausgabeverzeichnis - kein `smart-mount`-Subcommand mehr (siehe `smart_mount::cli::Commands`),
|
||||
//! sondern ein separates Build-Hilfsprogramm, das ausschließlich während der CI-Paketierung
|
||||
//! aufgerufen wird (siehe `.gitea/workflows/main.yaml`/`testing.yaml`): die erzeugten Dateien
|
||||
//! werden dort als normale Paket-Assets gebündelt, sodass sie beim Installieren des .deb/.rpm/
|
||||
//! .pkg.tar.zst-Pakets automatisch mit installiert und beim Entfernen automatisch mit entfernt
|
||||
//! werden - ganz ohne Zutun des Nutzers.
|
||||
//!
|
||||
//! Nutzt `smart_mount::cli::Cli` als einzige Quelle der CLI-Definition (kein separat gepflegtes
|
||||
//! Duplikat), da `cli` als `pub mod` Teil der Library ist und so auch von diesem zusätzlichen
|
||||
//! Binary-Target aus der Cargo-eigenen `src/bin/`-Autodiscovery erreichbar ist.
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use clap::CommandFactory;
|
||||
use clap_complete::Shell;
|
||||
use smart_mount::cli::Cli;
|
||||
|
||||
fn main() {
|
||||
let Some(out_dir) = std::env::args_os().nth(1).map(PathBuf::from) else {
|
||||
eprintln!("usage: generate-completions <output-dir>");
|
||||
std::process::exit(1);
|
||||
};
|
||||
std::fs::create_dir_all(&out_dir)
|
||||
.unwrap_or_else(|e| panic!("could not create '{}': {e}", out_dir.display()));
|
||||
|
||||
let mut cmd = Cli::command();
|
||||
let bin_name = cmd.get_name().to_string();
|
||||
for shell in [Shell::Bash, Shell::Zsh, Shell::Fish] {
|
||||
let path = clap_complete::generate_to(shell, &mut cmd, &bin_name, &out_dir)
|
||||
.unwrap_or_else(|e| panic!("could not generate {shell} completions: {e}"));
|
||||
println!("generated: {}", path.display());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
//! Richtet den Cron-Fallback für den periodischen `watch`-Lauf automatisch ein, sofern kein
|
||||
//! systemd verfügbar ist - kein `smart-mount service`-Subcommand mehr (siehe
|
||||
//! `smart_mount::cli::Commands`), sondern ein separates, nicht auf `PATH` liegendes
|
||||
//! Hilfsprogramm (installiert nach `/usr/lib/smart-mount/setup-cron`), das ausschließlich vom
|
||||
//! postinst-Skript der .deb/.rpm/.pkg.tar.zst-Pakete mit dem Argument `install` aufgerufen wird
|
||||
//! (siehe `packaging/deb/postinst`, `[package.metadata.generate-rpm].post_install_script` in
|
||||
//! `Cargo.toml`, sowie `scripts/package-arch.py`). Das Entfernen eines zuvor installierten
|
||||
//! Cron-Eintrags beim Deinstallieren übernehmen die jeweiligen postrm-Skripte/Scriptlets direkt
|
||||
//! (ein einfaches `rm -f`) statt dieses Programm mit `remove` aufzurufen: zum Zeitpunkt, zu dem
|
||||
//! `postrm remove` bei Debian bzw. `%postun` bei RPM laufen, sind die Paketdateien - und damit
|
||||
//! auch dieses Hilfsprogramm selbst - bereits von der Platte entfernt.
|
||||
//!
|
||||
//! Nutzt `smart_mount::systemd`/`smart_mount::config` als einzige Quelle der
|
||||
//! Cron-Installationslogik (kein separat gepflegtes Duplikat in Shell), da beide als `pub mod`
|
||||
//! Teil der Library sind und so auch von diesem zusätzlichen Binary-Target aus der
|
||||
//! Cargo-eigenen `src/bin/`-Autodiscovery erreichbar sind.
|
||||
|
||||
use smart_mount::config;
|
||||
use smart_mount::systemd;
|
||||
|
||||
fn main() {
|
||||
// Ein zusätzlicher Cron-Eintrag wäre redundant/unerwünscht, wenn systemd verfügbar ist - die
|
||||
// paketierten systemd-Units (siehe packaging/systemd/) übernehmen dann bereits alles.
|
||||
if systemd::is_available() {
|
||||
return;
|
||||
}
|
||||
|
||||
config::init();
|
||||
let watch_interval_secs = config::pairs::load()
|
||||
.map(|cfg| cfg.settings.watch_interval_secs)
|
||||
.unwrap_or(120);
|
||||
|
||||
match systemd::install_cron(watch_interval_secs) {
|
||||
Ok(systemd::CronInstallOutcome::SystemFile(path)) => {
|
||||
println!(
|
||||
"smart-mount: cron fallback installed at '{}'",
|
||||
path.display()
|
||||
);
|
||||
}
|
||||
Ok(systemd::CronInstallOutcome::Unavailable) => {
|
||||
eprintln!(
|
||||
"smart-mount: neither systemd nor '/etc/cron.d' found - the periodic 'watch' \
|
||||
call must be set up manually, e.g.:\n{}",
|
||||
systemd::crontab_equivalent(watch_interval_secs)
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("smart-mount: could not install the cron fallback: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
//! `smart-mount doctor` - prüft die im Laufe der Entwicklung angesammelten Voraussetzungen
|
||||
//! (Binaries, Gruppenmitgliedschaft, fstab-Setup, Scheduler) gebündelt an einer Stelle.
|
||||
|
||||
use smart_mount::config;
|
||||
use smart_mount::doctor::{self, CheckStatus};
|
||||
use crate::config;
|
||||
use crate::doctor::{self, CheckStatus};
|
||||
|
||||
pub async fn run(json: bool) -> anyhow::Result<()> {
|
||||
let cfg = config::pairs::load()?;
|
||||
|
||||
+45
-16
@@ -12,10 +12,8 @@ use std::str::FromStr;
|
||||
use clap::{Args, Subcommand};
|
||||
use dialoguer::{Confirm, Input, Password, Select};
|
||||
|
||||
use smart_mount::config::{
|
||||
self, AppConfig, CloudSide, DrivePair, LocalAddress, LocalSide, MountKind,
|
||||
};
|
||||
use smart_mount::db::credentials::{Credential, CredentialStore, Side};
|
||||
use crate::config::{self, AppConfig, CloudSide, DrivePair, LocalAddress, LocalSide, MountKind};
|
||||
use crate::db::credentials::{Credential, CredentialStore, Side};
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum DriveAction {
|
||||
@@ -56,6 +54,8 @@ pub struct DriveArgs {
|
||||
local_ip: Option<Ipv4Addr>,
|
||||
#[arg(long, conflicts_with = "local_ip")]
|
||||
local_mac: Option<String>,
|
||||
/// Meaning depends on '--local-kind': the SMB share name (e.g. 'media'), the NFS export
|
||||
/// path (e.g. '/export/media'), or the WebDAV URL path (e.g. '/remote.php/dav/files/user').
|
||||
#[arg(long)]
|
||||
local_share: Option<String>,
|
||||
#[arg(long)]
|
||||
@@ -72,6 +72,8 @@ pub struct DriveArgs {
|
||||
cloud_kind: Option<MountKind>,
|
||||
#[arg(long)]
|
||||
cloud_host: Option<String>,
|
||||
/// Meaning depends on '--cloud-kind': the SMB share name (e.g. 'media'), the NFS export
|
||||
/// path (e.g. '/export/media'), or the WebDAV URL path (e.g. '/remote.php/dav/files/user').
|
||||
#[arg(long)]
|
||||
cloud_share: Option<String>,
|
||||
#[arg(long)]
|
||||
@@ -90,6 +92,18 @@ pub struct DriveArgs {
|
||||
non_interactive: bool,
|
||||
}
|
||||
|
||||
/// Beschriftet das `share`-Feld abhängig vom bereits gewählten Mount-Typ statt des
|
||||
/// generischen "share/export path" - die drei Backends legen dort inhaltlich verschiedene
|
||||
/// Dinge ab (SMB-Freigabename, NFS-Export-Pfad, WebDAV-URL-Pfad), siehe
|
||||
/// `mount::target::format_source`.
|
||||
fn share_field_label(kind: MountKind, prefix: &str) -> String {
|
||||
match kind {
|
||||
MountKind::WebDav => format!("{prefix} WebDAV path (e.g. '/remote.php/dav/files/user')"),
|
||||
MountKind::Smb => format!("{prefix} share name (e.g. 'media')"),
|
||||
MountKind::Nfs => format!("{prefix} export path (e.g. '/export/media')"),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run(action: DriveAction) -> anyhow::Result<()> {
|
||||
match action {
|
||||
DriveAction::Add(args) => add(args).await,
|
||||
@@ -129,7 +143,7 @@ async fn remove(id: &str) -> anyhow::Result<()> {
|
||||
|
||||
// Vor dem Entfernen aus der Config nachschlagen, damit wir hinterher noch wissen, welche
|
||||
// Mount-Typen/Adressen betroffen sind - nötig, um die passenden Klartext-Zugangsdaten
|
||||
// (davfs2 secrets, .cred-Datei) aufzuräumen, siehe smart_mount::mount::cleanup_credentials.
|
||||
// (davfs2 secrets, .cred-Datei) aufzuräumen, siehe crate::mount::cleanup_credentials.
|
||||
let cfg = config::pairs::load()?;
|
||||
let pair = config::pairs::find_pair(&cfg, id)?;
|
||||
|
||||
@@ -138,7 +152,7 @@ async fn remove(id: &str) -> anyhow::Result<()> {
|
||||
// darf das Entfernen aus der Konfiguration nicht blockieren - sonst käme der Nutzer nicht
|
||||
// mehr an sein eigenes `drive remove` heran, ohne vorher erst als root/mit den richtigen
|
||||
// Rechten manuell auszuhängen.
|
||||
if let Err(e) = smart_mount::reconcile::unmount_pair(&pair, &cfg.settings).await {
|
||||
if let Err(e) = crate::reconcile::unmount_pair(&pair, &cfg.settings).await {
|
||||
logger_ctdra::warn(
|
||||
"drive",
|
||||
&format!("could not unmount drive pair '{id}' before removal: {e}"),
|
||||
@@ -147,8 +161,9 @@ async fn remove(id: &str) -> anyhow::Result<()> {
|
||||
config::pairs::remove_pair(id)?;
|
||||
let creds = CredentialStore::open().await?;
|
||||
creds.delete(id, None).await?;
|
||||
smart_mount::mount::cleanup_credentials(&pair, &cfg.settings);
|
||||
crate::mount::cleanup_credentials(&pair, &cfg.settings);
|
||||
|
||||
logger_ctdra::info("drive", &format!("drive pair '{id}' removed"));
|
||||
println!("Drive pair '{id}' removed.");
|
||||
Ok(())
|
||||
}
|
||||
@@ -165,8 +180,14 @@ async fn add(args: DriveArgs) -> anyhow::Result<()> {
|
||||
|
||||
let local_kind = resolve_kind(args.local_kind, None, "Local mount type", ni)?;
|
||||
let local_address = resolve_local_address(args.local_ip, args.local_mac, None, ni)?;
|
||||
let local_share = resolve_field(args.local_share, None, "Local share/export path", ni, true)?
|
||||
.expect("required");
|
||||
let local_share = resolve_field(
|
||||
args.local_share,
|
||||
None,
|
||||
&share_field_label(local_kind, "Local"),
|
||||
ni,
|
||||
true,
|
||||
)?
|
||||
.expect("required");
|
||||
let local_password_flag = read_password_flag(args.local_password, args.local_password_stdin)?;
|
||||
let (local_username, local_password) = resolve_credentials(
|
||||
local_kind,
|
||||
@@ -180,8 +201,14 @@ async fn add(args: DriveArgs) -> anyhow::Result<()> {
|
||||
let cloud_kind = resolve_kind(args.cloud_kind, None, "Cloud mount type", ni)?;
|
||||
let cloud_host = resolve_field(args.cloud_host, None, "Cloud server address/URL", ni, true)?
|
||||
.expect("required");
|
||||
let cloud_share = resolve_field(args.cloud_share, None, "Cloud share/export path", ni, true)?
|
||||
.expect("required");
|
||||
let cloud_share = resolve_field(
|
||||
args.cloud_share,
|
||||
None,
|
||||
&share_field_label(cloud_kind, "Cloud"),
|
||||
ni,
|
||||
true,
|
||||
)?
|
||||
.expect("required");
|
||||
let cloud_password_flag = read_password_flag(args.cloud_password, args.cloud_password_stdin)?;
|
||||
let (cloud_username, cloud_password) = resolve_credentials(
|
||||
cloud_kind,
|
||||
@@ -241,6 +268,7 @@ async fn add(args: DriveArgs) -> anyhow::Result<()> {
|
||||
.await?;
|
||||
}
|
||||
|
||||
logger_ctdra::info("drive", &format!("drive pair '{id}' created"));
|
||||
println!("Drive pair '{id}' created.");
|
||||
Ok(())
|
||||
}
|
||||
@@ -270,7 +298,7 @@ async fn edit(id: &str, args: DriveArgs) -> anyhow::Result<()> {
|
||||
let local_share = resolve_field(
|
||||
args.local_share,
|
||||
Some(&existing.local.share),
|
||||
"Local share/export path",
|
||||
&share_field_label(local_kind, "Local"),
|
||||
ni,
|
||||
true,
|
||||
)?
|
||||
@@ -305,7 +333,7 @@ async fn edit(id: &str, args: DriveArgs) -> anyhow::Result<()> {
|
||||
let cloud_share = resolve_field(
|
||||
args.cloud_share,
|
||||
Some(&existing.cloud.share),
|
||||
"Cloud share/export path",
|
||||
&share_field_label(cloud_kind, "Cloud"),
|
||||
ni,
|
||||
true,
|
||||
)?
|
||||
@@ -326,7 +354,7 @@ async fn edit(id: &str, args: DriveArgs) -> anyhow::Result<()> {
|
||||
// würde ein aktiver Mount verwaisen. Nur eine explizite '--mount-point'-Angabe ändert ihn.
|
||||
let mount_point = match args.mount_point {
|
||||
Some(p) => {
|
||||
if smart_mount::mount::target::active_side(&existing).is_some() {
|
||||
if crate::mount::target::active_side(&existing).is_some() {
|
||||
println!(
|
||||
"Warning: pair '{id}' appears to be actively mounted - the old backing \
|
||||
directories will be orphaned. Run 'sudo smart-mount unmount --name {id}' \
|
||||
@@ -373,6 +401,7 @@ async fn edit(id: &str, args: DriveArgs) -> anyhow::Result<()> {
|
||||
.await?;
|
||||
}
|
||||
|
||||
logger_ctdra::info("drive", &format!("drive pair '{id}' updated"));
|
||||
println!("Drive pair '{id}' updated.");
|
||||
Ok(())
|
||||
}
|
||||
@@ -484,7 +513,7 @@ fn invoking_user() -> Option<String> {
|
||||
|
||||
/// Optional: falls gesetzt, bekommt dieser Nutzer bei CIFS/WebDAV vollen Zugriff
|
||||
/// (uid=/gid=/file_mode=0700/dir_mode=0700) statt der sonst üblichen root-Ownership - der
|
||||
/// Mount selbst läuft immer als root (siehe [`smart_mount::systemd`]). `default_owner` ist der
|
||||
/// Mount selbst läuft immer als root (siehe [`crate::systemd`]). `default_owner` ist der
|
||||
/// vorbelegte/vorgeschlagene Wert (bei `add`: der einrichtende Nutzer, siehe `invoking_user`;
|
||||
/// bei `edit`: der bisherige `owner_user`) - immer überschreibbar per Flag, im interaktiven
|
||||
/// Prompt auch durch Ablehnen oder einen anderen Namen.
|
||||
@@ -662,7 +691,7 @@ fn resolve_credentials(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use smart_mount::config::LocalAddress;
|
||||
use crate::config::LocalAddress;
|
||||
use std::net::Ipv4Addr;
|
||||
|
||||
fn sample_pair(mount_point: &str) -> DrivePair {
|
||||
|
||||
+24
-24
@@ -3,16 +3,14 @@
|
||||
pub mod doctor;
|
||||
pub mod drive;
|
||||
pub mod mount_cmd;
|
||||
pub mod service;
|
||||
pub mod status;
|
||||
pub mod watch;
|
||||
|
||||
use clap::{CommandFactory, Parser, Subcommand};
|
||||
use clap_complete::Shell;
|
||||
use clap::{Parser, Subcommand};
|
||||
|
||||
/// Startet den Prozess bei Bedarf transparent über `sudo` neu, falls nicht bereits als root
|
||||
/// aufgerufen. Mounten läuft ausschließlich als root/System-Dienst (siehe
|
||||
/// [`smart_mount::systemd`]) - es gibt seit dem Wegfall des Nutzerkontexts keine
|
||||
/// [`crate::systemd`]) - es gibt seit dem Wegfall des Nutzerkontexts keine
|
||||
/// unprivilegierte Mount-Variante mehr, für die ein Rechte-Check hier zu früh käme.
|
||||
///
|
||||
/// `run_as_root()` ersetzt den aktuellen Prozess per `execve` und kehrt bei Erfolg nie zurück
|
||||
@@ -20,9 +18,14 @@ use clap_complete::Shell;
|
||||
/// `sudo` nicht gefunden).
|
||||
pub(crate) fn require_root(context: &str) -> anyhow::Result<()> {
|
||||
if !sudo_ctdra::is_run_as_root() {
|
||||
logger_ctdra::info(
|
||||
"cli",
|
||||
&format!("'{context}' requires root - re-executing via sudo"),
|
||||
);
|
||||
let err = sudo_ctdra::run_as_root();
|
||||
anyhow::bail!("'{context}' requires root privileges: could not re-exec via sudo: {err}");
|
||||
}
|
||||
logger_ctdra::debug("cli", &format!("'{context}': already running as root"));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -70,42 +73,39 @@ pub enum Commands {
|
||||
},
|
||||
/// A single reconcile pass (local/cloud switching) - meant for systemd timers/cron.
|
||||
Watch,
|
||||
/// Sets up (or removes) the cron fallback for systems without (or not using) systemd -
|
||||
/// the systemd service itself is installed/removed automatically by the .deb/.rpm/
|
||||
/// .pkg.tar.zst package, not via this CLI.
|
||||
Service {
|
||||
#[command(subcommand)]
|
||||
action: service::ServiceAction,
|
||||
},
|
||||
/// Checks prerequisites (binaries, scheduler).
|
||||
Doctor {
|
||||
/// Output as JSON instead of text - for scripts.
|
||||
#[arg(long)]
|
||||
json: bool,
|
||||
},
|
||||
/// Prints a shell completion script, e.g.:
|
||||
/// `smart-mount completions bash > /etc/bash_completion.d/smart-mount`.
|
||||
Completions { shell: Shell },
|
||||
}
|
||||
|
||||
/// Kurzname eines Subcommands fürs Logging (siehe [`dispatch`]) - kein `Debug`-Derive auf
|
||||
/// `Commands` nötig, das würde auch die (teils sensiblen) Argument-Felder mit abdrucken.
|
||||
fn command_label(cmd: &Commands) -> &'static str {
|
||||
match cmd {
|
||||
Commands::Drive { .. } => "drive",
|
||||
Commands::Mount { .. } => "mount",
|
||||
Commands::Unmount { .. } => "unmount",
|
||||
Commands::Status { .. } => "status",
|
||||
Commands::Watch => "watch",
|
||||
Commands::Doctor { .. } => "doctor",
|
||||
}
|
||||
}
|
||||
|
||||
/// Führt das per `Cli` geparste Subcommand aus.
|
||||
pub async fn dispatch(cli: Cli) -> anyhow::Result<()> {
|
||||
logger_ctdra::debug(
|
||||
"cli",
|
||||
&format!("dispatching '{}'", command_label(&cli.command)),
|
||||
);
|
||||
match cli.command {
|
||||
Commands::Drive { action } => drive::run(*action).await,
|
||||
Commands::Mount { name, all } => mount_cmd::run_mount(name, all).await,
|
||||
Commands::Unmount { name, all } => mount_cmd::run_unmount(name, all).await,
|
||||
Commands::Status { name, json } => status::run(name, json).await,
|
||||
Commands::Watch => watch::run().await,
|
||||
Commands::Service { action } => service::run(action),
|
||||
Commands::Doctor { json } => doctor::run(json).await,
|
||||
Commands::Completions { shell } => {
|
||||
clap_complete::generate(
|
||||
shell,
|
||||
&mut Cli::command(),
|
||||
"smart-mount",
|
||||
&mut std::io::stdout(),
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+33
-6
@@ -1,11 +1,11 @@
|
||||
//! `smart-mount mount` / `smart-mount unmount`.
|
||||
|
||||
use smart_mount::config::{self, DrivePair};
|
||||
use smart_mount::db::credentials::CredentialStore;
|
||||
use smart_mount::reconcile;
|
||||
use crate::config::{self, DrivePair};
|
||||
use crate::db::credentials::CredentialStore;
|
||||
use crate::reconcile::{self, Action};
|
||||
|
||||
fn select_pairs(
|
||||
cfg: &smart_mount::config::AppConfig,
|
||||
cfg: &crate::config::AppConfig,
|
||||
name: Option<&str>,
|
||||
all: bool,
|
||||
) -> anyhow::Result<Vec<DrivePair>> {
|
||||
@@ -26,11 +26,27 @@ pub async fn run_mount(name: Option<String>, all: bool) -> anyhow::Result<()> {
|
||||
println!("No matching drive pairs found.");
|
||||
return Ok(());
|
||||
}
|
||||
logger_ctdra::info(
|
||||
"mount",
|
||||
&format!("mount: processing {} drive pair(s)", pairs.len()),
|
||||
);
|
||||
|
||||
let creds = CredentialStore::open().await?;
|
||||
let mut had_failure = false;
|
||||
for pair in &pairs {
|
||||
let outcome = reconcile::reconcile_pair(pair, &cfg.settings, &creds).await;
|
||||
print_outcome(&outcome);
|
||||
if matches!(outcome.action, Action::Failed(_)) {
|
||||
had_failure = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Ohne dies wäre `smart-mount-mount.service` (siehe packaging/systemd/) beim Booten selbst
|
||||
// dann "erfolgreich" (Exit-Code 0), wenn tatsächlich kein einziges Laufwerkspaar gemountet
|
||||
// werden konnte - `systemctl status` würde den Fehlschlag also nie sichtbar machen. Analog
|
||||
// zu `watch::run` unten.
|
||||
if had_failure {
|
||||
anyhow::bail!("at least one drive pair could not be mounted");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -43,18 +59,29 @@ pub async fn run_unmount(name: Option<String>, all: bool) -> anyhow::Result<()>
|
||||
println!("No matching drive pairs found.");
|
||||
return Ok(());
|
||||
}
|
||||
logger_ctdra::info(
|
||||
"mount",
|
||||
&format!("unmount: processing {} drive pair(s)", pairs.len()),
|
||||
);
|
||||
|
||||
let mut had_failure = false;
|
||||
for pair in &pairs {
|
||||
match reconcile::unmount_pair(pair, &cfg.settings).await {
|
||||
Ok(_) => println!("{} ({}): unmounted", pair.name, pair.id),
|
||||
Err(e) => println!("{} ({}): ERROR: {e}", pair.name, pair.id),
|
||||
Err(e) => {
|
||||
println!("{} ({}): ERROR: {e}", pair.name, pair.id);
|
||||
had_failure = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if had_failure {
|
||||
anyhow::bail!("at least one drive pair could not be unmounted");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn print_outcome(outcome: &reconcile::ReconcileOutcome) {
|
||||
use reconcile::Action;
|
||||
let action_str = match &outcome.action {
|
||||
Action::NoOp => "no change".to_string(),
|
||||
Action::MountedLocal => "mounted local".to_string(),
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
//! `smart-mount service crontab [--remove]`.
|
||||
|
||||
use clap::Subcommand;
|
||||
use smart_mount::config;
|
||||
use smart_mount::systemd;
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum ServiceAction {
|
||||
/// Sets up (or, with '--remove', tears down) periodic execution via
|
||||
/// '/etc/cron.d/smart-mount' - a manual fallback for systems that don't use the packaged
|
||||
/// systemd service (see 'packaging/systemd/' in the source tree). Requires root, same as
|
||||
/// 'mount'/'watch' - mounting always runs as root.
|
||||
Crontab {
|
||||
/// Removes a previously installed cron entry instead of installing one.
|
||||
#[arg(long)]
|
||||
remove: bool,
|
||||
},
|
||||
}
|
||||
|
||||
pub fn run(action: ServiceAction) -> anyhow::Result<()> {
|
||||
crate::cli::require_root("service crontab")?;
|
||||
match action {
|
||||
ServiceAction::Crontab { remove: false } => install(),
|
||||
ServiceAction::Crontab { remove: true } => uninstall(),
|
||||
}
|
||||
}
|
||||
|
||||
fn install() -> anyhow::Result<()> {
|
||||
let cfg = config::pairs::load()?;
|
||||
let interval = cfg.settings.watch_interval_secs;
|
||||
|
||||
match systemd::install_cron(interval)? {
|
||||
systemd::CronInstallOutcome::SystemFile(path) => {
|
||||
println!("Cron entry written: {}", path.display());
|
||||
}
|
||||
systemd::CronInstallOutcome::Unavailable => {
|
||||
println!(
|
||||
"No cron mechanism found ('/etc/cron.d' is missing) - here are the lines for manual entry:"
|
||||
);
|
||||
print!("{}", systemd::crontab_equivalent(interval));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn uninstall() -> anyhow::Result<()> {
|
||||
match systemd::uninstall_cron()? {
|
||||
systemd::CronUninstallOutcome::Removed => println!("Cron entry removed."),
|
||||
systemd::CronUninstallOutcome::NotPresent => {
|
||||
println!("Nothing to remove - no cron entry was installed.")
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
+5
-5
@@ -2,10 +2,10 @@
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
use smart_mount::config;
|
||||
use smart_mount::db::credentials::Side;
|
||||
use smart_mount::mount::target;
|
||||
use smart_mount::network;
|
||||
use crate::config;
|
||||
use crate::db::credentials::Side;
|
||||
use crate::mount::target;
|
||||
use crate::network;
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct PairStatus {
|
||||
@@ -48,7 +48,7 @@ pub async fn run(name: Option<String>, json: bool) -> anyhow::Result<()> {
|
||||
// wiederverwendet, statt sie für beide Zwecke unabhängig voneinander ein zweites
|
||||
// Mal aufzulösen.
|
||||
let resolved_local_ip =
|
||||
smart_mount::network::address::resolve_ip(&pair.local.address, &cfg.settings);
|
||||
crate::network::address::resolve_ip(&pair.local.address, &cfg.settings);
|
||||
let local_source = resolved_local_ip
|
||||
.as_ref()
|
||||
.map(|ip| target::format_local_source(&pair.local, *ip))
|
||||
|
||||
+15
-4
@@ -1,10 +1,9 @@
|
||||
//! `smart-mount watch` - ein einzelner Reconcile-Durchlauf, gedacht für systemd-Timer/Cron.
|
||||
|
||||
use smart_mount::config;
|
||||
use smart_mount::db::credentials::CredentialStore;
|
||||
use smart_mount::reconcile::{self, Action};
|
||||
|
||||
use crate::cli::mount_cmd::print_outcome;
|
||||
use crate::config;
|
||||
use crate::db::credentials::CredentialStore;
|
||||
use crate::reconcile::{self, Action};
|
||||
|
||||
pub async fn run() -> anyhow::Result<()> {
|
||||
crate::cli::require_root("watch")?;
|
||||
@@ -19,6 +18,18 @@ pub async fn run() -> anyhow::Result<()> {
|
||||
had_failure = true;
|
||||
}
|
||||
}
|
||||
logger_ctdra::info(
|
||||
"watch",
|
||||
&format!(
|
||||
"watch pass done: {} pair(s), {}",
|
||||
outcomes.len(),
|
||||
if had_failure {
|
||||
"with failures"
|
||||
} else {
|
||||
"no failures"
|
||||
}
|
||||
),
|
||||
);
|
||||
|
||||
if had_failure {
|
||||
anyhow::bail!("at least one drive pair could not be reconciled");
|
||||
|
||||
+3
-1
@@ -19,5 +19,7 @@ pub use schema::{
|
||||
pub fn init() {
|
||||
config_ctdra::set_config_name("config");
|
||||
let program = config_ctdra::get_program_name();
|
||||
config_ctdra::set_custom_dir(std::path::PathBuf::from("/etc").join(program));
|
||||
let dir = std::path::PathBuf::from("/etc").join(program);
|
||||
logger_ctdra::debug("config", &format!("config directory: {}", dir.display()));
|
||||
config_ctdra::set_custom_dir(dir);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,10 @@ pub fn load() -> Result<AppConfig> {
|
||||
|
||||
/// Fügt ein neues Laufwerkspaar hinzu (load-mutate-store in einem atomaren Schritt).
|
||||
pub fn add_pair(pair: DrivePair) -> Result<AppConfig> {
|
||||
logger_ctdra::debug(
|
||||
"config",
|
||||
&format!("writing new pair '{}' to config", pair.id),
|
||||
);
|
||||
Ok(config_ctdra::modify::<AppConfig, _>(|cfg| {
|
||||
cfg.pairs.push(pair.clone());
|
||||
})?)
|
||||
@@ -18,6 +22,7 @@ pub fn add_pair(pair: DrivePair) -> Result<AppConfig> {
|
||||
/// Ersetzt ein bestehendes Laufwerkspaar (Vergleich über `id`).
|
||||
pub fn update_pair(pair: DrivePair) -> Result<AppConfig> {
|
||||
let id = pair.id.clone();
|
||||
logger_ctdra::debug("config", &format!("writing updated pair '{id}' to config"));
|
||||
let updated = config_ctdra::modify::<AppConfig, _>(move |cfg| {
|
||||
if let Some(existing) = cfg.pairs.iter_mut().find(|p| p.id == pair.id) {
|
||||
*existing = pair.clone();
|
||||
@@ -31,6 +36,7 @@ pub fn update_pair(pair: DrivePair) -> Result<AppConfig> {
|
||||
|
||||
/// Entfernt ein Laufwerkspaar per ID.
|
||||
pub fn remove_pair(id: &str) -> Result<AppConfig> {
|
||||
logger_ctdra::debug("config", &format!("removing pair '{id}' from config"));
|
||||
// Die "vorher"-Länge wird INNERHALB desselben `modify`-Aufrufs (auf der bereits frisch
|
||||
// geladenen `cfg`) ermittelt statt über einen separaten, vorgelagerten `load()`-Aufruf:
|
||||
// zwischen zwei getrennten Aufrufen könnte ein nebenläufiger Schreiber die Paarliste
|
||||
|
||||
@@ -32,8 +32,16 @@ mod file_key {
|
||||
|
||||
pub fn load_or_create(path: &Path) -> Result<[u8; 32]> {
|
||||
if path.exists() {
|
||||
logger_ctdra::debug(
|
||||
"crypto",
|
||||
&format!("loading master key from '{}'", path.display()),
|
||||
);
|
||||
return read(path);
|
||||
}
|
||||
logger_ctdra::info(
|
||||
"crypto",
|
||||
&format!("creating new master key at '{}'", path.display()),
|
||||
);
|
||||
create(path)
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ pub const NONCE_LEN: usize = 12;
|
||||
/// aes-gcms Re-Export-Kette nicht automatisch aktiviert wird). `getrandom::fill` ist
|
||||
/// unabhängig davon stabil und genau für diesen Zweck gedacht.
|
||||
pub fn encrypt(plaintext: &[u8], key: &[u8; 32]) -> Result<(Vec<u8>, Vec<u8>)> {
|
||||
logger_ctdra::debug("crypto", "encrypting credential data");
|
||||
let cipher = Aes256Gcm::new(&Key::<Aes256Gcm>::from(*key));
|
||||
|
||||
let mut nonce_bytes = [0u8; NONCE_LEN];
|
||||
@@ -44,6 +45,7 @@ pub fn encrypt(plaintext: &[u8], key: &[u8; 32]) -> Result<(Vec<u8>, Vec<u8>)> {
|
||||
|
||||
/// Entschlüsselt einen zuvor mit [`encrypt`] erzeugten Ciphertext.
|
||||
pub fn decrypt(ciphertext: &[u8], nonce: &[u8], key: &[u8; 32]) -> Result<Vec<u8>> {
|
||||
logger_ctdra::debug("crypto", "decrypting credential data");
|
||||
if nonce.len() != NONCE_LEN {
|
||||
return Err(Error::Crypto(format!(
|
||||
"invalid nonce length: expected {NONCE_LEN}, got {}",
|
||||
|
||||
@@ -58,6 +58,13 @@ impl CredentialStore {
|
||||
domain: Option<&str>,
|
||||
password: &str,
|
||||
) -> Result<()> {
|
||||
logger_ctdra::debug(
|
||||
"db",
|
||||
&format!(
|
||||
"storing credential for pair '{pair_id}' ({})",
|
||||
side.as_str()
|
||||
),
|
||||
);
|
||||
let key = resolve_master_key()?;
|
||||
let (ciphertext, nonce) = crypto::encrypt(password.as_bytes(), &key)?;
|
||||
let now = now_unix();
|
||||
@@ -85,6 +92,13 @@ impl CredentialStore {
|
||||
|
||||
/// Liest und entschlüsselt die Zugangsdaten für `pair_id`/`side`, falls vorhanden.
|
||||
pub async fn get(&self, pair_id: &str, side: Side) -> Result<Option<Credential>> {
|
||||
logger_ctdra::debug(
|
||||
"db",
|
||||
&format!(
|
||||
"reading credential for pair '{pair_id}' ({})",
|
||||
side.as_str()
|
||||
),
|
||||
);
|
||||
let mut rows = self
|
||||
.conn
|
||||
.query(
|
||||
@@ -117,6 +131,13 @@ impl CredentialStore {
|
||||
|
||||
/// Löscht Zugangsdaten. `side = None` löscht beide Seiten (z. B. beim Entfernen eines Paars).
|
||||
pub async fn delete(&self, pair_id: &str, side: Option<Side>) -> Result<()> {
|
||||
logger_ctdra::debug(
|
||||
"db",
|
||||
&format!(
|
||||
"deleting credential(s) for pair '{pair_id}' ({})",
|
||||
side.map(|s| s.as_str()).unwrap_or("both sides")
|
||||
),
|
||||
);
|
||||
match side {
|
||||
Some(side) => {
|
||||
self.conn
|
||||
|
||||
@@ -21,6 +21,7 @@ pub fn resolve_db_path() -> PathBuf {
|
||||
/// Öffnet (und initialisiert bei Bedarf) die lokale Datenbank am aufgelösten Pfad.
|
||||
pub async fn open() -> Result<turso::Connection> {
|
||||
let path = resolve_db_path();
|
||||
logger_ctdra::debug("db", &format!("opening database at '{}'", path.display()));
|
||||
if let Some(dir) = path.parent() {
|
||||
tokio::fs::create_dir_all(dir)
|
||||
.await
|
||||
|
||||
+224
-25
@@ -1,12 +1,86 @@
|
||||
//! Diagnose-Checks für `smart-mount doctor` - prüft die im Laufe der Entwicklung
|
||||
//! angesammelten Voraussetzungen (Binaries, Scheduler) gebündelt an einer Stelle, statt sie
|
||||
//! einzeln erst beim Mount-Fehlschlag zu entdecken.
|
||||
//!
|
||||
//! Installationshinweise (fehlendes Paket, Scheduler-Fallback) sind bewusst je nach
|
||||
//! erkannter Distribution unterschiedlich (siehe [`PackageFamily`]): auf einem Debian-System
|
||||
//! soll `doctor` nur den `apt`-Hinweis zeigen, nicht zusätzlich Fedora/Arch-Varianten, die dort
|
||||
//! ohnehin nicht anwendbar sind.
|
||||
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::config::{AppConfig, LocalAddress, MountKind};
|
||||
use crate::mount;
|
||||
|
||||
/// Grob erkannte Paketmanager-Familie des laufenden Systems, allein zur Auswahl der passenden
|
||||
/// Installationshinweise in `doctor` - keine vollständige Distributions-Erkennung.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum PackageFamily {
|
||||
/// Debian, Ubuntu und Ableitungen (`apt`).
|
||||
Debian,
|
||||
/// Fedora, RHEL/CentOS und Ableitungen (`dnf`).
|
||||
Fedora,
|
||||
/// Arch Linux und Ableitungen (Manjaro, EndeavourOS, ...) (`pacman`).
|
||||
Arch,
|
||||
/// Nicht erkannt - Installationshinweise bleiben generisch (nur Paketname, kein Befehl).
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl PackageFamily {
|
||||
fn install_command(self, package: &str) -> Option<String> {
|
||||
match self {
|
||||
PackageFamily::Debian => Some(format!("apt install {package}")),
|
||||
PackageFamily::Fedora => Some(format!("dnf install {package}")),
|
||||
PackageFamily::Arch => Some(format!("pacman -S {package}")),
|
||||
PackageFamily::Unknown => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Liest `/etc/os-release` (auf praktisch jedem modernen Linux-System vorhanden, siehe
|
||||
/// `man os-release`) und leitet daraus die [`PackageFamily`] ab.
|
||||
fn detect_package_family() -> PackageFamily {
|
||||
std::fs::read_to_string("/etc/os-release")
|
||||
.map(|contents| parse_package_family(&contents))
|
||||
.unwrap_or(PackageFamily::Unknown)
|
||||
}
|
||||
|
||||
/// Reine, testbare Parse-Funktion: sammelt `ID=`/`ID_LIKE=` aus dem `os-release`-Inhalt (deckt
|
||||
/// z. B. Ubuntu `ID=ubuntu ID_LIKE=debian` oder Manjaro `ID=manjaro ID_LIKE=arch` mit ab, nicht
|
||||
/// nur die jeweilige "reine" Distribution) und ordnet sie grob einer [`PackageFamily`] zu.
|
||||
fn parse_package_family(os_release: &str) -> PackageFamily {
|
||||
let mut fields = String::new();
|
||||
for line in os_release.lines() {
|
||||
if let Some(value) = line
|
||||
.strip_prefix("ID=")
|
||||
.or_else(|| line.strip_prefix("ID_LIKE="))
|
||||
{
|
||||
fields.push(' ');
|
||||
fields.push_str(value.trim_matches('"'));
|
||||
}
|
||||
}
|
||||
let fields = fields.to_lowercase();
|
||||
if fields.contains("debian") {
|
||||
PackageFamily::Debian
|
||||
} else if fields.contains("fedora") || fields.contains("rhel") {
|
||||
PackageFamily::Fedora
|
||||
} else if fields.contains("arch") {
|
||||
PackageFamily::Arch
|
||||
} else {
|
||||
PackageFamily::Unknown
|
||||
}
|
||||
}
|
||||
|
||||
/// Formatiert einen Installationshinweis für `package`, inkl. des passenden Befehls für die
|
||||
/// erkannte [`PackageFamily`] (kein Befehl bei [`PackageFamily::Unknown`] - dann nur der
|
||||
/// Paketname, ohne eine der drei Varianten zu raten).
|
||||
fn install_hint(package: &str, family: PackageFamily) -> String {
|
||||
match family.install_command(package) {
|
||||
Some(cmd) => format!("install package '{package}' ({cmd})"),
|
||||
None => format!("install package '{package}'"),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum CheckStatus {
|
||||
Ok,
|
||||
@@ -45,16 +119,17 @@ fn fail(label: impl Into<String>, detail: impl Into<String>) -> CheckResult {
|
||||
|
||||
/// Führt alle Checks gegen die aktuelle Konfiguration aus.
|
||||
pub fn run_checks(cfg: &AppConfig) -> Vec<CheckResult> {
|
||||
let family = detect_package_family();
|
||||
let mut results = Vec::new();
|
||||
|
||||
results.push(check_timeout_binary());
|
||||
results.push(check_scheduler());
|
||||
results.push(check_scheduler(family));
|
||||
results.push(check_mount_base_dir(&cfg.settings.mount_base_dir));
|
||||
|
||||
let used_kinds = used_mount_kinds(cfg);
|
||||
for kind in [MountKind::WebDav, MountKind::Smb, MountKind::Nfs] {
|
||||
if used_kinds.contains(&kind) {
|
||||
results.push(check_backend(kind));
|
||||
results.push(check_backend(kind, family));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,35 +181,99 @@ fn check_binary(name: &str, binary: &str, install_hint: &str) -> CheckResult {
|
||||
}
|
||||
}
|
||||
|
||||
fn check_backend(kind: MountKind) -> CheckResult {
|
||||
fn check_backend(kind: MountKind, family: PackageFamily) -> CheckResult {
|
||||
let backend = mount::backend_for(kind);
|
||||
match backend.check_available() {
|
||||
Ok(()) => ok(format!("Backend: {}", backend.name()), "available"),
|
||||
Err(e) => fail(format!("Backend: {}", backend.name()), e.to_string()),
|
||||
if backend.check_available().is_ok() {
|
||||
return ok(format!("Backend: {}", backend.name()), "available");
|
||||
}
|
||||
|
||||
let (missing_binary, package) = match kind {
|
||||
MountKind::WebDav => ("mount.davfs", "davfs2"),
|
||||
MountKind::Smb => ("mount.cifs", "cifs-utils"),
|
||||
MountKind::Nfs => (
|
||||
"mount.nfs/mount.nfs4",
|
||||
match family {
|
||||
PackageFamily::Debian => "nfs-common",
|
||||
_ => "nfs-utils",
|
||||
},
|
||||
),
|
||||
};
|
||||
fail(
|
||||
format!("Backend: {}", backend.name()),
|
||||
format!(
|
||||
"'{missing_binary}' not found - {}",
|
||||
install_hint(package, family)
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fn check_scheduler() -> CheckResult {
|
||||
let systemd = crate::systemd::is_available();
|
||||
let cron_d = std::path::Path::new("/etc/cron.d").is_dir();
|
||||
let crontab = mount::binary_available("crontab");
|
||||
/// Prüft, ob die periodische Ausführung tatsächlich eingerichtet ist UND läuft - nicht nur, ob
|
||||
/// `systemctl`/ein Cron-Mechanismus grundsätzlich auf dem System vorhanden wäre (das allein
|
||||
/// sagt nichts darüber aus, ob smart-mounts eigene Units/Cron-Eintrag auch tatsächlich
|
||||
/// installiert sind, siehe [`crate::systemd::is_unit_installed`]/[`crate::systemd::is_cron_installed`]).
|
||||
fn check_scheduler(family: PackageFamily) -> CheckResult {
|
||||
let timer_installed = crate::systemd::is_unit_installed("smart-mount-watch.timer");
|
||||
let mount_installed = crate::systemd::is_unit_installed("smart-mount-mount.service");
|
||||
|
||||
if systemd {
|
||||
ok(
|
||||
"Scheduler",
|
||||
"systemd found - the packaged .deb/.rpm/.pkg.tar.zst installs/enables the smart-mount systemd service automatically",
|
||||
)
|
||||
} else if cron_d || crontab {
|
||||
warn(
|
||||
"Scheduler",
|
||||
"no systemd, but cron found - run 'smart-mount service crontab' to set up the periodic 'watch' call",
|
||||
)
|
||||
} else {
|
||||
fail(
|
||||
"Scheduler",
|
||||
"neither systemd nor cron found - periodic 'watch' must be set up manually",
|
||||
)
|
||||
if timer_installed || mount_installed {
|
||||
let timer_enabled = crate::systemd::is_unit_enabled("smart-mount-watch.timer");
|
||||
let timer_active = crate::systemd::is_unit_active("smart-mount-watch.timer");
|
||||
let mount_enabled = crate::systemd::is_unit_enabled("smart-mount-mount.service");
|
||||
|
||||
return if timer_enabled && timer_active && mount_enabled {
|
||||
ok(
|
||||
"Scheduler",
|
||||
"systemd: 'smart-mount-watch.timer' is enabled and running, 'smart-mount-mount.service' is enabled",
|
||||
)
|
||||
} else {
|
||||
warn(
|
||||
"Scheduler",
|
||||
format!(
|
||||
"systemd units installed but not fully set up (smart-mount-watch.timer: enabled={timer_enabled} running={timer_active}, smart-mount-mount.service: enabled={mount_enabled}) - try 'sudo systemctl enable --now smart-mount-mount.service smart-mount-watch.timer'"
|
||||
),
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
if crate::systemd::is_available() {
|
||||
return fail(
|
||||
"Scheduler",
|
||||
"systemd found, but smart-mount's systemd units are not installed - was the package installed correctly?",
|
||||
);
|
||||
}
|
||||
|
||||
if crate::systemd::is_cron_installed() {
|
||||
return if crate::systemd::is_cron_daemon_running() {
|
||||
ok(
|
||||
"Scheduler",
|
||||
"cron: '/etc/cron.d/smart-mount' is installed, cron daemon is running",
|
||||
)
|
||||
} else {
|
||||
warn(
|
||||
"Scheduler",
|
||||
"cron: '/etc/cron.d/smart-mount' is installed, but no cron daemon process was found - is cron running?",
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
if std::path::Path::new("/etc/cron.d").is_dir() {
|
||||
return warn(
|
||||
"Scheduler",
|
||||
"no systemd, cron is available but smart-mount's entry ('/etc/cron.d/smart-mount') is missing - was the package installed correctly?",
|
||||
);
|
||||
}
|
||||
|
||||
let cron_package = match family {
|
||||
PackageFamily::Debian => "cron",
|
||||
_ => "cronie",
|
||||
};
|
||||
fail(
|
||||
"Scheduler",
|
||||
format!(
|
||||
"neither systemd nor cron found - periodic 'watch' must be set up manually ({})",
|
||||
install_hint(cron_package, family)
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fn check_mount_base_dir(dir: &std::path::Path) -> CheckResult {
|
||||
@@ -165,6 +304,66 @@ mod tests {
|
||||
use crate::config::{CloudSide, DrivePair, GlobalSettings, LocalSide};
|
||||
use std::net::Ipv4Addr;
|
||||
|
||||
#[test]
|
||||
fn parse_package_family_detects_debian_and_derivatives() {
|
||||
assert_eq!(
|
||||
parse_package_family("ID=debian\nVERSION_ID=\"13\"\n"),
|
||||
PackageFamily::Debian
|
||||
);
|
||||
assert_eq!(
|
||||
parse_package_family("ID=ubuntu\nID_LIKE=debian\n"),
|
||||
PackageFamily::Debian
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_package_family_detects_fedora_and_rhel_derivatives() {
|
||||
assert_eq!(parse_package_family("ID=fedora\n"), PackageFamily::Fedora);
|
||||
assert_eq!(
|
||||
parse_package_family("ID=centos\nID_LIKE=\"rhel fedora\"\n"),
|
||||
PackageFamily::Fedora
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_package_family_detects_arch_and_derivatives() {
|
||||
assert_eq!(parse_package_family("ID=arch\n"), PackageFamily::Arch);
|
||||
assert_eq!(
|
||||
parse_package_family("ID=manjaro\nID_LIKE=arch\n"),
|
||||
PackageFamily::Arch
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_package_family_falls_back_to_unknown() {
|
||||
assert_eq!(parse_package_family(""), PackageFamily::Unknown);
|
||||
assert_eq!(parse_package_family("ID=gentoo\n"), PackageFamily::Unknown);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn install_hint_includes_package_manager_command_for_known_families() {
|
||||
assert_eq!(
|
||||
install_hint("davfs2", PackageFamily::Debian),
|
||||
"install package 'davfs2' (apt install davfs2)"
|
||||
);
|
||||
assert_eq!(
|
||||
install_hint("davfs2", PackageFamily::Fedora),
|
||||
"install package 'davfs2' (dnf install davfs2)"
|
||||
);
|
||||
assert_eq!(
|
||||
install_hint("davfs2", PackageFamily::Arch),
|
||||
"install package 'davfs2' (pacman -S davfs2)"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn install_hint_omits_command_for_unknown_family() {
|
||||
assert_eq!(
|
||||
install_hint("davfs2", PackageFamily::Unknown),
|
||||
"install package 'davfs2'"
|
||||
);
|
||||
}
|
||||
|
||||
fn sample_pair(owner_user: Option<&str>, mac: bool) -> DrivePair {
|
||||
DrivePair {
|
||||
id: "pair-1".into(),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//! smart-mount: bindet lokale/Cloud-Laufwerkspaare (WebDAV/SMB/NFS) dynamisch ein und
|
||||
//! schaltet automatisch zwischen LAN und Cloud um.
|
||||
|
||||
pub mod cli;
|
||||
pub mod config;
|
||||
pub mod crypto;
|
||||
pub mod db;
|
||||
|
||||
+14
-2
@@ -1,18 +1,30 @@
|
||||
mod cli;
|
||||
|
||||
use std::process::ExitCode;
|
||||
|
||||
use clap::Parser;
|
||||
use smart_mount::cli;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> ExitCode {
|
||||
smart_mount::config::init();
|
||||
|
||||
// Persistentes, gut auffindbares Log-Verzeichnis statt logger-ctdras Standard-Fallback
|
||||
// (ein Ordner im System-Temp-Verzeichnis, siehe logger-ctdra-Doku) - smart-mount läuft
|
||||
// ausschließlich als root/System-Dienst (siehe crate::systemd), daher immer derselbe,
|
||||
// feste Pfad. Muss vor dem ersten Log-Aufruf stehen (siehe logger_ctdra::set_log_dir).
|
||||
logger_ctdra::set_log_dir("/var/log/smart-mount");
|
||||
|
||||
let log_level = match smart_mount::config::pairs::load() {
|
||||
Ok(cfg) => cfg.settings.log_level,
|
||||
Err(_) => "info".to_string(),
|
||||
};
|
||||
logger_ctdra::set_log_level(parse_log_level(&log_level));
|
||||
logger_ctdra::info(
|
||||
"main",
|
||||
&format!(
|
||||
"smart-mount {} starting (log level: {log_level})",
|
||||
env!("CARGO_PKG_VERSION")
|
||||
),
|
||||
);
|
||||
|
||||
let cli = cli::Cli::parse();
|
||||
match cli::dispatch(cli).await {
|
||||
|
||||
+13
-1
@@ -23,9 +23,16 @@ use crate::error::{Error, Result};
|
||||
/// Datei-Deskriptor), bis der Guard gedroppt wird - Schließen des Deskriptors gibt die Sperre
|
||||
/// implizit frei, ein explizites `unlock()` ist dafür nicht nötig.
|
||||
pub struct PairLock {
|
||||
pair_id: String,
|
||||
_file: File,
|
||||
}
|
||||
|
||||
impl Drop for PairLock {
|
||||
fn drop(&mut self) {
|
||||
logger_ctdra::debug("lock", &format!("pair '{}': lock released", self.pair_id));
|
||||
}
|
||||
}
|
||||
|
||||
/// Verzeichnis für die Sperrdateien: `/run/smart-mount/locks` im Root-/System-Kontext (root ist
|
||||
/// dort ohnehin die einzige Partei, die Paare in diesem Kontext mountet), sonst
|
||||
/// `$XDG_RUNTIME_DIR` (per-Nutzer, von systemd `0700`-geschützt angelegt) mit Fallback auf das
|
||||
@@ -59,8 +66,13 @@ fn acquire_blocking(pair_id: &str) -> Result<PairLock> {
|
||||
// Blockiert, bis die Sperre frei wird - `flock(2)` kennt keinen Async-Mechanismus, daher
|
||||
// läuft dieser gesamte Aufruf über `spawn_blocking` (siehe [`acquire`]) auf einem
|
||||
// Blocking-Thread statt einem Tokio-Worker-Thread.
|
||||
logger_ctdra::debug("lock", &format!("pair '{pair_id}': waiting for lock"));
|
||||
file.lock().map_err(|e| Error::io(&path, e))?;
|
||||
Ok(PairLock { _file: file })
|
||||
logger_ctdra::debug("lock", &format!("pair '{pair_id}': lock acquired"));
|
||||
Ok(PairLock {
|
||||
pair_id: pair_id.to_string(),
|
||||
_file: file,
|
||||
})
|
||||
}
|
||||
|
||||
/// Sperrt ein Laufwerkspaar prozessübergreifend für die Dauer des zurückgegebenen Guards.
|
||||
|
||||
+25
-9
@@ -93,6 +93,17 @@ fn run_tolerating_already_done_with_timeout(
|
||||
tolerate_timeout: bool,
|
||||
timeout_secs: u64,
|
||||
) -> Result<()> {
|
||||
logger_ctdra::debug(
|
||||
"mount",
|
||||
&format!(
|
||||
"{context}: running '{} {}' (timeout {timeout_secs}s)",
|
||||
cmd.get_program().to_string_lossy(),
|
||||
cmd.get_args()
|
||||
.map(|a| a.to_string_lossy().into_owned())
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ")
|
||||
),
|
||||
);
|
||||
let output =
|
||||
run_with_timeout(cmd, timeout_secs).map_err(|e| crate::error::Error::MountFailed {
|
||||
context: context.to_string(),
|
||||
@@ -100,6 +111,7 @@ fn run_tolerating_already_done_with_timeout(
|
||||
})?;
|
||||
|
||||
if output.status.success() {
|
||||
logger_ctdra::debug("mount", &format!("{context}: succeeded"));
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -173,16 +185,20 @@ fn cleanup_side_credentials(
|
||||
match kind {
|
||||
MountKind::Smb => {
|
||||
let path = smb::credentials_path(&pair.id, side);
|
||||
if path.exists()
|
||||
&& let Err(e) = std::fs::remove_file(&path)
|
||||
{
|
||||
logger_ctdra::warn(
|
||||
"mount",
|
||||
&format!(
|
||||
"Could not delete credentials file '{}': {e}",
|
||||
path.display()
|
||||
if path.exists() {
|
||||
match std::fs::remove_file(&path) {
|
||||
Ok(()) => logger_ctdra::debug(
|
||||
"mount",
|
||||
&format!("deleted credentials file '{}'", path.display()),
|
||||
),
|
||||
);
|
||||
Err(e) => logger_ctdra::warn(
|
||||
"mount",
|
||||
&format!(
|
||||
"Could not delete credentials file '{}': {e}",
|
||||
path.display()
|
||||
),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
MountKind::WebDav => {
|
||||
|
||||
+6
-1
@@ -29,7 +29,12 @@ impl MountBackend for SmbBackend {
|
||||
|
||||
fn prepare(&self, target: &MountTarget, cred: Option<&Credential>) -> Result<()> {
|
||||
if let Some(cred) = cred {
|
||||
write_credentials_file(&credentials_path(&target.pair_id, target.side), cred)?;
|
||||
let path = credentials_path(&target.pair_id, target.side);
|
||||
logger_ctdra::debug(
|
||||
"mount",
|
||||
&format!("cifs: writing credentials file '{}'", path.display()),
|
||||
);
|
||||
write_credentials_file(&path, cred)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -301,6 +301,15 @@ pub fn activate_symlink(pair: &DrivePair, side: Side) -> Result<()> {
|
||||
let _ = std::fs::remove_file(&tmp_path);
|
||||
std::os::unix::fs::symlink(&target, &tmp_path).map_err(|e| Error::io(&tmp_path, e))?;
|
||||
std::fs::rename(&tmp_path, link_path).map_err(|e| Error::io(link_path, e))?;
|
||||
logger_ctdra::info(
|
||||
"mount",
|
||||
&format!(
|
||||
"pair '{}': activated '{}' -> '{}'",
|
||||
pair.id,
|
||||
link_path.display(),
|
||||
target.display()
|
||||
),
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -46,6 +46,10 @@ impl MountBackend for WebDavBackend {
|
||||
"davfs2 credential has an empty username or password".to_string(),
|
||||
));
|
||||
}
|
||||
logger_ctdra::debug(
|
||||
"mount",
|
||||
&format!("davfs2: writing secrets entry for '{}'", target.source),
|
||||
);
|
||||
write_secrets_entry(
|
||||
&davfs2_secrets_path(),
|
||||
&target.source,
|
||||
|
||||
+19
-1
@@ -38,6 +38,12 @@ enum Mac2IpOutput {
|
||||
/// `binary` ist der konfigurierte Binary-Name/-Pfad (`GlobalSettings::mac2ip_binary`,
|
||||
/// standardmäßig `"mac2ip"`, per PATH aufgelöst).
|
||||
pub fn resolve(mac: &str, binary: &str) -> Result<Ipv4Addr> {
|
||||
logger_ctdra::info(
|
||||
"network",
|
||||
&format!(
|
||||
"resolving MAC '{mac}' via '{binary}' (may take up to {MAC2IP_TIMEOUT_SECS}s, e.g. for an nmap scan)"
|
||||
),
|
||||
);
|
||||
let output = Command::new("timeout")
|
||||
.arg(MAC2IP_TIMEOUT_SECS.to_string())
|
||||
.arg(binary)
|
||||
@@ -49,13 +55,25 @@ pub fn resolve(mac: &str, binary: &str) -> Result<Ipv4Addr> {
|
||||
})?;
|
||||
|
||||
if output.status.code() == Some(124) {
|
||||
logger_ctdra::warn(
|
||||
"network",
|
||||
&format!("'{binary}' did not respond within {MAC2IP_TIMEOUT_SECS}s for MAC '{mac}'"),
|
||||
);
|
||||
return Err(Error::Mac2Ip {
|
||||
mac: mac.to_string(),
|
||||
reason: format!("'{binary}' did not respond within {MAC2IP_TIMEOUT_SECS}s (timed out)"),
|
||||
});
|
||||
}
|
||||
|
||||
parse_output(&output.stdout, mac)
|
||||
let result = parse_output(&output.stdout, mac);
|
||||
match &result {
|
||||
Ok(ip) => logger_ctdra::info("network", &format!("MAC '{mac}' resolved to {ip}")),
|
||||
Err(e) => logger_ctdra::debug(
|
||||
"network",
|
||||
&format!("MAC '{mac}' could not be resolved: {e}"),
|
||||
),
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// Prüft, ob das konfigurierte `mac2ip`-Binary über `PATH` auffindbar ist.
|
||||
|
||||
@@ -17,6 +17,15 @@ use std::process::{Command, Stdio};
|
||||
/// (z. B. ein kaputter Reverse-Proxy) sonst bei jedem `watch`-Durchlauf einen vollen,
|
||||
/// letztlich erfolglosen Umschaltversuch (inkl. `MOUNT_TIMEOUT_SECS`-Wartezeit) auslösen würde.
|
||||
pub fn is_reachable(addr: &str) -> bool {
|
||||
let reachable = is_reachable_inner(addr);
|
||||
logger_ctdra::debug(
|
||||
"network",
|
||||
&format!("reachability check for '{addr}': {reachable}"),
|
||||
);
|
||||
reachable
|
||||
}
|
||||
|
||||
fn is_reachable_inner(addr: &str) -> bool {
|
||||
if addr.starts_with("http://") || addr.starts_with("https://") {
|
||||
Command::new("curl")
|
||||
.args([
|
||||
|
||||
+84
-12
@@ -50,6 +50,13 @@ pub struct ReconcileOutcome {
|
||||
/// Risiko für einen Effizienzgewinn, den die Reachability-Parallelisierung bereits liefert.
|
||||
pub async fn watch_once(cfg: &AppConfig, creds: &CredentialStore) -> Vec<ReconcileOutcome> {
|
||||
let enabled: Vec<&DrivePair> = cfg.pairs.iter().filter(|p| p.enabled).collect();
|
||||
logger_ctdra::debug(
|
||||
"reconcile",
|
||||
&format!(
|
||||
"watch: checking reachability for {} enabled pair(s)",
|
||||
enabled.len()
|
||||
),
|
||||
);
|
||||
|
||||
let mut checks = Vec::with_capacity(enabled.len());
|
||||
for pair in &enabled {
|
||||
@@ -123,16 +130,28 @@ async fn reconcile_pair_checked(
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(action) => ReconcileOutcome {
|
||||
pair_id,
|
||||
pair_name,
|
||||
action,
|
||||
},
|
||||
Err(e) => ReconcileOutcome {
|
||||
pair_id,
|
||||
pair_name,
|
||||
action: Action::Failed(e.to_string()),
|
||||
},
|
||||
Ok(action) => {
|
||||
logger_ctdra::info(
|
||||
"reconcile",
|
||||
&format!("pair '{pair_name}' ({pair_id}): {action:?}"),
|
||||
);
|
||||
ReconcileOutcome {
|
||||
pair_id,
|
||||
pair_name,
|
||||
action,
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
logger_ctdra::error(
|
||||
"reconcile",
|
||||
&format!("pair '{pair_name}' ({pair_id}): reconcile failed: {e}"),
|
||||
);
|
||||
ReconcileOutcome {
|
||||
pair_id,
|
||||
pair_name,
|
||||
action: Action::Failed(e.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,6 +166,13 @@ async fn reconcile_pair_inner(
|
||||
let _guard = lock::acquire(&pair.id).await?;
|
||||
|
||||
let active = target::active_side(pair);
|
||||
logger_ctdra::debug(
|
||||
"reconcile",
|
||||
&format!(
|
||||
"pair '{}': local_reachable={local_reachable} cloud_reachable={cloud_reachable} active={:?}",
|
||||
pair.id, active
|
||||
),
|
||||
);
|
||||
cleanup_orphaned_mounts(pair, settings, active).await;
|
||||
|
||||
if local_reachable {
|
||||
@@ -182,7 +208,13 @@ async fn reconcile_pair_inner(
|
||||
fn check_local_reachable(local: &LocalSide, settings: &GlobalSettings) -> (bool, Option<Ipv4Addr>) {
|
||||
match address::resolve_ip(&local.address, settings) {
|
||||
Ok(ip) => (network::is_reachable(&ip.to_string()), Some(ip)),
|
||||
Err(_) => (false, None),
|
||||
Err(e) => {
|
||||
logger_ctdra::debug(
|
||||
"reconcile",
|
||||
&format!("local address could not be resolved: {e}"),
|
||||
);
|
||||
(false, None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,6 +283,15 @@ async fn switch_to(
|
||||
old_active: Option<Side>,
|
||||
cached_local_ip: Option<Ipv4Addr>,
|
||||
) -> Result<()> {
|
||||
logger_ctdra::info(
|
||||
"reconcile",
|
||||
&format!(
|
||||
"pair '{}': switching to '{}' (previously active: {:?})",
|
||||
pair.id,
|
||||
new_side.as_str(),
|
||||
old_active
|
||||
),
|
||||
);
|
||||
mount_side(pair, settings, new_side, creds, cached_local_ip).await?;
|
||||
|
||||
// Falls das Aktivieren des Symlinks fehlschlägt, muss die gerade gemountete `new_side`
|
||||
@@ -280,6 +321,16 @@ async fn mount_side(
|
||||
) -> Result<()> {
|
||||
let mount_target =
|
||||
target::build_target_with_cached_local_ip(pair, settings, side, cached_local_ip)?;
|
||||
logger_ctdra::debug(
|
||||
"reconcile",
|
||||
&format!(
|
||||
"pair '{}' ({}): resolved source '{}' -> '{}'",
|
||||
pair.id,
|
||||
side.as_str(),
|
||||
mount_target.source,
|
||||
mount_target.mount_point.display()
|
||||
),
|
||||
);
|
||||
std::fs::create_dir_all(&mount_target.mount_point)
|
||||
.map_err(|e| crate::error::Error::io(&mount_target.mount_point, e))?;
|
||||
// Backing-Verzeichnis auf `owner_user` chownen (0700) - relevant vor allem für NFS, wo es
|
||||
@@ -293,6 +344,15 @@ async fn mount_side(
|
||||
backend.check_available()?;
|
||||
let cred = creds.get(&pair.id, side).await?;
|
||||
backend.prepare(&mount_target, cred.as_ref())?;
|
||||
logger_ctdra::info(
|
||||
"reconcile",
|
||||
&format!(
|
||||
"pair '{}' ({}): mounting via {} ...",
|
||||
pair.id,
|
||||
side.as_str(),
|
||||
backend.name()
|
||||
),
|
||||
);
|
||||
backend.mount(&mount_target)
|
||||
}
|
||||
|
||||
@@ -301,6 +361,7 @@ async fn mount_side(
|
||||
/// ein leeres, ausgehängtes Backing-Verzeichnis) - der nächste `mount`/`watch`-Lauf räumt das
|
||||
/// beim erneuten Aktivieren automatisch auf.
|
||||
pub async fn unmount_pair(pair: &DrivePair, settings: &GlobalSettings) -> Result<Action> {
|
||||
logger_ctdra::info("reconcile", &format!("pair '{}': unmounting", pair.id));
|
||||
let _guard = lock::acquire(&pair.id).await?;
|
||||
|
||||
// Beide Seiten werden unabhängig voneinander versucht - ein Fehler (auch ein
|
||||
@@ -363,7 +424,18 @@ async fn unmount_side(pair: &DrivePair, settings: &GlobalSettings, side: Side) -
|
||||
owner_user: pair.owner_user.clone(),
|
||||
}
|
||||
});
|
||||
mount::backend_for(target::side_kind(pair, side)).unmount(&mount_target)
|
||||
let backend = mount::backend_for(target::side_kind(pair, side));
|
||||
logger_ctdra::debug(
|
||||
"reconcile",
|
||||
&format!(
|
||||
"pair '{}' ({}): unmounting via {} ({})",
|
||||
pair.id,
|
||||
side.as_str(),
|
||||
backend.name(),
|
||||
mount_target.mount_point.display()
|
||||
),
|
||||
);
|
||||
backend.unmount(&mount_target)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
+103
-21
@@ -1,13 +1,13 @@
|
||||
//! Cron-Fallback (`smart-mount service crontab`) für Systeme ohne (oder ohne genutzten)
|
||||
//! systemd.
|
||||
//! Cron-Fallback für Systeme ohne (oder ohne genutzten) systemd.
|
||||
//!
|
||||
//! Die eigentliche systemd-Einrichtung passiert NICHT mehr zur Laufzeit über dieses Modul,
|
||||
//! sondern über die paketierten, statischen Unit-Dateien (siehe `packaging/systemd/` im
|
||||
//! Quellbaum) sowie die postinst/postrm-Skripte der .deb/.rpm/.pkg.tar.zst-Pakete - smart-mount
|
||||
//! richtet sich beim Installieren des Pakets automatisch als System-systemd-Dienst ein und
|
||||
//! entfernt sich beim Deinstallieren wieder. Dieses Modul bleibt für Systeme ohne systemd (oder
|
||||
//! zum bewussten Umgehen von systemd) als manueller Cron-Weg bestehen - immer systemweit
|
||||
//! (`/etc/cron.d/smart-mount`), da Mounten ohnehin immer Root-Rechte braucht (siehe
|
||||
//! Die eigentliche systemd-Einrichtung passiert NICHT zur Laufzeit über dieses Modul, sondern
|
||||
//! über die paketierten, statischen Unit-Dateien (siehe `packaging/systemd/` im Quellbaum)
|
||||
//! sowie die postinst/postrm-Skripte der .deb/.rpm/.pkg.tar.zst-Pakete. Dieses Modul stellt die
|
||||
//! Logik für den Cron-Fallback bereit - aufgerufen wird sie NICHT interaktiv über ein
|
||||
//! `smart-mount`-Subcommand, sondern automatisch vom separaten `setup-cron`-Hilfsprogramm
|
||||
//! (siehe `src/bin/setup-cron.rs`), das ebenfalls über die postinst-Skripte der Pakete beim
|
||||
//! Installieren läuft, sofern kein systemd verfügbar ist (siehe [`is_available`]). Immer
|
||||
//! systemweit (`/etc/cron.d/smart-mount`), da Mounten ohnehin immer Root-Rechte braucht (siehe
|
||||
//! [`crate::cli::require_root`]); eine persönliche Nutzer-Crontab liefe ins Leere.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
@@ -18,19 +18,96 @@ const CRON_D_PATH: &str = "/etc/cron.d/smart-mount";
|
||||
const CRON_BEGIN_MARKER: &str = "# BEGIN smart-mount managed block";
|
||||
const CRON_END_MARKER: &str = "# END smart-mount managed block";
|
||||
|
||||
fn binary_path() -> String {
|
||||
std::env::current_exe()
|
||||
.ok()
|
||||
.and_then(|p| p.to_str().map(str::to_string))
|
||||
.unwrap_or_else(|| "/usr/bin/smart-mount".to_string())
|
||||
/// Fester Installationspfad der Haupt-Binary (siehe `[package.metadata.deb/generate-rpm].assets`
|
||||
/// bzw. `scripts/package-arch.py`, die immer nach `/usr/bin/<name>` installieren) - smart-mount
|
||||
/// läuft ausschließlich als paketierter System-Dienst, es gibt keinen anderen Installationsort.
|
||||
/// Bewusst NICHT über `std::env::current_exe()`: der Cron-Eintrag wird vom separaten
|
||||
/// `setup-cron`-Hilfsprogramm geschrieben (siehe [`crate::systemd`]-Moduldoku), dessen eigener
|
||||
/// Pfad hier falsch wäre.
|
||||
fn binary_path() -> &'static str {
|
||||
"/usr/bin/smart-mount"
|
||||
}
|
||||
|
||||
/// Ob `systemctl` auf diesem System vorhanden ist - rein informativ für `doctor`; die
|
||||
/// eigentliche systemd-Einrichtung läuft über die Paketierung (siehe Moduldoku).
|
||||
/// Ob `systemctl` auf diesem System vorhanden ist - entscheidet für `doctor` UND für
|
||||
/// `setup-cron` (siehe Moduldoku), ob der Cron-Fallback überhaupt installiert werden soll: die
|
||||
/// eigentliche systemd-Einrichtung läuft über die Paketierung, ein zusätzlicher Cron-Eintrag
|
||||
/// wäre dort redundant.
|
||||
pub fn is_available() -> bool {
|
||||
crate::mount::binary_available("systemctl")
|
||||
}
|
||||
|
||||
/// `systemctl is-enabled`/`is-active <unit>` liefert bei Erfolg wie bei Misserfolg exakt eine
|
||||
/// Ausgabezeile auf stdout (z. B. "enabled"/"disabled"/"static"/"not-found" bzw.
|
||||
/// "active"/"inactive"/"failed") - der Exit-Code allein reicht nicht (z. B. ist `is-active`
|
||||
/// auch für eine deaktivierte, aber existierende Unit `1`). `None` bei fehlendem `systemctl`
|
||||
/// oder einem sonstigen Ausführungsfehler.
|
||||
fn systemctl_query(subcommand: &str, unit: &str) -> Option<String> {
|
||||
std::process::Command::new("systemctl")
|
||||
.args([subcommand, unit])
|
||||
.output()
|
||||
.ok()
|
||||
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
|
||||
}
|
||||
|
||||
/// Ob `systemctl` die Unit überhaupt kennt (eine geladene Unit-Datei existiert), unabhängig
|
||||
/// davon, ob sie aktiviert/aktiv ist - für `doctor`, um "installiert, aber nicht
|
||||
/// aktiviert/gestartet" von "gar nicht installiert" unterscheiden zu können. Eine komplett
|
||||
/// unbekannte Unit liefert bei `is-enabled` je nach systemd-Version "not-found" oder eine leere
|
||||
/// Ausgabe (Fehler auf stderr) - beides ungleich einem der hier aufgeführten, eine tatsächlich
|
||||
/// geladene Unit-Datei voraussetzenden Zustände.
|
||||
pub fn is_unit_installed(unit: &str) -> bool {
|
||||
matches!(
|
||||
systemctl_query("is-enabled", unit).as_deref(),
|
||||
Some(
|
||||
"enabled"
|
||||
| "disabled"
|
||||
| "static"
|
||||
| "alias"
|
||||
| "linked"
|
||||
| "generated"
|
||||
| "transient"
|
||||
| "masked"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/// Ob die Unit dauerhaft aktiviert ist (startet automatisch, siehe `man systemctl`,
|
||||
/// Abschnitt "is-enabled").
|
||||
pub fn is_unit_enabled(unit: &str) -> bool {
|
||||
systemctl_query("is-enabled", unit).as_deref() == Some("enabled")
|
||||
}
|
||||
|
||||
/// Ob die Unit aktuell läuft (bei einem Timer: wartet auf die nächste Auslösung). Bei einem
|
||||
/// `oneshot`-Service wie `smart-mount-mount.service` (läuft einmal beim Boot und beendet sich
|
||||
/// danach) ist "inactive" direkt nach einem erfolgreichen Lauf der korrekte, erwartete Zustand -
|
||||
/// dafür ist [`is_unit_installed`]/[`is_unit_enabled`] aussagekräftiger.
|
||||
pub fn is_unit_active(unit: &str) -> bool {
|
||||
systemctl_query("is-active", unit).as_deref() == Some("active")
|
||||
}
|
||||
|
||||
/// Ob ein von [`install_cron`] verwalteter Cron-Eintrag aktuell existiert - für `doctor`, um
|
||||
/// zwischen "kein Cron-Mechanismus vorhanden" und "vorhanden, aber smart-mounts Eintrag fehlt"
|
||||
/// (z. B. weil das Paket nicht sauber installiert wurde) unterscheiden zu können.
|
||||
pub fn is_cron_installed() -> bool {
|
||||
Path::new(CRON_D_PATH).exists()
|
||||
}
|
||||
|
||||
/// Ob aktuell ein Cron-Daemon-Prozess läuft - rein prozessbasiert über `pgrep` geprüft (statt
|
||||
/// z. B. über `systemctl is-active cron.service`), damit es unabhängig davon funktioniert, ob
|
||||
/// systemd überhaupt vorhanden ist: genau im Cron-Fallback-Fall (kein/kein genutztes systemd,
|
||||
/// siehe Moduldoku) ist das der einzige portable Weg. Prüft beide gängigen Prozessnamen
|
||||
/// (Debian/Ubuntu: `cron`, Fedora/Arch: `crond`), ohne selbst eine Distributions-Erkennung zu
|
||||
/// brauchen.
|
||||
pub fn is_cron_daemon_running() -> bool {
|
||||
["cron", "crond"].iter().any(|name| {
|
||||
std::process::Command::new("pgrep")
|
||||
.args(["-x", name])
|
||||
.output()
|
||||
.map(|o| o.status.success())
|
||||
.unwrap_or(false)
|
||||
})
|
||||
}
|
||||
|
||||
/// Erzeugt die Crontab-Äquivalente zu den paketierten systemd-Units, für Systeme ohne
|
||||
/// (genutzten) systemd. `watch_interval_secs` ist derselbe Wert wie
|
||||
/// `settings.watch_interval_secs`.
|
||||
@@ -49,11 +126,11 @@ pub enum CronInstallOutcome {
|
||||
Unavailable,
|
||||
}
|
||||
|
||||
/// Richtet die periodische Ausführung direkt über `/etc/cron.d/smart-mount` ein - manueller
|
||||
/// Fallback für Systeme ohne (genutzten) systemd, sofern `/etc/cron.d` existiert - sonst
|
||||
/// [`CronInstallOutcome::Unavailable`] statt eines Fehlers, der Aufrufer zeigt dann
|
||||
/// [`crontab_equivalent`] zur manuellen Einrichtung. Erfordert Root (siehe Moduldoku) - der
|
||||
/// Aufrufer (`cli::service`) prüft das bereits vorab.
|
||||
/// Richtet die periodische Ausführung direkt über `/etc/cron.d/smart-mount` ein - Fallback für
|
||||
/// Systeme ohne (genutzten) systemd, sofern `/etc/cron.d` existiert - sonst
|
||||
/// [`CronInstallOutcome::Unavailable`] statt eines Fehlers, der Aufrufer (`setup-cron`, siehe
|
||||
/// Moduldoku) zeigt dann [`crontab_equivalent`] zur manuellen Einrichtung. Erfordert Root
|
||||
/// (siehe Moduldoku) - beim Aufruf aus dem postinst-Skript der Pakete ohnehin gegeben.
|
||||
pub fn install_cron(watch_interval_secs: u64) -> Result<CronInstallOutcome> {
|
||||
if !Path::new("/etc/cron.d").is_dir() {
|
||||
return Ok(CronInstallOutcome::Unavailable);
|
||||
@@ -67,6 +144,7 @@ pub fn install_cron(watch_interval_secs: u64) -> Result<CronInstallOutcome> {
|
||||
std::fs::set_permissions(CRON_D_PATH, std::fs::Permissions::from_mode(0o644))
|
||||
.map_err(|e| Error::io(CRON_D_PATH, e))?;
|
||||
}
|
||||
logger_ctdra::info("systemd", &format!("cron entry written to '{CRON_D_PATH}'"));
|
||||
Ok(CronInstallOutcome::SystemFile(PathBuf::from(CRON_D_PATH)))
|
||||
}
|
||||
|
||||
@@ -96,6 +174,10 @@ pub fn uninstall_cron() -> Result<CronUninstallOutcome> {
|
||||
return Ok(CronUninstallOutcome::NotPresent);
|
||||
}
|
||||
std::fs::remove_file(path).map_err(|e| Error::io(CRON_D_PATH, e))?;
|
||||
logger_ctdra::info(
|
||||
"systemd",
|
||||
&format!("cron entry removed from '{CRON_D_PATH}'"),
|
||||
);
|
||||
Ok(CronUninstallOutcome::Removed)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user