Refactor: Systemd-Dienst laeuft nur noch systemweit, Paketierung uebernimmt Installation
SmartMount richtet sich beim Installieren des .deb/.rpm/.pkg.tar.zst-Pakets automatisch als System-systemd-Dienst ein (statische Units unter packaging/systemd/, aktiviert/deaktiviert ueber postinst/postrm bzw. die generate-rpm-/Arch-Paketierungs-Skriptlets) und entfernt sich beim Deinstallieren wieder - 'smart-mount service install/uninstall' faellt aus der CLI, 'smart-mount service crontab' bleibt als manueller Fallback fuer Systeme ohne systemd. Der bisherige User-Scope (MountContext::User, unprivilegiertes Mounten ueber /etc/fstab, 'setup fstab', systemd --user) faellt komplett weg: smart-mount mountet jetzt jedes konfigurierte Paar immer direkt als root, unabhaengig davon, ob/fuer welchen Nutzer 'owner_user' gesetzt ist. Damit einher gehen: - Eine einzige, systemweite Konfiguration (/etc/smart-mount/config.toml) statt Root-/Nutzerpfad-Umschaltung - 'drive add/edit/remove' sowie 'mount'/'unmount'/'watch' verlangen deshalb jetzt explizit Root. - Der Master-Schluessel fuer die Zugangsdaten-Verschluesselung kommt nur noch aus der Schluesseldatei (kein OS-Keyring mehr moeglich, da nie unprivilegiert ausgefuehrt) - die 'keyring'-Abhaengigkeit entfaellt. - Der Cron-Fallback schreibt nur noch /etc/cron.d/smart-mount (immer root), da eine persoenliche Nutzer-Crontab ohnehin nie mounten koennte. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Generated
-632
@@ -178,17 +178,6 @@ version = "1.0.104"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
|
||||
|
||||
[[package]]
|
||||
name = "apple-native-keyring-store"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b350bfd03649e07aa05c0a81b3e15934374e585c98204a57e20b9d49f49bb9a"
|
||||
dependencies = [
|
||||
"keyring-core",
|
||||
"log",
|
||||
"security-framework",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "arc-swap"
|
||||
version = "1.9.2"
|
||||
@@ -224,143 +213,6 @@ version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bfdc70193dadb9d7287fa4b633f15f90c876915b31f6af17da307fc59c9859a8"
|
||||
|
||||
[[package]]
|
||||
name = "async-broadcast"
|
||||
version = "0.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532"
|
||||
dependencies = [
|
||||
"event-listener",
|
||||
"event-listener-strategy",
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-channel"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2"
|
||||
dependencies = [
|
||||
"concurrent-queue",
|
||||
"event-listener-strategy",
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-executor"
|
||||
version = "1.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a"
|
||||
dependencies = [
|
||||
"async-task",
|
||||
"concurrent-queue",
|
||||
"fastrand",
|
||||
"futures-lite",
|
||||
"pin-project-lite",
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-io"
|
||||
version = "2.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"cfg-if",
|
||||
"concurrent-queue",
|
||||
"futures-io",
|
||||
"futures-lite",
|
||||
"parking",
|
||||
"polling",
|
||||
"rustix 1.1.4",
|
||||
"slab",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-lock"
|
||||
version = "3.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311"
|
||||
dependencies = [
|
||||
"event-listener",
|
||||
"event-listener-strategy",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-process"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75"
|
||||
dependencies = [
|
||||
"async-channel",
|
||||
"async-io",
|
||||
"async-lock",
|
||||
"async-signal",
|
||||
"async-task",
|
||||
"blocking",
|
||||
"cfg-if",
|
||||
"event-listener",
|
||||
"futures-lite",
|
||||
"rustix 1.1.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-recursion"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-signal"
|
||||
version = "0.2.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485"
|
||||
dependencies = [
|
||||
"async-io",
|
||||
"async-lock",
|
||||
"atomic-waker",
|
||||
"cfg-if",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"rustix 1.1.4",
|
||||
"signal-hook-registry",
|
||||
"slab",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-task"
|
||||
version = "4.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
|
||||
|
||||
[[package]]
|
||||
name = "async-trait"
|
||||
version = "0.1.92"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "atomic-waker"
|
||||
version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.5.1"
|
||||
@@ -436,28 +288,6 @@ dependencies = [
|
||||
"hybrid-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block-padding"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "710f1dd022ef4e93f8a438b4ba958de7f64308434fa6a87104481645cc30068b"
|
||||
dependencies = [
|
||||
"hybrid-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "blocking"
|
||||
version = "1.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a70e4329df6cb94385eed412ec92375c3cdd8a6e502493d1229b6414e4036dfa"
|
||||
dependencies = [
|
||||
"async-channel",
|
||||
"async-task",
|
||||
"futures-io",
|
||||
"futures-lite",
|
||||
"piper",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "branches"
|
||||
version = "0.4.6"
|
||||
@@ -505,15 +335,6 @@ version = "1.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
|
||||
|
||||
[[package]]
|
||||
name = "cbc"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce2dc9ee5f88d11e0beb842c88b33c8a5cf0d1329c4b19494af42b07dbfe8896"
|
||||
dependencies = [
|
||||
"cipher 0.5.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.4.6"
|
||||
@@ -714,22 +535,6 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "const-oid"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c"
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc 0.2.189",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation-sys"
|
||||
version = "0.8.7"
|
||||
@@ -860,18 +665,6 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2"
|
||||
dependencies = [
|
||||
"block-buffer",
|
||||
"const-oid",
|
||||
"crypto-common 0.2.2",
|
||||
"ctutils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "displaydoc"
|
||||
version = "0.2.7"
|
||||
@@ -895,33 +688,6 @@ version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
|
||||
|
||||
[[package]]
|
||||
name = "endi"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099"
|
||||
|
||||
[[package]]
|
||||
name = "enumflags2"
|
||||
version = "0.7.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef"
|
||||
dependencies = [
|
||||
"enumflags2_derive",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "enumflags2_derive"
|
||||
version = "0.7.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "env_filter"
|
||||
version = "2.0.0"
|
||||
@@ -969,26 +735,6 @@ dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "event-listener"
|
||||
version = "5.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2"
|
||||
dependencies = [
|
||||
"parking",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "event-listener-strategy"
|
||||
version = "0.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93"
|
||||
dependencies = [
|
||||
"event-listener",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fallible-iterator"
|
||||
version = "0.3.0"
|
||||
@@ -1030,36 +776,6 @@ version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
|
||||
|
||||
[[package]]
|
||||
name = "futures-io"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed"
|
||||
|
||||
[[package]]
|
||||
name = "futures-lite"
|
||||
version = "2.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"parking",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-macro"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-task"
|
||||
version = "0.3.34"
|
||||
@@ -1073,7 +789,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-macro",
|
||||
"futures-task",
|
||||
"pin-project-lite",
|
||||
"slab",
|
||||
@@ -1203,24 +918,6 @@ version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||
|
||||
[[package]]
|
||||
name = "hkdf"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4aaa26c720c68b866f2c96ef5c1264b3e6f473fe5d4ce61cd44bbe913e553018"
|
||||
dependencies = [
|
||||
"hmac",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hmac"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f"
|
||||
dependencies = [
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "home"
|
||||
version = "0.5.12"
|
||||
@@ -1453,7 +1150,6 @@ version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7"
|
||||
dependencies = [
|
||||
"block-padding",
|
||||
"hybrid-array",
|
||||
]
|
||||
|
||||
@@ -1518,27 +1214,6 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "keyring"
|
||||
version = "4.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2270074a3d26bcac93c1dc5d2845eb4c089e8d761ccf6e0ea266a16004640627"
|
||||
dependencies = [
|
||||
"apple-native-keyring-store",
|
||||
"keyring-core",
|
||||
"windows-native-keyring-store",
|
||||
"zbus-secret-service-keyring-store",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "keyring-core"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fb1e621458ca9c51aa110bd0339d4751a056b9576bf1253aee1aa560dda0fc9d"
|
||||
dependencies = [
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.5.0"
|
||||
@@ -1755,20 +1430,6 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23"
|
||||
dependencies = [
|
||||
"num-bigint",
|
||||
"num-complex",
|
||||
"num-integer",
|
||||
"num-iter",
|
||||
"num-rational",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-bigint"
|
||||
version = "0.4.8"
|
||||
@@ -1779,15 +1440,6 @@ dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-complex"
|
||||
version = "0.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-conv"
|
||||
version = "0.2.2"
|
||||
@@ -1803,27 +1455,6 @@ dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-iter"
|
||||
version = "0.1.46"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b"
|
||||
dependencies = [
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-rational"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824"
|
||||
dependencies = [
|
||||
"num-bigint",
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
@@ -1860,16 +1491,6 @@ version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
|
||||
|
||||
[[package]]
|
||||
name = "ordered-stream"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "owo-colors"
|
||||
version = "3.5.0"
|
||||
@@ -1885,12 +1506,6 @@ dependencies = [
|
||||
"bytemuck",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking"
|
||||
version = "2.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.12.5"
|
||||
@@ -1926,17 +1541,6 @@ version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||
|
||||
[[package]]
|
||||
name = "piper"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1"
|
||||
dependencies = [
|
||||
"atomic-waker",
|
||||
"fastrand",
|
||||
"futures-io",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "polling"
|
||||
version = "3.11.0"
|
||||
@@ -2010,15 +1614,6 @@ dependencies = [
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-crate"
|
||||
version = "3.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
|
||||
dependencies = [
|
||||
"toml_edit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.107"
|
||||
@@ -2307,48 +1902,6 @@ version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
|
||||
[[package]]
|
||||
name = "secret-service"
|
||||
version = "5.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5107b24b91445dd2aa449a258a1807b63240942157292354dc5bfdbeb8bc6db8"
|
||||
dependencies = [
|
||||
"aes 0.9.3",
|
||||
"cbc",
|
||||
"futures-util",
|
||||
"getrandom 0.4.3",
|
||||
"hkdf",
|
||||
"hybrid-array",
|
||||
"num",
|
||||
"once_cell",
|
||||
"serde",
|
||||
"sha2",
|
||||
"zbus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "security-framework"
|
||||
version = "3.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"core-foundation",
|
||||
"core-foundation-sys",
|
||||
"libc 0.2.189",
|
||||
"security-framework-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "security-framework-sys"
|
||||
version = "2.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc 0.2.189",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.28"
|
||||
@@ -2398,17 +1951,6 @@ dependencies = [
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_repr"
|
||||
version = "0.1.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_spanned"
|
||||
version = "1.1.1"
|
||||
@@ -2424,17 +1966,6 @@ version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d"
|
||||
|
||||
[[package]]
|
||||
name = "sha2"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures 0.3.1",
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sharded-slab"
|
||||
version = "0.1.7"
|
||||
@@ -2530,7 +2061,6 @@ dependencies = [
|
||||
"config-ctdra",
|
||||
"dialoguer",
|
||||
"getrandom 0.4.3",
|
||||
"keyring",
|
||||
"logger-ctdra",
|
||||
"program-ctdra",
|
||||
"serde",
|
||||
@@ -2803,18 +2333,6 @@ dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.25.15+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1340ea94a5856333492c9064b02c778b191dd2c853778d9609debdcdfea3a614"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"toml_datetime 1.1.1+spec-1.1.0",
|
||||
"toml_parser",
|
||||
"winnow 1.0.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_parser"
|
||||
version = "1.1.3+spec-1.1.0"
|
||||
@@ -3111,17 +2629,6 @@ version = "1.20.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
|
||||
|
||||
[[package]]
|
||||
name = "uds_windows"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e"
|
||||
dependencies = [
|
||||
"memoffset",
|
||||
"tempfile",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uncased"
|
||||
version = "0.9.10"
|
||||
@@ -3195,7 +2702,6 @@ checksum = "2ef6dac1e96601b4fb3acccccff2139741fcb757cb9a36089bf5be91cfb285ce"
|
||||
dependencies = [
|
||||
"getrandom 0.4.3",
|
||||
"js-sys",
|
||||
"serde_core",
|
||||
"sha1_smol",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
@@ -3325,19 +2831,6 @@ version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-native-keyring-store"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "063426e76fdec7438d56bb777f67e318a84a25c707b07e575cb8b78e10c028f8"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"keyring-core",
|
||||
"regex",
|
||||
"windows-sys 0.61.2",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.4.1"
|
||||
@@ -3449,9 +2942,6 @@ name = "winnow"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen"
|
||||
@@ -3503,87 +2993,6 @@ dependencies = [
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zbus"
|
||||
version = "5.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5db4be7c075cb421e4b7ee645541604239bd243ba7c357511f4ff3a74b555907"
|
||||
dependencies = [
|
||||
"async-broadcast",
|
||||
"async-executor",
|
||||
"async-io",
|
||||
"async-lock",
|
||||
"async-process",
|
||||
"async-recursion",
|
||||
"async-task",
|
||||
"async-trait",
|
||||
"blocking",
|
||||
"enumflags2",
|
||||
"event-listener",
|
||||
"futures-core",
|
||||
"futures-lite",
|
||||
"hex",
|
||||
"libc 0.2.189",
|
||||
"ordered-stream",
|
||||
"rustix 1.1.4",
|
||||
"serde",
|
||||
"serde_repr",
|
||||
"tracing",
|
||||
"uds_windows",
|
||||
"uuid",
|
||||
"windows-sys 0.61.2",
|
||||
"winnow 1.0.4",
|
||||
"zbus_macros",
|
||||
"zbus_names",
|
||||
"zvariant",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zbus-secret-service-keyring-store"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "74801d001b9e7729adb4f1825b67b398185fed424749aa3d8bacf70417137d9a"
|
||||
dependencies = [
|
||||
"keyring-core",
|
||||
"secret-service",
|
||||
"zbus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zbus_macros"
|
||||
version = "5.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2990635d09ade6df1868f72f8cac69a876a90981e8bd3c40b1be413f8dc88f40"
|
||||
dependencies = [
|
||||
"proc-macro-crate",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.5",
|
||||
"zbus_names",
|
||||
"zvariant",
|
||||
"zvariant_utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zbus_names"
|
||||
version = "4.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d8bf88b4a3ff53e883001e0e0115b297a9d53c31b9c1edd2bfdd853e3428624e"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"winnow 1.0.4",
|
||||
"zvariant",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zcheapstr"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d1afec51604565183aeb5c54c20aeab286120d4e4460f7f76e3e8bb8c0d99473"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.57"
|
||||
@@ -3671,44 +3080,3 @@ name = "zmij"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
|
||||
|
||||
[[package]]
|
||||
name = "zvariant"
|
||||
version = "5.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c1d34c27cc6cdd1f458427519dd6b8612f7b7e3f7b9a0b2355d041dda9869147"
|
||||
dependencies = [
|
||||
"endi",
|
||||
"enumflags2",
|
||||
"serde",
|
||||
"winnow 1.0.4",
|
||||
"zcheapstr",
|
||||
"zvariant_derive",
|
||||
"zvariant_utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zvariant_derive"
|
||||
version = "5.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "864155e69b4352db0c7f374917bf45d1e0c8d17659c8b3dbf9795f3673f8c497"
|
||||
dependencies = [
|
||||
"proc-macro-crate",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.5",
|
||||
"zvariant_utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zvariant_utils"
|
||||
version = "4.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bad0294361a320b694a328460dc73add56c306150f5cb6bfafc44446120008a3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"serde",
|
||||
"syn 3.0.5",
|
||||
"winnow 1.0.4",
|
||||
]
|
||||
|
||||
+24
-2
@@ -26,7 +26,6 @@ serde_json = "1"
|
||||
thiserror = "2"
|
||||
anyhow = "1"
|
||||
aes-gcm = "0.11.1"
|
||||
keyring = "4.2.0"
|
||||
dialoguer = "0.12.0"
|
||||
uuid = { version = "1", features = ["v4"] }
|
||||
# Eigene, direkte Zufallsquelle für Nonce-/Schlüssel-Generierung, entkoppelt von aes-gcms
|
||||
@@ -58,12 +57,17 @@ smart-mount bindet Paare aus einem lokalen (LAN, WebDAV/SMB/NFS) und einem Cloud
|
||||
ein: ist das lokale Laufwerk erreichbar, wird es gemountet, sonst automatisch das
|
||||
Cloud-Laufwerk. Ein Watchdog prüft periodisch die Erreichbarkeit und schaltet bei Bedarf
|
||||
zwischen beiden um. Zugangsdaten werden verschlüsselt in einer lokalen Turso-Datenbank
|
||||
gespeichert; smart-mount kann als root/System-Dienst und als Nutzer-Dienst laufen.\
|
||||
gespeichert; smart-mount richtet sich beim Installieren automatisch als System-systemd-Dienst
|
||||
ein und mountet konfigurierte Laufwerkspaare für die jeweils zugeordneten lokalen Nutzer.\
|
||||
"""
|
||||
maintainer-scripts = "packaging/deb"
|
||||
assets = [
|
||||
["target/release/smart-mount", "usr/bin/smart-mount", "755"],
|
||||
["README.md", "usr/share/doc/smart-mount/README.md", "644"],
|
||||
["LICENSE", "usr/share/doc/smart-mount/copyright", "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.timer", "usr/lib/systemd/system/smart-mount-watch.timer", "644"],
|
||||
]
|
||||
|
||||
[package.metadata.generate-rpm]
|
||||
@@ -71,9 +75,27 @@ assets = [
|
||||
{ source = "target/release/smart-mount", dest = "/usr/bin/smart-mount", mode = "755" },
|
||||
{ source = "README.md", dest = "/usr/share/doc/smart-mount/README.md", mode = "644", doc = true },
|
||||
{ source = "LICENSE", dest = "/usr/share/licenses/smart-mount/LICENSE", mode = "644", license = true },
|
||||
{ source = "packaging/systemd/smart-mount-mount.service", dest = "/usr/lib/systemd/system/smart-mount-mount.service", mode = "644" },
|
||||
{ source = "packaging/systemd/smart-mount-watch.service", dest = "/usr/lib/systemd/system/smart-mount-watch.service", mode = "644" },
|
||||
{ source = "packaging/systemd/smart-mount-watch.timer", dest = "/usr/lib/systemd/system/smart-mount-watch.timer", mode = "644" },
|
||||
]
|
||||
requires = { "mac2ip" = "*", "nmap" = "*" }
|
||||
suggests = { "davfs2" = "*", "cifs-utils" = "*", "nfs-utils" = "*" }
|
||||
# postinst/postrm-Äquivalente: richten den paketierten systemd-Dienst automatisch ein/entfernen
|
||||
# (Gegenstück zu packaging/deb/postinst+postrm). $1 in %postun: Anzahl verbleibender Versionen
|
||||
# nach diesem Schritt - 0 nur bei vollständiger Deinstallation, nicht bei einem Upgrade.
|
||||
post_install_script = """
|
||||
if command -v systemctl >/dev/null 2>&1; then
|
||||
systemctl daemon-reload || true
|
||||
systemctl enable --now smart-mount-mount.service smart-mount-watch.timer || true
|
||||
fi
|
||||
"""
|
||||
post_uninstall_script = """
|
||||
if [ "$1" = "0" ] && command -v systemctl >/dev/null 2>&1; then
|
||||
systemctl disable --now smart-mount-mount.service smart-mount-watch.timer smart-mount-watch.service || true
|
||||
systemctl daemon-reload || true
|
||||
fi
|
||||
"""
|
||||
|
||||
[package.metadata.arch]
|
||||
pkgrel = "1" # Wird in CI durch get-build-number.py dynamisch überschrieben
|
||||
|
||||
@@ -24,28 +24,31 @@ werden (z. B. über ein separates Sync-Tool).
|
||||
- `davfs2` für WebDAV-Laufwerke
|
||||
- `cifs-utils` für SMB/CIFS-Laufwerke
|
||||
- `nfs-common` (Debian/Ubuntu) bzw. `nfs-utils` (Fedora/Arch) für NFS-Laufwerke
|
||||
- Für MAC-basierte lokale Laufwerke im **Nutzerkontext**: `mac2ip` löst über `ip neigh` auf,
|
||||
ohne dafür Root-Rechte zu benötigen - nur der letzte Fallback-Schritt (ein aktiver
|
||||
`nmap`-Scan, falls das Zielgerät nicht in der ARP-Nachbartabelle steht) braucht
|
||||
passwortlosen `sudo`-Zugriff auf `nmap`. Ohne das schlägt die Auflösung in diesem Fall sauber
|
||||
fehl (kein Absturz) - entweder passwortlosen `sudo` für `nmap` einrichten, oder das
|
||||
entsprechende Laufwerkspaar im System-/root-Kontext betreiben.
|
||||
- `systemd`: smart-mount richtet sich beim Installieren des Pakets automatisch als
|
||||
System-Dienst ein (siehe "Automatischer Start beim Systemstart" unten). Ohne systemd bleibt
|
||||
`smart-mount service crontab` als manueller Fallback.
|
||||
|
||||
---
|
||||
|
||||
## CLI-Nutzung
|
||||
|
||||
smart-mount läuft ausschließlich als root/System-Dienst - es gibt nur eine einzige,
|
||||
systemweite Konfiguration. `drive add`/`edit`/`remove` sowie `mount`/`unmount`/`watch`
|
||||
verlangen daher explizit Root-Rechte (`sudo`), auch für ein Laufwerkspaar, das nur für einen
|
||||
bestimmten Nutzer gedacht ist (siehe `owner_user` unten). Rein lesende Befehle (`drive list`,
|
||||
`status`, `doctor`) brauchen kein `sudo`.
|
||||
|
||||
```bash
|
||||
# Neues Laufwerkspaar interaktiv anlegen (fragt Name, Kontext, Mount-Typ, Adresse,
|
||||
# Freigabe und ggf. Zugangsdaten ab)
|
||||
smart-mount drive add
|
||||
# Neues Laufwerkspaar interaktiv anlegen (fragt Name, Mount-Typ, Adresse, Freigabe und ggf.
|
||||
# Zugangsdaten ab)
|
||||
sudo smart-mount drive add
|
||||
|
||||
# Nicht-interaktiv per Flags (für Skripte/Automatisierung) - fehlende Pflichtfelder sind
|
||||
# dann ein Fehler statt eines (ohne Terminal ohnehin unmöglichen) Prompts. Jedes
|
||||
# *-password-stdin-Flag liest genau eine Zeile von stdin (bei mehreren im selben Aufruf
|
||||
# entsprechend mehrere Zeilen, eine pro Flag in Reihenfolge):
|
||||
smart-mount drive add --non-interactive \
|
||||
--name NAS --context system \
|
||||
sudo smart-mount drive add --non-interactive \
|
||||
--name NAS \
|
||||
--local-kind smb --local-ip 192.168.1.50 --local-share share --local-username nasuser --local-password-stdin \
|
||||
--cloud-kind webdav --cloud-host https://cloud.example.com/dav --cloud-share / --cloud-username clouduser --cloud-password cloud-pw-direkt \
|
||||
<<< "lokales-passwort"
|
||||
@@ -55,35 +58,33 @@ smart-mount drive add --non-interactive \
|
||||
# Bestehendes Laufwerkspaar bearbeiten - nur angegebene Felder ändern sich, alles andere
|
||||
# (inkl. gespeichertem Passwort) bleibt unangetastet. Ohne Flags: interaktiver Durchlauf,
|
||||
# vorbelegt mit den aktuellen Werten (inkl. Rückfrage, ob das Passwort geändert werden soll).
|
||||
smart-mount drive edit <id> --name "Neuer Name" --local-ip 192.168.1.99
|
||||
smart-mount drive edit <id> --non-interactive --local-password-stdin <<< "neues-passwort"
|
||||
sudo smart-mount drive edit <id> --name "Neuer Name" --local-ip 192.168.1.99
|
||||
sudo smart-mount drive edit <id> --non-interactive --local-password-stdin <<< "neues-passwort"
|
||||
|
||||
# Konfigurierte Laufwerkspaare auflisten / entfernen (--json für Skripte)
|
||||
smart-mount drive list
|
||||
smart-mount drive list --json
|
||||
smart-mount drive remove <id>
|
||||
sudo smart-mount drive remove <id>
|
||||
|
||||
# Ein einzelnes Paar oder alle einbinden/aushängen
|
||||
smart-mount mount --name <id>
|
||||
smart-mount mount --all
|
||||
smart-mount unmount --all
|
||||
sudo smart-mount mount --name <id>
|
||||
sudo smart-mount mount --all
|
||||
sudo smart-mount unmount --all
|
||||
|
||||
# Aktuellen Status (Mount-Zustand + Erreichbarkeit beider Seiten) anzeigen (--json für Skripte)
|
||||
smart-mount status
|
||||
smart-mount status --json
|
||||
|
||||
# Ein Reconcile-Durchlauf (lokal/Cloud-Umschaltung) - für systemd-Timer/Cron gedacht
|
||||
smart-mount watch
|
||||
sudo smart-mount watch
|
||||
|
||||
# systemd-Units installieren (System- bzw. Nutzerkontext)
|
||||
sudo smart-mount service install --system
|
||||
smart-mount service install --user
|
||||
# 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
|
||||
|
||||
# Einmaliges root-Setup, damit Nutzer-Kontext-Paare unprivilegiert (un)gemountet werden können
|
||||
sudo smart-mount setup fstab
|
||||
|
||||
# Voraussetzungen prüfen (Binaries, Gruppenmitgliedschaft, fstab-Setup, Scheduler) - deckt
|
||||
# gebündelt ab, was man sonst erst einzeln beim Mount-Fehlschlag entdecken würde
|
||||
# Voraussetzungen prüfen (Binaries, Scheduler) - deckt gebündelt ab, was man sonst erst
|
||||
# einzeln beim Mount-Fehlschlag entdecken würde
|
||||
smart-mount doctor
|
||||
smart-mount doctor --json
|
||||
|
||||
@@ -92,27 +93,22 @@ smart-mount completions bash > /etc/bash_completion.d/smart-mount
|
||||
smart-mount completions zsh > "${fpath[1]}/_smart-mount"
|
||||
```
|
||||
|
||||
Die Konfiguration liegt unter `~/.config/smart-mount/config.toml` (Nutzerkontext) bzw.
|
||||
`/etc/smart-mount/config.toml` (root/System-Kontext) - automatisch aufgelöst je nachdem, ob
|
||||
`smart-mount` mit Root-Rechten läuft. Die verschlüsselte Zugangsdaten-Datenbank
|
||||
(`smart-mount.db`) liegt im selben Verzeichnis.
|
||||
Die Konfiguration liegt immer unter `/etc/smart-mount/config.toml`, unabhängig davon, ob
|
||||
`smart-mount` selbst mit oder ohne Root-Rechte aufgerufen wird (siehe oben). Die verschlüsselte
|
||||
Zugangsdaten-Datenbank (`smart-mount.db`) liegt im selben Verzeichnis.
|
||||
|
||||
### Wo die Laufwerke eingebunden werden
|
||||
|
||||
Jedes Laufwerkspaar bekommt sein **eigenes** Unterverzeichnis unter `settings.mount_base_dir`
|
||||
(`<mount_base_dir>/<pair-id>`) - mehrere Paare stören sich also nie gegenseitig. Standardwert
|
||||
für `mount_base_dir`: `/run/media/smart-mount` im System-Kontext (root, ein gemeinsamer,
|
||||
keinem Nutzer zugeordneter Namensraum), `/run/media/<Nutzer>/smart-mount` im Nutzerkontext -
|
||||
`/run/media` ist auf den meisten Systemen bereits die übliche Konvention für eingebundene
|
||||
Wechseldatenträger/Netzlaufwerke (z. B. udisks2/GNOME) und liegt auf `tmpfs`, muss also nie
|
||||
persistieren. Der Mountpoint selbst (und alle nötigen Elternverzeichnisse) werden bei jedem
|
||||
`mount`/`watch`-Lauf automatisch angelegt, falls sie fehlen - eigenes Anlegen ist nicht nötig.
|
||||
|
||||
**Achtung bei Nutzer-Kontext-Paaren:** `/run/media` gehört standardmäßig `root:root` mit Modus
|
||||
`0755` - ein normaler Nutzer kann dort also nicht einmal sein eigenes Unterverzeichnis
|
||||
anlegen. `sudo smart-mount setup fstab` übernimmt das einmalig (legt `/run/media/<Nutzer>`
|
||||
sowie `/run/media/<Nutzer>/smart-mount` an und macht den Nutzer zum Besitzer) - ohne diesen
|
||||
Schritt schlägt das automatische Anlegen für Nutzer-Kontext-Paare fehl.
|
||||
(`<mount_base_dir>/<pair-id>`) - mehrere Paare stören sich also nie gegenseitig. Standardwert:
|
||||
`/run/media/smart-mount`, ein einzelner, flacher Namensraum (smart-mount mountet immer als
|
||||
root, siehe oben) - `/run/media` ist auf den meisten Systemen bereits die übliche Konvention
|
||||
für eingebundene Wechseldatenträger/Netzlaufwerke (z. B. udisks2/GNOME) und liegt auf `tmpfs`,
|
||||
muss also nie persistieren. Der Mountpoint selbst (und alle nötigen Elternverzeichnisse) werden
|
||||
bei jedem `mount`/`watch`-Lauf automatisch angelegt, falls sie fehlen. Ist für ein Paar
|
||||
`owner_user` gesetzt, bekommt genau dieser Nutzer über die Mount-Optionen (`uid=`/`gid=`, siehe
|
||||
"Architekturentscheidungen" unten) vollen Zugriff auf den Inhalt - die Trennung passiert also
|
||||
über Zugriffsrechte, nicht über getrennte Mountpoint-Namensräume pro Nutzer.
|
||||
|
||||
Der Standard lässt sich in `config.toml` unter `[settings] mount_base_dir = "..."` jederzeit
|
||||
auf einen beliebigen anderen Pfad ändern.
|
||||
@@ -121,45 +117,50 @@ auf einen beliebigen anderen Pfad ändern.
|
||||
|
||||
## Automatischer Start beim Systemstart
|
||||
|
||||
Das .deb/.rpm/.pkg.tar.zst-Paket richtet smart-mount beim Installieren **automatisch** als
|
||||
System-systemd-Dienst ein - kein manueller Schritt nötig. Die dafür paketierten Unit-Dateien
|
||||
(`packaging/systemd/` im Quellbaum, installiert nach `/usr/lib/systemd/system/`) bestehen aus:
|
||||
|
||||
- `smart-mount-mount.service`: ein `oneshot`-Service, der beim Boot `smart-mount mount --all`
|
||||
aufruft (nach `network-online.target`).
|
||||
- `smart-mount-watch.service` + `smart-mount-watch.timer`: ein Timer, der periodisch
|
||||
`smart-mount watch` aufruft (Standardintervall: 120s).
|
||||
|
||||
Die postinst/postrm-Skripte des Pakets (bzw. das `.INSTALL`-Skriptlet bei Arch) aktivieren und
|
||||
starten beide Units beim Installieren/Upgraden (`systemctl enable --now ...`) und deaktivieren
|
||||
sie wieder beim Deinstallieren (`systemctl disable --now ...`) - siehe `packaging/deb/postinst`
|
||||
+ `packaging/deb/postrm` bzw. `[package.metadata.generate-rpm]`/`scripts/package-arch.py` in
|
||||
`Cargo.toml`. Welche Laufwerkspaare dabei gemountet werden (und für welchen Nutzer, siehe
|
||||
`owner_user` unten) steuert ausschließlich `/etc/smart-mount/config.toml` - nicht die
|
||||
Unit-Dateien selbst.
|
||||
|
||||
**Anderes Watch-Intervall als der Standard (120s):** `settings.watch_interval_secs` in
|
||||
`config.toml` steuert nur den Cron-Fallback (s. u.) - der paketierte Timer hat ein fest
|
||||
eingebautes Intervall. Zum Anpassen:
|
||||
|
||||
```bash
|
||||
sudo smart-mount setup fstab # einmalig, nur nötig bei Nutzer-Kontext-Paaren
|
||||
sudo smart-mount service install --system # für System-Kontext-Paare
|
||||
smart-mount service install --user # für die eigenen Nutzer-Kontext-Paare
|
||||
sudo systemctl edit smart-mount-watch.timer
|
||||
# [Timer]
|
||||
# OnUnitActiveSec=60s
|
||||
```
|
||||
|
||||
`service install` wählt automatisch den passenden Mechanismus: ist `systemctl` vorhanden,
|
||||
werden systemd-Units installiert (ein `oneshot`-Service für den initialen Mount beim Boot
|
||||
sowie ein Timer, der periodisch `smart-mount watch` aufruft - Intervall:
|
||||
`settings.watch_interval_secs`, Standard 120s). Ist kein systemd vorhanden, wird automatisch
|
||||
auf Cron ausgewichen - als root wird `/etc/cron.d/smart-mount` geschrieben, als normaler
|
||||
Nutzer die eigene, persönliche Crontab über `crontab -l`/`crontab -` aktualisiert (ein
|
||||
verwalteter Block lässt dabei bereits vorhandene, unabhängige Cron-Einträge unangetastet und
|
||||
verhindert Duplikate bei wiederholten Aufrufen). Ist weder systemd noch Cron vorhanden, werden
|
||||
stattdessen die beiden äquivalenten Zeilen zum manuellen Eintragen ausgegeben:
|
||||
### Cron-Fallback (Systeme ohne systemd)
|
||||
|
||||
```bash
|
||||
sudo smart-mount service crontab # einrichten
|
||||
sudo smart-mount service crontab --remove # wieder entfernen
|
||||
```
|
||||
|
||||
Erfordert Root (wie `mount`/`watch` - Mounten läuft immer als root, siehe oben) und schreibt
|
||||
`/etc/cron.d/smart-mount` - das Intervall folgt `settings.watch_interval_secs`. Ist
|
||||
`/etc/cron.d` nicht vorhanden, werden stattdessen die beiden äquivalenten Zeilen zum manuellen
|
||||
Eintragen ausgegeben:
|
||||
|
||||
```cron
|
||||
@reboot smart-mount mount --all
|
||||
*/2 * * * * smart-mount watch
|
||||
```
|
||||
|
||||
`smart-mount service crontab` erzwingt gezielt den Cron-Weg (z. B. um systemd bewusst zu
|
||||
umgehen), mit identischem Verhalten wie der automatische Fallback von `install`.
|
||||
|
||||
```bash
|
||||
sudo smart-mount service uninstall --system
|
||||
smart-mount service uninstall --user
|
||||
```
|
||||
|
||||
räumt alles wieder auf, was `install`/`crontab`/`setup fstab` eingerichtet haben - systemd-
|
||||
Units (falls vorhanden), den Cron-Eintrag (falls vorhanden) und - nur bei `--system`, da
|
||||
`setup fstab` root-weit für alle Nutzer-Kontext-Paare gilt - den verwalteten `/etc/fstab`-
|
||||
Block. Jeder Teil wird unabhängig geprüft: fehlt etwas (z. B. weil nur Cron statt systemd
|
||||
installiert war), wird das ohne Fehler übersprungen; die Ausgabe listet, was tatsächlich
|
||||
entfernt wurde. Backing-Verzeichnisse, gemountete Daten und Gruppenmitgliedschaften (z. B. in
|
||||
der `davfs2`-Gruppe) werden dabei bewusst **nicht** angerührt - dafür gibt es keine
|
||||
automatische Umkehrung, da das ungewollte Nebenwirkungen haben könnte (siehe
|
||||
Architekturentscheidungen unten).
|
||||
|
||||
---
|
||||
|
||||
## Architekturentscheidungen
|
||||
@@ -189,39 +190,20 @@ Architekturentscheidungen unten).
|
||||
beim Umschalten zuerst die neue Seite gemountet und der Symlink umgebogen wird, bevor die
|
||||
alte Seite (mit eben diesem tolerierten Timeout) ausgehängt wird, blockiert ein
|
||||
hängender Alt-Mount den sichtbaren Wechsel ohnehin nicht.
|
||||
- **Privilegienmodell für Nutzer-Mounts**: `sudo smart-mount setup fstab` schreibt einmalig
|
||||
`/etc/fstab`-Einträge mit `user,exec,noauto` sowie nötige Gruppenmitgliedschaften (z. B.
|
||||
`davfs2`-Gruppe). Jede Seite (lokal/Cloud) bekommt dabei ihr **eigenes, eindeutiges**
|
||||
verstecktes Backing-Verzeichnis (nicht denselben Mountpoint für beide) - das entspricht
|
||||
exakt dem einzigen in `man 8 mount` ("Non-superuser mounts") dokumentierten Fall, statt sich
|
||||
auf unspezifiziertes Verhalten bei zwei Zeilen mit demselben Ziel zu verlassen. Der
|
||||
konfigurierte, sichtbare Mountpoint selbst ist ein Symlink, den SmartMount zur Laufzeit
|
||||
zwischen den beiden Backing-Verzeichnissen umschaltet. `exec` wird explizit gesetzt, weil die
|
||||
`user`-Option laut `man 8 mount` sonst für jedes Dateisystem automatisch `noexec` erzwingt -
|
||||
ohne das könnten auf einem Nutzer-Kontext-Laufwerk liegende Skripte nicht ausgeführt werden.
|
||||
- **Privilegienmodell**: SmartMount läuft ausschließlich als root/System-Dienst - es gibt
|
||||
keinen unprivilegierten Mount-Weg mehr (früher: `/etc/fstab`-Einträge mit `user,noauto`, über
|
||||
die ein einzelner Nutzer unprivilegiert selbst mounten konnte). Root mountet direkt
|
||||
(`mount -t <typ> <quelle> <ziel> -o <optionen>`) für jedes konfigurierte Paar, unabhängig
|
||||
davon, ob/für welchen Nutzer `owner_user` gesetzt ist - siehe `owner_user` unten für die
|
||||
Zugriffssteuerung. Das vereinfacht das Rechtemodell erheblich und schließt nebenbei die
|
||||
frühere strukturelle CIFS-Lücke (jeder unprivilegiert mount-berechtigte Nutzer konnte zuvor
|
||||
jedes CIFS-Nutzer-Kontext-Paar mounten, nicht nur sein eigenes) - Zugangsdaten sind jetzt nur
|
||||
noch root zugänglich.
|
||||
- **Voller Zugriff für einen bestimmten Nutzer (`owner_user`)**: Bei CIFS/WebDAV (keine
|
||||
nativen Unix-Rechte) setzt SmartMount automatisch `uid=`/`gid=`/`file_mode=0700`/
|
||||
`dir_mode=0700`, sobald ein Paar ein `owner_user` hat (Pflicht bei Nutzer-Kontext-Paaren,
|
||||
optional bei System-Kontext). Das ist nicht nur für vollen Zugriff (inkl. Skript-Ausführung)
|
||||
nötig, sondern bei WebDAV auch sicherheitsrelevant: `mount.davfs` erlaubt einem
|
||||
unprivilegierten Nutzer das Mounten einer `user`-Zeile laut `man mount.davfs` nur, wenn
|
||||
`uid=` auf ihn selbst zeigt - ohne das dürfte jedes Mitglied der Gruppe `davfs2` jedes
|
||||
konfigurierte Paar mounten, nicht nur sein eigenes (`setup fstab` verweigert daher Paare ohne
|
||||
`owner_user`).
|
||||
|
||||
**Bekannte, bewusst nicht behobene Lücke bei CIFS im Nutzerkontext:** `man mount.cifs`
|
||||
bestätigt, dass `uid=`/`gid=` dort **ausschließlich** die simulierte Datei-Ownership nach dem
|
||||
Mount betreffen - anders als bei davfs2 gibt es **keinen** Mechanismus, der das Mount-*Recht*
|
||||
einer `user`-fstab-Zeile auf eine bestimmte Person einschränkt. Jeder lokale Nutzer, der
|
||||
unprivilegiert mounten darf, kann daher aktuell jedes konfigurierte CIFS-Nutzer-Kontext-Paar
|
||||
mounten (nicht nur sein eigenes) und dabei dessen gespeicherte Zugangsdaten für die Dauer des
|
||||
Mounts mitbenutzen. Das ist eine strukturelle Grenze von `mount(8)`/`mount.cifs`, keine Lücke,
|
||||
die sich über Mount-Optionen schließen ließe. **Konsequenz:** CIFS-Nutzer-Kontext-Paare nur
|
||||
auf Einzelnutzer-Maschinen oder unter sich gegenseitig bereits vertrauenden lokalen Nutzern
|
||||
einsetzen. davfs2 ist von diesem Problem nicht betroffen (siehe oben); bei NFS gibt es keine
|
||||
clientseitige `uid=`/`gid=`-Option, Zugriff bestimmt dort ausschließlich der Server über die
|
||||
tatsächlichen Datei-Eigentümer/-Rechte des Exports - wer die Freigabe mounten kann, sieht
|
||||
dadurch nicht automatisch fremde Daten.
|
||||
`dir_mode=0700`, sobald ein Paar ein `owner_user` hat - optional, ohne `owner_user` gehört
|
||||
der Mount root. Bei NFS gibt es keine clientseitige `uid=`/`gid=`-Option, Zugriff bestimmt
|
||||
dort ausschließlich der Server über die tatsächlichen Datei-Eigentümer/-Rechte des Exports.
|
||||
- **davfs2-Konfiguration**: SmartMount setzt in `davfs2.conf` automatisch `gui_optimize 1`
|
||||
(bündelt PROPFIND-Anfragen, wichtig für grafische Dateimanager) sowie `buf_size 16384`
|
||||
(deutlich über dem Standard von 16 KiB) - Letzteres behebt ein bekanntes Praxisproblem, bei
|
||||
@@ -230,10 +212,9 @@ Architekturentscheidungen unten).
|
||||
kleinem `buf_size` stillschweigend abgeschnitten).
|
||||
- **Zugangsdaten-Verschlüsselung**: Turso hat aktuell keine produktionsreife eingebaute
|
||||
Verschlüsselung, daher verschlüsselt SmartMount Passwörter selbst (AES-256-GCM) vor der
|
||||
Ablage. Der Master-Schlüssel wird bevorzugt im OS-Keyring (GNOME Keyring/KWallet über
|
||||
secret-service) abgelegt; ist keins verfügbar (typisch für den root/System-Dienst sowie
|
||||
Headless-Systeme), wird automatisch auf eine Schlüsseldatei (`chmod 600`, neben der
|
||||
Konfiguration) zurückgegriffen.
|
||||
Ablage. Der Master-Schlüssel liegt in einer Schlüsseldatei (`chmod 600`, neben der
|
||||
Konfiguration, also `/etc/smart-mount/master.key`) - kein OS-Keyring, da SmartMount
|
||||
ausschließlich als root/System-Dienst läuft, für den es kein Nutzer-Keyring gibt.
|
||||
- **`mac2ip`-Integration**: SmartMount ruft `mac2ip --json --auto-trust-networks <mac>` auf.
|
||||
`--auto-trust-networks` lässt mac2ip die eigene "nmap-Scan in diesem Netzwerk erlauben?"-
|
||||
Rückfrage automatisch bejahen und dauerhaft in seiner eigenen Cache-Datenbank merken - das
|
||||
@@ -249,6 +230,10 @@ Architekturentscheidungen unten).
|
||||
│ └── config.toml # Cargo-Konfiguration (Linker für Cross-Compiling, Registry)
|
||||
├── .gitea/
|
||||
│ └── workflows/ # CI/CD-Pipelines (Build, Tests, Security-Scans, Releases)
|
||||
├── packaging/
|
||||
│ ├── systemd/ # Statische systemd-Unit-Dateien (paketiert nach
|
||||
│ │ # /usr/lib/systemd/system/)
|
||||
│ └── deb/ # postinst/postrm für das .deb-Paket
|
||||
├── scripts/
|
||||
│ ├── get-build-number.py # Dynamische Ermittlung der nächsten Paket-Revisionsnummer
|
||||
│ ├── package-arch.py # Erstellung von Arch Linux .pkg.tar.zst Paketen
|
||||
@@ -263,8 +248,7 @@ Architekturentscheidungen unten).
|
||||
│ ├── mount/ # WebDAV/SMB/NFS-Backends, dynamischer Dispatch
|
||||
│ ├── network/ # Erreichbarkeit, mac2ip-Integration
|
||||
│ ├── reconcile/ # Watchdog-Entscheidungslogik (lokal/Cloud-Umschaltung)
|
||||
│ ├── systemd/ # systemd-Unit-Generierung/-Installation
|
||||
│ └── fstab/ # Einmaliges root-Setup für Nutzer-Mounts
|
||||
│ └── systemd/ # Cron-Fallback (Systeme ohne systemd)
|
||||
├── tests/ # Cross-Modul-Integrationstests
|
||||
├── Cargo.toml # Cargo Manifest & Paketierungsmetadaten (deb, rpm, arch)
|
||||
├── LICENSE # Lizenzdatei (GPL-3.0-or-later)
|
||||
|
||||
Executable
+13
@@ -0,0 +1,13 @@
|
||||
#!/bin/sh
|
||||
# Richtet smart-mount beim Installieren/Upgraden des Pakets automatisch als
|
||||
# System-systemd-Dienst ein (Gegenstueck: postrm). Laeuft nur beim eigentlichen
|
||||
# "configure"-Schritt (siehe Debian Policy Manual, Abschnitt 6.5), nicht bei
|
||||
# "abort-upgrade"/"abort-remove" etc.
|
||||
set -e
|
||||
|
||||
if [ "$1" = "configure" ] && command -v systemctl >/dev/null 2>&1; then
|
||||
systemctl daemon-reload || true
|
||||
systemctl enable --now smart-mount-mount.service smart-mount-watch.timer || true
|
||||
fi
|
||||
|
||||
exit 0
|
||||
Executable
+13
@@ -0,0 +1,13 @@
|
||||
#!/bin/sh
|
||||
# Entfernt den beim Installieren eingerichteten systemd-Dienst wieder (Gegenstueck:
|
||||
# postinst). Nur bei tatsaechlicher Entfernung ("remove"/"purge"), nicht bei einem
|
||||
# Upgrade (dort ersetzt dpkg die Unit-Dateien einfach durch die neue Version, ohne den
|
||||
# laufenden Dienst zwischenzeitlich zu deaktivieren).
|
||||
set -e
|
||||
|
||||
if { [ "$1" = "remove" ] || [ "$1" = "purge" ]; } && command -v systemctl >/dev/null 2>&1; then
|
||||
systemctl disable --now smart-mount-mount.service smart-mount-watch.timer smart-mount-watch.service || true
|
||||
systemctl daemon-reload || true
|
||||
fi
|
||||
|
||||
exit 0
|
||||
@@ -0,0 +1,11 @@
|
||||
[Unit]
|
||||
Description=smart-mount: mount configured drive pairs at boot
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/bin/smart-mount mount --all
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,6 @@
|
||||
[Unit]
|
||||
Description=smart-mount: check reachability and switch local/cloud if needed
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/bin/smart-mount watch
|
||||
@@ -0,0 +1,11 @@
|
||||
[Unit]
|
||||
Description=smart-mount: periodic reconciling
|
||||
|
||||
[Timer]
|
||||
OnBootSec=1min
|
||||
OnUnitActiveSec=120s
|
||||
Persistent=true
|
||||
Unit=smart-mount-watch.service
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
+67
-1
@@ -45,6 +45,52 @@ def resolve_pkgrel(version=None, default="1"):
|
||||
return str(default)
|
||||
|
||||
|
||||
def collect_systemd_units(systemd_src_dir="packaging/systemd"):
|
||||
"""Findet die im Projekt paketierten systemd-Unit-Dateien (falls vorhanden) und
|
||||
unterscheidet dabei generisch - ohne einen Anwendungsnamen zu kennen - zwischen direkt
|
||||
aktivierbaren Units (die einen '[Install]'-Abschnitt haben, z. B. Timer oder ein beim Boot
|
||||
laufender oneshot-Service) und rein abhängigen Units (z. B. ein nur von einem Timer
|
||||
ausgelöster Service ohne eigenen '[Install]'-Abschnitt, siehe `man systemd.unit`)."""
|
||||
if not os.path.isdir(systemd_src_dir):
|
||||
return [], []
|
||||
|
||||
unit_files = sorted(
|
||||
f for f in os.listdir(systemd_src_dir) if f.endswith((".service", ".timer", ".socket"))
|
||||
)
|
||||
installable = []
|
||||
for f in unit_files:
|
||||
with open(os.path.join(systemd_src_dir, f)) as fh:
|
||||
if "[Install]" in fh.read():
|
||||
installable.append(f)
|
||||
return unit_files, installable
|
||||
|
||||
|
||||
def build_install_scriptlet(installable, all_units):
|
||||
"""Erzeugt den Inhalt einer Arch-'.INSTALL'-Datei (siehe `man PKGBUILD`, Abschnitt
|
||||
'install'), die den paketierten systemd-Dienst beim Installieren aktiviert/startet und beim
|
||||
Entfernen wieder deaktiviert/stoppt - rein generisch anhand der tatsächlich gefundenen
|
||||
Unit-Dateien, ohne Anwendungsnamen hart zu codieren."""
|
||||
installable_str = " ".join(installable)
|
||||
all_units_str = " ".join(all_units)
|
||||
return f"""post_install() {{
|
||||
systemctl daemon-reload >/dev/null 2>&1 || true
|
||||
systemctl enable --now {installable_str} >/dev/null 2>&1 || true
|
||||
}}
|
||||
|
||||
post_upgrade() {{
|
||||
systemctl daemon-reload >/dev/null 2>&1 || true
|
||||
}}
|
||||
|
||||
pre_remove() {{
|
||||
systemctl disable --now {all_units_str} >/dev/null 2>&1 || true
|
||||
}}
|
||||
|
||||
post_remove() {{
|
||||
systemctl daemon-reload >/dev/null 2>&1 || true
|
||||
}}
|
||||
"""
|
||||
|
||||
|
||||
def build_package(target_triple=None, target_arch=None, pkgrel=None):
|
||||
metadata = json.loads(subprocess.check_output(["cargo", "metadata", "--format-version", "1", "--no-deps"]))
|
||||
pkg = metadata["packages"][0]
|
||||
@@ -102,6 +148,17 @@ def build_package(target_triple=None, target_arch=None, pkgrel=None):
|
||||
if os.path.exists("README.md"):
|
||||
subprocess.run(["install", "-m", "644", "README.md", f"{doc_dir}/README.md"], check=True)
|
||||
|
||||
systemd_src_dir = "packaging/systemd"
|
||||
all_units, installable_units = collect_systemd_units(systemd_src_dir)
|
||||
if all_units:
|
||||
unit_dir = os.path.join(build_dir, "usr/lib/systemd/system")
|
||||
os.makedirs(unit_dir, exist_ok=True)
|
||||
for unit in all_units:
|
||||
subprocess.run(
|
||||
["install", "-m", "644", os.path.join(systemd_src_dir, unit), os.path.join(unit_dir, unit)],
|
||||
check=True,
|
||||
)
|
||||
|
||||
installed_size = subprocess.check_output(["du", "-sb", build_dir]).decode().split()[0]
|
||||
builddate = str(int(time.time()))
|
||||
|
||||
@@ -121,14 +178,23 @@ def build_package(target_triple=None, target_arch=None, pkgrel=None):
|
||||
pkginfo_lines.append(f"depend = {dep}")
|
||||
for optdep in optdepends:
|
||||
pkginfo_lines.append(f"optdepend = {optdep}")
|
||||
if all_units:
|
||||
pkginfo_lines.append(f"install = {name}.install")
|
||||
pkginfo_lines.append("makepkgopt = strip\n")
|
||||
|
||||
with open(os.path.join(build_dir, ".PKGINFO"), "w") as f:
|
||||
f.write("\n".join(pkginfo_lines))
|
||||
|
||||
tar_members = [".PKGINFO", "usr"]
|
||||
if all_units:
|
||||
install_script_name = f"{name}.install"
|
||||
with open(os.path.join(build_dir, install_script_name), "w") as f:
|
||||
f.write(build_install_scriptlet(installable_units, all_units))
|
||||
tar_members.append(install_script_name)
|
||||
|
||||
os.makedirs("target/arch", exist_ok=True)
|
||||
output_file = os.path.abspath(f"target/arch/{name}-{version}-{pkgrel}-{arch}.pkg.tar.zst")
|
||||
subprocess.run(["tar", "--zstd", "-cf", output_file, ".PKGINFO", "usr"], cwd=build_dir, check=True)
|
||||
subprocess.run(["tar", "--zstd", "-cf", output_file, *tar_members], cwd=build_dir, check=True)
|
||||
print(f"Arch-Paket erfolgreich erstellt: {output_file}")
|
||||
|
||||
|
||||
|
||||
+12
-76
@@ -12,7 +12,7 @@ use clap::{Args, Subcommand};
|
||||
use dialoguer::{Confirm, Input, Password, Select};
|
||||
|
||||
use smart_mount::config::{
|
||||
self, AppConfig, CloudSide, DrivePair, LocalAddress, LocalSide, MountContext, MountKind,
|
||||
self, AppConfig, CloudSide, DrivePair, LocalAddress, LocalSide, MountKind,
|
||||
};
|
||||
use smart_mount::db::credentials::{Credential, CredentialStore, Side};
|
||||
|
||||
@@ -40,8 +40,6 @@ pub enum DriveAction {
|
||||
pub struct DriveArgs {
|
||||
#[arg(long)]
|
||||
name: Option<String>,
|
||||
#[arg(long, value_enum)]
|
||||
context: Option<MountContext>,
|
||||
#[arg(long)]
|
||||
owner_user: Option<String>,
|
||||
|
||||
@@ -108,10 +106,9 @@ async fn list(json: bool) -> anyhow::Result<()> {
|
||||
}
|
||||
for pair in &cfg.pairs {
|
||||
println!(
|
||||
"{} \"{}\" [{:?}] lokal={} cloud={} -> {}",
|
||||
"{} \"{}\" lokal={} cloud={} -> {}",
|
||||
pair.id,
|
||||
pair.name,
|
||||
pair.context,
|
||||
pair.local.kind.as_str(),
|
||||
pair.cloud.kind.as_str(),
|
||||
pair.mount_point.display()
|
||||
@@ -121,6 +118,8 @@ async fn list(json: bool) -> anyhow::Result<()> {
|
||||
}
|
||||
|
||||
async fn remove(id: &str) -> anyhow::Result<()> {
|
||||
crate::cli::require_root("drive remove")?;
|
||||
|
||||
// Vor dem Entfernen aus der Config nachschlagen, damit wir hinterher noch wissen, welche
|
||||
// Mount-Typen/Adressen betroffen sind - nötig, um die passenden Klartext-Zugangsdaten
|
||||
// (davfs2 secrets, .cred-Datei) aufzuräumen, siehe smart_mount::mount::cleanup_credentials.
|
||||
@@ -148,11 +147,11 @@ async fn remove(id: &str) -> anyhow::Result<()> {
|
||||
}
|
||||
|
||||
async fn add(args: DriveArgs) -> anyhow::Result<()> {
|
||||
crate::cli::require_root("drive add")?;
|
||||
let ni = args.non_interactive;
|
||||
|
||||
let name = resolve_field(args.name, None, "Drive pair name", ni, true)?.expect("required");
|
||||
let context = resolve_context(args.context, None, ni)?;
|
||||
let owner_user = resolve_owner_user(args.owner_user, context, None, ni)?;
|
||||
let owner_user = resolve_owner_user(args.owner_user, None, 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)?;
|
||||
@@ -199,7 +198,6 @@ async fn add(args: DriveArgs) -> anyhow::Result<()> {
|
||||
id: id.clone(),
|
||||
name,
|
||||
enabled: true,
|
||||
context,
|
||||
owner_user,
|
||||
mount_point,
|
||||
local: LocalSide {
|
||||
@@ -233,22 +231,18 @@ async fn add(args: DriveArgs) -> anyhow::Result<()> {
|
||||
}
|
||||
|
||||
println!("Drive pair '{id}' created.");
|
||||
if context == MountContext::User {
|
||||
println!("Note: for user-context pairs, 'sudo smart-mount setup fstab' must be run once.");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn edit(id: &str, args: DriveArgs) -> anyhow::Result<()> {
|
||||
crate::cli::require_root("drive edit")?;
|
||||
let cfg = config::pairs::load()?;
|
||||
let existing = config::pairs::find_pair(&cfg, id)?;
|
||||
let ni = args.non_interactive;
|
||||
|
||||
let name = resolve_field(args.name, Some(&existing.name), "Drive pair name", ni, true)?
|
||||
.expect("required");
|
||||
let context = resolve_context(args.context, Some(existing.context), ni)?;
|
||||
let owner_user =
|
||||
resolve_owner_user(args.owner_user, context, existing.owner_user.as_deref(), ni)?;
|
||||
let owner_user = resolve_owner_user(args.owner_user, existing.owner_user.as_deref(), ni)?;
|
||||
|
||||
let local_kind = resolve_kind(
|
||||
args.local_kind,
|
||||
@@ -320,10 +314,9 @@ async fn edit(id: &str, args: DriveArgs) -> anyhow::Result<()> {
|
||||
id: id.to_string(),
|
||||
name,
|
||||
enabled: existing.enabled,
|
||||
context,
|
||||
owner_user,
|
||||
// Mountpoint (und damit die Backing-Verzeichnisse) bleiben unverändert - sonst würden
|
||||
// eventuell noch aktive Mounts/fstab-Einträge verwaisen.
|
||||
// eventuell noch aktive Mounts verwaisen.
|
||||
mount_point: existing.mount_point.clone(),
|
||||
local: LocalSide {
|
||||
kind: local_kind,
|
||||
@@ -398,72 +391,17 @@ fn resolve_field(
|
||||
Ok(Some(input.interact_text()?))
|
||||
}
|
||||
|
||||
fn resolve_context(
|
||||
flag: Option<MountContext>,
|
||||
current: Option<MountContext>,
|
||||
non_interactive: bool,
|
||||
) -> anyhow::Result<MountContext> {
|
||||
if let Some(c) = flag {
|
||||
return Ok(c);
|
||||
}
|
||||
if let Some(c) = current
|
||||
&& non_interactive
|
||||
{
|
||||
return Ok(c);
|
||||
}
|
||||
if non_interactive {
|
||||
anyhow::bail!(
|
||||
"Field 'context' is missing - specify it via '--context system|user' in non-interactive mode."
|
||||
);
|
||||
}
|
||||
let default_idx = if current == Some(MountContext::System) {
|
||||
0
|
||||
} else {
|
||||
1
|
||||
};
|
||||
let idx = Select::new()
|
||||
.with_prompt("Context")
|
||||
.items(["System (root)", "User"])
|
||||
.default(default_idx)
|
||||
.interact()?;
|
||||
Ok(if idx == 0 {
|
||||
MountContext::System
|
||||
} else {
|
||||
MountContext::User
|
||||
})
|
||||
}
|
||||
|
||||
/// Optional: falls gesetzt, bekommt dieser Nutzer bei CIFS/WebDAV vollen Zugriff
|
||||
/// (uid=/gid=/file_mode=0700/dir_mode=0700) statt der sonst üblichen root-Ownership - der
|
||||
/// Mount selbst läuft immer als root (siehe [`smart_mount::systemd`]).
|
||||
fn resolve_owner_user(
|
||||
flag: Option<String>,
|
||||
context: MountContext,
|
||||
current: Option<&str>,
|
||||
non_interactive: bool,
|
||||
) -> anyhow::Result<Option<String>> {
|
||||
if let Some(v) = flag {
|
||||
return Ok(Some(v));
|
||||
}
|
||||
match context {
|
||||
MountContext::User => {
|
||||
// Pflicht: wird auch für 'setup fstab' (Gruppenmitgliedschaft, Verzeichnis-Owner)
|
||||
// und für die uid=/gid=-Zugriffsrechte benötigt.
|
||||
if non_interactive {
|
||||
return current.map(str::to_string).map(Some).ok_or_else(|| {
|
||||
anyhow::anyhow!("Field 'owner_user' is required for context 'user'.")
|
||||
});
|
||||
}
|
||||
let default_user = current
|
||||
.map(str::to_string)
|
||||
.unwrap_or_else(|| std::env::var("USER").unwrap_or_else(|_| "unknown".to_string()));
|
||||
Ok(Some(
|
||||
Input::new()
|
||||
.with_prompt("Linux username (owner)")
|
||||
.default(default_user)
|
||||
.interact_text()?,
|
||||
))
|
||||
}
|
||||
MountContext::System => {
|
||||
// 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.
|
||||
if non_interactive {
|
||||
return Ok(current.map(str::to_string));
|
||||
}
|
||||
@@ -479,8 +417,6 @@ fn resolve_owner_user(
|
||||
input = input.default(c.to_string());
|
||||
}
|
||||
Ok(Some(input.interact_text()?))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_kind(
|
||||
|
||||
+15
-9
@@ -4,13 +4,23 @@ pub mod doctor;
|
||||
pub mod drive;
|
||||
pub mod mount_cmd;
|
||||
pub mod service;
|
||||
pub mod setup;
|
||||
pub mod status;
|
||||
pub mod watch;
|
||||
|
||||
use clap::{CommandFactory, Parser, Subcommand};
|
||||
use clap_complete::Shell;
|
||||
|
||||
/// Bricht mit einer klaren Fehlermeldung ab, falls nicht als root aufgerufen. Mounten läuft
|
||||
/// ausschließlich als root/System-Dienst (siehe [`smart_mount::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.
|
||||
pub(crate) fn require_root(context: &str) -> anyhow::Result<()> {
|
||||
if !sudo_ctdra::is_run_as_root() {
|
||||
anyhow::bail!("'{context}' requires root privileges (re-run with sudo).");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(
|
||||
name = "smart-mount",
|
||||
@@ -55,17 +65,14 @@ pub enum Commands {
|
||||
},
|
||||
/// A single reconcile pass (local/cloud switching) - meant for systemd timers/cron.
|
||||
Watch,
|
||||
/// Install/remove systemd units.
|
||||
/// 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,
|
||||
},
|
||||
/// One-time root setup for unprivileged user mounts.
|
||||
Setup {
|
||||
#[command(subcommand)]
|
||||
action: setup::SetupAction,
|
||||
},
|
||||
/// Checks prerequisites (binaries, group membership, fstab setup, scheduler).
|
||||
/// Checks prerequisites (binaries, scheduler).
|
||||
Doctor {
|
||||
/// Output as JSON instead of text - for scripts.
|
||||
#[arg(long)]
|
||||
@@ -85,7 +92,6 @@ pub async fn dispatch(cli: Cli) -> anyhow::Result<()> {
|
||||
Commands::Status { name, json } => status::run(name, json).await,
|
||||
Commands::Watch => watch::run().await,
|
||||
Commands::Service { action } => service::run(action),
|
||||
Commands::Setup { action } => setup::run(action),
|
||||
Commands::Doctor { json } => doctor::run(json).await,
|
||||
Commands::Completions { shell } => {
|
||||
clap_complete::generate(
|
||||
|
||||
+4
-19
@@ -1,6 +1,6 @@
|
||||
//! `smart-mount mount` / `smart-mount unmount`.
|
||||
|
||||
use smart_mount::config::{self, DrivePair, MountContext};
|
||||
use smart_mount::config::{self, DrivePair};
|
||||
use smart_mount::db::credentials::CredentialStore;
|
||||
use smart_mount::reconcile;
|
||||
|
||||
@@ -15,27 +15,11 @@ fn select_pairs(
|
||||
if !all {
|
||||
anyhow::bail!("Please specify '--name <id>' or '--all'.");
|
||||
}
|
||||
|
||||
let pairs = if sudo_ctdra::is_run_as_root() {
|
||||
cfg.pairs
|
||||
.iter()
|
||||
.filter(|p| p.context == MountContext::System)
|
||||
.cloned()
|
||||
.collect()
|
||||
} else {
|
||||
let user = std::env::var("USER").unwrap_or_default();
|
||||
cfg.pairs
|
||||
.iter()
|
||||
.filter(|p| {
|
||||
p.context == MountContext::User && p.owner_user.as_deref() == Some(user.as_str())
|
||||
})
|
||||
.cloned()
|
||||
.collect()
|
||||
};
|
||||
Ok(pairs)
|
||||
Ok(cfg.pairs.clone())
|
||||
}
|
||||
|
||||
pub async fn run_mount(name: Option<String>, all: bool) -> anyhow::Result<()> {
|
||||
crate::cli::require_root("mount")?;
|
||||
let cfg = config::pairs::load()?;
|
||||
let pairs = select_pairs(&cfg, name.as_deref(), all)?;
|
||||
if pairs.is_empty() {
|
||||
@@ -52,6 +36,7 @@ pub async fn run_mount(name: Option<String>, all: bool) -> anyhow::Result<()> {
|
||||
}
|
||||
|
||||
pub async fn run_unmount(name: Option<String>, all: bool) -> anyhow::Result<()> {
|
||||
crate::cli::require_root("unmount")?;
|
||||
let cfg = config::pairs::load()?;
|
||||
let pairs = select_pairs(&cfg, name.as_deref(), all)?;
|
||||
if pairs.is_empty() {
|
||||
|
||||
+43
-141
@@ -1,152 +1,54 @@
|
||||
//! `smart-mount service install|uninstall --system|--user`.
|
||||
//! `smart-mount service crontab [--remove]`.
|
||||
|
||||
use clap::{Args, Subcommand};
|
||||
use clap::Subcommand;
|
||||
use smart_mount::config;
|
||||
use smart_mount::fstab;
|
||||
use smart_mount::systemd::{self, Scope};
|
||||
use smart_mount::systemd;
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum ServiceAction {
|
||||
/// Sets up periodic execution: systemd if available, otherwise falls back to cron
|
||||
/// automatically (see `crontab`).
|
||||
Install(ScopeArgs),
|
||||
/// Removes everything that `install`/`crontab`/`setup fstab` have set up - systemd
|
||||
/// units, cron entry, and (only for `--system`) the managed `/etc/fstab` block.
|
||||
/// Missing parts are skipped, not treated as an error.
|
||||
Uninstall(ScopeArgs),
|
||||
/// Sets up periodic execution via cron (alternative to `install` for systems without
|
||||
/// systemd) - system or user context is chosen automatically based on the current
|
||||
/// privileges (root -> `/etc/cron.d/smart-mount`, otherwise personal crontab). If no
|
||||
/// cron mechanism is present, the lines for manual entry are printed instead.
|
||||
Crontab,
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
pub struct ScopeArgs {
|
||||
#[arg(long, conflicts_with = "user")]
|
||||
system: bool,
|
||||
#[arg(long, conflicts_with = "system")]
|
||||
user: bool,
|
||||
}
|
||||
|
||||
impl ScopeArgs {
|
||||
fn scope(&self) -> anyhow::Result<Scope> {
|
||||
match (self.system, self.user) {
|
||||
(true, false) => Ok(Scope::System),
|
||||
(false, true) => Ok(Scope::User),
|
||||
_ => anyhow::bail!("Please specify exactly one of '--system' or '--user'."),
|
||||
}
|
||||
}
|
||||
/// 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::Install(args) => {
|
||||
let scope = args.scope()?;
|
||||
if scope == Scope::System && !sudo_ctdra::is_run_as_root() {
|
||||
anyhow::bail!(
|
||||
"'service install --system' requires root privileges (re-run with sudo)."
|
||||
);
|
||||
}
|
||||
let cfg = config::pairs::load()?;
|
||||
let interval = cfg.settings.watch_interval_secs;
|
||||
|
||||
if systemd::is_available() {
|
||||
systemd::install(scope, interval)?;
|
||||
println!("systemd units installed and enabled ({scope:?}).");
|
||||
} else {
|
||||
println!("systemd not found - setting up cron instead.");
|
||||
match systemd::install_cron(scope, interval)? {
|
||||
systemd::CronInstallOutcome::SystemFile(path) => {
|
||||
println!("Cron entry written: {}", path.display());
|
||||
}
|
||||
systemd::CronInstallOutcome::UserCrontab => {
|
||||
println!("Personal crontab updated (see 'crontab -l').");
|
||||
}
|
||||
systemd::CronInstallOutcome::Unavailable => {
|
||||
println!(
|
||||
"Neither systemd nor cron found - here are the lines for manual entry:"
|
||||
);
|
||||
print!("{}", systemd::crontab_equivalent(interval));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
ServiceAction::Uninstall(args) => {
|
||||
let scope = args.scope()?;
|
||||
if scope == Scope::System && !sudo_ctdra::is_run_as_root() {
|
||||
anyhow::bail!(
|
||||
"'service uninstall --system' requires root privileges (re-run with sudo)."
|
||||
);
|
||||
}
|
||||
|
||||
let mut removed = Vec::new();
|
||||
|
||||
if systemd::is_available() {
|
||||
match systemd::uninstall(scope)? {
|
||||
systemd::SystemdUninstallOutcome::Removed => removed.push("systemd units"),
|
||||
systemd::SystemdUninstallOutcome::NotPresent => {}
|
||||
}
|
||||
}
|
||||
|
||||
match systemd::uninstall_cron(scope)? {
|
||||
systemd::CronUninstallOutcome::Removed => removed.push("cron entry"),
|
||||
systemd::CronUninstallOutcome::NotPresent => {}
|
||||
}
|
||||
|
||||
// fstab-Einträge sind unabhängig vom --system/--user-Scope des Aufrufers immer
|
||||
// root-weit (setup() betrifft alle User-Kontext-Paare) - nur bei --system mit
|
||||
// aufräumen, damit ein `--user`-Uninstall nicht versehentlich Root-Konfiguration
|
||||
// anfasst, die ein anderer Nutzer noch braucht.
|
||||
if scope == Scope::System {
|
||||
match fstab::teardown()? {
|
||||
fstab::FstabTeardownOutcome::Removed => removed.push("fstab entries"),
|
||||
fstab::FstabTeardownOutcome::NotPresent => {}
|
||||
}
|
||||
}
|
||||
|
||||
if removed.is_empty() {
|
||||
println!("Nothing to remove - nothing was installed ({scope:?}).");
|
||||
} else {
|
||||
println!("Removed ({scope:?}): {}", removed.join(", "));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
ServiceAction::Crontab => {
|
||||
let cfg = config::pairs::load()?;
|
||||
let interval = cfg.settings.watch_interval_secs;
|
||||
// Scope folgt automatisch den aktuellen Rechten, wie bei `mount --all` -
|
||||
// root pflegt den systemweiten Cron-Eintrag, ein normaler Nutzer seine eigene
|
||||
// Crontab. Anders als bei `install`/`uninstall` gibt es hier bewusst keine
|
||||
// expliziten `--system`/`--user`-Flags, weil die Wahl ohnehin durch die Rechte
|
||||
// vorgegeben ist (root kann nicht "versehentlich" die falsche Crontab treffen).
|
||||
let scope = if sudo_ctdra::is_run_as_root() {
|
||||
Scope::System
|
||||
} else {
|
||||
Scope::User
|
||||
};
|
||||
|
||||
match systemd::install_cron(scope, interval)? {
|
||||
systemd::CronInstallOutcome::SystemFile(path) => {
|
||||
println!("Cron entry written: {}", path.display());
|
||||
}
|
||||
systemd::CronInstallOutcome::UserCrontab => {
|
||||
println!("Personal crontab updated (see 'crontab -l').");
|
||||
}
|
||||
systemd::CronInstallOutcome::Unavailable => {
|
||||
println!(
|
||||
"No cron mechanism found ({}) - here are the lines for manual entry:",
|
||||
if scope == Scope::System {
|
||||
"/etc/cron.d is missing"
|
||||
} else {
|
||||
"'crontab' not in PATH"
|
||||
}
|
||||
);
|
||||
print!("{}", systemd::crontab_equivalent(interval));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
ServiceAction::Crontab { remove: false } => install(),
|
||||
ServiceAction::Crontab { remove: true } => uninstall(),
|
||||
}
|
||||
}
|
||||
|
||||
fn install() -> anyhow::Result<()> {
|
||||
let cfg = config::pairs::load()?;
|
||||
let interval = cfg.settings.watch_interval_secs;
|
||||
|
||||
match systemd::install_cron(interval)? {
|
||||
systemd::CronInstallOutcome::SystemFile(path) => {
|
||||
println!("Cron entry written: {}", path.display());
|
||||
}
|
||||
systemd::CronInstallOutcome::Unavailable => {
|
||||
println!(
|
||||
"No cron mechanism found ('/etc/cron.d' is missing) - here are the lines for manual entry:"
|
||||
);
|
||||
print!("{}", systemd::crontab_equivalent(interval));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn uninstall() -> anyhow::Result<()> {
|
||||
match systemd::uninstall_cron()? {
|
||||
systemd::CronUninstallOutcome::Removed => println!("Cron entry removed."),
|
||||
systemd::CronUninstallOutcome::NotPresent => {
|
||||
println!("Nothing to remove - no cron entry was installed.")
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
//! `smart-mount setup fstab`.
|
||||
|
||||
use clap::Subcommand;
|
||||
use smart_mount::fstab;
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum SetupAction {
|
||||
/// One-time root setup: `/etc/fstab` entries + group membership for
|
||||
/// unprivileged user mounts.
|
||||
Fstab,
|
||||
}
|
||||
|
||||
pub fn run(action: SetupAction) -> anyhow::Result<()> {
|
||||
match action {
|
||||
SetupAction::Fstab => {
|
||||
fstab::setup()?;
|
||||
println!("fstab setup complete.");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ use smart_mount::reconcile::{self, Action};
|
||||
use crate::cli::mount_cmd::print_outcome;
|
||||
|
||||
pub async fn run() -> anyhow::Result<()> {
|
||||
crate::cli::require_root("watch")?;
|
||||
let cfg = config::pairs::load()?;
|
||||
let creds = CredentialStore::open().await?;
|
||||
let outcomes = reconcile::watch_once(&cfg, &creds).await;
|
||||
|
||||
+12
-4
@@ -4,12 +4,20 @@ pub mod pairs;
|
||||
pub mod schema;
|
||||
|
||||
pub use schema::{
|
||||
AppConfig, CloudSide, DrivePair, GlobalSettings, LocalAddress, LocalSide, MountContext,
|
||||
MountKind,
|
||||
AppConfig, CloudSide, DrivePair, GlobalSettings, LocalAddress, LocalSide, MountKind,
|
||||
};
|
||||
|
||||
/// Initialisiert den Konfigurationsdateinamen bei `config-ctdra`. Muss vor dem ersten
|
||||
/// `load`/`store`/`get_config`-Aufruf laufen (globaler, prozessweiter Zustand).
|
||||
/// Initialisiert `config-ctdra`: Dateiname und - unabhängig von den Rechten des aufrufenden
|
||||
/// Prozesses - immer der System-Pfad (`/etc/<programmname>/config.toml`). Es gibt nur noch
|
||||
/// eine einzige, systemweite Konfiguration (smart-mount läuft ausschließlich als root/
|
||||
/// System-Dienst, siehe [`crate::systemd`]) statt einer je nach Aufrufer wechselnden
|
||||
/// Root-/Nutzerpfad-Auflösung - `drive add`/`edit`/`remove` verlangen deshalb explizit Root
|
||||
/// (siehe `cli::require_root`), auch wenn sie nicht direkt mounten.
|
||||
///
|
||||
/// Muss vor dem ersten `load`/`store`/`get_config`-Aufruf laufen (globaler, prozessweiter
|
||||
/// Zustand).
|
||||
pub fn init() {
|
||||
config_ctdra::set_config_name("config");
|
||||
let program = config_ctdra::get_program_name();
|
||||
config_ctdra::set_custom_dir(std::path::PathBuf::from("/etc").join(program));
|
||||
}
|
||||
|
||||
+19
-59
@@ -11,39 +11,13 @@ use serde::{Deserialize, Serialize};
|
||||
/// `/run/media` ist die auf diesem System bereits übliche Konvention für eingebundene
|
||||
/// Wechseldatenträger/Netzlaufwerke (z. B. udisks2/GNOME) - tmpfs-hinterlegt, wird also bei
|
||||
/// jedem Boot ohnehin leer neu angelegt, passend dazu, dass Mountpoints selbst nie
|
||||
/// persistieren müssen. Root-/System-Kontext-Paare landen flach unter `/run/media/smart-mount`
|
||||
/// (ein systemweiter Dienst, keinem einzelnen Nutzer zugeordnet); Nutzer-Kontext-Paare unter
|
||||
/// `/run/media/<Nutzer>/smart-mount`, damit mehrere lokale Nutzer mit eigenen Paaren sich
|
||||
/// nicht denselben Namensraum teilen.
|
||||
/// persistieren müssen. Ein einzelner, flacher Namensraum reicht: smart-mount läuft
|
||||
/// ausschließlich als root/System-Dienst (siehe [`crate::systemd`]) und mountet dort auch
|
||||
/// Paare mit gesetztem `owner_user` - die Trennung nach Linux-Nutzer passiert über die
|
||||
/// `uid=`/`gid=`-Mount-Optionen (siehe [`crate::mount::target::build_target`]), nicht über
|
||||
/// unterschiedliche Mountpoint-Namensräume.
|
||||
fn default_mount_base_dir() -> PathBuf {
|
||||
if sudo_ctdra::is_run_as_root() {
|
||||
PathBuf::from("/run/media/smart-mount")
|
||||
} else {
|
||||
// Absichtlich KEIN Fallback auf einen festen Platzhalter wie "user": unter `systemd
|
||||
// --user` (wo $USER nicht immer gesetzt ist) würden dann zwei verschiedene reale
|
||||
// Nutzer denselben Mountpoint-Namensraum `/run/media/user/smart-mount` teilen - genau
|
||||
// die Kollision, die die Aufteilung nach Nutzername eigentlich verhindern soll. `id
|
||||
// -un` liest den Nutzernamen stattdessen direkt vom Kernel.
|
||||
let run_id = |flag: &str| {
|
||||
std::process::Command::new("id")
|
||||
.arg(flag)
|
||||
.output()
|
||||
.ok()
|
||||
.filter(|o| o.status.success())
|
||||
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
|
||||
.filter(|s| !s.is_empty())
|
||||
};
|
||||
let user = std::env::var("USER")
|
||||
.ok()
|
||||
.filter(|u| !u.is_empty())
|
||||
.or_else(|| run_id("-un"))
|
||||
// Letzter Ausweg: die numerische UID ist immer verfügbar und auf jedem System
|
||||
// eindeutig, anders als ein fest codierter Platzhalter-String wie "user", der bei
|
||||
// mehreren betroffenen Nutzern denselben Namensraum kollidieren ließe.
|
||||
.or_else(|| run_id("-u").map(|uid| format!("uid-{uid}")))
|
||||
.unwrap_or_else(|| "user".to_string());
|
||||
PathBuf::from("/run/media").join(user).join("smart-mount")
|
||||
}
|
||||
}
|
||||
|
||||
fn default_log_level() -> String {
|
||||
@@ -58,9 +32,8 @@ fn default_mac2ip_binary() -> String {
|
||||
"mac2ip".to_string()
|
||||
}
|
||||
|
||||
/// Wurzel-Konfigurationsstruktur, gespeichert via `config-ctdra` unter
|
||||
/// `~/.config/smart-mount/config.toml` (Nutzerkontext) bzw. `/etc/smart-mount/config.toml`
|
||||
/// (Root-Kontext).
|
||||
/// Wurzel-Konfigurationsstruktur, gespeichert via `config-ctdra` immer unter
|
||||
/// `/etc/smart-mount/config.toml` (siehe [`crate::config::init`]).
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
|
||||
pub struct AppConfig {
|
||||
#[serde(default)]
|
||||
@@ -76,8 +49,10 @@ pub struct GlobalSettings {
|
||||
pub mount_base_dir: PathBuf,
|
||||
#[serde(default = "default_log_level")]
|
||||
pub log_level: String,
|
||||
/// Periode, mit der `smart-mount watch` über den generierten systemd-Timer bzw. die
|
||||
/// dokumentierte Crontab-Zeile ausgeführt werden soll.
|
||||
/// Periode, mit der `smart-mount watch` über den Cron-Fallback (`smart-mount service
|
||||
/// crontab`, siehe [`crate::systemd`]) ausgeführt werden soll. Der paketierte systemd-Timer
|
||||
/// hat ein fest eingebautes Intervall (siehe `packaging/systemd/smart-mount-watch.timer`)
|
||||
/// und liest dieses Feld nicht.
|
||||
#[serde(default = "default_watch_interval_secs")]
|
||||
pub watch_interval_secs: u64,
|
||||
/// Name/Pfad des `mac2ip`-Binaries (per PATH auflösbar, oder absoluter Pfad).
|
||||
@@ -101,14 +76,14 @@ impl Default for GlobalSettings {
|
||||
/// eingebunden - siehe [`crate::reconcile`].
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct DrivePair {
|
||||
/// Stabile ID (uuid-v4), Schlüssel für DB-Zugangsdaten, Mount-Unterverzeichnis,
|
||||
/// systemd-Unit-Namen und fstab-Einträge.
|
||||
/// Stabile ID (uuid-v4), Schlüssel für DB-Zugangsdaten sowie das Mount-Unterverzeichnis.
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
#[serde(default = "default_true")]
|
||||
pub enabled: bool,
|
||||
pub context: MountContext,
|
||||
/// Pflicht bei `context == User`: der Linux-Benutzername, dem dieses Paar gehört.
|
||||
/// Optional: der Linux-Benutzername, der vollen Zugriff (uid/gid) auf dieses Paar
|
||||
/// bekommen soll (siehe [`crate::mount::target::build_target`]). Ohne `owner_user` gehört
|
||||
/// der Mount root.
|
||||
#[serde(default)]
|
||||
pub owner_user: Option<String>,
|
||||
pub mount_point: PathBuf,
|
||||
@@ -120,16 +95,6 @@ fn default_true() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
/// Ob ein Laufwerkspaar systemweit (root, `/etc/fstab`+systemd-System-Service) oder als
|
||||
/// einzelner Nutzer (`systemd --user`, unprivilegiert über `setup fstab`) eingebunden wird.
|
||||
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, clap::ValueEnum)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[value(rename_all = "lowercase")]
|
||||
pub enum MountContext {
|
||||
System,
|
||||
User,
|
||||
}
|
||||
|
||||
/// Die lokale (LAN-)Seite eines Laufwerkspaars.
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct LocalSide {
|
||||
@@ -198,7 +163,6 @@ mod tests {
|
||||
id: "pair-1".into(),
|
||||
name: "NAS".into(),
|
||||
enabled: true,
|
||||
context: MountContext::User,
|
||||
owner_user: Some("dragon".into()),
|
||||
mount_point: PathBuf::from("/home/dragon/smart-mount/pair-1"),
|
||||
local: LocalSide {
|
||||
@@ -223,7 +187,6 @@ mod tests {
|
||||
|
||||
assert_eq!(round_tripped.pairs.len(), 1);
|
||||
assert_eq!(round_tripped.pairs[0].id, "pair-1");
|
||||
assert_eq!(round_tripped.pairs[0].context, MountContext::User);
|
||||
match &round_tripped.pairs[0].local.address {
|
||||
LocalAddress::Mac(mac) => assert_eq!(mac, "aa:bb:cc:dd:ee:ff"),
|
||||
LocalAddress::Ip(_) => panic!("expected Mac variant"),
|
||||
@@ -232,12 +195,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn default_mount_base_dir_uses_run_media() {
|
||||
// Tests laufen nie als root, daher greift hier immer der Nutzerkontext-Zweig.
|
||||
let dir = default_mount_base_dir();
|
||||
assert!(dir.starts_with("/run/media"));
|
||||
assert!(dir.ends_with("smart-mount"));
|
||||
if let Ok(user) = std::env::var("USER") {
|
||||
assert!(dir.to_string_lossy().contains(&user));
|
||||
}
|
||||
assert_eq!(
|
||||
default_mount_base_dir(),
|
||||
PathBuf::from("/run/media/smart-mount")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+5
-125
@@ -1,11 +1,8 @@
|
||||
//! Master-Schlüssel-Auflösung für die Zugangsdaten-Verschlüsselung.
|
||||
//!
|
||||
//! Reihenfolge (wie mit dem Nutzer abgestimmt):
|
||||
//! - Root-/System-Kontext: **immer** die Schlüsseldatei (kein Nutzer-Keyring im
|
||||
//! Systemdienst-Kontext verfügbar).
|
||||
//! - Nutzerkontext: zuerst das OS-Keyring (GNOME Keyring/KWallet über secret-service)
|
||||
//! versuchen, bei Nichtverfügbarkeit (z. B. Headless-Server, kein D-Bus-Secret-Service)
|
||||
//! transparent auf die Schlüsseldatei zurückfallen.
|
||||
//! Immer die Schlüsseldatei: SmartMount läuft ausschließlich als root/System-Dienst (siehe
|
||||
//! [`crate::systemd`]), für den es kein Nutzer-Keyring (GNOME Keyring/KWallet über
|
||||
//! secret-service) gibt.
|
||||
|
||||
use std::fs::{self, File, OpenOptions};
|
||||
use std::io::{Read, Write};
|
||||
@@ -13,64 +10,13 @@ use std::path::{Path, PathBuf};
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
|
||||
const KEYRING_SERVICE: &str = "smart-mount";
|
||||
const KEYRING_USERNAME: &str = "master-key";
|
||||
const KEY_FILE_NAME: &str = "master.key";
|
||||
const KEY_BACKEND_MARKER_NAME: &str = "master.key.backend";
|
||||
const KEY_LEN: usize = 32;
|
||||
|
||||
/// Ermittelt (und erzeugt bei Bedarf) den 256-Bit-Master-Schlüssel für die
|
||||
/// Zugangsdaten-Verschlüsselung, siehe Modul-Dokumentation für die Fallback-Reihenfolge.
|
||||
///
|
||||
/// Welcher Backend (Keyring oder Schlüsseldatei) für einen Nutzer verwendet wird, wird beim
|
||||
/// ersten Aufruf in einer Marker-Datei festgehalten und danach immer wieder verwendet. Ohne
|
||||
/// diese Festlegung würde eine vorübergehend nicht erreichbare Keyring (z. B. `systemd --user`
|
||||
/// ohne D-Bus-Secret-Service) sonst bei jedem Aufruf transparent einen *neuen* Datei-Schlüssel
|
||||
/// erzeugen und damit zuvor unter dem Keyring-Schlüssel verschlüsselte Zugangsdaten unwiderruflich
|
||||
/// unlesbar machen.
|
||||
/// Zugangsdaten-Verschlüsselung.
|
||||
pub fn resolve_master_key() -> Result<[u8; 32]> {
|
||||
if sudo_ctdra::is_run_as_root() {
|
||||
return file_key::load_or_create(&key_file_path());
|
||||
}
|
||||
|
||||
let marker_path = key_backend_marker_path();
|
||||
match fs::read_to_string(&marker_path) {
|
||||
Ok(backend) => match backend.trim() {
|
||||
"keyring" => keyring_key::load_or_create()
|
||||
.map_err(|reason| Error::Crypto(format!("OS keyring not available ({reason})"))),
|
||||
_ => file_key::load_or_create(&key_file_path()),
|
||||
},
|
||||
Err(_) => match keyring_key::load_or_create() {
|
||||
Ok(key) => {
|
||||
write_key_backend_marker(&marker_path, "keyring");
|
||||
Ok(key)
|
||||
}
|
||||
Err(reason) => {
|
||||
logger_ctdra::warn(
|
||||
"crypto",
|
||||
&format!("OS keyring not available ({reason}), using key file"),
|
||||
);
|
||||
let key = file_key::load_or_create(&key_file_path())?;
|
||||
write_key_backend_marker(&marker_path, "file");
|
||||
Ok(key)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn write_key_backend_marker(marker_path: &Path, backend: &str) {
|
||||
if let Some(dir) = marker_path.parent() {
|
||||
let _ = fs::create_dir_all(dir);
|
||||
}
|
||||
if let Err(e) = fs::write(marker_path, backend) {
|
||||
logger_ctdra::warn(
|
||||
"crypto",
|
||||
&format!(
|
||||
"could not persist key backend marker '{}': {e}",
|
||||
marker_path.display()
|
||||
),
|
||||
);
|
||||
}
|
||||
file_key::load_or_create(&key_file_path())
|
||||
}
|
||||
|
||||
fn key_file_path() -> PathBuf {
|
||||
@@ -81,14 +27,6 @@ fn key_file_path() -> PathBuf {
|
||||
.unwrap_or_else(|| PathBuf::from(KEY_FILE_NAME))
|
||||
}
|
||||
|
||||
fn key_backend_marker_path() -> PathBuf {
|
||||
let config_path = config_ctdra::get_config_path();
|
||||
config_path
|
||||
.parent()
|
||||
.map(|dir| dir.join(KEY_BACKEND_MARKER_NAME))
|
||||
.unwrap_or_else(|| PathBuf::from(KEY_BACKEND_MARKER_NAME))
|
||||
}
|
||||
|
||||
mod file_key {
|
||||
use super::*;
|
||||
|
||||
@@ -150,61 +88,3 @@ mod file_key {
|
||||
.map_err(|e| Error::Crypto(format!("Random number generator failed: {e}")))
|
||||
}
|
||||
}
|
||||
|
||||
mod keyring_key {
|
||||
use super::*;
|
||||
|
||||
pub fn load_or_create() -> std::result::Result<[u8; 32], String> {
|
||||
let entry = keyring::Entry::new(KEYRING_SERVICE, KEYRING_USERNAME)
|
||||
.map_err(|e| format!("Could not create keyring entry: {e}"))?;
|
||||
|
||||
match entry.get_password() {
|
||||
Ok(hex_key) => decode(&hex_key),
|
||||
Err(keyring::Error::NoEntry) => {
|
||||
let key = generate()?;
|
||||
if let Err(e) = entry.set_password(&encode(&key)) {
|
||||
// A concurrent first run may have already created the entry; use its key
|
||||
// instead of failing outright.
|
||||
return match entry.get_password() {
|
||||
Ok(hex_key) => decode(&hex_key),
|
||||
Err(_) => Err(format!("Could not store key in keyring: {e}")),
|
||||
};
|
||||
}
|
||||
// Re-read the entry: a concurrent writer may have overwritten ours after our
|
||||
// own `set_password` succeeded. Using whichever key ultimately "won" ensures
|
||||
// both processes agree on the same key instead of one silently using a key
|
||||
// that was never actually persisted.
|
||||
match entry.get_password() {
|
||||
Ok(hex_key) => decode(&hex_key),
|
||||
Err(_) => Ok(key),
|
||||
}
|
||||
}
|
||||
Err(e) => Err(format!("Keyring access failed: {e}")),
|
||||
}
|
||||
}
|
||||
|
||||
fn generate() -> std::result::Result<[u8; 32], String> {
|
||||
let mut key = [0u8; 32];
|
||||
::getrandom::fill(&mut key).map_err(|e| format!("Random number generator failed: {e}"))?;
|
||||
Ok(key)
|
||||
}
|
||||
|
||||
fn encode(key: &[u8; 32]) -> String {
|
||||
key.iter().map(|b| format!("{b:02x}")).collect()
|
||||
}
|
||||
|
||||
fn decode(hex_key: &str) -> std::result::Result<[u8; 32], String> {
|
||||
if hex_key.len() != 64 {
|
||||
return Err(format!(
|
||||
"unexpected key length in keyring ({} instead of 64 hex characters)",
|
||||
hex_key.len()
|
||||
));
|
||||
}
|
||||
let mut key = [0u8; 32];
|
||||
for (i, chunk) in hex_key.as_bytes().chunks(2).enumerate() {
|
||||
let byte_str = std::str::from_utf8(chunk).map_err(|e| e.to_string())?;
|
||||
key[i] = u8::from_str_radix(byte_str, 16).map_err(|e| e.to_string())?;
|
||||
}
|
||||
Ok(key)
|
||||
}
|
||||
}
|
||||
|
||||
+10
-124
@@ -1,11 +1,10 @@
|
||||
//! Diagnose-Checks für `smart-mount doctor` - prüft die im Laufe der Entwicklung
|
||||
//! angesammelten Voraussetzungen (Binaries, Gruppenmitgliedschaft, fstab-Setup, Scheduler)
|
||||
//! gebündelt an einer Stelle, statt sie einzeln erst beim Mount-Fehlschlag zu entdecken.
|
||||
//! angesammelten Voraussetzungen (Binaries, Scheduler) gebündelt an einer Stelle, statt sie
|
||||
//! einzeln erst beim Mount-Fehlschlag zu entdecken.
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::process::Command;
|
||||
|
||||
use crate::config::{AppConfig, DrivePair, LocalAddress, MountContext, MountKind};
|
||||
use crate::config::{AppConfig, LocalAddress, MountKind};
|
||||
use crate::mount;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
@@ -68,23 +67,6 @@ pub fn run_checks(cfg: &AppConfig) -> Vec<CheckResult> {
|
||||
results.push(check_binary("nmap", "nmap", "install package 'nmap'"));
|
||||
}
|
||||
|
||||
if cfg.pairs.iter().any(|p| p.context == MountContext::User) {
|
||||
results.push(check_fstab_setup(cfg));
|
||||
if used_kinds.contains(&MountKind::WebDav) {
|
||||
results.push(check_davfs2_group_membership(cfg));
|
||||
}
|
||||
}
|
||||
|
||||
for pair in &cfg.pairs {
|
||||
if pair.context == MountContext::User && pair.owner_user.is_none() {
|
||||
results.push(fail(
|
||||
format!("Pair '{}': owner_user", pair.name),
|
||||
"context 'user', but no owner_user set - 'setup fstab' will reject this."
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
results
|
||||
}
|
||||
|
||||
@@ -141,12 +123,12 @@ fn check_scheduler() -> CheckResult {
|
||||
if systemd {
|
||||
ok(
|
||||
"Scheduler",
|
||||
"systemd found - 'smart-mount service install' uses systemd timers",
|
||||
"systemd found - the packaged .deb/.rpm/.pkg.tar.zst installs/enables the smart-mount systemd service automatically",
|
||||
)
|
||||
} else if cron_d || crontab {
|
||||
warn(
|
||||
"Scheduler",
|
||||
"no systemd, but cron found - 'smart-mount service install' automatically falls back to cron",
|
||||
"no systemd, but cron found - run 'smart-mount service crontab' to set up the periodic 'watch' call",
|
||||
)
|
||||
} else {
|
||||
fail(
|
||||
@@ -178,89 +160,17 @@ fn check_mount_base_dir(dir: &std::path::Path) -> CheckResult {
|
||||
}
|
||||
}
|
||||
|
||||
fn check_fstab_setup(cfg: &AppConfig) -> CheckResult {
|
||||
let existing = std::fs::read_to_string("/etc/fstab").unwrap_or_default();
|
||||
if existing.contains("# BEGIN smart-mount managed block") {
|
||||
ok("setup fstab", "managed block found in /etc/fstab")
|
||||
} else {
|
||||
let count = cfg
|
||||
.pairs
|
||||
.iter()
|
||||
.filter(|p| p.context == MountContext::User)
|
||||
.count();
|
||||
fail(
|
||||
"setup fstab",
|
||||
format!(
|
||||
"no managed block found in /etc/fstab, but {count} user-context pair(s) configured - run 'sudo smart-mount setup fstab'"
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn check_davfs2_group_membership(cfg: &AppConfig) -> CheckResult {
|
||||
let owners: HashSet<&str> = cfg
|
||||
.pairs
|
||||
.iter()
|
||||
.filter(|p| {
|
||||
p.context == MountContext::User
|
||||
&& (p.local.kind == MountKind::WebDav || p.cloud.kind == MountKind::WebDav)
|
||||
})
|
||||
.filter_map(|p: &DrivePair| p.owner_user.as_deref())
|
||||
.collect();
|
||||
|
||||
if owners.is_empty() {
|
||||
return ok(
|
||||
"davfs2 group membership",
|
||||
"no WebDAV user-context pairs with owner_user - nothing to check",
|
||||
);
|
||||
}
|
||||
|
||||
let mut missing = Vec::new();
|
||||
for owner in &owners {
|
||||
let output = Command::new("id").args(["-nG", owner]).output();
|
||||
let is_member = output
|
||||
.map(|o| {
|
||||
String::from_utf8_lossy(&o.stdout)
|
||||
.split_whitespace()
|
||||
.any(|g| g == "davfs2")
|
||||
})
|
||||
.unwrap_or(false);
|
||||
if !is_member {
|
||||
missing.push(*owner);
|
||||
}
|
||||
}
|
||||
|
||||
if missing.is_empty() {
|
||||
ok(
|
||||
"davfs2 group membership",
|
||||
format!(
|
||||
"all affected users ({}) are members of the 'davfs2' group",
|
||||
owners.len()
|
||||
),
|
||||
)
|
||||
} else {
|
||||
warn(
|
||||
"davfs2 group membership",
|
||||
format!(
|
||||
"users without 'davfs2' group: {} - run 'sudo smart-mount setup fstab' (log out and back in afterwards if needed)",
|
||||
missing.join(", ")
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::config::{CloudSide, GlobalSettings, LocalSide};
|
||||
use crate::config::{CloudSide, DrivePair, GlobalSettings, LocalSide};
|
||||
use std::net::Ipv4Addr;
|
||||
|
||||
fn sample_pair(context: MountContext, owner_user: Option<&str>, mac: bool) -> DrivePair {
|
||||
fn sample_pair(owner_user: Option<&str>, mac: bool) -> DrivePair {
|
||||
DrivePair {
|
||||
id: "pair-1".into(),
|
||||
name: "Test".into(),
|
||||
enabled: true,
|
||||
context,
|
||||
owner_user: owner_user.map(str::to_string),
|
||||
mount_point: "/media/smart-mount/pair-1".into(),
|
||||
local: LocalSide {
|
||||
@@ -288,7 +198,7 @@ mod tests {
|
||||
fn used_mount_kinds_collects_both_sides_across_pairs() {
|
||||
let cfg = AppConfig {
|
||||
settings: GlobalSettings::default(),
|
||||
pairs: vec![sample_pair(MountContext::System, None, false)],
|
||||
pairs: vec![sample_pair(None, false)],
|
||||
};
|
||||
let kinds = used_mount_kinds(&cfg);
|
||||
assert!(kinds.contains(&MountKind::Nfs));
|
||||
@@ -300,40 +210,16 @@ mod tests {
|
||||
fn uses_mac_addressing_detects_mac_pairs() {
|
||||
let with_mac = AppConfig {
|
||||
settings: GlobalSettings::default(),
|
||||
pairs: vec![sample_pair(MountContext::System, None, true)],
|
||||
pairs: vec![sample_pair(None, true)],
|
||||
};
|
||||
let without_mac = AppConfig {
|
||||
settings: GlobalSettings::default(),
|
||||
pairs: vec![sample_pair(MountContext::System, None, false)],
|
||||
pairs: vec![sample_pair(None, false)],
|
||||
};
|
||||
assert!(uses_mac_addressing(&with_mac));
|
||||
assert!(!uses_mac_addressing(&without_mac));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flags_user_context_pair_without_owner_user() {
|
||||
let cfg = AppConfig {
|
||||
settings: GlobalSettings::default(),
|
||||
pairs: vec![sample_pair(MountContext::User, None, false)],
|
||||
};
|
||||
let results = run_checks(&cfg);
|
||||
assert!(
|
||||
results
|
||||
.iter()
|
||||
.any(|r| r.status == CheckStatus::Fail && r.label.contains("owner_user"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn does_not_flag_owner_user_when_present() {
|
||||
let cfg = AppConfig {
|
||||
settings: GlobalSettings::default(),
|
||||
pairs: vec![sample_pair(MountContext::User, Some("dragon"), false)],
|
||||
};
|
||||
let results = run_checks(&cfg);
|
||||
assert!(!results.iter().any(|r| r.label.contains("owner_user")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_config_still_runs_global_checks_without_panicking() {
|
||||
let cfg = AppConfig::default();
|
||||
|
||||
@@ -39,9 +39,6 @@ pub enum Error {
|
||||
#[error("mac2ip resolution failed for MAC {mac}: {reason}")]
|
||||
Mac2Ip { mac: String, reason: String },
|
||||
|
||||
#[error("No root context: {0}")]
|
||||
RequiresRoot(&'static str),
|
||||
|
||||
#[error("{0}")]
|
||||
Other(String),
|
||||
}
|
||||
|
||||
@@ -1,683 +0,0 @@
|
||||
//! Einmaliges root-Setup, das unprivilegierten `User`-Kontext-Paaren erlaubt, sich selbst
|
||||
//! (unprivilegiert) zu mounten/unmounten.
|
||||
//!
|
||||
//! Kernidee: pro Paar werden **zwei** `/etc/fstab`-Zeilen geschrieben - je eine pro Seite,
|
||||
//! auf das jeweils eindeutige Backing-Verzeichnis dieser Seite (siehe
|
||||
//! [`crate::mount::target::backing_dir`]), nicht auf einen gemeinsamen Mountpoint. Damit
|
||||
//! entspricht jede Zeile exakt dem einzigen in `man 8 mount` ("Non-superuser mounts")
|
||||
//! dokumentierten Fall - genau eine fstab-Zeile pro Ziel - statt sich auf unspezifiziertes
|
||||
//! Verhalten bei zwei Zeilen mit demselben Ziel zu verlassen. Der sichtbare `pair.mount_point`
|
||||
//! selbst erscheint dadurch gar nicht in `/etc/fstab` - er ist ein Symlink, den smart-mount
|
||||
//! zur Laufzeit zwischen den beiden Backing-Verzeichnissen umschaltet (siehe
|
||||
//! [`crate::reconcile`]).
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
|
||||
use crate::config::{AppConfig, DrivePair, GlobalSettings, MountContext, MountKind};
|
||||
use crate::db::credentials::Side;
|
||||
use crate::error::{Error, Result};
|
||||
use crate::mount::smb;
|
||||
use crate::mount::target::{self, backing_dir};
|
||||
|
||||
const BEGIN_MARKER: &str = "# BEGIN smart-mount managed block";
|
||||
const END_MARKER: &str = "# END smart-mount managed block";
|
||||
const FSTAB_PATH: &str = "/etc/fstab";
|
||||
|
||||
/// Führt das einmalige root-Setup für alle `User`-Kontext-Paare aus: fstab-Block
|
||||
/// regenerieren, Gruppenmitgliedschaft sicherstellen, Mountpoints anlegen.
|
||||
///
|
||||
/// Eskaliert selbst via `sudo_ctdra::run_as_root()`, falls nicht bereits root - das ist der
|
||||
/// einzige Befehl in smart-mount, der das tut (alle anderen root-Aktionen verlangen
|
||||
/// explizit, bereits als root aufgerufen zu werden).
|
||||
pub fn setup() -> Result<()> {
|
||||
if !sudo_ctdra::is_run_as_root() {
|
||||
let err = sudo_ctdra::run_as_root();
|
||||
return Err(Error::Other(format!(
|
||||
"Restart with root privileges failed: {err}"
|
||||
)));
|
||||
}
|
||||
|
||||
let sudo_user = std::env::var("SUDO_USER").ok().filter(|s| !s.is_empty());
|
||||
|
||||
if let Some(sudo_user) = &sudo_user
|
||||
&& config_ctdra::get_custom_path().is_none()
|
||||
{
|
||||
let user_config = user_config_path(sudo_user);
|
||||
config_ctdra::set_custom_path(&user_config);
|
||||
}
|
||||
|
||||
let cfg = crate::config::pairs::load()?;
|
||||
let user_pairs: Vec<&DrivePair> = cfg
|
||||
.pairs
|
||||
.iter()
|
||||
.filter(|p| p.context == MountContext::User)
|
||||
.collect();
|
||||
|
||||
if user_pairs.is_empty() {
|
||||
logger_ctdra::info("fstab", "No user-context pairs configured - nothing to do.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
validate_user_pairs_have_owner(&user_pairs)?;
|
||||
|
||||
for pair in &user_pairs {
|
||||
ensure_backing_dirs(pair)?;
|
||||
ensure_group_membership(pair)?;
|
||||
}
|
||||
|
||||
// Wessen zuvor installierte, jetzt aber nicht mehr konfigurierte Zeilen beim
|
||||
// Zusammenführen (siehe `merge_managed_block`) entfernt werden dürfen: bei einem
|
||||
// `sudo`-Aufruf im Namen eines bestimmten Nutzers ausschließlich dessen eigene Zeilen,
|
||||
// sonst die Menge der in der (dann root-eigenen) Konfiguration genannten `owner_user`.
|
||||
// Zeilen ANDERER Nutzer bleiben immer unangetastet - andernfalls würde ein zweiter Nutzer,
|
||||
// der `setup fstab` für sein eigenes Konto ausführt, die vom ersten Nutzer installierten
|
||||
// Zeilen löschen, weil sich beide denselben verwalteten Block in `/etc/fstab` teilen.
|
||||
let owner_scope: Vec<String> = match &sudo_user {
|
||||
Some(u) => vec![u.clone()],
|
||||
None => user_pairs
|
||||
.iter()
|
||||
.filter_map(|p| p.owner_user.clone())
|
||||
.collect::<HashSet<_>>()
|
||||
.into_iter()
|
||||
.collect(),
|
||||
};
|
||||
|
||||
write_managed_block(&user_pairs, &cfg.settings, &owner_scope)?;
|
||||
|
||||
logger_ctdra::info(
|
||||
"fstab",
|
||||
"Done. Affected users may need to log out and back in for new group memberships to \
|
||||
take effect. For MAC-based local drives in user context: set up passwordless sudo \
|
||||
access to 'nmap' if resolution does not already succeed via the ARP neighbor table \
|
||||
(see README).",
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Ergebnis von [`teardown`].
|
||||
pub enum FstabTeardownOutcome {
|
||||
/// Der verwaltete Block wurde gefunden und entfernt.
|
||||
Removed,
|
||||
/// Kein von smart-mount verwalteter Block vorhanden - nichts zu tun.
|
||||
NotPresent,
|
||||
}
|
||||
|
||||
/// Gegenstück zu [`setup`]: entfernt den von smart-mount verwalteten Block wieder aus
|
||||
/// `/etc/fstab` (Backup wie bei `setup` nach `/etc/fstab.smart-mount.bak`). Rührt bewusst
|
||||
/// **keine** Backing-Verzeichnisse, gemounteten Daten oder Gruppenmitgliedschaften an - nur
|
||||
/// die fstab-Zeilen selbst, da das Löschen von Verzeichnissen/Cache-Daten oder das Entfernen
|
||||
/// aus einer Gruppe ungewollte Nebenwirkungen haben könnte (die Gruppe könnte z. B. auch
|
||||
/// unabhängig von smart-mount genutzt werden).
|
||||
///
|
||||
/// Eskaliert selbst via `sudo_ctdra::run_as_root()`, falls nicht bereits root (wie `setup`).
|
||||
pub fn teardown() -> Result<FstabTeardownOutcome> {
|
||||
if !sudo_ctdra::is_run_as_root() {
|
||||
let err = sudo_ctdra::run_as_root();
|
||||
return Err(Error::Other(format!(
|
||||
"Restart with root privileges failed: {err}"
|
||||
)));
|
||||
}
|
||||
|
||||
let fstab_path = PathBuf::from(FSTAB_PATH);
|
||||
let existing = std::fs::read_to_string(&fstab_path).unwrap_or_default();
|
||||
|
||||
if !existing.contains(BEGIN_MARKER) {
|
||||
return Ok(FstabTeardownOutcome::NotPresent);
|
||||
}
|
||||
|
||||
backup(&fstab_path, &existing)?;
|
||||
let without_block = crate::util::strip_managed_block(&existing, BEGIN_MARKER, END_MARKER);
|
||||
write_atomic(&fstab_path, &without_block)?;
|
||||
Ok(FstabTeardownOutcome::Removed)
|
||||
}
|
||||
|
||||
/// Ohne `owner_user` würde die fstab-Zeile ohne `uid=`/`gid=` geschrieben - bei davfs2 heißt
|
||||
/// das laut `man mount.davfs` ("uid=user"/"gid=group"): JEDES Mitglied der Gruppe 'davfs2'
|
||||
/// dürfte dieses Paar mounten, nicht nur der vorgesehene Besitzer. Lieber hart fehlschlagen,
|
||||
/// bevor eine unsichere Zeile geschrieben wird, als das still zuzulassen.
|
||||
fn validate_user_pairs_have_owner(pairs: &[&DrivePair]) -> Result<()> {
|
||||
for pair in pairs {
|
||||
if pair.owner_user.is_none() {
|
||||
return Err(Error::Other(format!(
|
||||
"Drive pair '{}' has context 'user', but no owner_user set. \
|
||||
Without owner_user, mount access cannot be restricted to a specific \
|
||||
user - please set owner_user in the configuration \
|
||||
(e.g. via 'smart-mount drive add').",
|
||||
pair.id
|
||||
)));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Legt beide Backing-Verzeichnisse an (nicht `pair.mount_point` selbst - das bleibt ein
|
||||
/// Symlink, siehe Moduldoku) und macht `owner_user` zum Besitzer beider.
|
||||
fn ensure_backing_dirs(pair: &DrivePair) -> Result<()> {
|
||||
for side in [Side::Local, Side::Cloud] {
|
||||
create_dir_all_owned(&backing_dir(pair, side), pair.owner_user.as_deref())?;
|
||||
}
|
||||
|
||||
// Der Elternordner des sichtbaren Mountpoints (z. B. `/run/media/<Nutzer>/smart-mount`)
|
||||
// muss dem Nutzer ebenfalls gehören - dort legt `activate_symlink` bei JEDEM `mount`/
|
||||
// `watch`-Lauf den Symlink an/ersetzt ihn, und das läuft (anders als dieses einmalige
|
||||
// Setup) unprivilegiert als der Nutzer selbst. `/run/media` ist standardmäßig `root:root
|
||||
// 0755` - ohne diesen Schritt könnte der Nutzer dort nicht einmal ein eigenes
|
||||
// Unterverzeichnis anlegen.
|
||||
if let Some(parent) = pair.mount_point.parent() {
|
||||
create_dir_all_owned(parent, pair.owner_user.as_deref())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Wie `std::fs::create_dir_all`, macht aber zusätzlich `owner` zum Besitzer aller dabei
|
||||
/// **neu angelegten** Verzeichnisse - nicht bereits vorhandener Elternverzeichnisse (z. B.
|
||||
/// `/run/media` selbst, das root-eigen bleiben muss). Läuft von `dir` aus rückwärts nach
|
||||
/// oben, bis der erste bereits existierende Vorfahre gefunden ist.
|
||||
fn create_dir_all_owned(dir: &std::path::Path, owner: Option<&str>) -> Result<()> {
|
||||
let Some(owner) = owner else {
|
||||
return std::fs::create_dir_all(dir).map_err(|e| Error::io(dir, e));
|
||||
};
|
||||
|
||||
let mut newly_created = Vec::new();
|
||||
let mut current = dir;
|
||||
while !current.exists() {
|
||||
newly_created.push(current.to_path_buf());
|
||||
match current.parent() {
|
||||
Some(parent) => current = parent,
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
|
||||
std::fs::create_dir_all(dir).map_err(|e| Error::io(dir, e))?;
|
||||
|
||||
// Von oben nach unten chownen (Eltern vor Kindern) - rein kosmetisch, jeder Aufruf ist
|
||||
// unabhängig, aber so bleibt die Reihenfolge nachvollziehbar.
|
||||
for path in newly_created.iter().rev() {
|
||||
let status = Command::new("chown")
|
||||
.arg(format!("{owner}:{owner}"))
|
||||
.arg(path)
|
||||
.status()
|
||||
.map_err(|e| Error::Other(format!("could not run chown: {e}")))?;
|
||||
if !status.success() {
|
||||
return Err(Error::Other(format!(
|
||||
"chown failed for '{}'",
|
||||
path.display()
|
||||
)));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ensure_group_membership(pair: &DrivePair) -> Result<()> {
|
||||
let Some(owner) = &pair.owner_user else {
|
||||
return Ok(());
|
||||
};
|
||||
if pair.local.kind == MountKind::WebDav || pair.cloud.kind == MountKind::WebDav {
|
||||
let status = Command::new("usermod")
|
||||
.args(["-aG", "davfs2", owner])
|
||||
.status()
|
||||
.map_err(|e| Error::Other(format!("could not run usermod: {e}")))?;
|
||||
if !status.success() {
|
||||
logger_ctdra::warn(
|
||||
"fstab",
|
||||
&format!(
|
||||
"Could not add '{owner}' to group 'davfs2' - does the group exist (package 'davfs2' installed)?"
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_managed_block(
|
||||
pairs: &[&DrivePair],
|
||||
settings: &GlobalSettings,
|
||||
owner_scope: &[String],
|
||||
) -> Result<()> {
|
||||
let fstab_path = PathBuf::from(FSTAB_PATH);
|
||||
let existing = std::fs::read_to_string(&fstab_path).unwrap_or_default();
|
||||
|
||||
backup(&fstab_path, &existing)?;
|
||||
|
||||
let without_block = crate::util::strip_managed_block(&existing, BEGIN_MARKER, END_MARKER);
|
||||
let current_block =
|
||||
crate::util::extract_managed_block(&existing, BEGIN_MARKER, END_MARKER).unwrap_or_default();
|
||||
let new_block = merge_managed_block(¤t_block, pairs, settings, owner_scope);
|
||||
|
||||
let new_contents = format!(
|
||||
"{}\n{}\n{}\n{}\n",
|
||||
without_block.trim_end(),
|
||||
BEGIN_MARKER,
|
||||
new_block.trim_end(),
|
||||
END_MARKER
|
||||
);
|
||||
write_atomic(&fstab_path, &new_contents)
|
||||
}
|
||||
|
||||
fn backup(fstab_path: &PathBuf, contents: &str) -> Result<()> {
|
||||
let backup_path = PathBuf::from(format!("{FSTAB_PATH}.smart-mount.bak"));
|
||||
std::fs::write(&backup_path, contents).map_err(|e| Error::io(&backup_path, e))?;
|
||||
let _ = fstab_path; // nur zur Doku der Herkunft von `contents`.
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Schreibt `contents` atomar (Temp-Datei im selben Verzeichnis + `rename`) statt per direktem
|
||||
/// Trunkieren-und-Schreiben - ein Absturz oder ein volles Dateisystem mitten im Schreiben
|
||||
/// könnte `/etc/fstab` sonst in einem leeren/halb geschriebenen Zustand zurücklassen, was den
|
||||
/// nächsten Boot verhindern kann.
|
||||
fn write_atomic(path: &Path, contents: &str) -> Result<()> {
|
||||
let tmp_path = PathBuf::from(format!("{}.smart-mount-tmp", path.display()));
|
||||
std::fs::write(&tmp_path, contents).map_err(|e| Error::io(&tmp_path, e))?;
|
||||
std::fs::rename(&tmp_path, path).map_err(|e| Error::io(path, e))
|
||||
}
|
||||
|
||||
/// Tag-Kommentar, der an jede von smart-mount geschriebene fstab-Zeile angehängt wird
|
||||
/// (`man 5 fstab`: ein `#` leitet einen bis zum Zeilenende reichenden Kommentar ein, auch nach
|
||||
/// den 6 regulären Feldern - das stört `mount(8)` nicht). Erlaubt, beim nächsten `setup fstab`
|
||||
/// zeilenweise zu erkennen, zu welchem Paar/welcher Seite/welchem Besitzer eine bestehende
|
||||
/// Zeile gehört, statt den kompletten Block bei jedem Lauf zu ersetzen (siehe
|
||||
/// [`merge_managed_block`]).
|
||||
fn line_tag(pair: &DrivePair, side: Side) -> String {
|
||||
format!(
|
||||
"smart-mount pair={} side={} owner={}",
|
||||
pair.id,
|
||||
side.as_str(),
|
||||
pair.owner_user.as_deref().unwrap_or("-")
|
||||
)
|
||||
}
|
||||
|
||||
fn tagged_line(pair: &DrivePair, side: Side, settings: &GlobalSettings) -> Result<String> {
|
||||
let line = fstab_line(pair, side, settings)?;
|
||||
Ok(format!("{line} # {}", line_tag(pair, side)))
|
||||
}
|
||||
|
||||
/// Liest `(pair_id, side, owner)` aus dem von [`line_tag`] angehängten Kommentar einer
|
||||
/// bestehenden fstab-Zeile, falls vorhanden.
|
||||
fn parse_tag(line: &str) -> Option<(String, &'static str, String)> {
|
||||
let marker = "# smart-mount ";
|
||||
let idx = line.find(marker)?;
|
||||
let rest = &line[idx + marker.len()..];
|
||||
|
||||
let mut pair_id = None;
|
||||
let mut side = None;
|
||||
let mut owner = None;
|
||||
for token in rest.split_whitespace() {
|
||||
if let Some(v) = token.strip_prefix("pair=") {
|
||||
pair_id = Some(v.to_string());
|
||||
} else if let Some(v) = token.strip_prefix("side=") {
|
||||
side = match v {
|
||||
"local" => Some("local"),
|
||||
"cloud" => Some("cloud"),
|
||||
_ => None,
|
||||
};
|
||||
} else if let Some(v) = token.strip_prefix("owner=") {
|
||||
owner = Some(v.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
Some((pair_id?, side?, owner.unwrap_or_else(|| "-".to_string())))
|
||||
}
|
||||
|
||||
/// Führt den bestehenden verwalteten Block mit den frisch berechneten Zeilen für `pairs`
|
||||
/// zusammen, statt ihn komplett zu ersetzen:
|
||||
/// - eine bestehende Zeile, die zu einem der aktuell verarbeiteten Paare gehört, wird durch die
|
||||
/// frische Version ersetzt (oder, falls deren Neuberechnung fehlschlägt, z. B. weil ein
|
||||
/// MAC-adressiertes lokales Gerät gerade offline ist, unverändert beibehalten statt
|
||||
/// ersatzlos gelöscht - siehe [`fstab_line`]/[`tagged_line`]);
|
||||
/// - eine Zeile eines inzwischen aus der Konfiguration entfernten Paares DESSELBEN Nutzers
|
||||
/// (`owner_scope`) wird entfernt;
|
||||
/// - jede andere Zeile (insbesondere die eines ANDEREN Nutzers) bleibt unangetastet.
|
||||
///
|
||||
/// Ohne diese Unterscheidung würde ein zweiter Nutzer, der `setup fstab` für sein eigenes Konto
|
||||
/// ausführt, versehentlich die vom ersten Nutzer installierten Zeilen löschen, da beide
|
||||
/// denselben verwalteten Block in `/etc/fstab` teilen. Ebenso würde ein einzelnes Paar, dessen
|
||||
/// Neuberechnung gerade fehlschlägt, sonst den gesamten Block-Rebuild für alle anderen,
|
||||
/// gesunden Paare verhindern.
|
||||
fn merge_managed_block(
|
||||
current_block: &str,
|
||||
pairs: &[&DrivePair],
|
||||
settings: &GlobalSettings,
|
||||
owner_scope: &[String],
|
||||
) -> String {
|
||||
let current_pair_ids: HashSet<&str> = pairs.iter().map(|p| p.id.as_str()).collect();
|
||||
|
||||
let mut new_lines = Vec::new();
|
||||
let mut replaced: HashSet<(String, &'static str)> = HashSet::new();
|
||||
for pair in pairs {
|
||||
for side in [Side::Local, Side::Cloud] {
|
||||
match tagged_line(pair, side, settings) {
|
||||
Ok(line) => {
|
||||
replaced.insert((pair.id.clone(), side.as_str()));
|
||||
new_lines.push(line);
|
||||
}
|
||||
Err(e) => {
|
||||
logger_ctdra::warn(
|
||||
"fstab",
|
||||
&format!(
|
||||
"could not compute fstab entry for pair '{}' ({}): {e} - leaving \
|
||||
any existing entry for it untouched",
|
||||
pair.id,
|
||||
side.as_str()
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut kept: Vec<String> = current_block
|
||||
.lines()
|
||||
.filter(|line| match parse_tag(line) {
|
||||
Some((pair_id, side, _)) if replaced.contains(&(pair_id.clone(), side)) => false,
|
||||
Some((pair_id, _, owner))
|
||||
if !current_pair_ids.contains(pair_id.as_str())
|
||||
&& owner_scope.iter().any(|o| o == &owner) =>
|
||||
{
|
||||
false
|
||||
}
|
||||
_ => true,
|
||||
})
|
||||
.map(str::to_string)
|
||||
.collect();
|
||||
|
||||
kept.extend(new_lines);
|
||||
kept.join("\n")
|
||||
}
|
||||
|
||||
fn fstab_line(pair: &DrivePair, side: Side, settings: &GlobalSettings) -> Result<String> {
|
||||
// Wiederverwendet dieselbe Options-Berechnung wie der tatsächliche Mount-Aufruf
|
||||
// (`mount::target::build_target`, inkl. `apply_owner_permissions`) - insbesondere die
|
||||
// dort injizierten uid=/gid= sind hier nicht optional: laut `man mount.davfs`
|
||||
// ("uid=user"/"gid=group") darf ein unprivilegierter Nutzer eine Zeile nur mounten, wenn
|
||||
// uid= auf ihn selbst zeigt und er Mitglied der in gid= genannten Gruppe ist. Ohne diese
|
||||
// Optionen dürfte JEDES Mitglied der Gruppe 'davfs2' JEDES konfigurierte Paar mounten,
|
||||
// nicht nur der vorgesehene Besitzer (`setup()` verweigert daher bereits vorab Paare ohne
|
||||
// `owner_user`).
|
||||
//
|
||||
// Für CIFS ist per `man mount.cifs` BESTÄTIGT, dass dieselbe Beschränkung NICHT existiert:
|
||||
// uid=/gid= betreffen dort ausschließlich die simulierte Datei-Ownership nach dem Mount,
|
||||
// nicht das Mount-*Recht* selbst - mount(8)/mount.cifs bieten keinen Mechanismus, eine
|
||||
// 'user'-fstab-Zeile auf eine bestimmte Person einzuschränken. Für CIFS-Nutzer-Kontext-
|
||||
// Paare bleibt das eine bewusst akzeptierte, strukturelle Lücke (siehe README) statt einer
|
||||
// über Mount-Optionen behebbaren - die Optionen werden trotzdem gesetzt, da korrekte
|
||||
// Ownership unabhängig davon nötig ist.
|
||||
let target = target::build_target(pair, settings, side)?;
|
||||
let kind = target::side_kind(pair, side);
|
||||
|
||||
let (fstype, mut extra_opts) = match kind {
|
||||
MountKind::WebDav => ("davfs", String::new()),
|
||||
MountKind::Smb => {
|
||||
let creds = smb::credentials_path(&pair.id, side);
|
||||
("cifs", format!(",credentials={}", creds.display()))
|
||||
}
|
||||
MountKind::Nfs => ("nfs", String::new()),
|
||||
};
|
||||
|
||||
for opt in &target.options {
|
||||
extra_opts.push_str(&format!(",{opt}"));
|
||||
}
|
||||
|
||||
// Jede Seite bekommt ihr eigenes, eindeutiges Backing-Verzeichnis als Ziel - siehe
|
||||
// Moduldoku. `pair.mount_point` selbst taucht bewusst NICHT in fstab auf.
|
||||
//
|
||||
// WICHTIG: die `user`-Option impliziert laut `man 8 mount` ("Non-superuser mounts") für
|
||||
// JEDES Dateisystem `noexec,nosuid,nodev`, sofern nicht direkt im selben Optionslisten-
|
||||
// Eintrag überschrieben. Ohne das explizite `exec` hier könnten auf einem User-Kontext-
|
||||
// Laufwerk liegende Skripte NICHT ausgeführt werden. `nosuid`/`nodev` bleiben bewusst
|
||||
// implizit (sinnvolle Absicherung, dafür gab es keine Anforderung).
|
||||
Ok(format!(
|
||||
"{source} {mount_point} {fstype} user,exec,noauto{extra_opts} 0 0",
|
||||
source = target.source,
|
||||
mount_point = target.mount_point.display()
|
||||
))
|
||||
}
|
||||
|
||||
fn user_config_path(username: &str) -> PathBuf {
|
||||
let program_name = config_ctdra::get_program_name();
|
||||
let config_name = config_ctdra::get_config_name();
|
||||
let file_name = if config_name.ends_with(".toml") {
|
||||
config_name
|
||||
} else {
|
||||
format!("{config_name}.toml")
|
||||
};
|
||||
user_home_dir(username)
|
||||
.map(|h| h.join(".config").join(&program_name).join(&file_name))
|
||||
.unwrap_or_else(|| {
|
||||
PathBuf::from(format!(
|
||||
"/home/{username}/.config/{program_name}/{file_name}"
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
fn user_home_dir(username: &str) -> Option<PathBuf> {
|
||||
if let Ok(output) = Command::new("getent").args(["passwd", username]).output()
|
||||
&& output.status.success()
|
||||
{
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let fields: Vec<&str> = stdout.trim().split(':').collect();
|
||||
if fields.len() >= 6 && !fields[5].is_empty() {
|
||||
return Some(PathBuf::from(fields[5]));
|
||||
}
|
||||
}
|
||||
if let Ok(passwd) = std::fs::read_to_string("/etc/passwd") {
|
||||
for line in passwd.lines() {
|
||||
let fields: Vec<&str> = line.split(':').collect();
|
||||
if fields.len() >= 6 && fields[0] == username && !fields[5].is_empty() {
|
||||
return Some(PathBuf::from(fields[5]));
|
||||
}
|
||||
}
|
||||
}
|
||||
// Weder `getent` noch `/etc/passwd` konnten den Nutzer auflösen - das reine Erraten von
|
||||
// `/home/<user>` kann bei einem abweichenden Home-Verzeichnis (oder falsch geschriebenem
|
||||
// Nutzernamen) dazu führen, dass `setup()` anschließend die Konfigurationsdatei am
|
||||
// falschen Pfad lädt und stillschweigend "nichts zu tun" meldet. Warnen statt schweigen.
|
||||
logger_ctdra::warn(
|
||||
"fstab",
|
||||
&format!(
|
||||
"could not resolve home directory for user '{username}' via getent/passwd - \
|
||||
guessing '/home/{username}'"
|
||||
),
|
||||
);
|
||||
Some(PathBuf::from(format!("/home/{username}")))
|
||||
}
|
||||
|
||||
/// Zeigt an, dass diese Konfiguration bereits ein einmaliges `setup fstab` benötigt hat.
|
||||
pub fn requires_setup(cfg: &AppConfig) -> bool {
|
||||
cfg.pairs.iter().any(|p| p.context == MountContext::User)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::net::Ipv4Addr;
|
||||
|
||||
fn sample_pair() -> DrivePair {
|
||||
// Nutzt den tatsächlich ausführenden Testnutzer statt eines hartkodierten Namens,
|
||||
// da `fstab_line` jetzt `id -u`/`id -g` für `owner_user` aufruft (siehe
|
||||
// `mount::target::apply_owner_permissions`) - ein fester Name wäre auf anderen
|
||||
// Maschinen/CI nicht garantiert vorhanden.
|
||||
let user = std::env::var("USER").expect("USER env var set in test environment");
|
||||
DrivePair {
|
||||
id: "pair-1".into(),
|
||||
name: "Test".into(),
|
||||
enabled: true,
|
||||
context: MountContext::User,
|
||||
owner_user: Some(user.clone()),
|
||||
mount_point: "/home/dragon/smart-mount/pair-1".into(),
|
||||
local: crate::config::LocalSide {
|
||||
kind: MountKind::Smb,
|
||||
address: crate::config::LocalAddress::Ip(Ipv4Addr::new(192, 168, 1, 5)),
|
||||
share: "share".into(),
|
||||
username: Some("nasuser".into()),
|
||||
extra_options: vec![],
|
||||
},
|
||||
cloud: crate::config::CloudSide {
|
||||
kind: MountKind::Smb,
|
||||
host_or_url: "cloud.example.com".into(),
|
||||
share: "share".into(),
|
||||
username: Some(user),
|
||||
extra_options: vec![],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_user_pairs_have_owner_rejects_missing_owner() {
|
||||
let mut pair = sample_pair();
|
||||
pair.owner_user = None;
|
||||
let err = validate_user_pairs_have_owner(&[&pair]).unwrap_err();
|
||||
assert!(err.to_string().contains("owner_user"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_user_pairs_have_owner_accepts_pair_with_owner() {
|
||||
let pair = sample_pair();
|
||||
assert!(validate_user_pairs_have_owner(&[&pair]).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn renders_two_lines_per_pair_each_with_its_own_unique_target() {
|
||||
let pair = sample_pair();
|
||||
let block = merge_managed_block("", &[&pair], &GlobalSettings::default(), &[]);
|
||||
let lines: Vec<&str> = block.lines().collect();
|
||||
|
||||
assert_eq!(lines.len(), 2);
|
||||
assert!(lines[0].contains("user,exec,noauto"));
|
||||
assert!(lines[0].contains("uid="));
|
||||
assert!(lines[0].contains("gid="));
|
||||
|
||||
// Der sichtbare pair.mount_point selbst darf in KEINER Zeile als Ziel auftauchen -
|
||||
// das ist der Symlink, den smart-mount zur Laufzeit umschaltet, kein fstab-Ziel.
|
||||
let visible = pair.mount_point.display().to_string();
|
||||
let target_tokens: Vec<&str> = [lines[0], lines[1]]
|
||||
.iter()
|
||||
.map(|l| l.split_whitespace().nth(1).unwrap())
|
||||
.collect();
|
||||
assert!(!target_tokens.contains(&visible.as_str()));
|
||||
|
||||
// Jede Zeile hat ein eigenes, eindeutiges Ziel (Backing-Verzeichnis) - keine zwei
|
||||
// Zeilen mit demselben Mountpoint, auf dessen Disambiguierung sich mount(8) laut
|
||||
// `man 8 mount` nicht verlassen ließe.
|
||||
let target_of = |line: &str| line.split_whitespace().nth(1).unwrap().to_string();
|
||||
assert_ne!(target_of(lines[0]), target_of(lines[1]));
|
||||
assert_eq!(
|
||||
target_of(lines[0]),
|
||||
backing_dir(&pair, Side::Local).display().to_string()
|
||||
);
|
||||
assert_eq!(
|
||||
target_of(lines[1]),
|
||||
backing_dir(&pair, Side::Cloud).display().to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_managed_block_preserves_lines_belonging_to_other_users() {
|
||||
let pair = sample_pair();
|
||||
let other_users_line = "//other/share /backing/other cifs user,exec,noauto 0 0 \
|
||||
# smart-mount pair=other-pair side=local owner=someone-else";
|
||||
let block = merge_managed_block(
|
||||
other_users_line,
|
||||
&[&pair],
|
||||
&GlobalSettings::default(),
|
||||
&[pair.owner_user.clone().unwrap()],
|
||||
);
|
||||
|
||||
assert!(
|
||||
block.contains(other_users_line),
|
||||
"a run scoped to one user must not touch another user's fstab lines"
|
||||
);
|
||||
assert!(block.contains("pair=pair-1"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_managed_block_drops_stale_lines_for_a_removed_pair_of_the_same_owner() {
|
||||
let pair = sample_pair();
|
||||
let owner = pair.owner_user.clone().unwrap();
|
||||
let stale_line = format!(
|
||||
"//old/share /backing/old cifs user,exec,noauto 0 0 \
|
||||
# smart-mount pair=deleted-pair side=local owner={owner}"
|
||||
);
|
||||
// `deleted-pair` is no longer part of `pairs`, so its line should be dropped since it
|
||||
// belongs to the same owner this run is scoped to - but only then.
|
||||
let block =
|
||||
merge_managed_block(&stale_line, &[&pair], &GlobalSettings::default(), &[owner]);
|
||||
|
||||
assert!(!block.contains("deleted-pair"));
|
||||
assert!(block.contains("pair=pair-1"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_managed_block_keeps_stale_lines_of_a_different_owner() {
|
||||
let pair = sample_pair();
|
||||
let stale_line = "//old/share /backing/old cifs user,exec,noauto 0 0 \
|
||||
# smart-mount pair=deleted-pair side=local owner=someone-else";
|
||||
// `owner_scope` only covers `pair.owner_user`, not `someone-else` - the stale line must
|
||||
// survive even though its pair is absent from `pairs`.
|
||||
let block = merge_managed_block(
|
||||
stale_line,
|
||||
&[&pair],
|
||||
&GlobalSettings::default(),
|
||||
&[pair.owner_user.clone().unwrap()],
|
||||
);
|
||||
|
||||
assert!(block.contains("deleted-pair"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strip_managed_block_removes_only_the_marked_section() {
|
||||
let contents = "/dev/sda1 / ext4 defaults 0 1\n# BEGIN smart-mount managed block\nfoo\n# END smart-mount managed block\n";
|
||||
let stripped = crate::util::strip_managed_block(contents, BEGIN_MARKER, END_MARKER);
|
||||
assert!(stripped.contains("/dev/sda1"));
|
||||
assert!(!stripped.contains("foo"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_dir_all_owned_creates_multi_level_path_and_chowns_new_dirs() {
|
||||
// `chown` zu einem ANDEREN Nutzer bräuchte Root - hier wird bewusst auf den eigenen
|
||||
// Nutzer "umgechownt" (funktioniert unprivilegiert, ist ein No-op auf die tatsächliche
|
||||
// Ownership, prüft aber, dass der `chown`-Aufruf pro neu angelegtem Verzeichnis
|
||||
// fehlerfrei durchläuft und die Verzeichnisstruktur korrekt entsteht).
|
||||
let user = std::env::var("USER").expect("USER env var set in test environment");
|
||||
let base = tempfile::tempdir().expect("tempdir");
|
||||
let target = base.path().join("a").join("b").join("c");
|
||||
|
||||
create_dir_all_owned(&target, Some(&user)).expect("create_dir_all_owned");
|
||||
|
||||
assert!(target.is_dir());
|
||||
assert!(base.path().join("a").is_dir());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_dir_all_owned_does_not_touch_already_existing_ancestors() {
|
||||
let user = std::env::var("USER").expect("USER env var set in test environment");
|
||||
let base = tempfile::tempdir().expect("tempdir");
|
||||
let target = base.path().join("existing").join("new-child");
|
||||
std::fs::create_dir_all(base.path().join("existing")).expect("pre-create ancestor");
|
||||
|
||||
// Darf nicht versuchen, `base.path()` selbst zu chownen (das existierte schon vorher) -
|
||||
// nur `existing/new-child`. Schlägt fehl, falls die Funktion stattdessen versucht,
|
||||
// einen nicht existierenden Nutzer für einen bereits vorhandenen Ordner zu setzen o. Ä.
|
||||
create_dir_all_owned(&target, Some(&user)).expect("create_dir_all_owned");
|
||||
assert!(target.is_dir());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_dir_all_owned_without_owner_just_creates_directories() {
|
||||
let base = tempfile::tempdir().expect("tempdir");
|
||||
let target = base.path().join("x").join("y");
|
||||
create_dir_all_owned(&target, None).expect("create_dir_all_owned");
|
||||
assert!(target.is_dir());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn user_config_path_resolves_for_user() {
|
||||
let user = std::env::var("USER").expect("USER env var set in test environment");
|
||||
let path = user_config_path(&user);
|
||||
let s = path.to_str().unwrap();
|
||||
assert!(s.contains(&format!("/home/{user}/.config/")));
|
||||
assert!(s.ends_with("/config.toml"));
|
||||
}
|
||||
}
|
||||
@@ -6,11 +6,9 @@ pub mod crypto;
|
||||
pub mod db;
|
||||
pub mod doctor;
|
||||
pub mod error;
|
||||
pub mod fstab;
|
||||
pub mod mount;
|
||||
pub mod network;
|
||||
pub mod reconcile;
|
||||
pub mod systemd;
|
||||
pub(crate) mod util;
|
||||
|
||||
pub use error::{Error, Result};
|
||||
|
||||
+6
-19
@@ -18,21 +18,9 @@ use crate::config::{DrivePair, GlobalSettings, MountKind};
|
||||
use crate::db::credentials::{Credential, Side};
|
||||
use crate::error::Result;
|
||||
|
||||
/// Wie ein Mount-Aufruf ausgeführt wird.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum MountInvocation {
|
||||
/// Root, volle `-o`-Optionen: `mount -t <type> <source> <target> -o <opts>`.
|
||||
Direct,
|
||||
/// Unprivilegiert über eine passende `user,noauto`-fstab-Zeile: `mount <target>` (nur das
|
||||
/// Ziel, genau der in `man 8 mount` dokumentierte Fall `mount /cd`). Da jede Seite ihr
|
||||
/// eigenes, eindeutiges Backing-Verzeichnis hat (siehe [`target::backing_dir`]), gibt es
|
||||
/// dabei nie mehr als eine passende fstab-Zeile. Voraussetzung: `smart-mount setup fstab`
|
||||
/// wurde für dieses Paar bereits ausgeführt.
|
||||
ViaFstab,
|
||||
}
|
||||
|
||||
/// Alle Informationen, die ein Backend braucht, um eine Seite eines Laufwerkspaars ein-
|
||||
/// bzw. auszuhängen.
|
||||
/// bzw. auszuhängen. Der Mount-Aufruf ist immer `mount -t <type> <source> <target> -o <opts>`
|
||||
/// als root (smart-mount läuft ausschließlich als root/System-Dienst, siehe [`crate::systemd`]).
|
||||
pub struct MountTarget {
|
||||
pub pair_id: String,
|
||||
/// Welche Seite des Paars (lokal/cloud) dieses Ziel betrifft - bestimmt u. a. stabile
|
||||
@@ -41,12 +29,11 @@ pub struct MountTarget {
|
||||
pub mount_point: PathBuf,
|
||||
/// Vollständiger Quellstring, z. B. `//server/share`, `https://host/path`, `server:/export`.
|
||||
pub source: String,
|
||||
/// `-o`-Optionen als rohe Tokens (`"uid=1000"` oder bloße Flags wie `"soft"`), nur bei
|
||||
/// `MountInvocation::Direct` verwendet - mit Kommas verbindbar für `mount -o`.
|
||||
/// `-o`-Optionen als rohe Tokens (`"uid=1000"` oder bloße Flags wie `"soft"`) - mit Kommas
|
||||
/// verbindbar für `mount -o`.
|
||||
pub options: Vec<String>,
|
||||
pub invocation: MountInvocation,
|
||||
/// Für `MountContext::User`-Paare: der Linux-Benutzername, dem Zugangsdaten-/Secrets-
|
||||
/// Dateien gehören sollen.
|
||||
/// Für Paare mit gesetztem `owner_user`: der Linux-Benutzername, dem Zugangsdaten-/
|
||||
/// Secrets-Dateien gehören sollen.
|
||||
pub owner_user: Option<String>,
|
||||
}
|
||||
|
||||
|
||||
+1
-10
@@ -5,9 +5,7 @@ use std::process::Command;
|
||||
|
||||
use crate::db::credentials::Credential;
|
||||
use crate::error::{Error, Result};
|
||||
use crate::mount::{
|
||||
MountBackend, MountInvocation, MountTarget, binary_available, run_tolerating_already_done,
|
||||
};
|
||||
use crate::mount::{MountBackend, MountTarget, binary_available, run_tolerating_already_done};
|
||||
|
||||
pub struct NfsBackend;
|
||||
|
||||
@@ -34,8 +32,6 @@ impl MountBackend for NfsBackend {
|
||||
|
||||
fn mount(&self, target: &MountTarget) -> Result<()> {
|
||||
let mut cmd = Command::new("mount");
|
||||
match target.invocation {
|
||||
MountInvocation::Direct => {
|
||||
cmd.arg("-t")
|
||||
.arg("nfs")
|
||||
.arg(&target.source)
|
||||
@@ -43,11 +39,6 @@ impl MountBackend for NfsBackend {
|
||||
if !target.options.is_empty() {
|
||||
cmd.arg("-o").arg(target.options.join(","));
|
||||
}
|
||||
}
|
||||
MountInvocation::ViaFstab => {
|
||||
cmd.arg(&target.mount_point);
|
||||
}
|
||||
}
|
||||
run_tolerating_already_done(cmd, "nfs mount", false)
|
||||
}
|
||||
|
||||
|
||||
+3
-12
@@ -7,9 +7,7 @@ use std::process::Command;
|
||||
|
||||
use crate::db::credentials::Credential;
|
||||
use crate::error::{Error, Result};
|
||||
use crate::mount::{
|
||||
MountBackend, MountInvocation, MountTarget, binary_available, run_tolerating_already_done,
|
||||
};
|
||||
use crate::mount::{MountBackend, MountTarget, binary_available, run_tolerating_already_done};
|
||||
|
||||
pub struct SmbBackend;
|
||||
|
||||
@@ -38,8 +36,6 @@ impl MountBackend for SmbBackend {
|
||||
|
||||
fn mount(&self, target: &MountTarget) -> Result<()> {
|
||||
let mut cmd = Command::new("mount");
|
||||
match target.invocation {
|
||||
MountInvocation::Direct => {
|
||||
cmd.arg("-t")
|
||||
.arg("cifs")
|
||||
.arg(&target.source)
|
||||
@@ -50,11 +46,6 @@ impl MountBackend for SmbBackend {
|
||||
credentials_path(&target.pair_id, target.side).display()
|
||||
));
|
||||
cmd.arg("-o").arg(opts.join(","));
|
||||
}
|
||||
MountInvocation::ViaFstab => {
|
||||
cmd.arg(&target.mount_point);
|
||||
}
|
||||
}
|
||||
run_tolerating_already_done(cmd, "cifs mount", false)
|
||||
}
|
||||
|
||||
@@ -65,8 +56,8 @@ impl MountBackend for SmbBackend {
|
||||
}
|
||||
}
|
||||
|
||||
/// Stabiler Pfad (nicht ein Tempfile!), da bei `MountInvocation::ViaFstab` die fstab-Zeile
|
||||
/// (von `setup fstab` einmalig geschrieben) exakt auf diesen `credentials=`-Pfad verweist.
|
||||
/// Stabiler Pfad (nicht ein Tempfile!) - `prepare()` schreibt hierhin, `mount()` referenziert
|
||||
/// denselben Pfad über `-o credentials=...`.
|
||||
pub fn credentials_path(pair_id: &str, side: crate::db::credentials::Side) -> PathBuf {
|
||||
let base = config_ctdra::get_config_path()
|
||||
.parent()
|
||||
|
||||
+2
-8
@@ -17,10 +17,10 @@
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use crate::config::{CloudSide, DrivePair, GlobalSettings, LocalSide, MountContext, MountKind};
|
||||
use crate::config::{CloudSide, DrivePair, GlobalSettings, LocalSide, MountKind};
|
||||
use crate::db::credentials::Side;
|
||||
use crate::error::{Error, Result};
|
||||
use crate::mount::{self, MountInvocation, MountTarget};
|
||||
use crate::mount::{self, MountTarget};
|
||||
use crate::network::address;
|
||||
|
||||
/// Eindeutiges Backing-Verzeichnis für eine Seite eines Paars - hier (und nur hier) wird
|
||||
@@ -60,10 +60,6 @@ pub fn build_target_with_cached_local_ip(
|
||||
side: Side,
|
||||
cached_local_ip: Option<std::net::Ipv4Addr>,
|
||||
) -> Result<MountTarget> {
|
||||
let invocation = match pair.context {
|
||||
MountContext::System => MountInvocation::Direct,
|
||||
MountContext::User => MountInvocation::ViaFstab,
|
||||
};
|
||||
let kind = side_kind(pair, side);
|
||||
|
||||
let (source, mut options) = match side {
|
||||
@@ -88,7 +84,6 @@ pub fn build_target_with_cached_local_ip(
|
||||
mount_point: backing_dir(pair, side),
|
||||
source,
|
||||
options,
|
||||
invocation,
|
||||
owner_user: pair.owner_user.clone(),
|
||||
})
|
||||
}
|
||||
@@ -296,7 +291,6 @@ mod tests {
|
||||
id: "pair-1".into(),
|
||||
name: "Test".into(),
|
||||
enabled: true,
|
||||
context: MountContext::System,
|
||||
owner_user: None,
|
||||
mount_point: "/media/smart-mount/pair-1".into(),
|
||||
local: LocalSide {
|
||||
|
||||
+3
-24
@@ -7,9 +7,7 @@ use std::process::Command;
|
||||
|
||||
use crate::db::credentials::Credential;
|
||||
use crate::error::{Error, Result};
|
||||
use crate::mount::{
|
||||
MountBackend, MountInvocation, MountTarget, binary_available, run_tolerating_already_done,
|
||||
};
|
||||
use crate::mount::{MountBackend, MountTarget, binary_available, run_tolerating_already_done};
|
||||
|
||||
pub struct WebDavBackend;
|
||||
|
||||
@@ -60,8 +58,6 @@ impl MountBackend for WebDavBackend {
|
||||
|
||||
fn mount(&self, target: &MountTarget) -> Result<()> {
|
||||
let mut cmd = Command::new("mount");
|
||||
match target.invocation {
|
||||
MountInvocation::Direct => {
|
||||
cmd.arg("-t")
|
||||
.arg("davfs")
|
||||
.arg(&target.source)
|
||||
@@ -69,11 +65,6 @@ impl MountBackend for WebDavBackend {
|
||||
if !target.options.is_empty() {
|
||||
cmd.arg("-o").arg(target.options.join(","));
|
||||
}
|
||||
}
|
||||
MountInvocation::ViaFstab => {
|
||||
cmd.arg(&target.mount_point);
|
||||
}
|
||||
}
|
||||
run_tolerating_already_done(cmd, "davfs2 mount", false)
|
||||
}
|
||||
|
||||
@@ -84,26 +75,14 @@ impl MountBackend for WebDavBackend {
|
||||
}
|
||||
}
|
||||
|
||||
/// Mounten läuft ausschließlich als root/System-Dienst (siehe [`crate::systemd`]), daher immer
|
||||
/// der System-Pfad - kein Nutzerkontext-Zweig mehr nötig.
|
||||
fn davfs2_conf_path() -> PathBuf {
|
||||
if sudo_ctdra::is_run_as_root() {
|
||||
PathBuf::from("/etc/davfs2/davfs2.conf")
|
||||
} else {
|
||||
home_dir().join(".davfs2/davfs2.conf")
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn davfs2_secrets_path() -> PathBuf {
|
||||
if sudo_ctdra::is_run_as_root() {
|
||||
PathBuf::from("/etc/davfs2/secrets")
|
||||
} else {
|
||||
home_dir().join(".davfs2/secrets")
|
||||
}
|
||||
}
|
||||
|
||||
fn home_dir() -> PathBuf {
|
||||
std::env::var_os("HOME")
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|| PathBuf::from("."))
|
||||
}
|
||||
|
||||
/// Setzt `gui_optimize 1` in `davfs2.conf` idempotent - reduziert bei grafischen
|
||||
|
||||
@@ -354,10 +354,6 @@ async fn unmount_side(pair: &DrivePair, settings: &GlobalSettings, side: Side) -
|
||||
source: String::new(),
|
||||
mount_point: target::backing_dir(pair, side),
|
||||
options: vec![],
|
||||
invocation: match pair.context {
|
||||
crate::config::MountContext::System => crate::mount::MountInvocation::Direct,
|
||||
crate::config::MountContext::User => crate::mount::MountInvocation::ViaFstab,
|
||||
},
|
||||
owner_user: pair.owner_user.clone(),
|
||||
}
|
||||
});
|
||||
@@ -367,7 +363,7 @@ async fn unmount_side(pair: &DrivePair, settings: &GlobalSettings, side: Side) -
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::config::{CloudSide, MountContext, MountKind};
|
||||
use crate::config::{CloudSide, MountKind};
|
||||
use std::net::Ipv4Addr;
|
||||
|
||||
fn sample_pair(local_kind: MountKind, cloud_kind: MountKind) -> DrivePair {
|
||||
@@ -375,7 +371,6 @@ mod tests {
|
||||
id: "pair-1".into(),
|
||||
name: "Test".into(),
|
||||
enabled: true,
|
||||
context: MountContext::System,
|
||||
owner_user: None,
|
||||
mount_point: "/media/smart-mount/pair-1".into(),
|
||||
local: LocalSide {
|
||||
|
||||
+35
-294
@@ -1,30 +1,23 @@
|
||||
//! Generiert/installiert systemd-Units (System- und User-Kontext) sowie das Crontab-Äquivalent
|
||||
//! für Systeme ohne systemd.
|
||||
//! Cron-Fallback (`smart-mount service crontab`) für Systeme ohne (oder ohne genutzten)
|
||||
//! systemd.
|
||||
//!
|
||||
//! Die systemd-Units rufen ausschließlich einfache `smart-mount`-Subcommands auf, damit
|
||||
//! dieselben Zeilen 1:1 als Crontab-Einträge funktionieren.
|
||||
//! Die eigentliche systemd-Einrichtung passiert NICHT mehr zur Laufzeit über dieses Modul,
|
||||
//! sondern über die paketierten, statischen Unit-Dateien (siehe `packaging/systemd/` im
|
||||
//! Quellbaum) sowie die postinst/postrm-Skripte der .deb/.rpm/.pkg.tar.zst-Pakete - smart-mount
|
||||
//! richtet sich beim Installieren des Pakets automatisch als System-systemd-Dienst ein und
|
||||
//! entfernt sich beim Deinstallieren wieder. Dieses Modul bleibt für Systeme ohne systemd (oder
|
||||
//! zum bewussten Umgehen von systemd) als manueller Cron-Weg bestehen - immer systemweit
|
||||
//! (`/etc/cron.d/smart-mount`), da Mounten ohnehin immer Root-Rechte braucht (siehe
|
||||
//! [`crate::cli::require_root`]); eine persönliche Nutzer-Crontab liefe ins Leere.
|
||||
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::{Command, Stdio};
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
|
||||
const MOUNT_SERVICE: &str = "smart-mount-mount.service";
|
||||
const WATCH_SERVICE: &str = "smart-mount-watch.service";
|
||||
const WATCH_TIMER: &str = "smart-mount-watch.timer";
|
||||
|
||||
const CRON_D_PATH: &str = "/etc/cron.d/smart-mount";
|
||||
const CRON_BEGIN_MARKER: &str = "# BEGIN smart-mount managed block";
|
||||
const CRON_END_MARKER: &str = "# END smart-mount managed block";
|
||||
|
||||
/// System (root) oder User-Kontext für die Unit-Installation.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Scope {
|
||||
System,
|
||||
User,
|
||||
}
|
||||
|
||||
fn binary_path() -> String {
|
||||
std::env::current_exe()
|
||||
.ok()
|
||||
@@ -32,141 +25,15 @@ fn binary_path() -> String {
|
||||
.unwrap_or_else(|| "/usr/bin/smart-mount".to_string())
|
||||
}
|
||||
|
||||
fn mount_service_unit(scope: Scope) -> String {
|
||||
let wanted_by = match scope {
|
||||
Scope::System => "multi-user.target",
|
||||
Scope::User => "default.target",
|
||||
};
|
||||
// `network-online.target` ist ein Ziel des System-Managers - unter `systemctl --user` gibt
|
||||
// es dafür keine sinnvolle Entsprechung (die Unit existiert dort nicht bzw. wird nie
|
||||
// erreicht), die Ordnungsabhängigkeit wäre also für User-Scope-Units wirkungslos statt
|
||||
// schlicht harmlos. Nur im System-Kontext gesetzt.
|
||||
let network_wait = match scope {
|
||||
Scope::System => "After=network-online.target\nWants=network-online.target\n",
|
||||
Scope::User => "",
|
||||
};
|
||||
format!(
|
||||
"[Unit]\nDescription=smart-mount: mount configured drive pairs at boot\n{network_wait}\n[Service]\nType=oneshot\nExecStart={} mount --all\n\n[Install]\nWantedBy={wanted_by}\n",
|
||||
binary_path()
|
||||
)
|
||||
}
|
||||
|
||||
fn watch_service_unit() -> String {
|
||||
format!(
|
||||
"[Unit]\nDescription=smart-mount: check reachability and switch local/cloud if needed\n\n[Service]\nType=oneshot\nExecStart={} watch\n",
|
||||
binary_path()
|
||||
)
|
||||
}
|
||||
|
||||
fn watch_timer_unit(interval_secs: u64) -> String {
|
||||
format!(
|
||||
"[Unit]\nDescription=smart-mount: periodic reconciling\n\n[Timer]\nOnBootSec=1min\nOnUnitActiveSec={interval_secs}s\nPersistent=true\nUnit={WATCH_SERVICE}\n\n[Install]\nWantedBy=timers.target\n"
|
||||
)
|
||||
}
|
||||
|
||||
fn unit_dir(scope: Scope) -> Result<PathBuf> {
|
||||
match scope {
|
||||
Scope::System => {
|
||||
if !sudo_ctdra::is_run_as_root() {
|
||||
return Err(Error::RequiresRoot("service install --system"));
|
||||
}
|
||||
Ok(PathBuf::from("/etc/systemd/system"))
|
||||
}
|
||||
Scope::User => {
|
||||
let home = std::env::var_os("HOME")
|
||||
.map(PathBuf::from)
|
||||
.ok_or(Error::Other("HOME not set".to_string()))?;
|
||||
Ok(home.join(".config/systemd/user"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn systemctl(scope: Scope, args: &[&str]) -> Result<()> {
|
||||
let mut cmd = Command::new("systemctl");
|
||||
if scope == Scope::User {
|
||||
cmd.arg("--user");
|
||||
}
|
||||
cmd.args(args);
|
||||
let output = cmd
|
||||
.output()
|
||||
.map_err(|e| Error::Other(format!("could not run systemctl: {e}")))?;
|
||||
if !output.status.success() {
|
||||
return Err(Error::Other(format!(
|
||||
"systemctl {:?} failed: {}",
|
||||
args,
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Schreibt die Unit-Dateien, lädt systemd neu und aktiviert Mount- und Watch-Timer-Unit.
|
||||
pub fn install(scope: Scope, watch_interval_secs: u64) -> Result<()> {
|
||||
let dir = unit_dir(scope)?;
|
||||
std::fs::create_dir_all(&dir).map_err(|e| Error::io(&dir, e))?;
|
||||
|
||||
std::fs::write(dir.join(MOUNT_SERVICE), mount_service_unit(scope))
|
||||
.map_err(|e| Error::io(&dir, e))?;
|
||||
std::fs::write(dir.join(WATCH_SERVICE), watch_service_unit())
|
||||
.map_err(|e| Error::io(&dir, e))?;
|
||||
std::fs::write(dir.join(WATCH_TIMER), watch_timer_unit(watch_interval_secs))
|
||||
.map_err(|e| Error::io(&dir, e))?;
|
||||
|
||||
systemctl(scope, &["daemon-reload"])?;
|
||||
systemctl(scope, &["enable", "--now", MOUNT_SERVICE, WATCH_TIMER])?;
|
||||
|
||||
if scope == Scope::User {
|
||||
logger_ctdra::info(
|
||||
"systemd",
|
||||
"For boot-time operation without an active login session: run 'loginctl enable-linger <user>'. \
|
||||
Note: in that case the OS keyring may not yet be available at boot - \
|
||||
smart-mount then automatically falls back to the key file.",
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Ob `systemctl` auf diesem System überhaupt vorhanden ist - Voraussetzung, bevor
|
||||
/// [`install`]/[`uninstall`] sinnvoll aufgerufen werden können.
|
||||
/// Ob `systemctl` auf diesem System vorhanden ist - rein informativ für `doctor`; die
|
||||
/// eigentliche systemd-Einrichtung läuft über die Paketierung (siehe Moduldoku).
|
||||
pub fn is_available() -> bool {
|
||||
crate::mount::binary_available("systemctl")
|
||||
}
|
||||
|
||||
/// Ergebnis von [`uninstall`].
|
||||
pub enum SystemdUninstallOutcome {
|
||||
/// Mindestens eine Unit-Datei war vorhanden und wurde entfernt.
|
||||
Removed,
|
||||
/// Keine der Unit-Dateien war vorhanden - nichts zu tun.
|
||||
NotPresent,
|
||||
}
|
||||
|
||||
/// Deaktiviert und entfernt die Unit-Dateien, falls vorhanden.
|
||||
pub fn uninstall(scope: Scope) -> Result<SystemdUninstallOutcome> {
|
||||
let dir = unit_dir(scope)?;
|
||||
let units = [MOUNT_SERVICE, WATCH_SERVICE, WATCH_TIMER];
|
||||
|
||||
if !units.iter().any(|unit| dir.join(unit).exists()) {
|
||||
return Ok(SystemdUninstallOutcome::NotPresent);
|
||||
}
|
||||
|
||||
let _ = systemctl(scope, &["disable", "--now", MOUNT_SERVICE, WATCH_TIMER]);
|
||||
|
||||
for unit in units {
|
||||
let path = dir.join(unit);
|
||||
if path.exists() {
|
||||
std::fs::remove_file(&path).map_err(|e| Error::io(&path, e))?;
|
||||
}
|
||||
}
|
||||
|
||||
systemctl(scope, &["daemon-reload"])?;
|
||||
Ok(SystemdUninstallOutcome::Removed)
|
||||
}
|
||||
|
||||
/// Erzeugt die Crontab-Äquivalente zu den generierten Units, für Systeme ohne systemd.
|
||||
/// `watch_interval_secs` ist derselbe Wert wie `settings.watch_interval_secs`, der auch die
|
||||
/// `OnUnitActiveSec`-Periode des systemd-Timers steuert - beide Wege sollen dieselbe Kadenz
|
||||
/// ergeben, statt dass die Crontab-Variante einen unabhängigen, fest eingebauten Wert hat.
|
||||
/// Erzeugt die Crontab-Äquivalente zu den paketierten systemd-Units, für Systeme ohne
|
||||
/// (genutzten) systemd. `watch_interval_secs` ist derselbe Wert wie
|
||||
/// `settings.watch_interval_secs`.
|
||||
pub fn crontab_equivalent(watch_interval_secs: u64) -> String {
|
||||
let bin = binary_path();
|
||||
let schedule = cron_schedule_for_interval(watch_interval_secs);
|
||||
@@ -177,50 +44,22 @@ pub fn crontab_equivalent(watch_interval_secs: u64) -> String {
|
||||
pub enum CronInstallOutcome {
|
||||
/// Systemweiter Eintrag geschrieben (`/etc/cron.d/smart-mount`).
|
||||
SystemFile(PathBuf),
|
||||
/// Persönliche Crontab des aufrufenden Nutzers aktualisiert.
|
||||
UserCrontab,
|
||||
/// Kein Cron-Mechanismus auf diesem System gefunden - nichts geschrieben, der Aufrufer
|
||||
/// sollte stattdessen [`crontab_equivalent`] anzeigen.
|
||||
Unavailable,
|
||||
}
|
||||
|
||||
/// Richtet die periodische Ausführung direkt über Cron ein (Alternative zu [`install`] für
|
||||
/// Systeme ohne systemd), sofern ein Cron-Mechanismus gefunden wird - sonst [`CronInstallOutcome::Unavailable`]
|
||||
/// statt eines Fehlers, der Aufrufer zeigt dann [`crontab_equivalent`] zur manuellen Einrichtung.
|
||||
///
|
||||
/// `Scope::System` schreibt `/etc/cron.d/smart-mount` (Standard-Konvention für
|
||||
/// paketverwaltete Cron-Einträge, läuft als root; erfordert Root-Rechte, kein
|
||||
/// Self-Elevate - analog zu `install(Scope::System, ...)`). `Scope::User` aktualisiert die
|
||||
/// persönliche Crontab des aufrufenden Nutzers über `crontab -l`/`crontab -`, mit demselben
|
||||
/// verwalteten-Block-Muster wie `fstab::setup` für `/etc/fstab` - bestehende, unabhängige
|
||||
/// Cron-Einträge bleiben unangetastet.
|
||||
pub fn install_cron(scope: Scope, watch_interval_secs: u64) -> Result<CronInstallOutcome> {
|
||||
match scope {
|
||||
Scope::System => install_cron_system(watch_interval_secs),
|
||||
Scope::User => install_cron_user(watch_interval_secs),
|
||||
}
|
||||
}
|
||||
|
||||
fn managed_cron_block(watch_interval_secs: u64, user_field: Option<&str>) -> String {
|
||||
let bin = binary_path();
|
||||
let schedule = cron_schedule_for_interval(watch_interval_secs);
|
||||
let user_prefix = user_field.map(|u| format!("{u} ")).unwrap_or_default();
|
||||
format!(
|
||||
"{CRON_BEGIN_MARKER}\n@reboot {user_prefix}{bin} mount --all\n{schedule} {user_prefix}{bin} watch\n{CRON_END_MARKER}\n"
|
||||
)
|
||||
}
|
||||
|
||||
fn install_cron_system(watch_interval_secs: u64) -> Result<CronInstallOutcome> {
|
||||
if !sudo_ctdra::is_run_as_root() {
|
||||
return Err(Error::RequiresRoot("service crontab (system context)"));
|
||||
}
|
||||
/// Richtet die periodische Ausführung direkt über `/etc/cron.d/smart-mount` ein - manueller
|
||||
/// Fallback für Systeme ohne (genutzten) systemd, sofern `/etc/cron.d` existiert - sonst
|
||||
/// [`CronInstallOutcome::Unavailable`] statt eines Fehlers, der Aufrufer zeigt dann
|
||||
/// [`crontab_equivalent`] zur manuellen Einrichtung. Erfordert Root (siehe Moduldoku) - der
|
||||
/// Aufrufer (`cli::service`) prüft das bereits vorab.
|
||||
pub fn install_cron(watch_interval_secs: u64) -> Result<CronInstallOutcome> {
|
||||
if !Path::new("/etc/cron.d").is_dir() {
|
||||
return Ok(CronInstallOutcome::Unavailable);
|
||||
}
|
||||
|
||||
// /etc/cron.d-Zeilen brauchen (anders als persönliche Crontabs) ein Nutzerfeld - root,
|
||||
// passend dazu, dass System-Kontext-Paare auch sonst als root gemountet werden.
|
||||
let contents = managed_cron_block(watch_interval_secs, Some("root"));
|
||||
let contents = managed_cron_block(watch_interval_secs);
|
||||
std::fs::write(CRON_D_PATH, &contents).map_err(|e| Error::io(CRON_D_PATH, e))?;
|
||||
#[cfg(unix)]
|
||||
{
|
||||
@@ -231,19 +70,14 @@ fn install_cron_system(watch_interval_secs: u64) -> Result<CronInstallOutcome> {
|
||||
Ok(CronInstallOutcome::SystemFile(PathBuf::from(CRON_D_PATH)))
|
||||
}
|
||||
|
||||
fn install_cron_user(watch_interval_secs: u64) -> Result<CronInstallOutcome> {
|
||||
if !crate::mount::binary_available("crontab") {
|
||||
return Ok(CronInstallOutcome::Unavailable);
|
||||
}
|
||||
|
||||
let existing = read_current_user_crontab();
|
||||
let without_block =
|
||||
crate::util::strip_managed_block(&existing, CRON_BEGIN_MARKER, CRON_END_MARKER);
|
||||
let block = managed_cron_block(watch_interval_secs, None);
|
||||
let new_contents = format!("{}\n{block}", without_block.trim_end());
|
||||
|
||||
write_user_crontab(&new_contents)?;
|
||||
Ok(CronInstallOutcome::UserCrontab)
|
||||
fn managed_cron_block(watch_interval_secs: u64) -> String {
|
||||
let bin = binary_path();
|
||||
let schedule = cron_schedule_for_interval(watch_interval_secs);
|
||||
// /etc/cron.d-Zeilen brauchen (anders als persönliche Crontabs) ein Nutzerfeld - root,
|
||||
// passend dazu, dass Mounts auch sonst immer als root laufen.
|
||||
format!(
|
||||
"{CRON_BEGIN_MARKER}\n@reboot root {bin} mount --all\n{schedule} root {bin} watch\n{CRON_END_MARKER}\n"
|
||||
)
|
||||
}
|
||||
|
||||
/// Ergebnis von [`uninstall_cron`].
|
||||
@@ -255,22 +89,8 @@ pub enum CronUninstallOutcome {
|
||||
}
|
||||
|
||||
/// Gegenstück zu [`install_cron`]: entfernt einen zuvor über `install_cron` angelegten
|
||||
/// Cron-Eintrag wieder, sofern vorhanden. `Scope::User` rührt dabei - wie `install_cron` -
|
||||
/// nur den von smart-mount verwalteten Block in der persönlichen Crontab an, keine
|
||||
/// unabhängigen, bereits vorhandenen Einträge.
|
||||
pub fn uninstall_cron(scope: Scope) -> Result<CronUninstallOutcome> {
|
||||
match scope {
|
||||
Scope::System => uninstall_cron_system(),
|
||||
Scope::User => uninstall_cron_user(),
|
||||
}
|
||||
}
|
||||
|
||||
fn uninstall_cron_system() -> Result<CronUninstallOutcome> {
|
||||
if !sudo_ctdra::is_run_as_root() {
|
||||
return Err(Error::RequiresRoot(
|
||||
"service uninstall (system context, cron)",
|
||||
));
|
||||
}
|
||||
/// Cron-Eintrag wieder, sofern vorhanden.
|
||||
pub fn uninstall_cron() -> Result<CronUninstallOutcome> {
|
||||
let path = Path::new(CRON_D_PATH);
|
||||
if !path.exists() {
|
||||
return Ok(CronUninstallOutcome::NotPresent);
|
||||
@@ -279,53 +99,6 @@ fn uninstall_cron_system() -> Result<CronUninstallOutcome> {
|
||||
Ok(CronUninstallOutcome::Removed)
|
||||
}
|
||||
|
||||
fn uninstall_cron_user() -> Result<CronUninstallOutcome> {
|
||||
if !crate::mount::binary_available("crontab") {
|
||||
return Ok(CronUninstallOutcome::NotPresent);
|
||||
}
|
||||
let existing = read_current_user_crontab();
|
||||
if !existing.contains(CRON_BEGIN_MARKER) {
|
||||
return Ok(CronUninstallOutcome::NotPresent);
|
||||
}
|
||||
let without_block =
|
||||
crate::util::strip_managed_block(&existing, CRON_BEGIN_MARKER, CRON_END_MARKER);
|
||||
write_user_crontab(without_block.trim_end())?;
|
||||
Ok(CronUninstallOutcome::Removed)
|
||||
}
|
||||
|
||||
/// `crontab -l` meldet für einen Nutzer ohne bestehende Crontab einen Fehler ("no crontab for
|
||||
/// ...") - das ist der Normalfall bei der ersten Einrichtung, kein echter Fehler.
|
||||
fn read_current_user_crontab() -> String {
|
||||
Command::new("crontab")
|
||||
.arg("-l")
|
||||
.output()
|
||||
.ok()
|
||||
.filter(|o| o.status.success())
|
||||
.map(|o| String::from_utf8_lossy(&o.stdout).into_owned())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn write_user_crontab(contents: &str) -> Result<()> {
|
||||
let mut child = Command::new("crontab")
|
||||
.arg("-")
|
||||
.stdin(Stdio::piped())
|
||||
.spawn()
|
||||
.map_err(|e| Error::Other(format!("could not start 'crontab': {e}")))?;
|
||||
child
|
||||
.stdin
|
||||
.take()
|
||||
.ok_or_else(|| Error::Other("stdin of 'crontab -' not available".to_string()))?
|
||||
.write_all(contents.as_bytes())
|
||||
.map_err(|e| Error::Other(format!("writing to 'crontab -' failed: {e}")))?;
|
||||
let status = child
|
||||
.wait()
|
||||
.map_err(|e| Error::Other(format!("'crontab -' failed: {e}")))?;
|
||||
if !status.success() {
|
||||
return Err(Error::Other("'crontab -' reported an error".to_string()));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Rechnet ein Sekunden-Intervall in einen `*/N`-artigen Cron-Ausdruck um. Crons Granularität
|
||||
/// ist Minuten (keine Sekunden) - es wird auf die nächste Minute gerundet, mindestens 1
|
||||
/// (Cron kann nicht häufiger als minütlich auslösen). Ab 60 Minuten wird auf Stunden
|
||||
@@ -350,44 +123,12 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn mount_service_unit_system_scope_uses_multi_user_target() {
|
||||
let unit = mount_service_unit(Scope::System);
|
||||
assert!(unit.contains("WantedBy=multi-user.target"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mount_service_unit_user_scope_uses_default_target() {
|
||||
let unit = mount_service_unit(Scope::User);
|
||||
assert!(unit.contains("WantedBy=default.target"));
|
||||
assert!(!unit.contains("WantedBy=multi-user.target"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mount_service_unit_system_scope_waits_for_network_online() {
|
||||
let unit = mount_service_unit(Scope::System);
|
||||
assert!(unit.contains("network-online.target"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mount_service_unit_user_scope_does_not_reference_network_online_target() {
|
||||
let unit = mount_service_unit(Scope::User);
|
||||
assert!(!unit.contains("network-online.target"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn managed_cron_block_for_user_crontab_has_no_user_field() {
|
||||
let block = managed_cron_block(120, None);
|
||||
fn managed_cron_block_includes_root_user_field_and_both_lines() {
|
||||
let block = managed_cron_block(120);
|
||||
assert!(block.starts_with(CRON_BEGIN_MARKER));
|
||||
assert!(block.trim_end().ends_with(CRON_END_MARKER));
|
||||
assert!(block.contains("@reboot") && !block.contains("@reboot root"));
|
||||
assert!(block.contains("mount --all"));
|
||||
assert!(block.contains("*/2 * * * *"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn managed_cron_block_for_system_cron_d_includes_user_field() {
|
||||
let block = managed_cron_block(120, Some("root"));
|
||||
assert!(block.contains("@reboot root "));
|
||||
assert!(block.contains("mount --all"));
|
||||
assert!(block.contains("*/2 * * * * root "));
|
||||
}
|
||||
|
||||
|
||||
-103
@@ -1,103 +0,0 @@
|
||||
//! Kleine, modulübergreifend geteilte Hilfsfunktionen.
|
||||
|
||||
/// Entfernt einen durch `begin_marker`/`end_marker` abgegrenzten Abschnitt aus `contents`
|
||||
/// (Marker-Zeilen selbst eingeschlossen). Für das "verwalteter Block"-Muster, mit dem
|
||||
/// smart-mount eigene Zeilen in einer fremden Datei (`/etc/fstab`, Crontab) aktualisiert,
|
||||
/// ohne bestehende, unabhängige Einträge anzurühren - siehe [`crate::fstab`] und
|
||||
/// [`crate::systemd`].
|
||||
pub(crate) fn strip_managed_block(contents: &str, begin_marker: &str, end_marker: &str) -> String {
|
||||
let mut out = String::new();
|
||||
let mut inside = false;
|
||||
for line in contents.lines() {
|
||||
if line.trim() == begin_marker {
|
||||
inside = true;
|
||||
continue;
|
||||
}
|
||||
if line.trim() == end_marker {
|
||||
inside = false;
|
||||
continue;
|
||||
}
|
||||
if !inside {
|
||||
out.push_str(line);
|
||||
out.push('\n');
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Gegenstück zu [`strip_managed_block`]: gibt nur den Inhalt *innerhalb* des Blocks zurück
|
||||
/// (ohne die Marker-Zeilen selbst), oder `None`, falls kein solcher Block vorhanden ist. Für
|
||||
/// ein zeilenweises Zusammenführen (statt komplettem Ersetzen) des verwalteten Blocks.
|
||||
pub(crate) fn extract_managed_block(
|
||||
contents: &str,
|
||||
begin_marker: &str,
|
||||
end_marker: &str,
|
||||
) -> Option<String> {
|
||||
let mut out = String::new();
|
||||
let mut inside = false;
|
||||
let mut found = false;
|
||||
for line in contents.lines() {
|
||||
if line.trim() == begin_marker {
|
||||
inside = true;
|
||||
found = true;
|
||||
continue;
|
||||
}
|
||||
if line.trim() == end_marker {
|
||||
inside = false;
|
||||
continue;
|
||||
}
|
||||
if inside {
|
||||
out.push_str(line);
|
||||
out.push('\n');
|
||||
}
|
||||
}
|
||||
found.then_some(out)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn removes_only_the_marked_section() {
|
||||
let contents = "line1\n# BEGIN test\nfoo\nbar\n# END test\nline2\n";
|
||||
let stripped = strip_managed_block(contents, "# BEGIN test", "# END test");
|
||||
assert_eq!(stripped, "line1\nline2\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_a_noop_when_markers_are_absent() {
|
||||
let contents = "line1\nline2\n";
|
||||
assert_eq!(
|
||||
strip_managed_block(contents, "# BEGIN test", "# END test"),
|
||||
contents
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn handles_content_before_the_first_marker_and_no_trailing_content() {
|
||||
let contents = "keep-me\n# BEGIN x\ndrop-me\n# END x\n";
|
||||
assert_eq!(
|
||||
strip_managed_block(contents, "# BEGIN x", "# END x"),
|
||||
"keep-me\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_managed_block_returns_only_the_interior() {
|
||||
let contents = "line1\n# BEGIN test\nfoo\nbar\n# END test\nline2\n";
|
||||
assert_eq!(
|
||||
extract_managed_block(contents, "# BEGIN test", "# END test"),
|
||||
Some("foo\nbar\n".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_managed_block_is_none_when_markers_are_absent() {
|
||||
let contents = "line1\nline2\n";
|
||||
assert_eq!(
|
||||
extract_managed_block(contents, "# BEGIN test", "# END test"),
|
||||
None
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user