Compare commits
3
Commits
cc000a2bda
...
86d43761d9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
86d43761d9
|
||
|
|
abcc11f078
|
||
|
|
eff6dafa77
|
@@ -59,7 +59,7 @@ jobs:
|
|||||||
cargo-${{ runner.os }}-
|
cargo-${{ runner.os }}-
|
||||||
|
|
||||||
- name: Alte Paketierungs-Ausgaben aus dem Cache entfernen
|
- 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)
|
- name: Install Cross-Compilation Toolchains (apt)
|
||||||
run: |
|
run: |
|
||||||
@@ -111,6 +111,11 @@ jobs:
|
|||||||
cargo build --release --target x86_64-unknown-linux-gnu
|
cargo build --release --target x86_64-unknown-linux-gnu
|
||||||
cargo build --release --target aarch64-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
|
- name: Determine Build Number
|
||||||
id: build_num
|
id: build_num
|
||||||
env:
|
env:
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ jobs:
|
|||||||
cargo-${{ runner.os }}-
|
cargo-${{ runner.os }}-
|
||||||
|
|
||||||
- name: Alte Paketierungs-Ausgaben aus dem Cache entfernen
|
- 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)
|
- name: Install Cross-Compilation Toolchains (apt)
|
||||||
run: |
|
run: |
|
||||||
@@ -111,6 +111,11 @@ jobs:
|
|||||||
cargo build --release --target x86_64-unknown-linux-gnu
|
cargo build --release --target x86_64-unknown-linux-gnu
|
||||||
cargo build --release --target aarch64-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
|
- name: Determine Build Number
|
||||||
id: build_num
|
id: build_num
|
||||||
env:
|
env:
|
||||||
|
|||||||
+33
-6
@@ -68,6 +68,17 @@ assets = [
|
|||||||
["packaging/systemd/smart-mount-mount.service", "usr/lib/systemd/system/smart-mount-mount.service", "644"],
|
["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.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"],
|
["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]
|
[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-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.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" },
|
{ 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" = "*" }
|
requires = { "mac2ip" = "*" }
|
||||||
suggests = { "davfs2" = "*", "cifs-utils" = "*", "nfs-utils" = "*" }
|
suggests = { "davfs2" = "*", "cifs-utils" = "*", "nfs-utils" = "*" }
|
||||||
# postinst/postrm-Äquivalente: richten den paketierten systemd-Dienst automatisch ein/entfernen
|
# postinst/postrm-Äquivalente: richten den paketierten systemd-Dienst (bzw. ohne systemd den
|
||||||
# (Gegenstück zu packaging/deb/postinst+postrm). $1 in %postun: Anzahl verbleibender Versionen
|
# Cron-Fallback über setup-cron) automatisch ein/entfernen (Gegenstück zu
|
||||||
# nach diesem Schritt - 0 nur bei vollständiger Deinstallation, nicht bei einem Upgrade.
|
# 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 = """
|
post_install_script = """
|
||||||
if command -v systemctl >/dev/null 2>&1; then
|
if command -v systemctl >/dev/null 2>&1; then
|
||||||
systemctl daemon-reload || true
|
systemctl daemon-reload || true
|
||||||
systemctl enable --now smart-mount-mount.service smart-mount-watch.timer || true
|
systemctl enable --now smart-mount-mount.service smart-mount-watch.timer || true
|
||||||
fi
|
fi
|
||||||
|
/usr/lib/smart-mount/setup-cron || true
|
||||||
"""
|
"""
|
||||||
post_uninstall_script = """
|
post_uninstall_script = """
|
||||||
if [ "$1" = "0" ] && command -v systemctl >/dev/null 2>&1; then
|
if [ "$1" = "0" ]; then
|
||||||
systemctl disable --now smart-mount-mount.service smart-mount-watch.timer smart-mount-watch.service || true
|
if command -v systemctl >/dev/null 2>&1; then
|
||||||
systemctl daemon-reload || true
|
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
|
fi
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|||||||
@@ -26,8 +26,9 @@ werden (z. B. über ein separates Sync-Tool).
|
|||||||
- `cifs-utils` für SMB/CIFS-Laufwerke
|
- `cifs-utils` für SMB/CIFS-Laufwerke
|
||||||
- `nfs-common` (Debian/Ubuntu) bzw. `nfs-utils` (Fedora/Arch) für NFS-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
|
- `systemd`: smart-mount richtet sich beim Installieren des Pakets automatisch als
|
||||||
System-Dienst ein (siehe "Automatischer Start beim Systemstart" unten). Ohne systemd bleibt
|
System-Dienst ein (siehe "Automatischer Start beim Systemstart" unten). Ohne systemd, aber mit
|
||||||
`smart-mount service crontab` als manueller Fallback.
|
`/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
|
# Ein Reconcile-Durchlauf (lokal/Cloud-Umschaltung) - für systemd-Timer/Cron gedacht
|
||||||
sudo smart-mount watch
|
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
|
# Voraussetzungen prüfen (Binaries, Scheduler) - deckt gebündelt ab, was man sonst erst
|
||||||
# einzeln beim Mount-Fehlschlag entdecken würde
|
# einzeln beim Mount-Fehlschlag entdecken würde
|
||||||
smart-mount doctor
|
smart-mount doctor
|
||||||
smart-mount doctor --json
|
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
|
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
|
`smart-mount` selbst mit oder ohne Root-Rechte aufgerufen wird (siehe oben). Die verschlüsselte
|
||||||
Zugangsdaten-Datenbank (`smart-mount.db`) liegt im selben Verzeichnis.
|
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)
|
### Cron-Fallback (Systeme ohne systemd)
|
||||||
|
|
||||||
```bash
|
Ist beim Installieren/Upgraden des Pakets kein `systemctl` gefunden, aber `/etc/cron.d`
|
||||||
sudo smart-mount service crontab # einrichten
|
vorhanden, schreibt das Paket **automatisch** `/etc/cron.d/smart-mount` (kein manueller Schritt,
|
||||||
sudo smart-mount service crontab --remove # wieder entfernen
|
kein eigenes `smart-mount`-Subcommand) - das Intervall folgt `settings.watch_interval_secs`
|
||||||
```
|
(Standard 120s):
|
||||||
|
|
||||||
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:
|
|
||||||
|
|
||||||
```cron
|
```cron
|
||||||
@reboot smart-mount mount --all
|
@reboot root /usr/bin/smart-mount mount --all
|
||||||
*/2 * * * * smart-mount watch
|
*/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
|
## Architekturentscheidungen
|
||||||
@@ -268,7 +268,9 @@ Eintragen ausgegeben:
|
|||||||
│ └── report-security-issue.py # Security-Scan-Ergebnisse als Gitea-Issue melden
|
│ └── report-security-issue.py # Security-Scan-Ergebnisse als Gitea-Issue melden
|
||||||
├── src/
|
├── src/
|
||||||
│ ├── main.rs # Dünner Einstiegspunkt (CLI-Parsing, Dispatch)
|
│ ├── 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
|
│ ├── cli/ # `clap`-Subcommands
|
||||||
│ ├── config/ # Konfigurationsschema + CRUD (config-ctdra)
|
│ ├── config/ # Konfigurationsschema + CRUD (config-ctdra)
|
||||||
│ ├── crypto/ # Verschlüsselung + Master-Key-Auflösung
|
│ ├── crypto/ # Verschlüsselung + Master-Key-Auflösung
|
||||||
|
|||||||
+10
-6
@@ -1,13 +1,17 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
# Richtet smart-mount beim Installieren/Upgraden des Pakets automatisch als
|
# Richtet smart-mount beim Installieren/Upgraden des Pakets automatisch als
|
||||||
# System-systemd-Dienst ein (Gegenstueck: postrm). Laeuft nur beim eigentlichen
|
# System-systemd-Dienst ein (Gegenstueck: postrm); ist kein systemd verfuegbar, richtet
|
||||||
# "configure"-Schritt (siehe Debian Policy Manual, Abschnitt 6.5), nicht bei
|
# stattdessen setup-cron (siehe src/bin/setup-cron.rs) automatisch den Cron-Fallback ein,
|
||||||
# "abort-upgrade"/"abort-remove" etc.
|
# 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
|
set -e
|
||||||
|
|
||||||
if [ "$1" = "configure" ] && command -v systemctl >/dev/null 2>&1; then
|
if [ "$1" = "configure" ]; then
|
||||||
systemctl daemon-reload || true
|
if command -v systemctl >/dev/null 2>&1; then
|
||||||
systemctl enable --now smart-mount-mount.service smart-mount-watch.timer || true
|
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
|
fi
|
||||||
|
|
||||||
exit 0
|
exit 0
|
||||||
|
|||||||
+13
-6
@@ -1,13 +1,20 @@
|
|||||||
#!/bin/sh
|
#!/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
|
# postinst). Nur bei tatsaechlicher Entfernung ("remove"/"purge"), nicht bei einem
|
||||||
# Upgrade (dort ersetzt dpkg die Unit-Dateien einfach durch die neue Version, ohne den
|
# Upgrade (dort ersetzt dpkg die Unit-/Paket-Dateien einfach durch die neue Version, ohne
|
||||||
# laufenden Dienst zwischenzeitlich zu deaktivieren).
|
# 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
|
set -e
|
||||||
|
|
||||||
if { [ "$1" = "remove" ] || [ "$1" = "purge" ]; } && command -v systemctl >/dev/null 2>&1; then
|
if { [ "$1" = "remove" ] || [ "$1" = "purge" ]; }; then
|
||||||
systemctl disable --now smart-mount-mount.service smart-mount-watch.timer smart-mount-watch.service || true
|
if command -v systemctl >/dev/null 2>&1; then
|
||||||
systemctl daemon-reload || true
|
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
|
fi
|
||||||
|
|
||||||
exit 0
|
exit 0
|
||||||
|
|||||||
+87
-22
@@ -61,30 +61,76 @@ def collect_systemd_units(systemd_src_dir="packaging/systemd"):
|
|||||||
return unit_files, installable
|
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
|
"""Erzeugt den Inhalt einer Arch-'.INSTALL'-Datei (siehe `man PKGBUILD`, Abschnitt
|
||||||
'install'), die den paketierten systemd-Dienst beim Installieren aktiviert/startet und beim
|
'install'): aktiviert/startet paketierte systemd-Units beim Installieren/Upgraden und
|
||||||
Entfernen wieder deaktiviert/stoppt - rein generisch anhand der tatsächlich gefundenen
|
deaktiviert sie beim Entfernen, und ruft - sofern vorhanden - generisch das interne
|
||||||
Unit-Dateien, ohne Anwendungsnamen hart zu codieren."""
|
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)
|
installable_str = " ".join(installable)
|
||||||
all_units_str = " ".join(all_units)
|
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() {{
|
systemd_enable = (
|
||||||
systemctl daemon-reload >/dev/null 2>&1 || true
|
" 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() {{
|
return (
|
||||||
systemctl disable --now {all_units_str} >/dev/null 2>&1 || true
|
"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() {{
|
"post_remove() {\n" + systemd_reload + "}\n"
|
||||||
systemctl daemon-reload >/dev/null 2>&1 || true
|
)
|
||||||
}}
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
def build_package(target_triple=None, target_arch=None, pkgrel=None):
|
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,
|
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]
|
installed_size = subprocess.check_output(["du", "-sb", build_dir]).decode().split()[0]
|
||||||
builddate = str(int(time.time()))
|
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}")
|
pkginfo_lines.append(f"depend = {dep}")
|
||||||
for optdep in optdepends:
|
for optdep in optdepends:
|
||||||
pkginfo_lines.append(f"optdepend = {optdep}")
|
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(f"install = {name}.install")
|
||||||
pkginfo_lines.append("makepkgopt = strip\n")
|
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))
|
f.write("\n".join(pkginfo_lines))
|
||||||
|
|
||||||
tar_members = [".PKGINFO", "usr"]
|
tar_members = [".PKGINFO", "usr"]
|
||||||
if all_units:
|
if needs_install_scriptlet:
|
||||||
install_script_name = f"{name}.install"
|
install_script_name = f"{name}.install"
|
||||||
with open(os.path.join(build_dir, install_script_name), "w") as f:
|
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)
|
tar_members.append(install_script_name)
|
||||||
|
|
||||||
os.makedirs("target/arch", exist_ok=True)
|
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
|
//! `smart-mount doctor` - prüft die im Laufe der Entwicklung angesammelten Voraussetzungen
|
||||||
//! (Binaries, Gruppenmitgliedschaft, fstab-Setup, Scheduler) gebündelt an einer Stelle.
|
//! (Binaries, Gruppenmitgliedschaft, fstab-Setup, Scheduler) gebündelt an einer Stelle.
|
||||||
|
|
||||||
use smart_mount::config;
|
use crate::config;
|
||||||
use smart_mount::doctor::{self, CheckStatus};
|
use crate::doctor::{self, CheckStatus};
|
||||||
|
|
||||||
pub async fn run(json: bool) -> anyhow::Result<()> {
|
pub async fn run(json: bool) -> anyhow::Result<()> {
|
||||||
let cfg = config::pairs::load()?;
|
let cfg = config::pairs::load()?;
|
||||||
|
|||||||
+42
-16
@@ -12,10 +12,8 @@ use std::str::FromStr;
|
|||||||
use clap::{Args, Subcommand};
|
use clap::{Args, Subcommand};
|
||||||
use dialoguer::{Confirm, Input, Password, Select};
|
use dialoguer::{Confirm, Input, Password, Select};
|
||||||
|
|
||||||
use smart_mount::config::{
|
use crate::config::{self, AppConfig, CloudSide, DrivePair, LocalAddress, LocalSide, MountKind};
|
||||||
self, AppConfig, CloudSide, DrivePair, LocalAddress, LocalSide, MountKind,
|
use crate::db::credentials::{Credential, CredentialStore, Side};
|
||||||
};
|
|
||||||
use smart_mount::db::credentials::{Credential, CredentialStore, Side};
|
|
||||||
|
|
||||||
#[derive(Subcommand)]
|
#[derive(Subcommand)]
|
||||||
pub enum DriveAction {
|
pub enum DriveAction {
|
||||||
@@ -56,6 +54,8 @@ pub struct DriveArgs {
|
|||||||
local_ip: Option<Ipv4Addr>,
|
local_ip: Option<Ipv4Addr>,
|
||||||
#[arg(long, conflicts_with = "local_ip")]
|
#[arg(long, conflicts_with = "local_ip")]
|
||||||
local_mac: Option<String>,
|
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)]
|
#[arg(long)]
|
||||||
local_share: Option<String>,
|
local_share: Option<String>,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
@@ -72,6 +72,8 @@ pub struct DriveArgs {
|
|||||||
cloud_kind: Option<MountKind>,
|
cloud_kind: Option<MountKind>,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
cloud_host: Option<String>,
|
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)]
|
#[arg(long)]
|
||||||
cloud_share: Option<String>,
|
cloud_share: Option<String>,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
@@ -90,6 +92,18 @@ pub struct DriveArgs {
|
|||||||
non_interactive: bool,
|
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<()> {
|
pub async fn run(action: DriveAction) -> anyhow::Result<()> {
|
||||||
match action {
|
match action {
|
||||||
DriveAction::Add(args) => add(args).await,
|
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
|
// 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
|
// 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 cfg = config::pairs::load()?;
|
||||||
let pair = config::pairs::find_pair(&cfg, id)?;
|
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
|
// 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
|
// mehr an sein eigenes `drive remove` heran, ohne vorher erst als root/mit den richtigen
|
||||||
// Rechten manuell auszuhängen.
|
// 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(
|
logger_ctdra::warn(
|
||||||
"drive",
|
"drive",
|
||||||
&format!("could not unmount drive pair '{id}' before removal: {e}"),
|
&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)?;
|
config::pairs::remove_pair(id)?;
|
||||||
let creds = CredentialStore::open().await?;
|
let creds = CredentialStore::open().await?;
|
||||||
creds.delete(id, None).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"));
|
logger_ctdra::info("drive", &format!("drive pair '{id}' removed"));
|
||||||
println!("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_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_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)?
|
let local_share = resolve_field(
|
||||||
.expect("required");
|
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_password_flag = read_password_flag(args.local_password, args.local_password_stdin)?;
|
||||||
let (local_username, local_password) = resolve_credentials(
|
let (local_username, local_password) = resolve_credentials(
|
||||||
local_kind,
|
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_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)?
|
let cloud_host = resolve_field(args.cloud_host, None, "Cloud server address/URL", ni, true)?
|
||||||
.expect("required");
|
.expect("required");
|
||||||
let cloud_share = resolve_field(args.cloud_share, None, "Cloud share/export path", ni, true)?
|
let cloud_share = resolve_field(
|
||||||
.expect("required");
|
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_password_flag = read_password_flag(args.cloud_password, args.cloud_password_stdin)?;
|
||||||
let (cloud_username, cloud_password) = resolve_credentials(
|
let (cloud_username, cloud_password) = resolve_credentials(
|
||||||
cloud_kind,
|
cloud_kind,
|
||||||
@@ -272,7 +298,7 @@ async fn edit(id: &str, args: DriveArgs) -> anyhow::Result<()> {
|
|||||||
let local_share = resolve_field(
|
let local_share = resolve_field(
|
||||||
args.local_share,
|
args.local_share,
|
||||||
Some(&existing.local.share),
|
Some(&existing.local.share),
|
||||||
"Local share/export path",
|
&share_field_label(local_kind, "Local"),
|
||||||
ni,
|
ni,
|
||||||
true,
|
true,
|
||||||
)?
|
)?
|
||||||
@@ -307,7 +333,7 @@ async fn edit(id: &str, args: DriveArgs) -> anyhow::Result<()> {
|
|||||||
let cloud_share = resolve_field(
|
let cloud_share = resolve_field(
|
||||||
args.cloud_share,
|
args.cloud_share,
|
||||||
Some(&existing.cloud.share),
|
Some(&existing.cloud.share),
|
||||||
"Cloud share/export path",
|
&share_field_label(cloud_kind, "Cloud"),
|
||||||
ni,
|
ni,
|
||||||
true,
|
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.
|
// würde ein aktiver Mount verwaisen. Nur eine explizite '--mount-point'-Angabe ändert ihn.
|
||||||
let mount_point = match args.mount_point {
|
let mount_point = match args.mount_point {
|
||||||
Some(p) => {
|
Some(p) => {
|
||||||
if smart_mount::mount::target::active_side(&existing).is_some() {
|
if crate::mount::target::active_side(&existing).is_some() {
|
||||||
println!(
|
println!(
|
||||||
"Warning: pair '{id}' appears to be actively mounted - the old backing \
|
"Warning: pair '{id}' appears to be actively mounted - the old backing \
|
||||||
directories will be orphaned. Run 'sudo smart-mount unmount --name {id}' \
|
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
|
/// 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
|
/// (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`;
|
/// vorbelegte/vorgeschlagene Wert (bei `add`: der einrichtende Nutzer, siehe `invoking_user`;
|
||||||
/// bei `edit`: der bisherige `owner_user`) - immer überschreibbar per Flag, im interaktiven
|
/// bei `edit`: der bisherige `owner_user`) - immer überschreibbar per Flag, im interaktiven
|
||||||
/// Prompt auch durch Ablehnen oder einen anderen Namen.
|
/// Prompt auch durch Ablehnen oder einen anderen Namen.
|
||||||
@@ -665,7 +691,7 @@ fn resolve_credentials(
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use smart_mount::config::LocalAddress;
|
use crate::config::LocalAddress;
|
||||||
use std::net::Ipv4Addr;
|
use std::net::Ipv4Addr;
|
||||||
|
|
||||||
fn sample_pair(mount_point: &str) -> DrivePair {
|
fn sample_pair(mount_point: &str) -> DrivePair {
|
||||||
|
|||||||
+2
-26
@@ -3,16 +3,14 @@
|
|||||||
pub mod doctor;
|
pub mod doctor;
|
||||||
pub mod drive;
|
pub mod drive;
|
||||||
pub mod mount_cmd;
|
pub mod mount_cmd;
|
||||||
pub mod service;
|
|
||||||
pub mod status;
|
pub mod status;
|
||||||
pub mod watch;
|
pub mod watch;
|
||||||
|
|
||||||
use clap::{CommandFactory, Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
use clap_complete::Shell;
|
|
||||||
|
|
||||||
/// Startet den Prozess bei Bedarf transparent über `sudo` neu, falls nicht bereits als root
|
/// 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
|
/// 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.
|
/// 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
|
/// `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.
|
/// A single reconcile pass (local/cloud switching) - meant for systemd timers/cron.
|
||||||
Watch,
|
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).
|
/// Checks prerequisites (binaries, scheduler).
|
||||||
Doctor {
|
Doctor {
|
||||||
/// Output as JSON instead of text - for scripts.
|
/// Output as JSON instead of text - for scripts.
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
json: bool,
|
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
|
/// 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::Unmount { .. } => "unmount",
|
||||||
Commands::Status { .. } => "status",
|
Commands::Status { .. } => "status",
|
||||||
Commands::Watch => "watch",
|
Commands::Watch => "watch",
|
||||||
Commands::Service { .. } => "service",
|
|
||||||
Commands::Doctor { .. } => "doctor",
|
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::Unmount { name, all } => mount_cmd::run_unmount(name, all).await,
|
||||||
Commands::Status { name, json } => status::run(name, json).await,
|
Commands::Status { name, json } => status::run(name, json).await,
|
||||||
Commands::Watch => watch::run().await,
|
Commands::Watch => watch::run().await,
|
||||||
Commands::Service { action } => service::run(action),
|
|
||||||
Commands::Doctor { json } => doctor::run(json).await,
|
Commands::Doctor { json } => doctor::run(json).await,
|
||||||
Commands::Completions { shell } => {
|
|
||||||
clap_complete::generate(
|
|
||||||
shell,
|
|
||||||
&mut Cli::command(),
|
|
||||||
"smart-mount",
|
|
||||||
&mut std::io::stdout(),
|
|
||||||
);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
//! `smart-mount mount` / `smart-mount unmount`.
|
//! `smart-mount mount` / `smart-mount unmount`.
|
||||||
|
|
||||||
use smart_mount::config::{self, DrivePair};
|
use crate::config::{self, DrivePair};
|
||||||
use smart_mount::db::credentials::CredentialStore;
|
use crate::db::credentials::CredentialStore;
|
||||||
use smart_mount::reconcile;
|
use crate::reconcile;
|
||||||
|
|
||||||
fn select_pairs(
|
fn select_pairs(
|
||||||
cfg: &smart_mount::config::AppConfig,
|
cfg: &crate::config::AppConfig,
|
||||||
name: Option<&str>,
|
name: Option<&str>,
|
||||||
all: bool,
|
all: bool,
|
||||||
) -> anyhow::Result<Vec<DrivePair>> {
|
) -> anyhow::Result<Vec<DrivePair>> {
|
||||||
|
|||||||
@@ -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
@@ -2,10 +2,10 @@
|
|||||||
|
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
use smart_mount::config;
|
use crate::config;
|
||||||
use smart_mount::db::credentials::Side;
|
use crate::db::credentials::Side;
|
||||||
use smart_mount::mount::target;
|
use crate::mount::target;
|
||||||
use smart_mount::network;
|
use crate::network;
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
struct PairStatus {
|
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
|
// wiederverwendet, statt sie für beide Zwecke unabhängig voneinander ein zweites
|
||||||
// Mal aufzulösen.
|
// Mal aufzulösen.
|
||||||
let resolved_local_ip =
|
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
|
let local_source = resolved_local_ip
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|ip| target::format_local_source(&pair.local, *ip))
|
.map(|ip| target::format_local_source(&pair.local, *ip))
|
||||||
|
|||||||
+3
-4
@@ -1,10 +1,9 @@
|
|||||||
//! `smart-mount watch` - ein einzelner Reconcile-Durchlauf, gedacht für systemd-Timer/Cron.
|
//! `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::cli::mount_cmd::print_outcome;
|
||||||
|
use crate::config;
|
||||||
|
use crate::db::credentials::CredentialStore;
|
||||||
|
use crate::reconcile::{self, Action};
|
||||||
|
|
||||||
pub async fn run() -> anyhow::Result<()> {
|
pub async fn run() -> anyhow::Result<()> {
|
||||||
crate::cli::require_root("watch")?;
|
crate::cli::require_root("watch")?;
|
||||||
|
|||||||
+224
-25
@@ -1,12 +1,86 @@
|
|||||||
//! Diagnose-Checks für `smart-mount doctor` - prüft die im Laufe der Entwicklung
|
//! 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
|
//! angesammelten Voraussetzungen (Binaries, Scheduler) gebündelt an einer Stelle, statt sie
|
||||||
//! einzeln erst beim Mount-Fehlschlag zu entdecken.
|
//! 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 std::collections::HashSet;
|
||||||
|
|
||||||
use crate::config::{AppConfig, LocalAddress, MountKind};
|
use crate::config::{AppConfig, LocalAddress, MountKind};
|
||||||
use crate::mount;
|
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)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum CheckStatus {
|
pub enum CheckStatus {
|
||||||
Ok,
|
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.
|
/// Führt alle Checks gegen die aktuelle Konfiguration aus.
|
||||||
pub fn run_checks(cfg: &AppConfig) -> Vec<CheckResult> {
|
pub fn run_checks(cfg: &AppConfig) -> Vec<CheckResult> {
|
||||||
|
let family = detect_package_family();
|
||||||
let mut results = Vec::new();
|
let mut results = Vec::new();
|
||||||
|
|
||||||
results.push(check_timeout_binary());
|
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));
|
results.push(check_mount_base_dir(&cfg.settings.mount_base_dir));
|
||||||
|
|
||||||
let used_kinds = used_mount_kinds(cfg);
|
let used_kinds = used_mount_kinds(cfg);
|
||||||
for kind in [MountKind::WebDav, MountKind::Smb, MountKind::Nfs] {
|
for kind in [MountKind::WebDav, MountKind::Smb, MountKind::Nfs] {
|
||||||
if used_kinds.contains(&kind) {
|
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);
|
let backend = mount::backend_for(kind);
|
||||||
match backend.check_available() {
|
if backend.check_available().is_ok() {
|
||||||
Ok(()) => ok(format!("Backend: {}", backend.name()), "available"),
|
return ok(format!("Backend: {}", backend.name()), "available");
|
||||||
Err(e) => fail(format!("Backend: {}", backend.name()), e.to_string()),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
/// Prüft, ob die periodische Ausführung tatsächlich eingerichtet ist UND läuft - nicht nur, ob
|
||||||
let systemd = crate::systemd::is_available();
|
/// `systemctl`/ein Cron-Mechanismus grundsätzlich auf dem System vorhanden wäre (das allein
|
||||||
let cron_d = std::path::Path::new("/etc/cron.d").is_dir();
|
/// sagt nichts darüber aus, ob smart-mounts eigene Units/Cron-Eintrag auch tatsächlich
|
||||||
let crontab = mount::binary_available("crontab");
|
/// 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 {
|
if timer_installed || mount_installed {
|
||||||
ok(
|
let timer_enabled = crate::systemd::is_unit_enabled("smart-mount-watch.timer");
|
||||||
"Scheduler",
|
let timer_active = crate::systemd::is_unit_active("smart-mount-watch.timer");
|
||||||
"systemd found - the packaged .deb/.rpm/.pkg.tar.zst installs/enables the smart-mount systemd service automatically",
|
let mount_enabled = crate::systemd::is_unit_enabled("smart-mount-mount.service");
|
||||||
)
|
|
||||||
} else if cron_d || crontab {
|
return if timer_enabled && timer_active && mount_enabled {
|
||||||
warn(
|
ok(
|
||||||
"Scheduler",
|
"Scheduler",
|
||||||
"no systemd, but cron found - run 'smart-mount service crontab' to set up the periodic 'watch' call",
|
"systemd: 'smart-mount-watch.timer' is enabled and running, 'smart-mount-mount.service' is enabled",
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
fail(
|
warn(
|
||||||
"Scheduler",
|
"Scheduler",
|
||||||
"neither systemd nor cron found - periodic 'watch' must be set up manually",
|
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 {
|
fn check_mount_base_dir(dir: &std::path::Path) -> CheckResult {
|
||||||
@@ -165,6 +304,66 @@ mod tests {
|
|||||||
use crate::config::{CloudSide, DrivePair, GlobalSettings, LocalSide};
|
use crate::config::{CloudSide, DrivePair, GlobalSettings, LocalSide};
|
||||||
use std::net::Ipv4Addr;
|
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 {
|
fn sample_pair(owner_user: Option<&str>, mac: bool) -> DrivePair {
|
||||||
DrivePair {
|
DrivePair {
|
||||||
id: "pair-1".into(),
|
id: "pair-1".into(),
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
//! smart-mount: bindet lokale/Cloud-Laufwerkspaare (WebDAV/SMB/NFS) dynamisch ein und
|
//! smart-mount: bindet lokale/Cloud-Laufwerkspaare (WebDAV/SMB/NFS) dynamisch ein und
|
||||||
//! schaltet automatisch zwischen LAN und Cloud um.
|
//! schaltet automatisch zwischen LAN und Cloud um.
|
||||||
|
|
||||||
|
pub mod cli;
|
||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod crypto;
|
pub mod crypto;
|
||||||
pub mod db;
|
pub mod db;
|
||||||
|
|||||||
+1
-2
@@ -1,8 +1,7 @@
|
|||||||
mod cli;
|
|
||||||
|
|
||||||
use std::process::ExitCode;
|
use std::process::ExitCode;
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
use smart_mount::cli;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> ExitCode {
|
async fn main() -> ExitCode {
|
||||||
|
|||||||
+98
-21
@@ -1,13 +1,13 @@
|
|||||||
//! Cron-Fallback (`smart-mount service crontab`) für Systeme ohne (oder ohne genutzten)
|
//! Cron-Fallback für Systeme ohne (oder ohne genutzten) systemd.
|
||||||
//! systemd.
|
|
||||||
//!
|
//!
|
||||||
//! Die eigentliche systemd-Einrichtung passiert NICHT mehr zur Laufzeit über dieses Modul,
|
//! Die eigentliche systemd-Einrichtung passiert NICHT zur Laufzeit über dieses Modul, sondern
|
||||||
//! sondern über die paketierten, statischen Unit-Dateien (siehe `packaging/systemd/` im
|
//! über die paketierten, statischen Unit-Dateien (siehe `packaging/systemd/` im Quellbaum)
|
||||||
//! Quellbaum) sowie die postinst/postrm-Skripte der .deb/.rpm/.pkg.tar.zst-Pakete - smart-mount
|
//! sowie die postinst/postrm-Skripte der .deb/.rpm/.pkg.tar.zst-Pakete. Dieses Modul stellt die
|
||||||
//! richtet sich beim Installieren des Pakets automatisch als System-systemd-Dienst ein und
|
//! Logik für den Cron-Fallback bereit - aufgerufen wird sie NICHT interaktiv über ein
|
||||||
//! entfernt sich beim Deinstallieren wieder. Dieses Modul bleibt für Systeme ohne systemd (oder
|
//! `smart-mount`-Subcommand, sondern automatisch vom separaten `setup-cron`-Hilfsprogramm
|
||||||
//! zum bewussten Umgehen von systemd) als manueller Cron-Weg bestehen - immer systemweit
|
//! (siehe `src/bin/setup-cron.rs`), das ebenfalls über die postinst-Skripte der Pakete beim
|
||||||
//! (`/etc/cron.d/smart-mount`), da Mounten ohnehin immer Root-Rechte braucht (siehe
|
//! 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.
|
//! [`crate::cli::require_root`]); eine persönliche Nutzer-Crontab liefe ins Leere.
|
||||||
|
|
||||||
use std::path::{Path, PathBuf};
|
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_BEGIN_MARKER: &str = "# BEGIN smart-mount managed block";
|
||||||
const CRON_END_MARKER: &str = "# END smart-mount managed block";
|
const CRON_END_MARKER: &str = "# END smart-mount managed block";
|
||||||
|
|
||||||
fn binary_path() -> String {
|
/// Fester Installationspfad der Haupt-Binary (siehe `[package.metadata.deb/generate-rpm].assets`
|
||||||
std::env::current_exe()
|
/// bzw. `scripts/package-arch.py`, die immer nach `/usr/bin/<name>` installieren) - smart-mount
|
||||||
.ok()
|
/// läuft ausschließlich als paketierter System-Dienst, es gibt keinen anderen Installationsort.
|
||||||
.and_then(|p| p.to_str().map(str::to_string))
|
/// Bewusst NICHT über `std::env::current_exe()`: der Cron-Eintrag wird vom separaten
|
||||||
.unwrap_or_else(|| "/usr/bin/smart-mount".to_string())
|
/// `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
|
/// Ob `systemctl` auf diesem System vorhanden ist - entscheidet für `doctor` UND für
|
||||||
/// eigentliche systemd-Einrichtung läuft über die Paketierung (siehe Moduldoku).
|
/// `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 {
|
pub fn is_available() -> bool {
|
||||||
crate::mount::binary_available("systemctl")
|
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
|
/// Erzeugt die Crontab-Äquivalente zu den paketierten systemd-Units, für Systeme ohne
|
||||||
/// (genutzten) systemd. `watch_interval_secs` ist derselbe Wert wie
|
/// (genutzten) systemd. `watch_interval_secs` ist derselbe Wert wie
|
||||||
/// `settings.watch_interval_secs`.
|
/// `settings.watch_interval_secs`.
|
||||||
@@ -49,11 +126,11 @@ pub enum CronInstallOutcome {
|
|||||||
Unavailable,
|
Unavailable,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Richtet die periodische Ausführung direkt über `/etc/cron.d/smart-mount` ein - manueller
|
/// Richtet die periodische Ausführung direkt über `/etc/cron.d/smart-mount` ein - Fallback für
|
||||||
/// Fallback für Systeme ohne (genutzten) systemd, sofern `/etc/cron.d` existiert - sonst
|
/// Systeme ohne (genutzten) systemd, sofern `/etc/cron.d` existiert - sonst
|
||||||
/// [`CronInstallOutcome::Unavailable`] statt eines Fehlers, der Aufrufer zeigt dann
|
/// [`CronInstallOutcome::Unavailable`] statt eines Fehlers, der Aufrufer (`setup-cron`, siehe
|
||||||
/// [`crontab_equivalent`] zur manuellen Einrichtung. Erfordert Root (siehe Moduldoku) - der
|
/// Moduldoku) zeigt dann [`crontab_equivalent`] zur manuellen Einrichtung. Erfordert Root
|
||||||
/// Aufrufer (`cli::service`) prüft das bereits vorab.
|
/// (siehe Moduldoku) - beim Aufruf aus dem postinst-Skript der Pakete ohnehin gegeben.
|
||||||
pub fn install_cron(watch_interval_secs: u64) -> Result<CronInstallOutcome> {
|
pub fn install_cron(watch_interval_secs: u64) -> Result<CronInstallOutcome> {
|
||||||
if !Path::new("/etc/cron.d").is_dir() {
|
if !Path::new("/etc/cron.d").is_dir() {
|
||||||
return Ok(CronInstallOutcome::Unavailable);
|
return Ok(CronInstallOutcome::Unavailable);
|
||||||
|
|||||||
Reference in New Issue
Block a user