Author SHA1 Message Date
DragonSlayer_14 105545634d Merge pull request 'Merge dev in testing: Update doctor, Remove service, Add Logs, Rename Prompts' (#5) from dev into testing
Unit-Tests / Unit-Tests (pull_request) Skipped
Auto-PR (Testing → Main) / Erstelle automatisch PR von testing nach main (push) Successful in 12s
Testing Build, Publish & Preview Release / Erkenne relevante Code-Änderungen (push) Successful in 12s
TruffleHog Secret Scan / TruffleHog (pull_request) Successful in 25s
Security Scans / Trivy & OSV-Scanner (push) Successful in 41s
Security Scans / Trivy & OSV-Scanner (pull_request) Successful in 42s
Testing Build, Publish & Preview Release / Build, Publish Packages (Testing) & Create Preview Release (push) Successful in 16m9s
TruffleHog Secret Scan / TruffleHog (push) Successful in 11s
Reviewed-on: #5
2026-09-20 15:43:07 +00:00
Gitea-Bot f4220e34ae Chore: Erhöht Patch-Version auf 2.0.2 für Promotion nach testing
Testing Build, Publish & Preview Release / Build, Publish Packages (Testing) & Create Preview Release (push) Skipped
TruffleHog Secret Scan / TruffleHog (push) Successful in 28s
TruffleHog Secret Scan / TruffleHog (pull_request) Successful in 26s
Security Scans / Trivy & OSV-Scanner (push) Successful in 57s
Security Scans / Trivy & OSV-Scanner (pull_request) Successful in 55s
Code Quality (Auto-Format & Clippy-Fix) / Formatierung & Clippy automatisch beheben (push) Successful in 1m39s
Unit-Tests / Unit-Tests (pull_request) Successful in 3m18s
2026-09-20 15:37:05 +00:00
DragonSlayer_14andClaude Sonnet 5 86d43761d9 Feature: Entfernt 'service'-Befehl, Cron-Fallback wird automatisch mitpaketiert, doctor prüft distro- und statusgenau nach
Testing Build, Publish & Preview Release / Build, Publish Packages (Testing) & Create Preview Release (push) Skipped
TruffleHog Secret Scan / TruffleHog (push) Successful in 17s
Security Scans / Trivy & OSV-Scanner (push) Successful in 27s
Code Quality (Auto-Format & Clippy-Fix) / Formatierung & Clippy automatisch beheben (push) Successful in 36s
Auto Patch-Version-Bump (Dev → Testing PR) / Erkenne relevante Änderungen im PR (pull_request) Successful in 11s
TruffleHog Secret Scan / TruffleHog (pull_request) Successful in 18s
Auto Patch-Version-Bump (Dev → Testing PR) / Patch-Version erhöhen & auf Dev pushen (pull_request) Successful in 11s
Security Scans / Trivy & OSV-Scanner (pull_request) Successful in 35s
Unit-Tests / Unit-Tests (pull_request) Successful in 4m4s
Kein 'smart-mount service crontab'-Subcommand mehr - das neue interne
Hilfsprogramm setup-cron (src/bin/setup-cron.rs, installiert nach
/usr/lib/smart-mount/setup-cron) richtet den Cron-Fallback beim
Paketieren automatisch über die postinst-Skripte ein, sofern kein
systemd verfügbar ist; postrm/das Arch-.install-Skriptlet entfernen den
Cron-Eintrag beim Deinstallieren ebenso automatisch wieder.
systemd::binary_path() ist dafür jetzt fest auf '/usr/bin/smart-mount'
gesetzt statt current_exe() zu nutzen, da der Cron-Eintrag nun von
einem separaten Prozess geschrieben wird.

'smart-mount doctor' zeigt Installationshinweise jetzt nur noch für die
über /etc/os-release erkannte Distribution (Debian/Fedora/Arch) statt
immer alle drei Varianten zu nennen, und der Scheduler-Check prüft
echten Status statt bloßer Tool-Präsenz: systemctl is-enabled/is-active
für die smart-mount-Units sowie tatsächliche Existenz von
/etc/cron.d/smart-mount plus laufendem Cron-Daemon-Prozess.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-20 17:32:18 +02:00
DragonSlayer_14andClaude Sonnet 5 abcc11f078 Feature: Shell-Completion wird automatisch mitpaketiert statt über eigenen Befehl
Entfernt den 'smart-mount completions'-Befehl komplett - Shell-Completion
(bash/zsh/fish) wird stattdessen beim Bauen des .deb/.rpm/.pkg.tar.zst-Pakets
generiert und als normales Paket-Asset ausgeliefert, sodass sie beim
Installieren automatisch mit installiert und beim Entfernen automatisch mit
entfernt wird, ganz ohne Zutun des Nutzers.

Das 'cli'-Modul ist dafür Teil der Library geworden (pub mod cli), damit ein
neues, separates Build-Hilfsprogramm (src/bin/generate-completions.rs) die
bestehende Cli-Definition als einzige Quelle wiederverwenden kann, statt sie
zu duplizieren. Die Gitea-Workflows rufen es nach dem Release-Build auf;
Cargo.toml und scripts/package-arch.py bündeln die erzeugten Dateien für
alle drei Paketformate an den jeweils distributionsüblichen Pfaden.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-20 17:07:11 +02:00
DragonSlayer_14andClaude Sonnet 5 eff6dafa77 Fix: Klarere Feldbezeichnung für 'share'/Export-Pfad in 'drive add'/'edit'
Der generische Prompt-/Hilfetext "Local/Cloud share/export path" war
unabhängig vom gewählten Mount-Typ identisch, obwohl das Feld inhaltlich
je nach Backend etwas anderes ist (SMB-Freigabename, NFS-Export-Pfad,
WebDAV-URL-Pfad). share_field_label() wählt jetzt passend zum bereits
gewählten local_kind/cloud_kind einen eindeutigen Text mit Beispiel.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-20 16:55:24 +02:00
DragonSlayer_14 705ecd8142 Merge pull request 'Merge dev in testing: Automatischer Aufruf von sudo' (#4) from dev into testing
Unit-Tests / Unit-Tests (pull_request) Skipped
Auto-PR (Testing → Main) / Erstelle automatisch PR von testing nach main (push) Successful in 11s
Testing Build, Publish & Preview Release / Erkenne relevante Code-Änderungen (push) Successful in 14s
TruffleHog Secret Scan / TruffleHog (pull_request) Successful in 25s
Security Scans / Trivy & OSV-Scanner (push) Successful in 41s
Security Scans / Trivy & OSV-Scanner (pull_request) Successful in 42s
Testing Build, Publish & Preview Release / Build, Publish Packages (Testing) & Create Preview Release (push) Successful in 12m0s
TruffleHog Secret Scan / TruffleHog (push) Successful in 10s
Reviewed-on: #4
2026-09-20 14:24:48 +00:00
DragonSlayer_14 7f5c8db6ce Merge pull request 'Merge dev in testing: Ref: Entfernt vollständige 32-Bit-Unterstützung (i686)' (#3) from dev into testing
Unit-Tests / Unit-Tests (pull_request) Skipped
Auto-PR (Testing → Main) / Erstelle automatisch PR von testing nach main (push) Successful in 12s
Testing Build, Publish & Preview Release / Erkenne relevante Code-Änderungen (push) Successful in 13s
TruffleHog Secret Scan / TruffleHog (pull_request) Successful in 27s
Security Scans / Trivy & OSV-Scanner (push) Successful in 44s
Security Scans / Trivy & OSV-Scanner (pull_request) Successful in 45s
Testing Build, Publish & Preview Release / Build, Publish Packages (Testing) & Create Preview Release (push) Successful in 12m11s
TruffleHog Secret Scan / TruffleHog (push) Successful in 10s
Reviewed-on: #3
2026-09-20 12:20:30 +00:00
DragonSlayer_14 2f33e2202d Merge pull request 'Merge dev in testing: Neue Version 2.0.0 mit kompletter Überarbeitung' (#1) from dev into testing
Testing Build, Publish & Preview Release / Erkenne relevante Code-Änderungen (push) Successful in 10s
Unit-Tests / Unit-Tests (pull_request) Skipped
TruffleHog Secret Scan / TruffleHog (push) Successful in 20s
Auto-PR (Testing → Main) / Erstelle automatisch PR von testing nach main (push) Successful in 15s
Security Scans / Trivy & OSV-Scanner (push) Successful in 36s
TruffleHog Secret Scan / TruffleHog (pull_request) Successful in 24s
Security Scans / Trivy & OSV-Scanner (pull_request) Successful in 34s
Testing Build, Publish & Preview Release / Build, Publish Packages (Testing) & Create Preview Release (push) Failing after 13m0s
Reviewed-on: #1
2026-09-20 11:44:44 +00:00
21 changed files with 649 additions and 233 deletions
+6 -1
View File
@@ -59,7 +59,7 @@ jobs:
cargo-${{ runner.os }}-
- 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:
+6 -1
View File
@@ -59,7 +59,7 @@ jobs:
cargo-${{ runner.os }}-
- 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:
Generated
+1 -1
View File
@@ -2052,7 +2052,7 @@ checksum = "ba467056f1b547ed52077911161fc86985becbc60e8e1857c8a144dab0def891"
[[package]]
name = "smart-mount"
version = "2.0.1"
version = "2.0.2"
dependencies = [
"aes-gcm 0.11.1",
"anyhow",
+34 -7
View File
@@ -1,6 +1,6 @@
[package]
name = "smart-mount"
version = "2.0.1"
version = "2.0.2"
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,38 @@ 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
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
"""
+25 -23
View File
@@ -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.
@@ -168,21 +164,25 @@ sudo systemctl edit smart-mount-watch.timer
### 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 +268,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
+10 -6
View File
@@ -1,13 +1,17 @@
#!/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
systemctl enable --now smart-mount-mount.service smart-mount-watch.timer || true
fi
/usr/lib/smart-mount/setup-cron || true
fi
exit 0
+13 -6
View File
@@ -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
+87 -22
View File
@@ -61,30 +61,76 @@ 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."""
'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)."""
installable_str = " ".join(installable)
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"
f" systemctl enable --now {installable_str} >/dev/null 2>&1 || true\n"
if installable
else ""
)
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 ""
pre_remove() {{
systemctl disable --now {all_units_str} >/dev/null 2>&1 || true
}}
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 +201,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 +238,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 +247,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)
+34
View File
@@ -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());
}
}
+51
View File
@@ -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
View File
@@ -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()?;
+42 -16
View File
@@ -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,7 +161,7 @@ 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.");
@@ -166,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,
@@ -181,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,
@@ -272,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,
)?
@@ -307,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,
)?
@@ -328,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}' \
@@ -487,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.
@@ -665,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 {
+2 -26
View File
@@ -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
@@ -75,22 +73,12 @@ 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
@@ -102,9 +90,7 @@ fn command_label(cmd: &Commands) -> &'static str {
Commands::Unmount { .. } => "unmount",
Commands::Status { .. } => "status",
Commands::Watch => "watch",
Commands::Service { .. } => "service",
Commands::Doctor { .. } => "doctor",
Commands::Completions { .. } => "completions",
}
}
@@ -120,16 +106,6 @@ pub async fn dispatch(cli: Cli) -> anyhow::Result<()> {
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(())
}
}
}
+4 -4
View File
@@ -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;
fn select_pairs(
cfg: &smart_mount::config::AppConfig,
cfg: &crate::config::AppConfig,
name: Option<&str>,
all: bool,
) -> anyhow::Result<Vec<DrivePair>> {
-61
View File
@@ -1,61 +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) => {
logger_ctdra::info(
"service",
&format!("cron entry written: {}", path.display()),
);
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 => {
logger_ctdra::info("service", "cron entry removed");
println!("Cron entry removed.");
}
systemd::CronUninstallOutcome::NotPresent => {
println!("Nothing to remove - no cron entry was installed.")
}
}
Ok(())
}
+5 -5
View File
@@ -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))
+3 -4
View File
@@ -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")?;
+224 -25
View File
@@ -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
View File
@@ -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;
+1 -2
View File
@@ -1,8 +1,7 @@
mod cli;
use std::process::ExitCode;
use clap::Parser;
use smart_mount::cli;
#[tokio::main]
async fn main() -> ExitCode {
+98 -21
View File
@@ -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);