Author SHA1 Message Date
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
25 changed files with 42 additions and 299 deletions
Generated
+1 -1
View File
@@ -2052,7 +2052,7 @@ checksum = "ba467056f1b547ed52077911161fc86985becbc60e8e1857c8a144dab0def891"
[[package]] [[package]]
name = "smart-mount" name = "smart-mount"
version = "2.0.1" version = "2.0.0"
dependencies = [ dependencies = [
"aes-gcm 0.11.1", "aes-gcm 0.11.1",
"anyhow", "anyhow",
+7 -8
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "smart-mount" name = "smart-mount"
version = "2.0.1" version = "2.0.0"
edition = "2024" edition = "2024"
authors = ['DragonSlayer_14'] authors = ['DragonSlayer_14']
readme = "README.md" readme = "README.md"
@@ -46,11 +46,10 @@ maintainer = "DragonSlayer_14"
copyright = "2026 DragonSlayer_14" copyright = "2026 DragonSlayer_14"
section = "net" section = "net"
priority = "optional" priority = "optional"
# mac2ip ist eine harte Laufzeitabhängigkeit (MAC->IP-Auflösung; # mac2ip und nmap sind harte Laufzeitabhängigkeiten (MAC->IP-Auflösung); davfs2/cifs-utils/
# davfs2/cifs-utils/nfs-common werden bewusst NICHT hart verlangt, # nfs-common werden bewusst NICHT hart verlangt, da smart-mount pro konfiguriertem Laufwerk
# da smart-mount pro konfiguriertem Laufwerk nur das jeweils benötigte Mount-Backend # nur das jeweils benötigte Mount-Backend zur Laufzeit prüft/lädt (siehe MountBackend::check_available).
# zur Laufzeit prüft/lädt (siehe MountBackend::check_available). depends = "$auto, mac2ip, nmap"
depends = "$auto, mac2ip"
recommends = "davfs2, cifs-utils, nfs-common" recommends = "davfs2, cifs-utils, nfs-common"
extended-description = """\ extended-description = """\
smart-mount bindet Paare aus einem lokalen (LAN, WebDAV/SMB/NFS) und einem Cloud-Laufwerk smart-mount bindet Paare aus einem lokalen (LAN, WebDAV/SMB/NFS) und einem Cloud-Laufwerk
@@ -79,7 +78,7 @@ assets = [
{ 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" },
] ]
requires = { "mac2ip" = "*" } requires = { "mac2ip" = "*", "nmap" = "*" }
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 automatisch ein/entfernen
# (Gegenstück zu packaging/deb/postinst+postrm). $1 in %postun: Anzahl verbleibender Versionen # (Gegenstück zu packaging/deb/postinst+postrm). $1 in %postun: Anzahl verbleibender Versionen
@@ -100,7 +99,7 @@ fi
[package.metadata.arch] [package.metadata.arch]
pkgrel = "1" # Wird in CI durch get-build-number.py dynamisch überschrieben pkgrel = "1" # Wird in CI durch get-build-number.py dynamisch überschrieben
arch = "x86_64" arch = "x86_64"
depends = ["gcc-libs", "glibc", "mac2ip"] depends = ["gcc-libs", "glibc", "mac2ip", "nmap"]
optdepends = [ optdepends = [
"davfs2: WebDAV-Laufwerke einbinden", "davfs2: WebDAV-Laufwerke einbinden",
"cifs-utils: SMB/CIFS-Laufwerke einbinden", "cifs-utils: SMB/CIFS-Laufwerke einbinden",
+2 -3
View File
@@ -14,9 +14,8 @@ werden (z. B. über ein separates Sync-Tool).
## Voraussetzungen ## Voraussetzungen
- **[`mac2ip`](https://gitea.creative-dragonslayer.de/Linuxapps/Mac2Ip)**: hart benötigt, sofern - **[`mac2ip`](https://gitea.creative-dragonslayer.de/Linuxapps/Mac2Ip)** und **`nmap`**: hart
lokale Laufwerke per MAC-Adresse adressiert werden (Auflösung MAC → IP). `mac2ip` verlangt benötigt, sofern lokale Laufwerke per MAC-Adresse adressiert werden (Auflösung MAC → IP).
`nmap` bereits selbst als eigene Paketabhängigkeit.
- **`timeout`** (GNU coreutils): auf praktisch jedem Linux-System bereits vorhanden (Basis- - **`timeout`** (GNU coreutils): auf praktisch jedem Linux-System bereits vorhanden (Basis-
Systempaket). Wird verwendet, um `mount`/`umount`-Aufrufe zeitlich zu begrenzen - siehe Systempaket). Wird verwendet, um `mount`/`umount`-Aufrufe zeitlich zu begrenzen - siehe
"Verhalten bei nicht mehr erreichbarem Server" unten. "Verhalten bei nicht mehr erreichbarem Server" unten.
-3
View File
@@ -149,7 +149,6 @@ async fn remove(id: &str) -> anyhow::Result<()> {
creds.delete(id, None).await?; creds.delete(id, None).await?;
smart_mount::mount::cleanup_credentials(&pair, &cfg.settings); smart_mount::mount::cleanup_credentials(&pair, &cfg.settings);
logger_ctdra::info("drive", &format!("drive pair '{id}' removed"));
println!("Drive pair '{id}' removed."); println!("Drive pair '{id}' removed.");
Ok(()) Ok(())
} }
@@ -242,7 +241,6 @@ async fn add(args: DriveArgs) -> anyhow::Result<()> {
.await?; .await?;
} }
logger_ctdra::info("drive", &format!("drive pair '{id}' created"));
println!("Drive pair '{id}' created."); println!("Drive pair '{id}' created.");
Ok(()) Ok(())
} }
@@ -375,7 +373,6 @@ async fn edit(id: &str, args: DriveArgs) -> anyhow::Result<()> {
.await?; .await?;
} }
logger_ctdra::info("drive", &format!("drive pair '{id}' updated"));
println!("Drive pair '{id}' updated."); println!("Drive pair '{id}' updated.");
Ok(()) Ok(())
} }
+5 -34
View File
@@ -10,24 +10,14 @@ pub mod watch;
use clap::{CommandFactory, Parser, Subcommand}; use clap::{CommandFactory, Parser, Subcommand};
use clap_complete::Shell; use clap_complete::Shell;
/// Startet den Prozess bei Bedarf transparent über `sudo` neu, falls nicht bereits als root /// Bricht mit einer klaren Fehlermeldung ab, falls nicht als root aufgerufen. Mounten läuft
/// aufgerufen. Mounten läuft ausschließlich als root/System-Dienst (siehe /// ausschließlich als root/System-Dienst (siehe [`smart_mount::systemd`]) - es gibt seit dem
/// [`smart_mount::systemd`]) - es gibt seit dem Wegfall des Nutzerkontexts keine /// Wegfall des Nutzerkontexts keine unprivilegierte Mount-Variante mehr, für die ein
/// unprivilegierte Mount-Variante mehr, für die ein Rechte-Check hier zu früh käme. /// Rechte-Check hier zu früh käme.
///
/// `run_as_root()` ersetzt den aktuellen Prozess per `execve` und kehrt bei Erfolg nie zurück
/// (siehe sudo-ctdra); der `io::Error`-Rückgabewert ist ausschließlich der Fehlerfall (z. B.
/// `sudo` nicht gefunden).
pub(crate) fn require_root(context: &str) -> anyhow::Result<()> { pub(crate) fn require_root(context: &str) -> anyhow::Result<()> {
if !sudo_ctdra::is_run_as_root() { if !sudo_ctdra::is_run_as_root() {
logger_ctdra::info( anyhow::bail!("'{context}' requires root privileges (re-run with sudo).");
"cli",
&format!("'{context}' requires root - re-executing via sudo"),
);
let err = sudo_ctdra::run_as_root();
anyhow::bail!("'{context}' requires root privileges: could not re-exec via sudo: {err}");
} }
logger_ctdra::debug("cli", &format!("'{context}': already running as root"));
Ok(()) Ok(())
} }
@@ -93,27 +83,8 @@ pub enum Commands {
Completions { shell: Shell }, Completions { shell: Shell },
} }
/// Kurzname eines Subcommands fürs Logging (siehe [`dispatch`]) - kein `Debug`-Derive auf
/// `Commands` nötig, das würde auch die (teils sensiblen) Argument-Felder mit abdrucken.
fn command_label(cmd: &Commands) -> &'static str {
match cmd {
Commands::Drive { .. } => "drive",
Commands::Mount { .. } => "mount",
Commands::Unmount { .. } => "unmount",
Commands::Status { .. } => "status",
Commands::Watch => "watch",
Commands::Service { .. } => "service",
Commands::Doctor { .. } => "doctor",
Commands::Completions { .. } => "completions",
}
}
/// Führt das per `Cli` geparste Subcommand aus. /// Führt das per `Cli` geparste Subcommand aus.
pub async fn dispatch(cli: Cli) -> anyhow::Result<()> { pub async fn dispatch(cli: Cli) -> anyhow::Result<()> {
logger_ctdra::debug(
"cli",
&format!("dispatching '{}'", command_label(&cli.command)),
);
match cli.command { match cli.command {
Commands::Drive { action } => drive::run(*action).await, Commands::Drive { action } => drive::run(*action).await,
Commands::Mount { name, all } => mount_cmd::run_mount(name, all).await, Commands::Mount { name, all } => mount_cmd::run_mount(name, all).await,
-8
View File
@@ -26,10 +26,6 @@ pub async fn run_mount(name: Option<String>, all: bool) -> anyhow::Result<()> {
println!("No matching drive pairs found."); println!("No matching drive pairs found.");
return Ok(()); return Ok(());
} }
logger_ctdra::info(
"mount",
&format!("mount: processing {} drive pair(s)", pairs.len()),
);
let creds = CredentialStore::open().await?; let creds = CredentialStore::open().await?;
for pair in &pairs { for pair in &pairs {
@@ -47,10 +43,6 @@ pub async fn run_unmount(name: Option<String>, all: bool) -> anyhow::Result<()>
println!("No matching drive pairs found."); println!("No matching drive pairs found.");
return Ok(()); return Ok(());
} }
logger_ctdra::info(
"mount",
&format!("unmount: processing {} drive pair(s)", pairs.len()),
);
for pair in &pairs { for pair in &pairs {
match reconcile::unmount_pair(pair, &cfg.settings).await { match reconcile::unmount_pair(pair, &cfg.settings).await {
+1 -8
View File
@@ -31,10 +31,6 @@ fn install() -> anyhow::Result<()> {
match systemd::install_cron(interval)? { match systemd::install_cron(interval)? {
systemd::CronInstallOutcome::SystemFile(path) => { systemd::CronInstallOutcome::SystemFile(path) => {
logger_ctdra::info(
"service",
&format!("cron entry written: {}", path.display()),
);
println!("Cron entry written: {}", path.display()); println!("Cron entry written: {}", path.display());
} }
systemd::CronInstallOutcome::Unavailable => { systemd::CronInstallOutcome::Unavailable => {
@@ -49,10 +45,7 @@ fn install() -> anyhow::Result<()> {
fn uninstall() -> anyhow::Result<()> { fn uninstall() -> anyhow::Result<()> {
match systemd::uninstall_cron()? { match systemd::uninstall_cron()? {
systemd::CronUninstallOutcome::Removed => { systemd::CronUninstallOutcome::Removed => println!("Cron entry removed."),
logger_ctdra::info("service", "cron entry removed");
println!("Cron entry removed.");
}
systemd::CronUninstallOutcome::NotPresent => { systemd::CronUninstallOutcome::NotPresent => {
println!("Nothing to remove - no cron entry was installed.") println!("Nothing to remove - no cron entry was installed.")
} }
-12
View File
@@ -19,18 +19,6 @@ pub async fn run() -> anyhow::Result<()> {
had_failure = true; had_failure = true;
} }
} }
logger_ctdra::info(
"watch",
&format!(
"watch pass done: {} pair(s), {}",
outcomes.len(),
if had_failure {
"with failures"
} else {
"no failures"
}
),
);
if had_failure { if had_failure {
anyhow::bail!("at least one drive pair could not be reconciled"); anyhow::bail!("at least one drive pair could not be reconciled");
+1 -3
View File
@@ -19,7 +19,5 @@ pub use schema::{
pub fn init() { pub fn init() {
config_ctdra::set_config_name("config"); config_ctdra::set_config_name("config");
let program = config_ctdra::get_program_name(); let program = config_ctdra::get_program_name();
let dir = std::path::PathBuf::from("/etc").join(program); config_ctdra::set_custom_dir(std::path::PathBuf::from("/etc").join(program));
logger_ctdra::debug("config", &format!("config directory: {}", dir.display()));
config_ctdra::set_custom_dir(dir);
} }
-6
View File
@@ -10,10 +10,6 @@ pub fn load() -> Result<AppConfig> {
/// Fügt ein neues Laufwerkspaar hinzu (load-mutate-store in einem atomaren Schritt). /// Fügt ein neues Laufwerkspaar hinzu (load-mutate-store in einem atomaren Schritt).
pub fn add_pair(pair: DrivePair) -> Result<AppConfig> { pub fn add_pair(pair: DrivePair) -> Result<AppConfig> {
logger_ctdra::debug(
"config",
&format!("writing new pair '{}' to config", pair.id),
);
Ok(config_ctdra::modify::<AppConfig, _>(|cfg| { Ok(config_ctdra::modify::<AppConfig, _>(|cfg| {
cfg.pairs.push(pair.clone()); cfg.pairs.push(pair.clone());
})?) })?)
@@ -22,7 +18,6 @@ pub fn add_pair(pair: DrivePair) -> Result<AppConfig> {
/// Ersetzt ein bestehendes Laufwerkspaar (Vergleich über `id`). /// Ersetzt ein bestehendes Laufwerkspaar (Vergleich über `id`).
pub fn update_pair(pair: DrivePair) -> Result<AppConfig> { pub fn update_pair(pair: DrivePair) -> Result<AppConfig> {
let id = pair.id.clone(); let id = pair.id.clone();
logger_ctdra::debug("config", &format!("writing updated pair '{id}' to config"));
let updated = config_ctdra::modify::<AppConfig, _>(move |cfg| { let updated = config_ctdra::modify::<AppConfig, _>(move |cfg| {
if let Some(existing) = cfg.pairs.iter_mut().find(|p| p.id == pair.id) { if let Some(existing) = cfg.pairs.iter_mut().find(|p| p.id == pair.id) {
*existing = pair.clone(); *existing = pair.clone();
@@ -36,7 +31,6 @@ pub fn update_pair(pair: DrivePair) -> Result<AppConfig> {
/// Entfernt ein Laufwerkspaar per ID. /// Entfernt ein Laufwerkspaar per ID.
pub fn remove_pair(id: &str) -> Result<AppConfig> { pub fn remove_pair(id: &str) -> Result<AppConfig> {
logger_ctdra::debug("config", &format!("removing pair '{id}' from config"));
// Die "vorher"-Länge wird INNERHALB desselben `modify`-Aufrufs (auf der bereits frisch // Die "vorher"-Länge wird INNERHALB desselben `modify`-Aufrufs (auf der bereits frisch
// geladenen `cfg`) ermittelt statt über einen separaten, vorgelagerten `load()`-Aufruf: // geladenen `cfg`) ermittelt statt über einen separaten, vorgelagerten `load()`-Aufruf:
// zwischen zwei getrennten Aufrufen könnte ein nebenläufiger Schreiber die Paarliste // zwischen zwei getrennten Aufrufen könnte ein nebenläufiger Schreiber die Paarliste
-8
View File
@@ -32,16 +32,8 @@ mod file_key {
pub fn load_or_create(path: &Path) -> Result<[u8; 32]> { pub fn load_or_create(path: &Path) -> Result<[u8; 32]> {
if path.exists() { if path.exists() {
logger_ctdra::debug(
"crypto",
&format!("loading master key from '{}'", path.display()),
);
return read(path); return read(path);
} }
logger_ctdra::info(
"crypto",
&format!("creating new master key at '{}'", path.display()),
);
create(path) create(path)
} }
-2
View File
@@ -29,7 +29,6 @@ pub const NONCE_LEN: usize = 12;
/// aes-gcms Re-Export-Kette nicht automatisch aktiviert wird). `getrandom::fill` ist /// aes-gcms Re-Export-Kette nicht automatisch aktiviert wird). `getrandom::fill` ist
/// unabhängig davon stabil und genau für diesen Zweck gedacht. /// unabhängig davon stabil und genau für diesen Zweck gedacht.
pub fn encrypt(plaintext: &[u8], key: &[u8; 32]) -> Result<(Vec<u8>, Vec<u8>)> { pub fn encrypt(plaintext: &[u8], key: &[u8; 32]) -> Result<(Vec<u8>, Vec<u8>)> {
logger_ctdra::debug("crypto", "encrypting credential data");
let cipher = Aes256Gcm::new(&Key::<Aes256Gcm>::from(*key)); let cipher = Aes256Gcm::new(&Key::<Aes256Gcm>::from(*key));
let mut nonce_bytes = [0u8; NONCE_LEN]; let mut nonce_bytes = [0u8; NONCE_LEN];
@@ -45,7 +44,6 @@ pub fn encrypt(plaintext: &[u8], key: &[u8; 32]) -> Result<(Vec<u8>, Vec<u8>)> {
/// Entschlüsselt einen zuvor mit [`encrypt`] erzeugten Ciphertext. /// Entschlüsselt einen zuvor mit [`encrypt`] erzeugten Ciphertext.
pub fn decrypt(ciphertext: &[u8], nonce: &[u8], key: &[u8; 32]) -> Result<Vec<u8>> { pub fn decrypt(ciphertext: &[u8], nonce: &[u8], key: &[u8; 32]) -> Result<Vec<u8>> {
logger_ctdra::debug("crypto", "decrypting credential data");
if nonce.len() != NONCE_LEN { if nonce.len() != NONCE_LEN {
return Err(Error::Crypto(format!( return Err(Error::Crypto(format!(
"invalid nonce length: expected {NONCE_LEN}, got {}", "invalid nonce length: expected {NONCE_LEN}, got {}",
-21
View File
@@ -58,13 +58,6 @@ impl CredentialStore {
domain: Option<&str>, domain: Option<&str>,
password: &str, password: &str,
) -> Result<()> { ) -> Result<()> {
logger_ctdra::debug(
"db",
&format!(
"storing credential for pair '{pair_id}' ({})",
side.as_str()
),
);
let key = resolve_master_key()?; let key = resolve_master_key()?;
let (ciphertext, nonce) = crypto::encrypt(password.as_bytes(), &key)?; let (ciphertext, nonce) = crypto::encrypt(password.as_bytes(), &key)?;
let now = now_unix(); let now = now_unix();
@@ -92,13 +85,6 @@ impl CredentialStore {
/// Liest und entschlüsselt die Zugangsdaten für `pair_id`/`side`, falls vorhanden. /// Liest und entschlüsselt die Zugangsdaten für `pair_id`/`side`, falls vorhanden.
pub async fn get(&self, pair_id: &str, side: Side) -> Result<Option<Credential>> { pub async fn get(&self, pair_id: &str, side: Side) -> Result<Option<Credential>> {
logger_ctdra::debug(
"db",
&format!(
"reading credential for pair '{pair_id}' ({})",
side.as_str()
),
);
let mut rows = self let mut rows = self
.conn .conn
.query( .query(
@@ -131,13 +117,6 @@ impl CredentialStore {
/// Löscht Zugangsdaten. `side = None` löscht beide Seiten (z. B. beim Entfernen eines Paars). /// Löscht Zugangsdaten. `side = None` löscht beide Seiten (z. B. beim Entfernen eines Paars).
pub async fn delete(&self, pair_id: &str, side: Option<Side>) -> Result<()> { pub async fn delete(&self, pair_id: &str, side: Option<Side>) -> Result<()> {
logger_ctdra::debug(
"db",
&format!(
"deleting credential(s) for pair '{pair_id}' ({})",
side.map(|s| s.as_str()).unwrap_or("both sides")
),
);
match side { match side {
Some(side) => { Some(side) => {
self.conn self.conn
-1
View File
@@ -21,7 +21,6 @@ pub fn resolve_db_path() -> PathBuf {
/// Öffnet (und initialisiert bei Bedarf) die lokale Datenbank am aufgelösten Pfad. /// Öffnet (und initialisiert bei Bedarf) die lokale Datenbank am aufgelösten Pfad.
pub async fn open() -> Result<turso::Connection> { pub async fn open() -> Result<turso::Connection> {
let path = resolve_db_path(); let path = resolve_db_path();
logger_ctdra::debug("db", &format!("opening database at '{}'", path.display()));
if let Some(dir) = path.parent() { if let Some(dir) = path.parent() {
tokio::fs::create_dir_all(dir) tokio::fs::create_dir_all(dir)
.await .await
+1
View File
@@ -64,6 +64,7 @@ pub fn run_checks(cfg: &AppConfig) -> Vec<CheckResult> {
&cfg.settings.mac2ip_binary, &cfg.settings.mac2ip_binary,
"install mac2ip (private tool, see README)", "install mac2ip (private tool, see README)",
)); ));
results.push(check_binary("nmap", "nmap", "install package 'nmap'"));
} }
results results
-7
View File
@@ -13,13 +13,6 @@ async fn main() -> ExitCode {
Err(_) => "info".to_string(), Err(_) => "info".to_string(),
}; };
logger_ctdra::set_log_level(parse_log_level(&log_level)); logger_ctdra::set_log_level(parse_log_level(&log_level));
logger_ctdra::info(
"main",
&format!(
"smart-mount {} starting (log level: {log_level})",
env!("CARGO_PKG_VERSION")
),
);
let cli = cli::Cli::parse(); let cli = cli::Cli::parse();
match cli::dispatch(cli).await { match cli::dispatch(cli).await {
+1 -13
View File
@@ -23,16 +23,9 @@ use crate::error::{Error, Result};
/// Datei-Deskriptor), bis der Guard gedroppt wird - Schließen des Deskriptors gibt die Sperre /// Datei-Deskriptor), bis der Guard gedroppt wird - Schließen des Deskriptors gibt die Sperre
/// implizit frei, ein explizites `unlock()` ist dafür nicht nötig. /// implizit frei, ein explizites `unlock()` ist dafür nicht nötig.
pub struct PairLock { pub struct PairLock {
pair_id: String,
_file: File, _file: File,
} }
impl Drop for PairLock {
fn drop(&mut self) {
logger_ctdra::debug("lock", &format!("pair '{}': lock released", self.pair_id));
}
}
/// Verzeichnis für die Sperrdateien: `/run/smart-mount/locks` im Root-/System-Kontext (root ist /// Verzeichnis für die Sperrdateien: `/run/smart-mount/locks` im Root-/System-Kontext (root ist
/// dort ohnehin die einzige Partei, die Paare in diesem Kontext mountet), sonst /// dort ohnehin die einzige Partei, die Paare in diesem Kontext mountet), sonst
/// `$XDG_RUNTIME_DIR` (per-Nutzer, von systemd `0700`-geschützt angelegt) mit Fallback auf das /// `$XDG_RUNTIME_DIR` (per-Nutzer, von systemd `0700`-geschützt angelegt) mit Fallback auf das
@@ -66,13 +59,8 @@ fn acquire_blocking(pair_id: &str) -> Result<PairLock> {
// Blockiert, bis die Sperre frei wird - `flock(2)` kennt keinen Async-Mechanismus, daher // Blockiert, bis die Sperre frei wird - `flock(2)` kennt keinen Async-Mechanismus, daher
// läuft dieser gesamte Aufruf über `spawn_blocking` (siehe [`acquire`]) auf einem // läuft dieser gesamte Aufruf über `spawn_blocking` (siehe [`acquire`]) auf einem
// Blocking-Thread statt einem Tokio-Worker-Thread. // Blocking-Thread statt einem Tokio-Worker-Thread.
logger_ctdra::debug("lock", &format!("pair '{pair_id}': waiting for lock"));
file.lock().map_err(|e| Error::io(&path, e))?; file.lock().map_err(|e| Error::io(&path, e))?;
logger_ctdra::debug("lock", &format!("pair '{pair_id}': lock acquired")); Ok(PairLock { _file: file })
Ok(PairLock {
pair_id: pair_id.to_string(),
_file: file,
})
} }
/// Sperrt ein Laufwerkspaar prozessübergreifend für die Dauer des zurückgegebenen Guards. /// Sperrt ein Laufwerkspaar prozessübergreifend für die Dauer des zurückgegebenen Guards.
+5 -21
View File
@@ -93,17 +93,6 @@ fn run_tolerating_already_done_with_timeout(
tolerate_timeout: bool, tolerate_timeout: bool,
timeout_secs: u64, timeout_secs: u64,
) -> Result<()> { ) -> Result<()> {
logger_ctdra::debug(
"mount",
&format!(
"{context}: running '{} {}' (timeout {timeout_secs}s)",
cmd.get_program().to_string_lossy(),
cmd.get_args()
.map(|a| a.to_string_lossy().into_owned())
.collect::<Vec<_>>()
.join(" ")
),
);
let output = let output =
run_with_timeout(cmd, timeout_secs).map_err(|e| crate::error::Error::MountFailed { run_with_timeout(cmd, timeout_secs).map_err(|e| crate::error::Error::MountFailed {
context: context.to_string(), context: context.to_string(),
@@ -111,7 +100,6 @@ fn run_tolerating_already_done_with_timeout(
})?; })?;
if output.status.success() { if output.status.success() {
logger_ctdra::debug("mount", &format!("{context}: succeeded"));
return Ok(()); return Ok(());
} }
@@ -185,20 +173,16 @@ fn cleanup_side_credentials(
match kind { match kind {
MountKind::Smb => { MountKind::Smb => {
let path = smb::credentials_path(&pair.id, side); let path = smb::credentials_path(&pair.id, side);
if path.exists() { if path.exists()
match std::fs::remove_file(&path) { && let Err(e) = std::fs::remove_file(&path)
Ok(()) => logger_ctdra::debug( {
"mount", logger_ctdra::warn(
&format!("deleted credentials file '{}'", path.display()),
),
Err(e) => logger_ctdra::warn(
"mount", "mount",
&format!( &format!(
"Could not delete credentials file '{}': {e}", "Could not delete credentials file '{}': {e}",
path.display() path.display()
), ),
), );
}
} }
} }
MountKind::WebDav => { MountKind::WebDav => {
+1 -6
View File
@@ -29,12 +29,7 @@ impl MountBackend for SmbBackend {
fn prepare(&self, target: &MountTarget, cred: Option<&Credential>) -> Result<()> { fn prepare(&self, target: &MountTarget, cred: Option<&Credential>) -> Result<()> {
if let Some(cred) = cred { if let Some(cred) = cred {
let path = credentials_path(&target.pair_id, target.side); write_credentials_file(&credentials_path(&target.pair_id, target.side), cred)?;
logger_ctdra::debug(
"mount",
&format!("cifs: writing credentials file '{}'", path.display()),
);
write_credentials_file(&path, cred)?;
} }
Ok(()) Ok(())
} }
-9
View File
@@ -301,15 +301,6 @@ pub fn activate_symlink(pair: &DrivePair, side: Side) -> Result<()> {
let _ = std::fs::remove_file(&tmp_path); let _ = std::fs::remove_file(&tmp_path);
std::os::unix::fs::symlink(&target, &tmp_path).map_err(|e| Error::io(&tmp_path, e))?; std::os::unix::fs::symlink(&target, &tmp_path).map_err(|e| Error::io(&tmp_path, e))?;
std::fs::rename(&tmp_path, link_path).map_err(|e| Error::io(link_path, e))?; std::fs::rename(&tmp_path, link_path).map_err(|e| Error::io(link_path, e))?;
logger_ctdra::info(
"mount",
&format!(
"pair '{}': activated '{}' -> '{}'",
pair.id,
link_path.display(),
target.display()
),
);
Ok(()) Ok(())
} }
-4
View File
@@ -46,10 +46,6 @@ impl MountBackend for WebDavBackend {
"davfs2 credential has an empty username or password".to_string(), "davfs2 credential has an empty username or password".to_string(),
)); ));
} }
logger_ctdra::debug(
"mount",
&format!("davfs2: writing secrets entry for '{}'", target.source),
);
write_secrets_entry( write_secrets_entry(
&davfs2_secrets_path(), &davfs2_secrets_path(),
&target.source, &target.source,
+1 -19
View File
@@ -38,12 +38,6 @@ enum Mac2IpOutput {
/// `binary` ist der konfigurierte Binary-Name/-Pfad (`GlobalSettings::mac2ip_binary`, /// `binary` ist der konfigurierte Binary-Name/-Pfad (`GlobalSettings::mac2ip_binary`,
/// standardmäßig `"mac2ip"`, per PATH aufgelöst). /// standardmäßig `"mac2ip"`, per PATH aufgelöst).
pub fn resolve(mac: &str, binary: &str) -> Result<Ipv4Addr> { pub fn resolve(mac: &str, binary: &str) -> Result<Ipv4Addr> {
logger_ctdra::info(
"network",
&format!(
"resolving MAC '{mac}' via '{binary}' (may take up to {MAC2IP_TIMEOUT_SECS}s, e.g. for an nmap scan)"
),
);
let output = Command::new("timeout") let output = Command::new("timeout")
.arg(MAC2IP_TIMEOUT_SECS.to_string()) .arg(MAC2IP_TIMEOUT_SECS.to_string())
.arg(binary) .arg(binary)
@@ -55,25 +49,13 @@ pub fn resolve(mac: &str, binary: &str) -> Result<Ipv4Addr> {
})?; })?;
if output.status.code() == Some(124) { if output.status.code() == Some(124) {
logger_ctdra::warn(
"network",
&format!("'{binary}' did not respond within {MAC2IP_TIMEOUT_SECS}s for MAC '{mac}'"),
);
return Err(Error::Mac2Ip { return Err(Error::Mac2Ip {
mac: mac.to_string(), mac: mac.to_string(),
reason: format!("'{binary}' did not respond within {MAC2IP_TIMEOUT_SECS}s (timed out)"), reason: format!("'{binary}' did not respond within {MAC2IP_TIMEOUT_SECS}s (timed out)"),
}); });
} }
let result = parse_output(&output.stdout, mac); parse_output(&output.stdout, mac)
match &result {
Ok(ip) => logger_ctdra::info("network", &format!("MAC '{mac}' resolved to {ip}")),
Err(e) => logger_ctdra::debug(
"network",
&format!("MAC '{mac}' could not be resolved: {e}"),
),
}
result
} }
/// Prüft, ob das konfigurierte `mac2ip`-Binary über `PATH` auffindbar ist. /// Prüft, ob das konfigurierte `mac2ip`-Binary über `PATH` auffindbar ist.
-9
View File
@@ -17,15 +17,6 @@ use std::process::{Command, Stdio};
/// (z. B. ein kaputter Reverse-Proxy) sonst bei jedem `watch`-Durchlauf einen vollen, /// (z. B. ein kaputter Reverse-Proxy) sonst bei jedem `watch`-Durchlauf einen vollen,
/// letztlich erfolglosen Umschaltversuch (inkl. `MOUNT_TIMEOUT_SECS`-Wartezeit) auslösen würde. /// letztlich erfolglosen Umschaltversuch (inkl. `MOUNT_TIMEOUT_SECS`-Wartezeit) auslösen würde.
pub fn is_reachable(addr: &str) -> bool { pub fn is_reachable(addr: &str) -> bool {
let reachable = is_reachable_inner(addr);
logger_ctdra::debug(
"network",
&format!("reachability check for '{addr}': {reachable}"),
);
reachable
}
fn is_reachable_inner(addr: &str) -> bool {
if addr.starts_with("http://") || addr.starts_with("https://") { if addr.starts_with("http://") || addr.starts_with("https://") {
Command::new("curl") Command::new("curl")
.args([ .args([
+6 -78
View File
@@ -50,13 +50,6 @@ pub struct ReconcileOutcome {
/// Risiko für einen Effizienzgewinn, den die Reachability-Parallelisierung bereits liefert. /// Risiko für einen Effizienzgewinn, den die Reachability-Parallelisierung bereits liefert.
pub async fn watch_once(cfg: &AppConfig, creds: &CredentialStore) -> Vec<ReconcileOutcome> { pub async fn watch_once(cfg: &AppConfig, creds: &CredentialStore) -> Vec<ReconcileOutcome> {
let enabled: Vec<&DrivePair> = cfg.pairs.iter().filter(|p| p.enabled).collect(); let enabled: Vec<&DrivePair> = cfg.pairs.iter().filter(|p| p.enabled).collect();
logger_ctdra::debug(
"reconcile",
&format!(
"watch: checking reachability for {} enabled pair(s)",
enabled.len()
),
);
let mut checks = Vec::with_capacity(enabled.len()); let mut checks = Vec::with_capacity(enabled.len());
for pair in &enabled { for pair in &enabled {
@@ -130,28 +123,16 @@ async fn reconcile_pair_checked(
) )
.await .await
{ {
Ok(action) => { Ok(action) => ReconcileOutcome {
logger_ctdra::info(
"reconcile",
&format!("pair '{pair_name}' ({pair_id}): {action:?}"),
);
ReconcileOutcome {
pair_id, pair_id,
pair_name, pair_name,
action, action,
} },
} Err(e) => ReconcileOutcome {
Err(e) => {
logger_ctdra::error(
"reconcile",
&format!("pair '{pair_name}' ({pair_id}): reconcile failed: {e}"),
);
ReconcileOutcome {
pair_id, pair_id,
pair_name, pair_name,
action: Action::Failed(e.to_string()), action: Action::Failed(e.to_string()),
} },
}
} }
} }
@@ -166,13 +147,6 @@ async fn reconcile_pair_inner(
let _guard = lock::acquire(&pair.id).await?; let _guard = lock::acquire(&pair.id).await?;
let active = target::active_side(pair); let active = target::active_side(pair);
logger_ctdra::debug(
"reconcile",
&format!(
"pair '{}': local_reachable={local_reachable} cloud_reachable={cloud_reachable} active={:?}",
pair.id, active
),
);
cleanup_orphaned_mounts(pair, settings, active).await; cleanup_orphaned_mounts(pair, settings, active).await;
if local_reachable { if local_reachable {
@@ -208,13 +182,7 @@ async fn reconcile_pair_inner(
fn check_local_reachable(local: &LocalSide, settings: &GlobalSettings) -> (bool, Option<Ipv4Addr>) { fn check_local_reachable(local: &LocalSide, settings: &GlobalSettings) -> (bool, Option<Ipv4Addr>) {
match address::resolve_ip(&local.address, settings) { match address::resolve_ip(&local.address, settings) {
Ok(ip) => (network::is_reachable(&ip.to_string()), Some(ip)), Ok(ip) => (network::is_reachable(&ip.to_string()), Some(ip)),
Err(e) => { Err(_) => (false, None),
logger_ctdra::debug(
"reconcile",
&format!("local address could not be resolved: {e}"),
);
(false, None)
}
} }
} }
@@ -283,15 +251,6 @@ async fn switch_to(
old_active: Option<Side>, old_active: Option<Side>,
cached_local_ip: Option<Ipv4Addr>, cached_local_ip: Option<Ipv4Addr>,
) -> Result<()> { ) -> Result<()> {
logger_ctdra::info(
"reconcile",
&format!(
"pair '{}': switching to '{}' (previously active: {:?})",
pair.id,
new_side.as_str(),
old_active
),
);
mount_side(pair, settings, new_side, creds, cached_local_ip).await?; mount_side(pair, settings, new_side, creds, cached_local_ip).await?;
// Falls das Aktivieren des Symlinks fehlschlägt, muss die gerade gemountete `new_side` // Falls das Aktivieren des Symlinks fehlschlägt, muss die gerade gemountete `new_side`
@@ -321,16 +280,6 @@ async fn mount_side(
) -> Result<()> { ) -> Result<()> {
let mount_target = let mount_target =
target::build_target_with_cached_local_ip(pair, settings, side, cached_local_ip)?; target::build_target_with_cached_local_ip(pair, settings, side, cached_local_ip)?;
logger_ctdra::debug(
"reconcile",
&format!(
"pair '{}' ({}): resolved source '{}' -> '{}'",
pair.id,
side.as_str(),
mount_target.source,
mount_target.mount_point.display()
),
);
std::fs::create_dir_all(&mount_target.mount_point) std::fs::create_dir_all(&mount_target.mount_point)
.map_err(|e| crate::error::Error::io(&mount_target.mount_point, e))?; .map_err(|e| crate::error::Error::io(&mount_target.mount_point, e))?;
// Backing-Verzeichnis auf `owner_user` chownen (0700) - relevant vor allem für NFS, wo es // Backing-Verzeichnis auf `owner_user` chownen (0700) - relevant vor allem für NFS, wo es
@@ -344,15 +293,6 @@ async fn mount_side(
backend.check_available()?; backend.check_available()?;
let cred = creds.get(&pair.id, side).await?; let cred = creds.get(&pair.id, side).await?;
backend.prepare(&mount_target, cred.as_ref())?; backend.prepare(&mount_target, cred.as_ref())?;
logger_ctdra::info(
"reconcile",
&format!(
"pair '{}' ({}): mounting via {} ...",
pair.id,
side.as_str(),
backend.name()
),
);
backend.mount(&mount_target) backend.mount(&mount_target)
} }
@@ -361,7 +301,6 @@ async fn mount_side(
/// ein leeres, ausgehängtes Backing-Verzeichnis) - der nächste `mount`/`watch`-Lauf räumt das /// ein leeres, ausgehängtes Backing-Verzeichnis) - der nächste `mount`/`watch`-Lauf räumt das
/// beim erneuten Aktivieren automatisch auf. /// beim erneuten Aktivieren automatisch auf.
pub async fn unmount_pair(pair: &DrivePair, settings: &GlobalSettings) -> Result<Action> { pub async fn unmount_pair(pair: &DrivePair, settings: &GlobalSettings) -> Result<Action> {
logger_ctdra::info("reconcile", &format!("pair '{}': unmounting", pair.id));
let _guard = lock::acquire(&pair.id).await?; let _guard = lock::acquire(&pair.id).await?;
// Beide Seiten werden unabhängig voneinander versucht - ein Fehler (auch ein // Beide Seiten werden unabhängig voneinander versucht - ein Fehler (auch ein
@@ -424,18 +363,7 @@ async fn unmount_side(pair: &DrivePair, settings: &GlobalSettings, side: Side) -
owner_user: pair.owner_user.clone(), owner_user: pair.owner_user.clone(),
} }
}); });
let backend = mount::backend_for(target::side_kind(pair, side)); mount::backend_for(target::side_kind(pair, side)).unmount(&mount_target)
logger_ctdra::debug(
"reconcile",
&format!(
"pair '{}' ({}): unmounting via {} ({})",
pair.id,
side.as_str(),
backend.name(),
mount_target.mount_point.display()
),
);
backend.unmount(&mount_target)
} }
#[cfg(test)] #[cfg(test)]
-5
View File
@@ -67,7 +67,6 @@ pub fn install_cron(watch_interval_secs: u64) -> Result<CronInstallOutcome> {
std::fs::set_permissions(CRON_D_PATH, std::fs::Permissions::from_mode(0o644)) std::fs::set_permissions(CRON_D_PATH, std::fs::Permissions::from_mode(0o644))
.map_err(|e| Error::io(CRON_D_PATH, e))?; .map_err(|e| Error::io(CRON_D_PATH, e))?;
} }
logger_ctdra::info("systemd", &format!("cron entry written to '{CRON_D_PATH}'"));
Ok(CronInstallOutcome::SystemFile(PathBuf::from(CRON_D_PATH))) Ok(CronInstallOutcome::SystemFile(PathBuf::from(CRON_D_PATH)))
} }
@@ -97,10 +96,6 @@ pub fn uninstall_cron() -> Result<CronUninstallOutcome> {
return Ok(CronUninstallOutcome::NotPresent); return Ok(CronUninstallOutcome::NotPresent);
} }
std::fs::remove_file(path).map_err(|e| Error::io(CRON_D_PATH, e))?; std::fs::remove_file(path).map_err(|e| Error::io(CRON_D_PATH, e))?;
logger_ctdra::info(
"systemd",
&format!("cron entry removed from '{CRON_D_PATH}'"),
);
Ok(CronUninstallOutcome::Removed) Ok(CronUninstallOutcome::Removed)
} }