Compare commits
23
Commits
@@ -10,6 +10,3 @@ git-fetch-with-cli = true
|
|||||||
|
|
||||||
[target.aarch64-unknown-linux-gnu]
|
[target.aarch64-unknown-linux-gnu]
|
||||||
linker = "aarch64-linux-gnu-gcc"
|
linker = "aarch64-linux-gnu-gcc"
|
||||||
|
|
||||||
[target.i686-unknown-linux-gnu]
|
|
||||||
linker = "i686-linux-gnu-gcc"
|
|
||||||
|
|||||||
@@ -24,16 +24,14 @@ jobs:
|
|||||||
components: clippy, rustfmt
|
components: clippy, rustfmt
|
||||||
cache: false
|
cache: false
|
||||||
|
|
||||||
|
# Ein rohes 'actions/cache' auf target/ liefert zwar einen technischen Cache-Hit (Dateien
|
||||||
|
# werden wiederhergestellt), Cargo kompiliert die Abhängigkeiten aber oft trotzdem neu:
|
||||||
|
# Der tar-basierte Restore-Vorgang setzt bei allen wiederhergestellten Dateien dieselbe
|
||||||
|
# Mtime, wodurch Cargos Fingerprinting nicht mehr zuverlässig erkennen kann, was
|
||||||
|
# älter/neuer als was ist, und sicherheitshalber alles neu baut. Swatinem/rust-cache ist
|
||||||
|
# genau dafür gebaut (u. a. gezielte Mtime-Korrektur nach dem Restore).
|
||||||
- name: Cache Cargo-Abhängigkeiten & Build-Artefakte
|
- name: Cache Cargo-Abhängigkeiten & Build-Artefakte
|
||||||
uses: actions/cache@v6
|
uses: Swatinem/rust-cache@v2
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.cargo/registry
|
|
||||||
~/.cargo/git
|
|
||||||
target
|
|
||||||
key: cargo-${{ runner.os }}-${{ hashFiles('Cargo.lock') }}
|
|
||||||
restore-keys: |
|
|
||||||
cargo-${{ runner.os }}-
|
|
||||||
|
|
||||||
- name: Formatierung automatisch beheben
|
- name: Formatierung automatisch beheben
|
||||||
run: cargo fmt
|
run: cargo fmt
|
||||||
|
|||||||
+17
-17
@@ -47,24 +47,24 @@ jobs:
|
|||||||
toolchain: stable
|
toolchain: stable
|
||||||
cache: false
|
cache: false
|
||||||
|
|
||||||
|
# Ein rohes 'actions/cache' auf target/ liefert zwar einen technischen Cache-Hit
|
||||||
|
# (Dateien werden restauriert), Cargo kompiliert die Abhängigkeiten aber trotzdem neu:
|
||||||
|
# Der tar-basierte Restore-Vorgang setzt bei allen wiederhergestellten Dateien
|
||||||
|
# (Quelldateien wie kompilierte .rlib/.d-Dateien) dieselbe Mtime, wodurch Cargos
|
||||||
|
# Fingerprinting nicht mehr zuverlässig erkennen kann, was tatsächlich älter/neuer als
|
||||||
|
# was ist, und sicherheitshalber alles neu baut. Swatinem/rust-cache ist genau dafür
|
||||||
|
# gebaut (u. a. gezielte Mtime-Korrektur nach dem Restore) und cached dabei automatisch
|
||||||
|
# auch beide Cross-Compilation-Targets (x86_64 + aarch64) in diesem Job.
|
||||||
- name: Cache Cargo-Abhängigkeiten & Build-Artefakte
|
- name: Cache Cargo-Abhängigkeiten & Build-Artefakte
|
||||||
uses: actions/cache@v6
|
uses: Swatinem/rust-cache@v2
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.cargo/registry
|
|
||||||
~/.cargo/git
|
|
||||||
target
|
|
||||||
key: cargo-${{ runner.os }}-${{ hashFiles('Cargo.lock') }}
|
|
||||||
restore-keys: |
|
|
||||||
cargo-${{ runner.os }}-
|
|
||||||
|
|
||||||
- name: Alte Paketierungs-Ausgaben aus dem Cache entfernen
|
- name: Alte Paketierungs-Ausgaben aus dem Cache entfernen
|
||||||
run: rm -rf target/debian target/generate-rpm target/arch
|
run: rm -rf target/debian target/generate-rpm target/arch target/completions
|
||||||
|
|
||||||
- name: Install Cross-Compilation Toolchains (apt)
|
- name: Install Cross-Compilation Toolchains (apt)
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y gcc-aarch64-linux-gnu gcc-i686-linux-gnu
|
sudo apt-get install -y gcc-aarch64-linux-gnu
|
||||||
|
|
||||||
- name: Ermittle Rust-Version für Rustup-Target-Cache-Key
|
- name: Ermittle Rust-Version für Rustup-Target-Cache-Key
|
||||||
run: echo "RUST_VERSION=$(rustc --version | awk '{print $2}')" >> "$GITHUB_ENV"
|
run: echo "RUST_VERSION=$(rustc --version | awk '{print $2}')" >> "$GITHUB_ENV"
|
||||||
@@ -75,12 +75,11 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/aarch64-unknown-linux-gnu
|
~/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/aarch64-unknown-linux-gnu
|
||||||
~/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/i686-unknown-linux-gnu
|
|
||||||
key: rustup-targets-${{ runner.os }}-${{ env.RUST_VERSION }}
|
key: rustup-targets-${{ runner.os }}-${{ env.RUST_VERSION }}
|
||||||
|
|
||||||
- name: Add Rust Cross-Compilation Targets
|
- name: Add Rust Cross-Compilation Targets
|
||||||
if: steps.cache-rustup-targets.outputs.cache-hit != 'true'
|
if: steps.cache-rustup-targets.outputs.cache-hit != 'true'
|
||||||
run: rustup target add aarch64-unknown-linux-gnu i686-unknown-linux-gnu
|
run: rustup target add aarch64-unknown-linux-gnu
|
||||||
|
|
||||||
- name: PATH um Cargo-bin-Verzeichnis ergänzen
|
- name: PATH um Cargo-bin-Verzeichnis ergänzen
|
||||||
run: |
|
run: |
|
||||||
@@ -111,7 +110,11 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
cargo build --release --target x86_64-unknown-linux-gnu
|
cargo build --release --target x86_64-unknown-linux-gnu
|
||||||
cargo build --release --target aarch64-unknown-linux-gnu
|
cargo build --release --target aarch64-unknown-linux-gnu
|
||||||
cargo build --release --target i686-unknown-linux-gnu
|
|
||||||
|
- name: Generate Shell Completions
|
||||||
|
run: |
|
||||||
|
cargo build --release --bin generate-completions
|
||||||
|
./target/release/generate-completions target/completions
|
||||||
|
|
||||||
- name: Determine Build Number
|
- name: Determine Build Number
|
||||||
id: build_num
|
id: build_num
|
||||||
@@ -130,20 +133,17 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
cargo deb --target x86_64-unknown-linux-gnu --deb-revision "${BUILD_NUMBER}" --no-build
|
cargo deb --target x86_64-unknown-linux-gnu --deb-revision "${BUILD_NUMBER}" --no-build
|
||||||
cargo deb --target aarch64-unknown-linux-gnu --deb-revision "${BUILD_NUMBER}" --no-build
|
cargo deb --target aarch64-unknown-linux-gnu --deb-revision "${BUILD_NUMBER}" --no-build
|
||||||
cargo deb --target i686-unknown-linux-gnu --deb-revision "${BUILD_NUMBER}" --no-build
|
|
||||||
|
|
||||||
- name: Build Fedora / RPM Packages (.rpm)
|
- name: Build Fedora / RPM Packages (.rpm)
|
||||||
run: |
|
run: |
|
||||||
mkdir -p target/generate-rpm
|
mkdir -p target/generate-rpm
|
||||||
cargo generate-rpm --target x86_64-unknown-linux-gnu -s "release=\"${BUILD_NUMBER}\"" -o target/generate-rpm
|
cargo generate-rpm --target x86_64-unknown-linux-gnu -s "release=\"${BUILD_NUMBER}\"" -o target/generate-rpm
|
||||||
cargo generate-rpm --target aarch64-unknown-linux-gnu -s "release=\"${BUILD_NUMBER}\"" -o target/generate-rpm
|
cargo generate-rpm --target aarch64-unknown-linux-gnu -s "release=\"${BUILD_NUMBER}\"" -o target/generate-rpm
|
||||||
cargo generate-rpm --target i686-unknown-linux-gnu -s "release=\"${BUILD_NUMBER}\"" -o target/generate-rpm
|
|
||||||
|
|
||||||
- name: Build Arch Linux Packages (.pkg.tar.zst)
|
- name: Build Arch Linux Packages (.pkg.tar.zst)
|
||||||
run: |
|
run: |
|
||||||
python3 scripts/package-arch.py --target x86_64-unknown-linux-gnu --pkgrel "${BUILD_NUMBER}"
|
python3 scripts/package-arch.py --target x86_64-unknown-linux-gnu --pkgrel "${BUILD_NUMBER}"
|
||||||
python3 scripts/package-arch.py --target aarch64-unknown-linux-gnu --pkgrel "${BUILD_NUMBER}"
|
python3 scripts/package-arch.py --target aarch64-unknown-linux-gnu --pkgrel "${BUILD_NUMBER}"
|
||||||
python3 scripts/package-arch.py --target i686-unknown-linux-gnu --pkgrel "${BUILD_NUMBER}"
|
|
||||||
|
|
||||||
- name: Publish Packages to Gitea Package Registry
|
- name: Publish Packages to Gitea Package Registry
|
||||||
env:
|
env:
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
name: Nightly Auto-Merge (Dev → Nightly)
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
# Gitea Actions interpretiert 'schedule'-Cron standardmaessig in der lokalen
|
||||||
|
# Zeitzone des Gitea-Servers (anders als GitHub Actions, das immer UTC nutzt).
|
||||||
|
# Das CRON_TZ-Praefix ist eine Gitea-Erweiterung und legt die Zeitzone explizit
|
||||||
|
# und DST-sicher fest, unabhaengig von der Server-Konfiguration.
|
||||||
|
- cron: "CRON_TZ=Europe/Berlin 0 9 * * *"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
merge-dev-into-nightly:
|
||||||
|
name: Erstelle & merge automatisch PR von dev nach nightly
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout Repository
|
||||||
|
uses: actions/checkout@v7
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
token: ${{ secrets.PACKAGE_TOKEN || secrets.RELEASE_TOKEN || secrets.PUBLISH_TOKEN || secrets.API_TOKEN || secrets.PAT_TOKEN || secrets.CUSTOM_TOKEN || secrets.GITEA_TOKEN || secrets.GITHUB_TOKEN || github.token }}
|
||||||
|
|
||||||
|
- name: Stelle sicher, dass der nightly-Branch existiert
|
||||||
|
run: |
|
||||||
|
git fetch origin dev
|
||||||
|
if git ls-remote --exit-code --heads origin nightly > /dev/null 2>&1; then
|
||||||
|
echo "nightly-Branch existiert bereits."
|
||||||
|
else
|
||||||
|
echo "nightly-Branch existiert noch nicht, erstelle ihn initial von dev..."
|
||||||
|
git push origin origin/dev:refs/heads/nightly
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Prüfe auf Unterschiede zwischen dev und nightly
|
||||||
|
id: diff
|
||||||
|
run: |
|
||||||
|
git fetch origin nightly
|
||||||
|
if git diff --quiet origin/nightly origin/dev; then
|
||||||
|
echo "Keine Unterschiede zwischen dev und nightly, überspringe."
|
||||||
|
echo "has_changes=false" >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
echo "has_changes=true" >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Prüfe auf bereits offenen PR nach nightly
|
||||||
|
id: check_pr
|
||||||
|
if: steps.diff.outputs.has_changes == 'true'
|
||||||
|
env:
|
||||||
|
GITEA_URL: ${{ gitea.server_url || github.server_url }}
|
||||||
|
REPO: ${{ gitea.repository || github.repository }}
|
||||||
|
TOKEN: ${{ secrets.PACKAGE_TOKEN || secrets.RELEASE_TOKEN || secrets.PUBLISH_TOKEN || secrets.API_TOKEN || secrets.PAT_TOKEN || secrets.CUSTOM_TOKEN || secrets.GITEA_TOKEN || secrets.GITHUB_TOKEN || github.token }}
|
||||||
|
run: |
|
||||||
|
OPEN_PRS=$(curl -s -H "Authorization: token ${TOKEN}" "${GITEA_URL}/api/v1/repos/${REPO}/pulls?state=open&limit=50")
|
||||||
|
PR_NUMBER=$(echo "$OPEN_PRS" | jq -r '[.[] | select(.base.ref == "nightly" and .head.ref == "dev")][0].number // empty')
|
||||||
|
echo "Bereits offener dev→nightly PR: ${PR_NUMBER:-keiner}"
|
||||||
|
echo "pr_number=${PR_NUMBER}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Erstelle PR (dev -> nightly)
|
||||||
|
id: create_pr
|
||||||
|
if: steps.diff.outputs.has_changes == 'true' && steps.check_pr.outputs.pr_number == ''
|
||||||
|
env:
|
||||||
|
GITEA_URL: ${{ gitea.server_url || github.server_url }}
|
||||||
|
REPO: ${{ gitea.repository || github.repository }}
|
||||||
|
TOKEN: ${{ secrets.PACKAGE_TOKEN || secrets.RELEASE_TOKEN || secrets.PUBLISH_TOKEN || secrets.API_TOKEN || secrets.PAT_TOKEN || secrets.CUSTOM_TOKEN || secrets.GITEA_TOKEN || secrets.GITHUB_TOKEN || github.token }}
|
||||||
|
run: |
|
||||||
|
DATE="$(date -u +%Y-%m-%d)"
|
||||||
|
PAYLOAD=$(jq -n --arg title "Nightly-Build: Merge dev in nightly (${DATE})" --arg head "dev" --arg base "nightly" \
|
||||||
|
'{title: $title, head: $head, base: $base}')
|
||||||
|
RESP=$(curl -f -s -S -X POST \
|
||||||
|
-H "Authorization: token ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "$PAYLOAD" \
|
||||||
|
"${GITEA_URL}/api/v1/repos/${REPO}/pulls")
|
||||||
|
PR_NUMBER=$(echo "$RESP" | jq -r '.number')
|
||||||
|
echo "PR #${PR_NUMBER} erstellt."
|
||||||
|
echo "pr_number=${PR_NUMBER}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Merge PR automatisch
|
||||||
|
if: steps.diff.outputs.has_changes == 'true'
|
||||||
|
env:
|
||||||
|
GITEA_URL: ${{ gitea.server_url || github.server_url }}
|
||||||
|
REPO: ${{ gitea.repository || github.repository }}
|
||||||
|
TOKEN: ${{ secrets.PACKAGE_TOKEN || secrets.RELEASE_TOKEN || secrets.PUBLISH_TOKEN || secrets.API_TOKEN || secrets.PAT_TOKEN || secrets.CUSTOM_TOKEN || secrets.GITEA_TOKEN || secrets.GITHUB_TOKEN || github.token }}
|
||||||
|
PR_NUMBER: ${{ steps.create_pr.outputs.pr_number || steps.check_pr.outputs.pr_number }}
|
||||||
|
run: |
|
||||||
|
echo "Merge PR #${PR_NUMBER} automatisch (dev -> nightly)..."
|
||||||
|
curl -f -s -S -X POST \
|
||||||
|
-H "Authorization: token ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"Do": "merge"}' \
|
||||||
|
"${GITEA_URL}/api/v1/repos/${REPO}/pulls/${PR_NUMBER}/merge"
|
||||||
|
echo "PR #${PR_NUMBER} erfolgreich gemerged."
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
name: Nightly Build & Publish
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- nightly
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
detect-changes:
|
||||||
|
name: Erkenne relevante Code-Änderungen
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
code_changed: ${{ steps.filter.outputs.code }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout Repository
|
||||||
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
|
- name: Prüfe auf Änderungen am Programmcode
|
||||||
|
uses: dorny/paths-filter@v4
|
||||||
|
id: filter
|
||||||
|
with:
|
||||||
|
filters: |
|
||||||
|
code:
|
||||||
|
- 'src/**'
|
||||||
|
- 'Cargo.toml'
|
||||||
|
- 'Cargo.lock'
|
||||||
|
- 'scripts/**'
|
||||||
|
- '.cargo/**'
|
||||||
|
- '.gitea/workflows/nightly.yaml'
|
||||||
|
|
||||||
|
build-and-publish:
|
||||||
|
name: Build & Publish Packages (Nightly)
|
||||||
|
needs: detect-changes
|
||||||
|
if: needs.detect-changes.outputs.code_changed == 'true'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
CARGO_BINSTALL_VERSION: "1.23.0"
|
||||||
|
CARGO_DEB_VERSION: "3.8.0"
|
||||||
|
CARGO_GENERATE_RPM_VERSION: "0.21.0"
|
||||||
|
steps:
|
||||||
|
- name: Checkout Repository
|
||||||
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
|
- name: Install Rust Toolchain
|
||||||
|
uses: actions-rust-lang/setup-rust-toolchain@v2
|
||||||
|
with:
|
||||||
|
toolchain: stable
|
||||||
|
cache: false
|
||||||
|
|
||||||
|
- name: Cache Cargo-Abhängigkeiten & Build-Artefakte
|
||||||
|
uses: Swatinem/rust-cache@v2
|
||||||
|
|
||||||
|
- name: Alte Paketierungs-Ausgaben aus dem Cache entfernen
|
||||||
|
run: rm -rf target/debian target/generate-rpm target/arch target/completions
|
||||||
|
|
||||||
|
- name: Install Cross-Compilation Toolchains (apt)
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y gcc-aarch64-linux-gnu
|
||||||
|
|
||||||
|
- name: Ermittle Rust-Version für Rustup-Target-Cache-Key
|
||||||
|
run: echo "RUST_VERSION=$(rustc --version | awk '{print $2}')" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
|
- name: Cache Rustup Cross-Compilation-Targets
|
||||||
|
id: cache-rustup-targets
|
||||||
|
uses: actions/cache@v6
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/aarch64-unknown-linux-gnu
|
||||||
|
key: rustup-targets-${{ runner.os }}-${{ env.RUST_VERSION }}
|
||||||
|
|
||||||
|
- name: Add Rust Cross-Compilation Targets
|
||||||
|
if: steps.cache-rustup-targets.outputs.cache-hit != 'true'
|
||||||
|
run: rustup target add aarch64-unknown-linux-gnu
|
||||||
|
|
||||||
|
- name: PATH um Cargo-bin-Verzeichnis ergänzen
|
||||||
|
run: |
|
||||||
|
mkdir -p ~/.cargo/bin
|
||||||
|
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
||||||
|
|
||||||
|
- name: Cache Packaging-Tools (cargo-binstall, cargo-deb, cargo-generate-rpm)
|
||||||
|
id: cache-packaging-tools
|
||||||
|
uses: actions/cache@v6
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cargo/bin/cargo-binstall
|
||||||
|
~/.cargo/bin/cargo-deb
|
||||||
|
~/.cargo/bin/cargo-generate-rpm
|
||||||
|
key: packaging-tools-${{ runner.os }}-${{ env.CARGO_BINSTALL_VERSION }}-${{ env.CARGO_DEB_VERSION }}-${{ env.CARGO_GENERATE_RPM_VERSION }}
|
||||||
|
|
||||||
|
- name: Install Packaging Tools (Prebuilt Binaries)
|
||||||
|
if: steps.cache-packaging-tools.outputs.cache-hit != 'true'
|
||||||
|
run: |
|
||||||
|
curl -fsSL "https://github.com/cargo-bins/cargo-binstall/releases/download/v${CARGO_BINSTALL_VERSION}/cargo-binstall-x86_64-unknown-linux-musl.tgz" | tar -xz -C ~/.cargo/bin
|
||||||
|
~/.cargo/bin/cargo-binstall -y --no-symlinks "cargo-deb@${CARGO_DEB_VERSION}" "cargo-generate-rpm@${CARGO_GENERATE_RPM_VERSION}"
|
||||||
|
|
||||||
|
- name: Run Tests
|
||||||
|
run: |
|
||||||
|
cargo test
|
||||||
|
|
||||||
|
- name: Build Release Binaries
|
||||||
|
run: |
|
||||||
|
cargo build --release --target x86_64-unknown-linux-gnu
|
||||||
|
cargo build --release --target aarch64-unknown-linux-gnu
|
||||||
|
|
||||||
|
- name: Generate Shell Completions
|
||||||
|
run: |
|
||||||
|
cargo build --release --bin generate-completions
|
||||||
|
./target/release/generate-completions target/completions
|
||||||
|
|
||||||
|
- name: Determine Build Number
|
||||||
|
id: build_num
|
||||||
|
env:
|
||||||
|
GITEA_URL: ${{ gitea.server_url || github.server_url }}
|
||||||
|
REPO: ${{ gitea.repository || github.repository }}
|
||||||
|
REPO_OWNER: ${{ gitea.repository_owner || github.repository_owner }}
|
||||||
|
TOKEN: ${{ secrets.PACKAGE_TOKEN || secrets.RELEASE_TOKEN || secrets.PUBLISH_TOKEN || secrets.API_TOKEN || secrets.PAT_TOKEN || secrets.CUSTOM_TOKEN || secrets.GITEA_TOKEN || secrets.GITHUB_TOKEN || github.token }}
|
||||||
|
run: |
|
||||||
|
BUILD_NUM=$(python3 scripts/get-build-number.py)
|
||||||
|
echo "build_number=${BUILD_NUM}" >> $GITHUB_OUTPUT
|
||||||
|
echo "BUILD_NUMBER=${BUILD_NUM}" >> $GITHUB_ENV
|
||||||
|
echo "Ermittelte Build-Nummer: ${BUILD_NUM}"
|
||||||
|
|
||||||
|
- name: Build Debian Packages (.deb)
|
||||||
|
run: |
|
||||||
|
cargo deb --target x86_64-unknown-linux-gnu --deb-revision "${BUILD_NUMBER}" --no-build
|
||||||
|
cargo deb --target aarch64-unknown-linux-gnu --deb-revision "${BUILD_NUMBER}" --no-build
|
||||||
|
|
||||||
|
- name: Build Fedora / RPM Packages (.rpm)
|
||||||
|
run: |
|
||||||
|
mkdir -p target/generate-rpm
|
||||||
|
cargo generate-rpm --target x86_64-unknown-linux-gnu -s "release=\"${BUILD_NUMBER}\"" -o target/generate-rpm
|
||||||
|
cargo generate-rpm --target aarch64-unknown-linux-gnu -s "release=\"${BUILD_NUMBER}\"" -o target/generate-rpm
|
||||||
|
|
||||||
|
- name: Build Arch Linux Packages (.pkg.tar.zst)
|
||||||
|
run: |
|
||||||
|
python3 scripts/package-arch.py --target x86_64-unknown-linux-gnu --pkgrel "${BUILD_NUMBER}"
|
||||||
|
python3 scripts/package-arch.py --target aarch64-unknown-linux-gnu --pkgrel "${BUILD_NUMBER}"
|
||||||
|
|
||||||
|
- name: Publish Packages to Gitea Package Registry
|
||||||
|
env:
|
||||||
|
GITEA_URL: ${{ gitea.server_url || github.server_url }}
|
||||||
|
REPO_OWNER: ${{ gitea.repository_owner || github.repository_owner }}
|
||||||
|
TOKEN: ${{ secrets.PACKAGE_TOKEN || secrets.RELEASE_TOKEN || secrets.PUBLISH_TOKEN || secrets.API_TOKEN || secrets.PAT_TOKEN || secrets.CUSTOM_TOKEN || secrets.GITEA_TOKEN || secrets.GITHUB_TOKEN || github.token }}
|
||||||
|
run: |
|
||||||
|
echo "Veröffentliche Debian-Paket (Distribution: nightly, Component: main)..."
|
||||||
|
for deb in target/debian/*.deb; do
|
||||||
|
[ -f "$deb" ] || continue
|
||||||
|
curl -f -s -S -X PUT \
|
||||||
|
-H "Authorization: token ${TOKEN}" \
|
||||||
|
--upload-file "$deb" \
|
||||||
|
"${GITEA_URL}/api/packages/${REPO_OWNER}/debian/pool/nightly/main/upload"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Veröffentliche Fedora/RPM-Paket (Gruppe: nightly)..."
|
||||||
|
for rpm in target/generate-rpm/*.rpm; do
|
||||||
|
[ -f "$rpm" ] || continue
|
||||||
|
curl -f -s -S -X PUT \
|
||||||
|
-H "Authorization: token ${TOKEN}" \
|
||||||
|
--upload-file "$rpm" \
|
||||||
|
"${GITEA_URL}/api/packages/${REPO_OWNER}/rpm/nightly/upload"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Veröffentliche Arch Linux-Paket (Repository: nightly)..."
|
||||||
|
for pkg in target/arch/*.pkg.tar.zst; do
|
||||||
|
[ -f "$pkg" ] || continue
|
||||||
|
curl -f -s -S -X PUT \
|
||||||
|
-H "Authorization: token ${TOKEN}" \
|
||||||
|
--upload-file "$pkg" \
|
||||||
|
"${GITEA_URL}/api/packages/${REPO_OWNER}/arch/nightly"
|
||||||
|
done
|
||||||
@@ -8,7 +8,7 @@ on:
|
|||||||
- dev
|
- dev
|
||||||
pull_request:
|
pull_request:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: "0 5 * * 1"
|
- cron: "CRON_TZ=Europe/Berlin 0 5 * * 1"
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|||||||
@@ -47,24 +47,24 @@ jobs:
|
|||||||
toolchain: stable
|
toolchain: stable
|
||||||
cache: false
|
cache: false
|
||||||
|
|
||||||
|
# Ein rohes 'actions/cache' auf target/ liefert zwar einen technischen Cache-Hit
|
||||||
|
# (Dateien werden restauriert), Cargo kompiliert die Abhängigkeiten aber trotzdem neu:
|
||||||
|
# Der tar-basierte Restore-Vorgang setzt bei allen wiederhergestellten Dateien
|
||||||
|
# (Quelldateien wie kompilierte .rlib/.d-Dateien) dieselbe Mtime, wodurch Cargos
|
||||||
|
# Fingerprinting nicht mehr zuverlässig erkennen kann, was tatsächlich älter/neuer als
|
||||||
|
# was ist, und sicherheitshalber alles neu baut. Swatinem/rust-cache ist genau dafür
|
||||||
|
# gebaut (u. a. gezielte Mtime-Korrektur nach dem Restore) und cached dabei automatisch
|
||||||
|
# auch beide Cross-Compilation-Targets (x86_64 + aarch64) in diesem Job.
|
||||||
- name: Cache Cargo-Abhängigkeiten & Build-Artefakte
|
- name: Cache Cargo-Abhängigkeiten & Build-Artefakte
|
||||||
uses: actions/cache@v6
|
uses: Swatinem/rust-cache@v2
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.cargo/registry
|
|
||||||
~/.cargo/git
|
|
||||||
target
|
|
||||||
key: cargo-${{ runner.os }}-${{ hashFiles('Cargo.lock') }}
|
|
||||||
restore-keys: |
|
|
||||||
cargo-${{ runner.os }}-
|
|
||||||
|
|
||||||
- name: Alte Paketierungs-Ausgaben aus dem Cache entfernen
|
- name: Alte Paketierungs-Ausgaben aus dem Cache entfernen
|
||||||
run: rm -rf target/debian target/generate-rpm target/arch
|
run: rm -rf target/debian target/generate-rpm target/arch target/completions
|
||||||
|
|
||||||
- name: Install Cross-Compilation Toolchains (apt)
|
- name: Install Cross-Compilation Toolchains (apt)
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y gcc-aarch64-linux-gnu gcc-i686-linux-gnu
|
sudo apt-get install -y gcc-aarch64-linux-gnu
|
||||||
|
|
||||||
- name: Ermittle Rust-Version für Rustup-Target-Cache-Key
|
- name: Ermittle Rust-Version für Rustup-Target-Cache-Key
|
||||||
run: echo "RUST_VERSION=$(rustc --version | awk '{print $2}')" >> "$GITHUB_ENV"
|
run: echo "RUST_VERSION=$(rustc --version | awk '{print $2}')" >> "$GITHUB_ENV"
|
||||||
@@ -75,12 +75,11 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/aarch64-unknown-linux-gnu
|
~/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/aarch64-unknown-linux-gnu
|
||||||
~/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/i686-unknown-linux-gnu
|
|
||||||
key: rustup-targets-${{ runner.os }}-${{ env.RUST_VERSION }}
|
key: rustup-targets-${{ runner.os }}-${{ env.RUST_VERSION }}
|
||||||
|
|
||||||
- name: Add Rust Cross-Compilation Targets
|
- name: Add Rust Cross-Compilation Targets
|
||||||
if: steps.cache-rustup-targets.outputs.cache-hit != 'true'
|
if: steps.cache-rustup-targets.outputs.cache-hit != 'true'
|
||||||
run: rustup target add aarch64-unknown-linux-gnu i686-unknown-linux-gnu
|
run: rustup target add aarch64-unknown-linux-gnu
|
||||||
|
|
||||||
- name: PATH um Cargo-bin-Verzeichnis ergänzen
|
- name: PATH um Cargo-bin-Verzeichnis ergänzen
|
||||||
run: |
|
run: |
|
||||||
@@ -111,7 +110,11 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
cargo build --release --target x86_64-unknown-linux-gnu
|
cargo build --release --target x86_64-unknown-linux-gnu
|
||||||
cargo build --release --target aarch64-unknown-linux-gnu
|
cargo build --release --target aarch64-unknown-linux-gnu
|
||||||
cargo build --release --target i686-unknown-linux-gnu
|
|
||||||
|
- name: Generate Shell Completions
|
||||||
|
run: |
|
||||||
|
cargo build --release --bin generate-completions
|
||||||
|
./target/release/generate-completions target/completions
|
||||||
|
|
||||||
- name: Determine Build Number
|
- name: Determine Build Number
|
||||||
id: build_num
|
id: build_num
|
||||||
@@ -130,20 +133,17 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
cargo deb --target x86_64-unknown-linux-gnu --deb-revision "${BUILD_NUMBER}" --no-build
|
cargo deb --target x86_64-unknown-linux-gnu --deb-revision "${BUILD_NUMBER}" --no-build
|
||||||
cargo deb --target aarch64-unknown-linux-gnu --deb-revision "${BUILD_NUMBER}" --no-build
|
cargo deb --target aarch64-unknown-linux-gnu --deb-revision "${BUILD_NUMBER}" --no-build
|
||||||
cargo deb --target i686-unknown-linux-gnu --deb-revision "${BUILD_NUMBER}" --no-build
|
|
||||||
|
|
||||||
- name: Build Fedora / RPM Packages (.rpm)
|
- name: Build Fedora / RPM Packages (.rpm)
|
||||||
run: |
|
run: |
|
||||||
mkdir -p target/generate-rpm
|
mkdir -p target/generate-rpm
|
||||||
cargo generate-rpm --target x86_64-unknown-linux-gnu -s "release=\"${BUILD_NUMBER}\"" -o target/generate-rpm
|
cargo generate-rpm --target x86_64-unknown-linux-gnu -s "release=\"${BUILD_NUMBER}\"" -o target/generate-rpm
|
||||||
cargo generate-rpm --target aarch64-unknown-linux-gnu -s "release=\"${BUILD_NUMBER}\"" -o target/generate-rpm
|
cargo generate-rpm --target aarch64-unknown-linux-gnu -s "release=\"${BUILD_NUMBER}\"" -o target/generate-rpm
|
||||||
cargo generate-rpm --target i686-unknown-linux-gnu -s "release=\"${BUILD_NUMBER}\"" -o target/generate-rpm
|
|
||||||
|
|
||||||
- name: Build Arch Linux Packages (.pkg.tar.zst)
|
- name: Build Arch Linux Packages (.pkg.tar.zst)
|
||||||
run: |
|
run: |
|
||||||
python3 scripts/package-arch.py --target x86_64-unknown-linux-gnu --pkgrel "${BUILD_NUMBER}"
|
python3 scripts/package-arch.py --target x86_64-unknown-linux-gnu --pkgrel "${BUILD_NUMBER}"
|
||||||
python3 scripts/package-arch.py --target aarch64-unknown-linux-gnu --pkgrel "${BUILD_NUMBER}"
|
python3 scripts/package-arch.py --target aarch64-unknown-linux-gnu --pkgrel "${BUILD_NUMBER}"
|
||||||
python3 scripts/package-arch.py --target i686-unknown-linux-gnu --pkgrel "${BUILD_NUMBER}"
|
|
||||||
|
|
||||||
- name: Publish Packages to Gitea Package Registry
|
- name: Publish Packages to Gitea Package Registry
|
||||||
env:
|
env:
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ on:
|
|||||||
push:
|
push:
|
||||||
pull_request:
|
pull_request:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: "0 6 * * 1"
|
- cron: "CRON_TZ=Europe/Berlin 0 6 * * 1"
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ on:
|
|||||||
- reopened
|
- reopened
|
||||||
branches:
|
branches:
|
||||||
- testing
|
- testing
|
||||||
|
- dev
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
@@ -23,16 +24,11 @@ jobs:
|
|||||||
toolchain: stable
|
toolchain: stable
|
||||||
cache: false
|
cache: false
|
||||||
|
|
||||||
|
# Siehe main.yaml/testing.yaml: Swatinem/rust-cache statt eines rohen 'actions/cache' auf
|
||||||
|
# target/, das trotz Cache-Hit wegen verschobener Mtimes nach dem Restore oft trotzdem
|
||||||
|
# alles neu kompiliert.
|
||||||
- name: Cache Cargo-Abhängigkeiten & Build-Artefakte
|
- name: Cache Cargo-Abhängigkeiten & Build-Artefakte
|
||||||
uses: actions/cache@v6
|
uses: Swatinem/rust-cache@v2
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.cargo/registry
|
|
||||||
~/.cargo/git
|
|
||||||
target
|
|
||||||
key: cargo-${{ runner.os }}-${{ hashFiles('Cargo.lock') }}
|
|
||||||
restore-keys: |
|
|
||||||
cargo-${{ runner.os }}-
|
|
||||||
|
|
||||||
- name: Run Tests
|
- name: Run Tests
|
||||||
run: cargo test
|
run: cargo test
|
||||||
|
|||||||
Generated
+16
@@ -3,6 +3,22 @@
|
|||||||
<component name="JsonSchemaMappingsProjectConfiguration">
|
<component name="JsonSchemaMappingsProjectConfiguration">
|
||||||
<state>
|
<state>
|
||||||
<map>
|
<map>
|
||||||
|
<entry key="Cargo Config">
|
||||||
|
<value>
|
||||||
|
<SchemaInfo>
|
||||||
|
<option name="name" value="Cargo Config" />
|
||||||
|
<option name="relativePathToSchema" value="https://www.schemastore.org/cargo-config.json" />
|
||||||
|
<option name="applicationDefined" value="true" />
|
||||||
|
<option name="patterns">
|
||||||
|
<list>
|
||||||
|
<Item>
|
||||||
|
<option name="path" value=".cargo/config.toml" />
|
||||||
|
</Item>
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
</SchemaInfo>
|
||||||
|
</value>
|
||||||
|
</entry>
|
||||||
<entry key="GitHub Workflow">
|
<entry key="GitHub Workflow">
|
||||||
<value>
|
<value>
|
||||||
<SchemaInfo>
|
<SchemaInfo>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ Dieses Dokument dient als technischer Leitfaden und Kontextdokument für KI-Codi
|
|||||||
## 1. Projektübersicht & Philosophie
|
## 1. Projektübersicht & Philosophie
|
||||||
|
|
||||||
Dieses Repository ist ein **Rust-Projekt-Template** für Linux-Anwendungen und CLI-Tools mit Fokus auf:
|
Dieses Repository ist ein **Rust-Projekt-Template** für Linux-Anwendungen und CLI-Tools mit Fokus auf:
|
||||||
- Automatisierte Multi-Architektur-Kompilierung (`x86_64`, `aarch64`, `i686`).
|
- Automatisierte Multi-Architektur-Kompilierung (`x86_64`, `aarch64`).
|
||||||
- Native Paketierung für Debian (`.deb`), Fedora/RHEL (`.rpm`) und Arch Linux (`.pkg.tar.zst`) sowie Docker-Container-Images.
|
- Native Paketierung für Debian (`.deb`), Fedora/RHEL (`.rpm`) und Arch Linux (`.pkg.tar.zst`) sowie Docker-Container-Images.
|
||||||
- Vollständig automatisierte CI/CD-Pipelines via Gitea Actions (kompatibel mit Forgejo / GitHub Actions).
|
- Vollständig automatisierte CI/CD-Pipelines via Gitea Actions (kompatibel mit Forgejo / GitHub Actions).
|
||||||
- Automatisierte Sicherheits-Scans (Schwachstellen, Secrets) und Dependency-Updates.
|
- Automatisierte Sicherheits-Scans (Schwachstellen, Secrets) und Dependency-Updates.
|
||||||
@@ -15,7 +15,7 @@ Dieses Repository ist ein **Rust-Projekt-Template** für Linux-Anwendungen und C
|
|||||||
### Standards & Tech-Stack
|
### Standards & Tech-Stack
|
||||||
- **Sprache**: Rust (Edition 2024), Python 3 (für Hilfsskripte in `scripts/`).
|
- **Sprache**: Rust (Edition 2024), Python 3 (für Hilfsskripte in `scripts/`).
|
||||||
- **Rust Toolchain**: Stable.
|
- **Rust Toolchain**: Stable.
|
||||||
- **Zielplattform**: Linux (GLIBC-basiert, Cross-Kompilierung für `x86_64-unknown-linux-gnu`, `aarch64-unknown-linux-gnu`, `i686-unknown-linux-gnu`).
|
- **Zielplattform**: Linux (GLIBC-basiert, Cross-Kompilierung für `x86_64-unknown-linux-gnu`, `aarch64-unknown-linux-gnu`).
|
||||||
- **Container**: Docker-Images werden zusätzlich zu den nativen Paketen gebaut und in die Gitea Container Registry veröffentlicht.
|
- **Container**: Docker-Images werden zusätzlich zu den nativen Paketen gebaut und in die Gitea Container Registry veröffentlicht.
|
||||||
- **Sicherheits-Tooling**: Trivy, OSV-Scanner, TruffleHog (Secret-Scanning), Renovate (Dependency-Updates), Qodana (statische Codeanalyse).
|
- **Sicherheits-Tooling**: Trivy, OSV-Scanner, TruffleHog (Secret-Scanning), Renovate (Dependency-Updates), Qodana (statische Codeanalyse).
|
||||||
- **Lizenz**: GPL-3.0-or-later (sofern nicht im abgeleiteten Projekt anders definiert).
|
- **Lizenz**: GPL-3.0-or-later (sofern nicht im abgeleiteten Projekt anders definiert).
|
||||||
@@ -118,7 +118,10 @@ Alle Workflows liegen unter `.gitea/workflows/` und nutzen gecachte Abhängigkei
|
|||||||
### 5.1 Build & Release
|
### 5.1 Build & Release
|
||||||
- **`main.yaml`** (Trigger: `push` auf `main`): Baut Binaries für alle 3 Architekturen, führt `cargo test` aus, baut `.deb`, `.rpm` und `.pkg.tar.zst`, lädt sie in die Gitea Package Registry hoch, erstellt ein Gitea Release `v<VERSION>` und baut/veröffentlicht zusätzlich ein Docker-Image (`:latest`, `:<VERSION>`, `:v<VERSION>`, `:<VERSION>.<BUILD_NUMBER>`).
|
- **`main.yaml`** (Trigger: `push` auf `main`): Baut Binaries für alle 3 Architekturen, führt `cargo test` aus, baut `.deb`, `.rpm` und `.pkg.tar.zst`, lädt sie in die Gitea Package Registry hoch, erstellt ein Gitea Release `v<VERSION>` und baut/veröffentlicht zusätzlich ein Docker-Image (`:latest`, `:<VERSION>`, `:v<VERSION>`, `:<VERSION>.<BUILD_NUMBER>`).
|
||||||
- **`testing.yaml`** (Trigger: `push` auf `testing`): Analog zu `main.yaml`, veröffentlicht jedoch in die `testing`-Kanäle der Paket-Registry, erstellt ein Pre-Release `v<VERSION>-preview` und taggt Docker-Images mit `:testing`, `:<VERSION>-preview`, `:<VERSION>-testing` etc.
|
- **`testing.yaml`** (Trigger: `push` auf `testing`): Analog zu `main.yaml`, veröffentlicht jedoch in die `testing`-Kanäle der Paket-Registry, erstellt ein Pre-Release `v<VERSION>-preview` und taggt Docker-Images mit `:testing`, `:<VERSION>-preview`, `:<VERSION>-testing` etc.
|
||||||
- **`unit-tests.yaml`** (Trigger: `pull_request` → `testing`, bei `opened`/`synchronize`/`reopened`): Schnelle CI-Prüfung (`cargo test`) für Pull Requests, ohne Paketierung oder Veröffentlichung.
|
- **`nightly.yaml`** (Trigger: `push` auf `nightly`): Analog zu `testing.yaml`/`main.yaml` (Build, Tests, Paketierung für alle 3 Architekturen), veröffentlicht die Pakete jedoch ausschließlich in den `nightly`-Kanal der Paket-Registry (Distribution/Gruppe/Repository jeweils `nightly`) und erstellt **kein** Gitea Release.
|
||||||
|
- **`nightly-pr.yaml`** (Trigger: `schedule` täglich `CRON_TZ=Europe/Berlin 0 9 * * *`, zusätzlich `workflow_dispatch`): Erstellt automatisch einen Pull Request von `dev` nach `nightly` (legt den `nightly`-Branch bei Bedarf initial von `dev` an) und merged diesen sofort ohne manuelle Freigabe, sofern es Unterschiede zwischen beiden Branches gibt. Der Merge löst anschließend `nightly.yaml` aus.
|
||||||
|
> **Hinweis:** Gitea Actions interpretiert `schedule`-Cron standardmäßig in der lokalen Zeitzone des Gitea-Servers (anders als GitHub Actions, das immer UTC nutzt). Das `CRON_TZ=`-Präfix ist eine Gitea-Erweiterung, die die Zeitzone unabhängig von der Server-Konfiguration DST-sicher festlegt — bei neuen zeitgesteuerten Workflows bevorzugt verwenden statt einer fest verdrahteten UTC-Zeit (betrifft z. B. `security-scan.yaml`s `0 5 * * 1`, das ohne `CRON_TZ=` von der Server-Zeitzone abhängt).
|
||||||
|
- **`unit-tests.yaml`** (Trigger: `pull_request` → `testing`/`dev`, bei `opened`/`synchronize`/`reopened`): Schnelle CI-Prüfung (`cargo test`) für Pull Requests, ohne Paketierung oder Veröffentlichung.
|
||||||
|
|
||||||
### 5.2 Sicherheit & Qualität
|
### 5.2 Sicherheit & Qualität
|
||||||
- **`security-scan.yaml`** (Trigger: `push` auf `main`/`testing`/`dev`, `pull_request`, wöchentlich montags 05:00 UTC, `workflow_dispatch`): Führt Trivy (`vuln`, `secret`, `misconfig`; Schweregrad `CRITICAL`/`HIGH`) und OSV-Scanner aus und meldet Funde über `scripts/report-security-issue.py` als Gitea-Issue (Label `security-scan`).
|
- **`security-scan.yaml`** (Trigger: `push` auf `main`/`testing`/`dev`, `pull_request`, wöchentlich montags 05:00 UTC, `workflow_dispatch`): Führt Trivy (`vuln`, `secret`, `misconfig`; Schweregrad `CRITICAL`/`HIGH`) und OSV-Scanner aus und meldet Funde über `scripts/report-security-issue.py` als Gitea-Issue (Label `security-scan`).
|
||||||
|
|||||||
Generated
+1
-634
@@ -178,17 +178,6 @@ version = "1.0.104"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
|
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]]
|
[[package]]
|
||||||
name = "arc-swap"
|
name = "arc-swap"
|
||||||
version = "1.9.2"
|
version = "1.9.2"
|
||||||
@@ -224,143 +213,6 @@ version = "0.1.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bfdc70193dadb9d7287fa4b633f15f90c876915b31f6af17da307fc59c9859a8"
|
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]]
|
[[package]]
|
||||||
name = "autocfg"
|
name = "autocfg"
|
||||||
version = "1.5.1"
|
version = "1.5.1"
|
||||||
@@ -436,28 +288,6 @@ dependencies = [
|
|||||||
"hybrid-array",
|
"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]]
|
[[package]]
|
||||||
name = "branches"
|
name = "branches"
|
||||||
version = "0.4.6"
|
version = "0.4.6"
|
||||||
@@ -505,15 +335,6 @@ version = "1.12.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
|
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]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.4.6"
|
version = "1.4.6"
|
||||||
@@ -714,22 +535,6 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"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]]
|
[[package]]
|
||||||
name = "core-foundation-sys"
|
name = "core-foundation-sys"
|
||||||
version = "0.8.7"
|
version = "0.8.7"
|
||||||
@@ -860,18 +665,6 @@ dependencies = [
|
|||||||
"zeroize",
|
"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]]
|
[[package]]
|
||||||
name = "displaydoc"
|
name = "displaydoc"
|
||||||
version = "0.2.7"
|
version = "0.2.7"
|
||||||
@@ -895,33 +688,6 @@ version = "1.0.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
|
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]]
|
[[package]]
|
||||||
name = "env_filter"
|
name = "env_filter"
|
||||||
version = "2.0.0"
|
version = "2.0.0"
|
||||||
@@ -969,26 +735,6 @@ dependencies = [
|
|||||||
"windows-sys 0.59.0",
|
"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]]
|
[[package]]
|
||||||
name = "fallible-iterator"
|
name = "fallible-iterator"
|
||||||
version = "0.3.0"
|
version = "0.3.0"
|
||||||
@@ -1030,36 +776,6 @@ version = "0.3.34"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
|
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]]
|
[[package]]
|
||||||
name = "futures-task"
|
name = "futures-task"
|
||||||
version = "0.3.34"
|
version = "0.3.34"
|
||||||
@@ -1073,7 +789,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
|
checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-macro",
|
|
||||||
"futures-task",
|
"futures-task",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"slab",
|
"slab",
|
||||||
@@ -1203,24 +918,6 @@ version = "0.4.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
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]]
|
[[package]]
|
||||||
name = "home"
|
name = "home"
|
||||||
version = "0.5.12"
|
version = "0.5.12"
|
||||||
@@ -1453,7 +1150,6 @@ version = "0.2.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7"
|
checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"block-padding",
|
|
||||||
"hybrid-array",
|
"hybrid-array",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1518,27 +1214,6 @@ dependencies = [
|
|||||||
"wasm-bindgen",
|
"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]]
|
[[package]]
|
||||||
name = "lazy_static"
|
name = "lazy_static"
|
||||||
version = "1.5.0"
|
version = "1.5.0"
|
||||||
@@ -1755,20 +1430,6 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"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]]
|
[[package]]
|
||||||
name = "num-bigint"
|
name = "num-bigint"
|
||||||
version = "0.4.8"
|
version = "0.4.8"
|
||||||
@@ -1779,15 +1440,6 @@ dependencies = [
|
|||||||
"num-traits",
|
"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]]
|
[[package]]
|
||||||
name = "num-conv"
|
name = "num-conv"
|
||||||
version = "0.2.2"
|
version = "0.2.2"
|
||||||
@@ -1803,27 +1455,6 @@ dependencies = [
|
|||||||
"num-traits",
|
"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]]
|
[[package]]
|
||||||
name = "num-traits"
|
name = "num-traits"
|
||||||
version = "0.2.19"
|
version = "0.2.19"
|
||||||
@@ -1860,16 +1491,6 @@ version = "0.3.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
|
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]]
|
[[package]]
|
||||||
name = "owo-colors"
|
name = "owo-colors"
|
||||||
version = "3.5.0"
|
version = "3.5.0"
|
||||||
@@ -1885,12 +1506,6 @@ dependencies = [
|
|||||||
"bytemuck",
|
"bytemuck",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "parking"
|
|
||||||
version = "2.2.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parking_lot"
|
name = "parking_lot"
|
||||||
version = "0.12.5"
|
version = "0.12.5"
|
||||||
@@ -1926,17 +1541,6 @@ version = "0.2.17"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
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]]
|
[[package]]
|
||||||
name = "polling"
|
name = "polling"
|
||||||
version = "3.11.0"
|
version = "3.11.0"
|
||||||
@@ -2010,15 +1614,6 @@ dependencies = [
|
|||||||
"syn 2.0.119",
|
"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]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.107"
|
version = "1.0.107"
|
||||||
@@ -2307,48 +1902,6 @@ version = "1.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
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]]
|
[[package]]
|
||||||
name = "semver"
|
name = "semver"
|
||||||
version = "1.0.28"
|
version = "1.0.28"
|
||||||
@@ -2398,17 +1951,6 @@ dependencies = [
|
|||||||
"zmij",
|
"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]]
|
[[package]]
|
||||||
name = "serde_spanned"
|
name = "serde_spanned"
|
||||||
version = "1.1.1"
|
version = "1.1.1"
|
||||||
@@ -2424,17 +1966,6 @@ version = "1.0.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d"
|
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]]
|
[[package]]
|
||||||
name = "sharded-slab"
|
name = "sharded-slab"
|
||||||
version = "0.1.7"
|
version = "0.1.7"
|
||||||
@@ -2521,7 +2052,7 @@ checksum = "ba467056f1b547ed52077911161fc86985becbc60e8e1857c8a144dab0def891"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "smart-mount"
|
name = "smart-mount"
|
||||||
version = "2.0.0"
|
version = "2.0.4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes-gcm 0.11.1",
|
"aes-gcm 0.11.1",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
@@ -2530,9 +2061,7 @@ dependencies = [
|
|||||||
"config-ctdra",
|
"config-ctdra",
|
||||||
"dialoguer",
|
"dialoguer",
|
||||||
"getrandom 0.4.3",
|
"getrandom 0.4.3",
|
||||||
"keyring",
|
|
||||||
"logger-ctdra",
|
"logger-ctdra",
|
||||||
"program-ctdra",
|
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sudo-ctdra",
|
"sudo-ctdra",
|
||||||
@@ -2803,18 +2332,6 @@ dependencies = [
|
|||||||
"serde_core",
|
"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]]
|
[[package]]
|
||||||
name = "toml_parser"
|
name = "toml_parser"
|
||||||
version = "1.1.3+spec-1.1.0"
|
version = "1.1.3+spec-1.1.0"
|
||||||
@@ -3111,17 +2628,6 @@ version = "1.20.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
|
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]]
|
[[package]]
|
||||||
name = "uncased"
|
name = "uncased"
|
||||||
version = "0.9.10"
|
version = "0.9.10"
|
||||||
@@ -3195,7 +2701,6 @@ checksum = "2ef6dac1e96601b4fb3acccccff2139741fcb757cb9a36089bf5be91cfb285ce"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"getrandom 0.4.3",
|
"getrandom 0.4.3",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"serde_core",
|
|
||||||
"sha1_smol",
|
"sha1_smol",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
@@ -3325,19 +2830,6 @@ version = "0.2.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
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]]
|
[[package]]
|
||||||
name = "windows-result"
|
name = "windows-result"
|
||||||
version = "0.4.1"
|
version = "0.4.1"
|
||||||
@@ -3449,9 +2941,6 @@ name = "winnow"
|
|||||||
version = "1.0.4"
|
version = "1.0.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81"
|
checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81"
|
||||||
dependencies = [
|
|
||||||
"memchr",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wit-bindgen"
|
name = "wit-bindgen"
|
||||||
@@ -3503,87 +2992,6 @@ dependencies = [
|
|||||||
"synstructure",
|
"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]]
|
[[package]]
|
||||||
name = "zerocopy"
|
name = "zerocopy"
|
||||||
version = "0.8.57"
|
version = "0.8.57"
|
||||||
@@ -3671,44 +3079,3 @@ name = "zmij"
|
|||||||
version = "1.0.23"
|
version = "1.0.23"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
|
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",
|
|
||||||
]
|
|
||||||
|
|||||||
+65
-10
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "smart-mount"
|
name = "smart-mount"
|
||||||
version = "2.0.0"
|
version = "2.0.4"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
authors = ['DragonSlayer_14']
|
authors = ['DragonSlayer_14']
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
@@ -11,7 +11,6 @@ description = "Bindet lokale/Cloud-Laufwerkspaare (WebDAV/SMB/NFS) dynamisch ein
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
config-ctdra = { version = "1.0.6", registry = "gitea" }
|
config-ctdra = { version = "1.0.6", registry = "gitea" }
|
||||||
logger-ctdra = { version = "1.0.5", registry = "gitea" }
|
logger-ctdra = { version = "1.0.5", registry = "gitea" }
|
||||||
program-ctdra = { version = "1.0.1", registry = "gitea" }
|
|
||||||
sudo-ctdra = { version = "1.0.1", registry = "gitea" }
|
sudo-ctdra = { version = "1.0.1", registry = "gitea" }
|
||||||
clap = { version = "4", features = ["derive", "env"] }
|
clap = { version = "4", features = ["derive", "env"] }
|
||||||
clap_complete = "4"
|
clap_complete = "4"
|
||||||
@@ -26,7 +25,6 @@ serde_json = "1"
|
|||||||
thiserror = "2"
|
thiserror = "2"
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
aes-gcm = "0.11.1"
|
aes-gcm = "0.11.1"
|
||||||
keyring = "4.2.0"
|
|
||||||
dialoguer = "0.12.0"
|
dialoguer = "0.12.0"
|
||||||
uuid = { version = "1", features = ["v4"] }
|
uuid = { version = "1", features = ["v4"] }
|
||||||
# Eigene, direkte Zufallsquelle für Nonce-/Schlüssel-Generierung, entkoppelt von aes-gcms
|
# Eigene, direkte Zufallsquelle für Nonce-/Schlüssel-Generierung, entkoppelt von aes-gcms
|
||||||
@@ -48,22 +46,39 @@ maintainer = "DragonSlayer_14"
|
|||||||
copyright = "2026 DragonSlayer_14"
|
copyright = "2026 DragonSlayer_14"
|
||||||
section = "net"
|
section = "net"
|
||||||
priority = "optional"
|
priority = "optional"
|
||||||
# mac2ip und nmap sind harte Laufzeitabhängigkeiten (MAC->IP-Auflösung); davfs2/cifs-utils/
|
# mac2ip ist eine harte Laufzeitabhängigkeit (MAC->IP-Auflösung;
|
||||||
# nfs-common werden bewusst NICHT hart verlangt, da smart-mount pro konfiguriertem Laufwerk
|
# davfs2/cifs-utils/nfs-common werden bewusst NICHT hart verlangt,
|
||||||
# nur das jeweils benötigte Mount-Backend zur Laufzeit prüft/lädt (siehe MountBackend::check_available).
|
# da smart-mount pro konfiguriertem Laufwerk nur das jeweils benötigte Mount-Backend
|
||||||
depends = "$auto, mac2ip, nmap"
|
# zur Laufzeit prüft/lädt (siehe MountBackend::check_available).
|
||||||
|
depends = "$auto, mac2ip"
|
||||||
recommends = "davfs2, cifs-utils, nfs-common"
|
recommends = "davfs2, cifs-utils, nfs-common"
|
||||||
extended-description = """\
|
extended-description = """\
|
||||||
smart-mount bindet Paare aus einem lokalen (LAN, WebDAV/SMB/NFS) und einem Cloud-Laufwerk
|
smart-mount bindet Paare aus einem lokalen (LAN, WebDAV/SMB/NFS) und einem Cloud-Laufwerk
|
||||||
ein: ist das lokale Laufwerk erreichbar, wird es gemountet, sonst automatisch das
|
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
|
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
|
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 = [
|
assets = [
|
||||||
["target/release/smart-mount", "usr/bin/smart-mount", "755"],
|
["target/release/smart-mount", "usr/bin/smart-mount", "755"],
|
||||||
["README.md", "usr/share/doc/smart-mount/README.md", "644"],
|
["README.md", "usr/share/doc/smart-mount/README.md", "644"],
|
||||||
["LICENSE", "usr/share/doc/smart-mount/copyright", "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"],
|
||||||
|
# Von 'cargo build --bin generate-completions' erzeugt (siehe .gitea/workflows/main.yaml) -
|
||||||
|
# kein 'smart-mount completions'-Subcommand mehr, Shell-Completion wird stattdessen wie jedes
|
||||||
|
# andere Paket-Asset automatisch mit installiert/entfernt. Debian/Ubuntu laden Zsh-Completions
|
||||||
|
# aus 'vendor-completions' (anders als Fedora/Arch, siehe [package.metadata.generate-rpm]).
|
||||||
|
["target/completions/smart-mount.bash", "usr/share/bash-completion/completions/smart-mount", "644"],
|
||||||
|
["target/completions/_smart-mount", "usr/share/zsh/vendor-completions/_smart-mount", "644"],
|
||||||
|
["target/completions/smart-mount.fish", "usr/share/fish/vendor_completions.d/smart-mount.fish", "644"],
|
||||||
|
# Kein 'smart-mount service'-Subcommand mehr - packaging/deb/postinst ruft dieses interne
|
||||||
|
# Hilfsprogramm (siehe src/bin/setup-cron.rs) stattdessen automatisch auf und richtet damit
|
||||||
|
# bei Bedarf (kein systemd, aber /etc/cron.d vorhanden) den Cron-Fallback ein.
|
||||||
|
["target/release/setup-cron", "usr/lib/smart-mount/setup-cron", "755"],
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.metadata.generate-rpm]
|
[package.metadata.generate-rpm]
|
||||||
@@ -71,14 +86,54 @@ assets = [
|
|||||||
{ source = "target/release/smart-mount", dest = "/usr/bin/smart-mount", mode = "755" },
|
{ 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 = "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 = "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" },
|
||||||
|
# Fedora/RHEL (wie Arch, siehe scripts/package-arch.py) laden Zsh-Completions aus
|
||||||
|
# 'site-functions' statt Debians 'vendor-completions' (siehe [package.metadata.deb]).
|
||||||
|
{ source = "target/completions/smart-mount.bash", dest = "/usr/share/bash-completion/completions/smart-mount", mode = "644" },
|
||||||
|
{ source = "target/completions/_smart-mount", dest = "/usr/share/zsh/site-functions/_smart-mount", mode = "644" },
|
||||||
|
{ source = "target/completions/smart-mount.fish", dest = "/usr/share/fish/vendor_completions.d/smart-mount.fish", mode = "644" },
|
||||||
|
# Kein 'smart-mount service'-Subcommand mehr - post_install_script ruft dieses interne
|
||||||
|
# Hilfsprogramm (siehe src/bin/setup-cron.rs) stattdessen automatisch auf und richtet damit
|
||||||
|
# bei Bedarf (kein systemd, aber /etc/cron.d vorhanden) den Cron-Fallback ein.
|
||||||
|
{ source = "target/release/setup-cron", dest = "/usr/lib/smart-mount/setup-cron", mode = "755" },
|
||||||
]
|
]
|
||||||
requires = { "mac2ip" = "*", "nmap" = "*" }
|
requires = { "mac2ip" = "*" }
|
||||||
suggests = { "davfs2" = "*", "cifs-utils" = "*", "nfs-utils" = "*" }
|
suggests = { "davfs2" = "*", "cifs-utils" = "*", "nfs-utils" = "*" }
|
||||||
|
# postinst/postrm-Äquivalente: richten den paketierten systemd-Dienst (bzw. ohne systemd den
|
||||||
|
# Cron-Fallback über setup-cron) automatisch ein/entfernen (Gegenstück zu
|
||||||
|
# packaging/deb/postinst+postrm). $1 in %postun: Anzahl verbleibender Versionen nach diesem
|
||||||
|
# Schritt - 0 nur bei vollständiger Deinstallation, nicht bei einem Upgrade. Die Cron-Datei wird
|
||||||
|
# in %postun direkt per "rm -f" entfernt statt über setup-cron selbst: zu diesem Zeitpunkt sind
|
||||||
|
# die Paketdateien - und damit auch setup-cron selbst - bereits entfernt.
|
||||||
|
post_install_script = """
|
||||||
|
if command -v systemctl >/dev/null 2>&1; then
|
||||||
|
systemctl daemon-reload || true
|
||||||
|
# smart-mount-mount.service mountet echte Netzlaufwerke (kann bei nicht erreichbaren
|
||||||
|
# Servern pro Paar mehrere zehn Sekunden dauern) - "enable --now" wuerde synchron darauf
|
||||||
|
# warten und damit die Paketinstallation blockieren, daher aktivieren + nur als
|
||||||
|
# asynchroner Job starten ("--no-block", kehrt sofort zurueck).
|
||||||
|
systemctl enable smart-mount-mount.service || true
|
||||||
|
systemctl start --no-block smart-mount-mount.service || true
|
||||||
|
systemctl enable --now smart-mount-watch.timer || true
|
||||||
|
fi
|
||||||
|
/usr/lib/smart-mount/setup-cron || true
|
||||||
|
"""
|
||||||
|
post_uninstall_script = """
|
||||||
|
if [ "$1" = "0" ]; then
|
||||||
|
if command -v systemctl >/dev/null 2>&1; then
|
||||||
|
systemctl disable --now smart-mount-mount.service smart-mount-watch.timer smart-mount-watch.service || true
|
||||||
|
systemctl daemon-reload || true
|
||||||
|
fi
|
||||||
|
rm -f /etc/cron.d/smart-mount
|
||||||
|
fi
|
||||||
|
"""
|
||||||
|
|
||||||
[package.metadata.arch]
|
[package.metadata.arch]
|
||||||
pkgrel = "1" # Wird in CI durch get-build-number.py dynamisch überschrieben
|
pkgrel = "1" # Wird in CI durch get-build-number.py dynamisch überschrieben
|
||||||
arch = "x86_64"
|
arch = "x86_64"
|
||||||
depends = ["gcc-libs", "glibc", "mac2ip", "nmap"]
|
depends = ["gcc-libs", "glibc", "mac2ip"]
|
||||||
optdepends = [
|
optdepends = [
|
||||||
"davfs2: WebDAV-Laufwerke einbinden",
|
"davfs2: WebDAV-Laufwerke einbinden",
|
||||||
"cifs-utils: SMB/CIFS-Laufwerke einbinden",
|
"cifs-utils: SMB/CIFS-Laufwerke einbinden",
|
||||||
|
|||||||
@@ -14,8 +14,9 @@ werden (z. B. über ein separates Sync-Tool).
|
|||||||
|
|
||||||
## Voraussetzungen
|
## Voraussetzungen
|
||||||
|
|
||||||
- **[`mac2ip`](https://gitea.creative-dragonslayer.de/Linuxapps/Mac2Ip)** und **`nmap`**: hart
|
- **[`mac2ip`](https://gitea.creative-dragonslayer.de/Linuxapps/Mac2Ip)**: hart benötigt, sofern
|
||||||
benötigt, sofern lokale Laufwerke per MAC-Adresse adressiert werden (Auflösung MAC → IP).
|
lokale Laufwerke per MAC-Adresse adressiert werden (Auflösung MAC → IP). `mac2ip` verlangt
|
||||||
|
`nmap` bereits selbst als eigene Paketabhängigkeit.
|
||||||
- **`timeout`** (GNU coreutils): auf praktisch jedem Linux-System bereits vorhanden (Basis-
|
- **`timeout`** (GNU coreutils): auf praktisch jedem Linux-System bereits vorhanden (Basis-
|
||||||
Systempaket). Wird verwendet, um `mount`/`umount`-Aufrufe zeitlich zu begrenzen - siehe
|
Systempaket). Wird verwendet, um `mount`/`umount`-Aufrufe zeitlich zu begrenzen - siehe
|
||||||
"Verhalten bei nicht mehr erreichbarem Server" unten.
|
"Verhalten bei nicht mehr erreichbarem Server" unten.
|
||||||
@@ -24,28 +25,32 @@ werden (z. B. über ein separates Sync-Tool).
|
|||||||
- `davfs2` für WebDAV-Laufwerke
|
- `davfs2` für WebDAV-Laufwerke
|
||||||
- `cifs-utils` für SMB/CIFS-Laufwerke
|
- `cifs-utils` für SMB/CIFS-Laufwerke
|
||||||
- `nfs-common` (Debian/Ubuntu) bzw. `nfs-utils` (Fedora/Arch) für NFS-Laufwerke
|
- `nfs-common` (Debian/Ubuntu) bzw. `nfs-utils` (Fedora/Arch) für NFS-Laufwerke
|
||||||
- Für MAC-basierte lokale Laufwerke im **Nutzerkontext**: `mac2ip` löst über `ip neigh` auf,
|
- `systemd`: smart-mount richtet sich beim Installieren des Pakets automatisch als
|
||||||
ohne dafür Root-Rechte zu benötigen - nur der letzte Fallback-Schritt (ein aktiver
|
System-Dienst ein (siehe "Automatischer Start beim Systemstart" unten). Ohne systemd, aber mit
|
||||||
`nmap`-Scan, falls das Zielgerät nicht in der ARP-Nachbartabelle steht) braucht
|
`/etc/cron.d`, wird beim Installieren automatisch ein Cron-Fallback eingerichtet - kein
|
||||||
passwortlosen `sudo`-Zugriff auf `nmap`. Ohne das schlägt die Auflösung in diesem Fall sauber
|
manueller Schritt nötig.
|
||||||
fehl (kein Absturz) - entweder passwortlosen `sudo` für `nmap` einrichten, oder das
|
|
||||||
entsprechende Laufwerkspaar im System-/root-Kontext betreiben.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## CLI-Nutzung
|
## 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
|
```bash
|
||||||
# Neues Laufwerkspaar interaktiv anlegen (fragt Name, Kontext, Mount-Typ, Adresse,
|
# Neues Laufwerkspaar interaktiv anlegen (fragt Name, Mount-Typ, Adresse, Freigabe und ggf.
|
||||||
# Freigabe und ggf. Zugangsdaten ab)
|
# Zugangsdaten ab)
|
||||||
smart-mount drive add
|
sudo smart-mount drive add
|
||||||
|
|
||||||
# Nicht-interaktiv per Flags (für Skripte/Automatisierung) - fehlende Pflichtfelder sind
|
# Nicht-interaktiv per Flags (für Skripte/Automatisierung) - fehlende Pflichtfelder sind
|
||||||
# dann ein Fehler statt eines (ohne Terminal ohnehin unmöglichen) Prompts. Jedes
|
# 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
|
# *-password-stdin-Flag liest genau eine Zeile von stdin (bei mehreren im selben Aufruf
|
||||||
# entsprechend mehrere Zeilen, eine pro Flag in Reihenfolge):
|
# entsprechend mehrere Zeilen, eine pro Flag in Reihenfolge):
|
||||||
smart-mount drive add --non-interactive \
|
sudo smart-mount drive add --non-interactive \
|
||||||
--name NAS --context system \
|
--name NAS \
|
||||||
--local-kind smb --local-ip 192.168.1.50 --local-share share --local-username nasuser --local-password-stdin \
|
--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 \
|
--cloud-kind webdav --cloud-host https://cloud.example.com/dav --cloud-share / --cloud-username clouduser --cloud-password cloud-pw-direkt \
|
||||||
<<< "lokales-passwort"
|
<<< "lokales-passwort"
|
||||||
@@ -55,110 +60,143 @@ smart-mount drive add --non-interactive \
|
|||||||
# Bestehendes Laufwerkspaar bearbeiten - nur angegebene Felder ändern sich, alles andere
|
# Bestehendes Laufwerkspaar bearbeiten - nur angegebene Felder ändern sich, alles andere
|
||||||
# (inkl. gespeichertem Passwort) bleibt unangetastet. Ohne Flags: interaktiver Durchlauf,
|
# (inkl. gespeichertem Passwort) bleibt unangetastet. Ohne Flags: interaktiver Durchlauf,
|
||||||
# vorbelegt mit den aktuellen Werten (inkl. Rückfrage, ob das Passwort geändert werden soll).
|
# 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
|
sudo 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> --non-interactive --local-password-stdin <<< "neues-passwort"
|
||||||
|
|
||||||
# Konfigurierte Laufwerkspaare auflisten / entfernen (--json für Skripte)
|
# Konfigurierte Laufwerkspaare auflisten / entfernen (--json für Skripte)
|
||||||
smart-mount drive list
|
smart-mount drive list
|
||||||
smart-mount drive list --json
|
smart-mount drive list --json
|
||||||
smart-mount drive remove <id>
|
sudo smart-mount drive remove <id>
|
||||||
|
|
||||||
# Ein einzelnes Paar oder alle einbinden/aushängen
|
# Ein einzelnes Paar oder alle einbinden/aushängen
|
||||||
smart-mount mount --name <id>
|
sudo smart-mount mount --name <id>
|
||||||
smart-mount mount --all
|
sudo smart-mount mount --all
|
||||||
smart-mount unmount --all
|
sudo smart-mount unmount --all
|
||||||
|
|
||||||
# Aktuellen Status (Mount-Zustand + Erreichbarkeit beider Seiten) anzeigen (--json für Skripte)
|
# Aktuellen Status (Mount-Zustand + Erreichbarkeit beider Seiten) anzeigen (--json für Skripte)
|
||||||
smart-mount status
|
smart-mount status
|
||||||
smart-mount status --json
|
smart-mount status --json
|
||||||
|
|
||||||
# Ein Reconcile-Durchlauf (lokal/Cloud-Umschaltung) - für systemd-Timer/Cron gedacht
|
# Ein Reconcile-Durchlauf (lokal/Cloud-Umschaltung) - für systemd-Timer/Cron gedacht
|
||||||
smart-mount watch
|
sudo smart-mount watch
|
||||||
|
|
||||||
# systemd-Units installieren (System- bzw. Nutzerkontext)
|
# Voraussetzungen prüfen (Binaries, Scheduler) - deckt gebündelt ab, was man sonst erst
|
||||||
sudo smart-mount service install --system
|
# einzeln beim Mount-Fehlschlag entdecken würde
|
||||||
smart-mount service install --user
|
|
||||||
|
|
||||||
# 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
|
|
||||||
smart-mount doctor
|
smart-mount doctor
|
||||||
smart-mount doctor --json
|
smart-mount doctor --json
|
||||||
|
|
||||||
# Shell-Completion-Skript ausgeben (bash/zsh/fish/elvish/powershell)
|
|
||||||
smart-mount completions bash > /etc/bash_completion.d/smart-mount
|
|
||||||
smart-mount completions zsh > "${fpath[1]}/_smart-mount"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Die Konfiguration liegt unter `~/.config/smart-mount/config.toml` (Nutzerkontext) bzw.
|
Shell-Completion (bash/zsh/fish) wird beim Installieren des .deb/.rpm/.pkg.tar.zst-Pakets
|
||||||
`/etc/smart-mount/config.toml` (root/System-Kontext) - automatisch aufgelöst je nachdem, ob
|
automatisch mit installiert (und beim Entfernen des Pakets automatisch mit entfernt) - dafür ist
|
||||||
`smart-mount` mit Root-Rechten läuft. Die verschlüsselte Zugangsdaten-Datenbank
|
kein eigenes `smart-mount`-Subcommand nötig.
|
||||||
(`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
|
### Wo die Laufwerke eingebunden werden
|
||||||
|
|
||||||
Jedes Laufwerkspaar bekommt sein **eigenes** Unterverzeichnis unter `settings.mount_base_dir`
|
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
|
standardmäßig `<mount_base_dir>/<Name des Paares>` (z. B. `/run/media/NAS`) - mehrere Paare
|
||||||
für `mount_base_dir`: `/run/media/smart-mount` im System-Kontext (root, ein gemeinsamer,
|
stören sich also nie gegenseitig, und der Ordnername ist beim Durchsuchen sofort erkennbar
|
||||||
keinem Nutzer zugeordneter Namensraum), `/run/media/<Nutzer>/smart-mount` im Nutzerkontext -
|
(statt einer kryptischen ID). Standardwert für `mount_base_dir`: `/run/media` - auf den
|
||||||
`/run/media` ist auf den meisten Systemen bereits die übliche Konvention für eingebundene
|
meisten Systemen bereits die übliche Konvention für eingebundene Wechseldatenträger/
|
||||||
Wechseldatenträger/Netzlaufwerke (z. B. udisks2/GNOME) und liegt auf `tmpfs`, muss also nie
|
Netzlaufwerke (z. B. udisks2/GNOME) und liegt auf `tmpfs`, muss also nie persistieren. Der
|
||||||
persistieren. Der Mountpoint selbst (und alle nötigen Elternverzeichnisse) werden bei jedem
|
Name wird dabei zu einem sicheren Verzeichnisnamen bereinigt (nur alphanumerische Zeichen
|
||||||
`mount`/`watch`-Lauf automatisch angelegt, falls sie fehlen - eigenes Anlegen ist nicht nötig.
|
sowie `-`/`_`, alles andere wird zu `-`); kollidiert das Ergebnis mit einem bereits
|
||||||
|
bestehenden Paar (Namen müssen nicht eindeutig sein), wird `-2`, `-3`, ... angehängt.
|
||||||
|
|
||||||
**Achtung bei Nutzer-Kontext-Paaren:** `/run/media` gehört standardmäßig `root:root` mit Modus
|
**Der Mountpoint lässt sich beim Anlegen frei überschreiben** - per `--mount-point <pfad>`
|
||||||
`0755` - ein normaler Nutzer kann dort also nicht einmal sein eigenes Unterverzeichnis
|
(nicht-interaktiv) oder im interaktiven Prompt, komplett unabhängig von `mount_base_dir`:
|
||||||
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.
|
|
||||||
|
|
||||||
Der Standard lässt sich in `config.toml` unter `[settings] mount_base_dir = "..."` jederzeit
|
```bash
|
||||||
auf einen beliebigen anderen Pfad ändern.
|
sudo smart-mount drive add --mount-point /mnt/nas --non-interactive ...
|
||||||
|
```
|
||||||
|
|
||||||
|
Bei `drive edit` bleibt der Mountpoint standardmäßig unverändert (kein Prompt dafür) - sonst
|
||||||
|
würde ein aktiver Mount verwaisen. Nur eine explizite `--mount-point`-Angabe ändert ihn (mit
|
||||||
|
einer Warnung, falls das Paar gerade aktiv gemountet ist - dann erst `unmount`, editieren,
|
||||||
|
dann erneut `mount`).
|
||||||
|
|
||||||
|
Der Mountpoint selbst (und alle nötigen Elternverzeichnisse) werden bei jedem `mount`/
|
||||||
|
`watch`-Lauf automatisch angelegt, falls sie fehlen - der gemeinsame Elternordner
|
||||||
|
(`mount_base_dir`, bzw. bei einem komplett eigenen `--mount-point` dessen Elternordner) fest
|
||||||
|
mit `0755` (durchquerbar für jeden lokalen Nutzer, unabhängig vom Umask des root-Prozesses),
|
||||||
|
das jeweilige Backing-Verzeichnis (siehe unten) bei gesetztem `owner_user` zusätzlich fest
|
||||||
|
`chown`t und `0700` (nur der Owner). Ist für ein Paar `owner_user` gesetzt, bekommt genau dieser
|
||||||
|
Nutzer bei CIFS/WebDAV zusätzlich ü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 für `mount_base_dir` lässt sich in `config.toml` unter
|
||||||
|
`[settings] mount_base_dir = "..."` jederzeit auf einen beliebigen anderen Pfad ändern.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Automatischer Start beim Systemstart
|
## 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
|
||||||
|
deaktivieren beide Units beim Installieren/Deinstallieren - siehe `packaging/deb/postinst` +
|
||||||
|
`packaging/deb/postrm` bzw. `[package.metadata.generate-rpm]`/`scripts/package-arch.py` in
|
||||||
|
`Cargo.toml`. Welche Laufwerkspaare dabei gemountet werden (und für welchen Nutzer, siehe
|
||||||
|
`owner_user` unten) steuert ausschließlich `/etc/smart-mount/config.toml` - nicht die
|
||||||
|
Unit-Dateien selbst.
|
||||||
|
|
||||||
|
`smart-mount-mount.service` mountet dabei echte Netzlaufwerke, was bei einem gerade nicht
|
||||||
|
erreichbaren Server pro Paar mehrere zehn Sekunden dauern kann (begrenzt auf max. 10 Minuten
|
||||||
|
insgesamt, siehe `TimeoutStartSec` in der Unit) - das Paket-Setup wird dadurch **nicht**
|
||||||
|
blockiert: `systemctl start --no-block` stößt den ersten Mount-Versuch beim Installieren nur an,
|
||||||
|
statt synchron auf ihn zu warten. `smart-mount-watch.timer` wird dagegen sofort synchron
|
||||||
|
gestartet (das Timer-"Armen" selbst ist immer augenblicklich fertig).
|
||||||
|
|
||||||
|
**Anderes Watch-Intervall als der Standard (120s):** `settings.watch_interval_secs` in
|
||||||
|
`config.toml` steuert nur den Cron-Fallback (s. u.) - der paketierte Timer hat ein fest
|
||||||
|
eingebautes Intervall. Zum Anpassen:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo smart-mount setup fstab # einmalig, nur nötig bei Nutzer-Kontext-Paaren
|
sudo systemctl edit smart-mount-watch.timer
|
||||||
sudo smart-mount service install --system # für System-Kontext-Paare
|
# [Timer]
|
||||||
smart-mount service install --user # für die eigenen Nutzer-Kontext-Paare
|
# OnUnitActiveSec=60s
|
||||||
```
|
```
|
||||||
|
|
||||||
`service install` wählt automatisch den passenden Mechanismus: ist `systemctl` vorhanden,
|
**Logs:** beide Units schreiben mit festem `SyslogIdentifier=smart-mount` ins systemd-Journal -
|
||||||
werden systemd-Units installiert (ein `oneshot`-Service für den initialen Mount beim Boot
|
`journalctl -u smart-mount-mount` / `-u smart-mount-watch` (oder gebündelt `journalctl -t
|
||||||
sowie ein Timer, der periodisch `smart-mount watch` aufruft - Intervall:
|
smart-mount`) zeigt sie an. Zusätzlich schreibt smart-mount dieselben Meldungen dauerhaft nach
|
||||||
`settings.watch_interval_secs`, Standard 120s). Ist kein systemd vorhanden, wird automatisch
|
`/var/log/smart-mount/log-<Datum>.log` (siehe `settings.log_level` für die Ausführlichkeit,
|
||||||
auf Cron ausgewichen - als root wird `/etc/cron.d/smart-mount` geschrieben, als normaler
|
Standard `info`) - unabhängig von der Journal-Rotation. `smart-mount-mount.service` bleibt nach
|
||||||
Nutzer die eigene, persönliche Crontab über `crontab -l`/`crontab -` aktualisiert (ein
|
einem erfolgreichen Lauf als "active (exited)" sichtbar (`RemainAfterExit=yes`), und ein
|
||||||
verwalteter Block lässt dabei bereits vorhandene, unabhängige Cron-Einträge unangetastet und
|
Fehlschlag beim Mounten mindestens eines konfigurierten Laufwerkspaars lässt den Dienst jetzt
|
||||||
verhindert Duplikate bei wiederholten Aufrufen). Ist weder systemd noch Cron vorhanden, werden
|
auch tatsächlich als "failed" erscheinen, statt es stillschweigend zu ignorieren.
|
||||||
stattdessen die beiden äquivalenten Zeilen zum manuellen Eintragen ausgegeben:
|
|
||||||
|
### Cron-Fallback (Systeme ohne systemd)
|
||||||
|
|
||||||
|
Ist beim Installieren/Upgraden des Pakets kein `systemctl` gefunden, aber `/etc/cron.d`
|
||||||
|
vorhanden, schreibt das Paket **automatisch** `/etc/cron.d/smart-mount` (kein manueller Schritt,
|
||||||
|
kein eigenes `smart-mount`-Subcommand) - das Intervall folgt `settings.watch_interval_secs`
|
||||||
|
(Standard 120s):
|
||||||
|
|
||||||
```cron
|
```cron
|
||||||
@reboot smart-mount mount --all
|
@reboot root /usr/bin/smart-mount mount --all
|
||||||
*/2 * * * * smart-mount watch
|
*/2 * * * * root /usr/bin/smart-mount watch
|
||||||
```
|
```
|
||||||
|
|
||||||
`smart-mount service crontab` erzwingt gezielt den Cron-Weg (z. B. um systemd bewusst zu
|
Zuständig dafür ist `setup-cron` (`src/bin/setup-cron.rs`, installiert nach
|
||||||
umgehen), mit identischem Verhalten wie der automatische Fallback von `install`.
|
`/usr/lib/smart-mount/setup-cron`) - ein internes Hilfsprogramm, kein Teil der öffentlichen
|
||||||
|
`smart-mount`-CLI, das die postinst-Skripte des Pakets (bzw. das `.INSTALL`-Skriptlet bei Arch)
|
||||||
```bash
|
automatisch aufrufen; beim Deinstallieren wird `/etc/cron.d/smart-mount` ebenso automatisch
|
||||||
sudo smart-mount service uninstall --system
|
wieder entfernt. Ist weder systemd noch `/etc/cron.d` vorhanden, meldet `setup-cron` das nur
|
||||||
smart-mount service uninstall --user
|
auf der Konsole (Paketinstallation schlägt dadurch nicht fehl) - die periodische Ausführung
|
||||||
```
|
muss dann manuell eingerichtet werden, z. B. über eine eigene Crontab-Zeile analog zu obigem
|
||||||
|
Beispiel.
|
||||||
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).
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -189,39 +227,26 @@ Architekturentscheidungen unten).
|
|||||||
beim Umschalten zuerst die neue Seite gemountet und der Symlink umgebogen wird, bevor die
|
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
|
alte Seite (mit eben diesem tolerierten Timeout) ausgehängt wird, blockiert ein
|
||||||
hängender Alt-Mount den sichtbaren Wechsel ohnehin nicht.
|
hängender Alt-Mount den sichtbaren Wechsel ohnehin nicht.
|
||||||
- **Privilegienmodell für Nutzer-Mounts**: `sudo smart-mount setup fstab` schreibt einmalig
|
- **Privilegienmodell**: SmartMount läuft ausschließlich als root/System-Dienst - es gibt
|
||||||
`/etc/fstab`-Einträge mit `user,exec,noauto` sowie nötige Gruppenmitgliedschaften (z. B.
|
keinen unprivilegierten Mount-Weg mehr (früher: `/etc/fstab`-Einträge mit `user,noauto`, über
|
||||||
`davfs2`-Gruppe). Jede Seite (lokal/Cloud) bekommt dabei ihr **eigenes, eindeutiges**
|
die ein einzelner Nutzer unprivilegiert selbst mounten konnte). Root mountet direkt
|
||||||
verstecktes Backing-Verzeichnis (nicht denselben Mountpoint für beide) - das entspricht
|
(`mount -t <typ> <quelle> <ziel> -o <optionen>`) für jedes konfigurierte Paar, unabhängig
|
||||||
exakt dem einzigen in `man 8 mount` ("Non-superuser mounts") dokumentierten Fall, statt sich
|
davon, ob/für welchen Nutzer `owner_user` gesetzt ist - siehe `owner_user` unten für die
|
||||||
auf unspezifiziertes Verhalten bei zwei Zeilen mit demselben Ziel zu verlassen. Der
|
Zugriffssteuerung. Das vereinfacht das Rechtemodell erheblich und schließt nebenbei die
|
||||||
konfigurierte, sichtbare Mountpoint selbst ist ein Symlink, den SmartMount zur Laufzeit
|
frühere strukturelle CIFS-Lücke (jeder unprivilegiert mount-berechtigte Nutzer konnte zuvor
|
||||||
zwischen den beiden Backing-Verzeichnissen umschaltet. `exec` wird explizit gesetzt, weil die
|
jedes CIFS-Nutzer-Kontext-Paar mounten, nicht nur sein eigenes) - Zugangsdaten sind jetzt nur
|
||||||
`user`-Option laut `man 8 mount` sonst für jedes Dateisystem automatisch `noexec` erzwingt -
|
noch root zugänglich.
|
||||||
ohne das könnten auf einem Nutzer-Kontext-Laufwerk liegende Skripte nicht ausgeführt werden.
|
|
||||||
- **Voller Zugriff für einen bestimmten Nutzer (`owner_user`)**: Bei CIFS/WebDAV (keine
|
- **Voller Zugriff für einen bestimmten Nutzer (`owner_user`)**: Bei CIFS/WebDAV (keine
|
||||||
nativen Unix-Rechte) setzt SmartMount automatisch `uid=`/`gid=`/`file_mode=0700`/
|
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,
|
`dir_mode=0700`, sobald ein Paar ein `owner_user` hat - optional, ohne `owner_user` gehört
|
||||||
optional bei System-Kontext). Das ist nicht nur für vollen Zugriff (inkl. Skript-Ausführung)
|
der Mount root. `smart-mount drive add` setzt `owner_user` standardmäßig auf den Nutzer, der
|
||||||
nötig, sondern bei WebDAV auch sicherheitsrelevant: `mount.davfs` erlaubt einem
|
`sudo` aufgerufen hat (`$SUDO_USER`) - wer ein Paar einrichtet, bekommt also automatisch
|
||||||
unprivilegierten Nutzer das Mounten einer `user`-Zeile laut `man mount.davfs` nur, wenn
|
vollen Zugriff darauf, sofern nicht per `--owner-user` (nicht-interaktiv) oder im
|
||||||
`uid=` auf ihn selbst zeigt - ohne das dürfte jedes Mitglied der Gruppe `davfs2` jedes
|
interaktiven Prompt bewusst ein anderer Nutzer angegeben bzw. abgelehnt wird. Bei NFS gibt es
|
||||||
konfigurierte Paar mounten, nicht nur sein eigenes (`setup fstab` verweigert daher Paare ohne
|
keine clientseitige `uid=`/`gid=`-Option, Zugriff bestimmt dort ausschließlich der Server über
|
||||||
`owner_user`).
|
die tatsächlichen Datei-Eigentümer/-Rechte des Exports - `owner_user` wirkt sich hier nur auf
|
||||||
|
das lokale Backing-Verzeichnis selbst aus (`chown`+`0700`, s. o.), nicht auf das, was der
|
||||||
**Bekannte, bewusst nicht behobene Lücke bei CIFS im Nutzerkontext:** `man mount.cifs`
|
Server tatsächlich exportiert.
|
||||||
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.
|
|
||||||
- **davfs2-Konfiguration**: SmartMount setzt in `davfs2.conf` automatisch `gui_optimize 1`
|
- **davfs2-Konfiguration**: SmartMount setzt in `davfs2.conf` automatisch `gui_optimize 1`
|
||||||
(bündelt PROPFIND-Anfragen, wichtig für grafische Dateimanager) sowie `buf_size 16384`
|
(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
|
(deutlich über dem Standard von 16 KiB) - Letzteres behebt ein bekanntes Praxisproblem, bei
|
||||||
@@ -230,10 +255,9 @@ Architekturentscheidungen unten).
|
|||||||
kleinem `buf_size` stillschweigend abgeschnitten).
|
kleinem `buf_size` stillschweigend abgeschnitten).
|
||||||
- **Zugangsdaten-Verschlüsselung**: Turso hat aktuell keine produktionsreife eingebaute
|
- **Zugangsdaten-Verschlüsselung**: Turso hat aktuell keine produktionsreife eingebaute
|
||||||
Verschlüsselung, daher verschlüsselt SmartMount Passwörter selbst (AES-256-GCM) vor der
|
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
|
Ablage. Der Master-Schlüssel liegt in einer Schlüsseldatei (`chmod 600`, neben der
|
||||||
secret-service) abgelegt; ist keins verfügbar (typisch für den root/System-Dienst sowie
|
Konfiguration, also `/etc/smart-mount/master.key`) - kein OS-Keyring, da SmartMount
|
||||||
Headless-Systeme), wird automatisch auf eine Schlüsseldatei (`chmod 600`, neben der
|
ausschließlich als root/System-Dienst läuft, für den es kein Nutzer-Keyring gibt.
|
||||||
Konfiguration) zurückgegriffen.
|
|
||||||
- **`mac2ip`-Integration**: SmartMount ruft `mac2ip --json --auto-trust-networks <mac>` auf.
|
- **`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?"-
|
`--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
|
Rückfrage automatisch bejahen und dauerhaft in seiner eigenen Cache-Datenbank merken - das
|
||||||
@@ -249,13 +273,19 @@ Architekturentscheidungen unten).
|
|||||||
│ └── config.toml # Cargo-Konfiguration (Linker für Cross-Compiling, Registry)
|
│ └── config.toml # Cargo-Konfiguration (Linker für Cross-Compiling, Registry)
|
||||||
├── .gitea/
|
├── .gitea/
|
||||||
│ └── workflows/ # CI/CD-Pipelines (Build, Tests, Security-Scans, Releases)
|
│ └── 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/
|
├── scripts/
|
||||||
│ ├── get-build-number.py # Dynamische Ermittlung der nächsten Paket-Revisionsnummer
|
│ ├── get-build-number.py # Dynamische Ermittlung der nächsten Paket-Revisionsnummer
|
||||||
│ ├── package-arch.py # Erstellung von Arch Linux .pkg.tar.zst Paketen
|
│ ├── package-arch.py # Erstellung von Arch Linux .pkg.tar.zst Paketen
|
||||||
│ └── report-security-issue.py # Security-Scan-Ergebnisse als Gitea-Issue melden
|
│ └── report-security-issue.py # Security-Scan-Ergebnisse als Gitea-Issue melden
|
||||||
├── src/
|
├── src/
|
||||||
│ ├── main.rs # Dünner Einstiegspunkt (CLI-Parsing, Dispatch)
|
│ ├── main.rs # Dünner Einstiegspunkt (CLI-Parsing, Dispatch)
|
||||||
│ ├── lib.rs # Bibliotheks-Wurzel
|
│ ├── lib.rs # Bibliotheks-Wurzel (inkl. `pub mod cli`)
|
||||||
|
│ ├── bin/ # Interne Hilfsprogramme (kein Teil der `smart-mount`-CLI):
|
||||||
|
│ │ # generate-completions (Build-Zeit), setup-cron (Paket-Installation)
|
||||||
│ ├── cli/ # `clap`-Subcommands
|
│ ├── cli/ # `clap`-Subcommands
|
||||||
│ ├── config/ # Konfigurationsschema + CRUD (config-ctdra)
|
│ ├── config/ # Konfigurationsschema + CRUD (config-ctdra)
|
||||||
│ ├── crypto/ # Verschlüsselung + Master-Key-Auflösung
|
│ ├── crypto/ # Verschlüsselung + Master-Key-Auflösung
|
||||||
@@ -263,8 +293,7 @@ Architekturentscheidungen unten).
|
|||||||
│ ├── mount/ # WebDAV/SMB/NFS-Backends, dynamischer Dispatch
|
│ ├── mount/ # WebDAV/SMB/NFS-Backends, dynamischer Dispatch
|
||||||
│ ├── network/ # Erreichbarkeit, mac2ip-Integration
|
│ ├── network/ # Erreichbarkeit, mac2ip-Integration
|
||||||
│ ├── reconcile/ # Watchdog-Entscheidungslogik (lokal/Cloud-Umschaltung)
|
│ ├── reconcile/ # Watchdog-Entscheidungslogik (lokal/Cloud-Umschaltung)
|
||||||
│ ├── systemd/ # systemd-Unit-Generierung/-Installation
|
│ └── systemd/ # Cron-Fallback (Systeme ohne systemd)
|
||||||
│ └── fstab/ # Einmaliges root-Setup für Nutzer-Mounts
|
|
||||||
├── tests/ # Cross-Modul-Integrationstests
|
├── tests/ # Cross-Modul-Integrationstests
|
||||||
├── Cargo.toml # Cargo Manifest & Paketierungsmetadaten (deb, rpm, arch)
|
├── Cargo.toml # Cargo Manifest & Paketierungsmetadaten (deb, rpm, arch)
|
||||||
├── LICENSE # Lizenzdatei (GPL-3.0-or-later)
|
├── LICENSE # Lizenzdatei (GPL-3.0-or-later)
|
||||||
|
|||||||
Executable
+25
@@ -0,0 +1,25 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Richtet smart-mount beim Installieren/Upgraden des Pakets automatisch als
|
||||||
|
# System-systemd-Dienst ein (Gegenstueck: postrm); ist kein systemd verfuegbar, richtet
|
||||||
|
# stattdessen setup-cron (siehe src/bin/setup-cron.rs) automatisch den Cron-Fallback ein,
|
||||||
|
# sofern /etc/cron.d existiert. Laeuft nur beim eigentlichen "configure"-Schritt (siehe
|
||||||
|
# Debian Policy Manual, Abschnitt 6.5), nicht bei "abort-upgrade"/"abort-remove" etc.
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if [ "$1" = "configure" ]; then
|
||||||
|
if command -v systemctl >/dev/null 2>&1; then
|
||||||
|
systemctl daemon-reload || true
|
||||||
|
# smart-mount-mount.service mountet echte Netzlaufwerke (kann pro konfiguriertem Paar
|
||||||
|
# mehrere zehn Sekunden dauern, wenn ein Server gerade nicht erreichbar ist) - "enable
|
||||||
|
# --now" wuerde synchron darauf warten und damit "apt"/"dpkg" fuer die gesamte Dauer
|
||||||
|
# blockieren. Daher: aktivieren (fuer den naechsten Boot) und der eigentliche Mount-Lauf
|
||||||
|
# nur als asynchroner Job ("--no-block", kehrt sofort zurueck) - das Paket-Setup selbst
|
||||||
|
# wartet also nie auf Netzwerk-Timeouts.
|
||||||
|
systemctl enable smart-mount-mount.service || true
|
||||||
|
systemctl start --no-block smart-mount-mount.service || true
|
||||||
|
systemctl enable --now smart-mount-watch.timer || true
|
||||||
|
fi
|
||||||
|
/usr/lib/smart-mount/setup-cron || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit 0
|
||||||
Executable
+20
@@ -0,0 +1,20 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Entfernt den beim Installieren eingerichteten systemd-Dienst sowie einen eventuell von
|
||||||
|
# setup-cron (siehe src/bin/setup-cron.rs) angelegten Cron-Fallback wieder (Gegenstueck:
|
||||||
|
# postinst). Nur bei tatsaechlicher Entfernung ("remove"/"purge"), nicht bei einem
|
||||||
|
# Upgrade (dort ersetzt dpkg die Unit-/Paket-Dateien einfach durch die neue Version, ohne
|
||||||
|
# den laufenden Dienst/Cron-Eintrag zwischenzeitlich zu deaktivieren). Die Cron-Datei wird
|
||||||
|
# hier direkt per "rm -f" entfernt statt ueber setup-cron selbst aufgerufen zu werden: zum
|
||||||
|
# Zeitpunkt, zu dem "postrm remove" laeuft, hat dpkg die Paketdateien - und damit auch
|
||||||
|
# setup-cron selbst - bereits entfernt (siehe Debian Policy Manual, Abschnitt 6.5).
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if { [ "$1" = "remove" ] || [ "$1" = "purge" ]; }; then
|
||||||
|
if command -v systemctl >/dev/null 2>&1; then
|
||||||
|
systemctl disable --now smart-mount-mount.service smart-mount-watch.timer smart-mount-watch.service || true
|
||||||
|
systemctl daemon-reload || true
|
||||||
|
fi
|
||||||
|
rm -f /etc/cron.d/smart-mount
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit 0
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=smart-mount: mount configured drive pairs at boot
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
# Bleibt nach einem erfolgreichen Lauf als "active (exited)" sichtbar (statt sofort wieder
|
||||||
|
# "inactive") - macht in 'systemctl status' auf einen Blick erkennbar, dass der letzte Boot-Lauf
|
||||||
|
# tatsächlich durchlief, statt nur "inactive" zu zeigen (das genauso gut "nie gelaufen" heißen
|
||||||
|
# könnte).
|
||||||
|
RemainAfterExit=yes
|
||||||
|
ExecStart=/usr/bin/smart-mount mount --all
|
||||||
|
# Harte Obergrenze statt des systemd-Standards (üblicherweise 90s): pro konfiguriertem Paar
|
||||||
|
# werden Erreichbarkeit UND Mount-Versuch sequenziell geprüft (bis zu ~20s Erreichbarkeits-
|
||||||
|
# check + 30s Mount-Timeout, siehe crate::mount::MOUNT_TIMEOUT_SECS), bei mehreren Paaren
|
||||||
|
# summiert sich das - 90s würde bei zwei oder mehr gerade nicht erreichbaren Servern schon
|
||||||
|
# reichen, um mitten im Mount-Versuch abgebrochen zu werden. 10 Minuten sind großzügig genug für
|
||||||
|
# eine typische Anzahl Paare, ohne bei einem echten Hänger (z. B. einer verwaisten Dateisperre)
|
||||||
|
# unbegrenzt zu warten.
|
||||||
|
TimeoutStartSec=600
|
||||||
|
# Explizit (statt sich auf den systemd-Standard zu verlassen): stdout/stderr gehen ins Journal,
|
||||||
|
# unter einem festen, von der jeweiligen Unit unabhängigen Tag - 'journalctl -t smart-mount'
|
||||||
|
# zeigt damit die Ausgaben aller smart-mount-Units gebündelt.
|
||||||
|
StandardOutput=journal
|
||||||
|
StandardError=journal
|
||||||
|
SyslogIdentifier=smart-mount
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=smart-mount: check reachability and switch local/cloud if needed
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart=/usr/bin/smart-mount watch
|
||||||
|
StandardOutput=journal
|
||||||
|
StandardError=journal
|
||||||
|
SyslogIdentifier=smart-mount
|
||||||
@@ -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
|
||||||
+35
-5
@@ -1,29 +1,59 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||||
"extends": ["config:recommended"],
|
"extends": [
|
||||||
|
"config:recommended"
|
||||||
|
],
|
||||||
"timezone": "Europe/Berlin",
|
"timezone": "Europe/Berlin",
|
||||||
"schedule": ["before 6am on monday"],
|
"schedule": [
|
||||||
|
"before 6am"
|
||||||
|
],
|
||||||
"baseBranchPatterns": [
|
"baseBranchPatterns": [
|
||||||
"dev"
|
"dev"
|
||||||
],
|
],
|
||||||
"packageRules": [
|
"packageRules": [
|
||||||
{
|
{
|
||||||
"matchFileNames": [".gitea/workflows/**"],
|
"matchFileNames": [
|
||||||
|
".gitea/workflows/**"
|
||||||
|
],
|
||||||
"groupName": "Gitea Actions",
|
"groupName": "Gitea Actions",
|
||||||
"separateMajorMinor": false,
|
"separateMajorMinor": false,
|
||||||
"separateMinorPatch": false
|
"separateMinorPatch": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"matchManagers": ["cargo"],
|
"matchManagers": [
|
||||||
|
"cargo"
|
||||||
|
],
|
||||||
"groupName": "Cargo Dependencies",
|
"groupName": "Cargo Dependencies",
|
||||||
"separateMajorMinor": false,
|
"separateMajorMinor": false,
|
||||||
"separateMinorPatch": false
|
"separateMinorPatch": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"matchManagers": ["dockerfile", "docker-compose"],
|
"matchManagers": [
|
||||||
|
"dockerfile",
|
||||||
|
"docker-compose"
|
||||||
|
],
|
||||||
"groupName": "Docker-Images",
|
"groupName": "Docker-Images",
|
||||||
"separateMajorMinor": false,
|
"separateMajorMinor": false,
|
||||||
"separateMinorPatch": false
|
"separateMinorPatch": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Patch-/Minor-/Digest-Updates automatisch mergen, sobald alle CI-Checks (inkl. Unit-Tests) erfolgreich sind - Major-Updates sind unten explizit ausgenommen (siehe nächste Regel).",
|
||||||
|
"matchUpdateTypes": [
|
||||||
|
"patch",
|
||||||
|
"minor",
|
||||||
|
"digest",
|
||||||
|
"lockFileMaintenance"
|
||||||
|
],
|
||||||
|
"automerge": true,
|
||||||
|
"automergeType": "pr",
|
||||||
|
"platformAutomerge": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Major-Updates immer manuell prüfen, da potenziell brechende Änderungen.",
|
||||||
|
"matchUpdateTypes": [
|
||||||
|
"major"
|
||||||
|
],
|
||||||
|
"automerge": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"customManagers": [
|
"customManagers": [
|
||||||
|
|||||||
+144
-7
@@ -17,10 +17,6 @@ TARGET_ARCH_MAP = {
|
|||||||
"aarch64-unknown-linux-musl": "aarch64",
|
"aarch64-unknown-linux-musl": "aarch64",
|
||||||
"aarch64": "aarch64",
|
"aarch64": "aarch64",
|
||||||
"arm64": "aarch64",
|
"arm64": "aarch64",
|
||||||
"i686-unknown-linux-gnu": "i686",
|
|
||||||
"i686-unknown-linux-musl": "i686",
|
|
||||||
"i686": "i686",
|
|
||||||
"i386": "i686",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -45,6 +41,108 @@ def resolve_pkgrel(version=None, default="1"):
|
|||||||
return str(default)
|
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 collect_completions(completions_dir="target/completions"):
|
||||||
|
"""Findet die von 'cargo build --bin generate-completions' erzeugten Shell-Completion-
|
||||||
|
Skripte (siehe src/bin/generate-completions.rs), falls vorhanden, und ordnet sie generisch -
|
||||||
|
ohne den Anwendungsnamen zu kennen - allein anhand ihrer clap_complete-Namenskonvention
|
||||||
|
(Bash: '<name>.bash', Zsh: '_<name>', Fish: '<name>.fish') dem jeweiligen
|
||||||
|
System-Verzeichnis zu. Gibt eine Liste aus (Quellpfad, Zielverzeichnis, Zieldateiname) zurück."""
|
||||||
|
if not os.path.isdir(completions_dir):
|
||||||
|
return []
|
||||||
|
|
||||||
|
mapping = []
|
||||||
|
for f in sorted(os.listdir(completions_dir)):
|
||||||
|
src = os.path.join(completions_dir, f)
|
||||||
|
if f.endswith(".bash"):
|
||||||
|
mapping.append((src, "usr/share/bash-completion/completions", f[: -len(".bash")]))
|
||||||
|
elif f.endswith(".fish"):
|
||||||
|
mapping.append((src, "usr/share/fish/vendor_completions.d", f))
|
||||||
|
elif f.startswith("_"):
|
||||||
|
mapping.append((src, "usr/share/zsh/site-functions", f))
|
||||||
|
return mapping
|
||||||
|
|
||||||
|
|
||||||
|
def find_helper_binary(name, helper, target_triple=None):
|
||||||
|
"""Sucht eine optionale interne Hilfs-Binary (eigenes Cargo-Binary-Target unter
|
||||||
|
'src/bin/<helper>.rs', z. B. fuer einen Cron-Fallback) am selben kompilierten Ort wie die
|
||||||
|
Haupt-Binary - generisch ueber den fest bekannten Hilfsprogramm-Namen, ohne den
|
||||||
|
Anwendungsnamen selbst zu kennen. Gibt None zurueck, falls nicht gefunden (z. B. weil das
|
||||||
|
abgeleitete Projekt kein solches Hilfsprogramm hat)."""
|
||||||
|
candidates = []
|
||||||
|
if target_triple:
|
||||||
|
candidates.append(f"target/{target_triple}/release/{helper}")
|
||||||
|
candidates.append(f"target/release/{helper}")
|
||||||
|
for p in candidates:
|
||||||
|
if os.path.exists(p):
|
||||||
|
return p
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def build_install_scriptlet(name, installable, all_units, has_cron_helper):
|
||||||
|
"""Erzeugt den Inhalt einer Arch-'.INSTALL'-Datei (siehe `man PKGBUILD`, Abschnitt
|
||||||
|
'install'): aktiviert/startet paketierte systemd-Units beim Installieren/Upgraden und
|
||||||
|
deaktiviert sie beim Entfernen, und ruft - sofern vorhanden - generisch das interne
|
||||||
|
Cron-Fallback-Hilfsprogramm ('usr/lib/<name>/setup-cron', siehe src/bin/setup-cron.rs) beim
|
||||||
|
Installieren/Upgraden auf bzw. entfernt dessen Cron-Datei beim Entfernen wieder. Rein anhand
|
||||||
|
der tatsächlich gefundenen Unit-Dateien/Hilfsprogramme zusammengesetzt, ohne den
|
||||||
|
Anwendungsnamen selbst hart zu codieren (der als `name`-Parameter hereinkommt).
|
||||||
|
|
||||||
|
'.service'-Units werden bewusst NICHT über 'enable --now' gestartet: Bei einem oneshot-
|
||||||
|
Service (typischer Fall für ein Boot-Setup-Skript) würde das den ExecStart-Befehl synchron
|
||||||
|
ausführen und damit 'pacman' für die gesamte Laufzeit des Skripts blockieren (siehe
|
||||||
|
packaging/deb/postinst für den konkreten Fall, der das erst sichtbar gemacht hat: ein
|
||||||
|
Mount-Versuch gegen ein gerade nicht erreichbares Netzlaufwerk). '.timer'-Units sind davon
|
||||||
|
nicht betroffen - sie nur zu "armen" ist immer sofort fertig - und starten daher weiterhin
|
||||||
|
synchron mit 'enable --now'."""
|
||||||
|
timers = [u for u in installable if u.endswith(".timer")]
|
||||||
|
services = [u for u in installable if not u.endswith(".timer")]
|
||||||
|
all_units_str = " ".join(all_units)
|
||||||
|
|
||||||
|
systemd_enable = " systemctl daemon-reload >/dev/null 2>&1 || true\n" if installable else ""
|
||||||
|
if timers:
|
||||||
|
systemd_enable += f" systemctl enable --now {' '.join(timers)} >/dev/null 2>&1 || true\n"
|
||||||
|
for service in services:
|
||||||
|
systemd_enable += f" systemctl enable {service} >/dev/null 2>&1 || true\n"
|
||||||
|
systemd_enable += f" systemctl start --no-block {service} >/dev/null 2>&1 || true\n"
|
||||||
|
|
||||||
|
systemd_reload = " systemctl daemon-reload >/dev/null 2>&1 || true\n" if all_units else ""
|
||||||
|
systemd_disable = (
|
||||||
|
f" systemctl disable --now {all_units_str} >/dev/null 2>&1 || true\n" if all_units else ""
|
||||||
|
)
|
||||||
|
# Ein zusaetzlicher Cron-Eintrag ist redundant, wenn systemd verfuegbar ist - das
|
||||||
|
# Hilfsprogramm selbst prueft das (siehe src/bin/setup-cron.rs), hier wird es einfach
|
||||||
|
# unconditional aufgerufen.
|
||||||
|
cron_install = f" /usr/lib/{name}/setup-cron >/dev/null 2>&1 || true\n" if has_cron_helper else ""
|
||||||
|
cron_remove = f" rm -f /etc/cron.d/{name}\n" if has_cron_helper else ""
|
||||||
|
|
||||||
|
return (
|
||||||
|
"post_install() {\n" + systemd_enable + cron_install + "}\n\n"
|
||||||
|
"post_upgrade() {\n" + systemd_reload + cron_install + "}\n\n"
|
||||||
|
"pre_remove() {\n" + systemd_disable + cron_remove + "}\n\n"
|
||||||
|
"post_remove() {\n" + systemd_reload + "}\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def build_package(target_triple=None, target_arch=None, pkgrel=None):
|
def build_package(target_triple=None, target_arch=None, pkgrel=None):
|
||||||
metadata = json.loads(subprocess.check_output(["cargo", "metadata", "--format-version", "1", "--no-deps"]))
|
metadata = json.loads(subprocess.check_output(["cargo", "metadata", "--format-version", "1", "--no-deps"]))
|
||||||
pkg = metadata["packages"][0]
|
pkg = metadata["packages"][0]
|
||||||
@@ -102,6 +200,35 @@ def build_package(target_triple=None, target_arch=None, pkgrel=None):
|
|||||||
if os.path.exists("README.md"):
|
if os.path.exists("README.md"):
|
||||||
subprocess.run(["install", "-m", "644", "README.md", f"{doc_dir}/README.md"], check=True)
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
|
for src, dest_dir, dest_name in collect_completions():
|
||||||
|
target_dir = os.path.join(build_dir, dest_dir)
|
||||||
|
os.makedirs(target_dir, exist_ok=True)
|
||||||
|
subprocess.run(
|
||||||
|
["install", "-m", "644", src, os.path.join(target_dir, dest_name)],
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
setup_cron_path = find_helper_binary(name, "setup-cron", target_triple)
|
||||||
|
has_cron_helper = setup_cron_path is not None
|
||||||
|
if has_cron_helper:
|
||||||
|
helper_dir = os.path.join(build_dir, f"usr/lib/{name}")
|
||||||
|
os.makedirs(helper_dir, exist_ok=True)
|
||||||
|
subprocess.run(
|
||||||
|
["install", "-m", "755", setup_cron_path, os.path.join(helper_dir, "setup-cron")],
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
|
||||||
installed_size = subprocess.check_output(["du", "-sb", build_dir]).decode().split()[0]
|
installed_size = subprocess.check_output(["du", "-sb", build_dir]).decode().split()[0]
|
||||||
builddate = str(int(time.time()))
|
builddate = str(int(time.time()))
|
||||||
|
|
||||||
@@ -121,21 +248,31 @@ def build_package(target_triple=None, target_arch=None, pkgrel=None):
|
|||||||
pkginfo_lines.append(f"depend = {dep}")
|
pkginfo_lines.append(f"depend = {dep}")
|
||||||
for optdep in optdepends:
|
for optdep in optdepends:
|
||||||
pkginfo_lines.append(f"optdepend = {optdep}")
|
pkginfo_lines.append(f"optdepend = {optdep}")
|
||||||
|
needs_install_scriptlet = bool(all_units) or has_cron_helper
|
||||||
|
if needs_install_scriptlet:
|
||||||
|
pkginfo_lines.append(f"install = {name}.install")
|
||||||
pkginfo_lines.append("makepkgopt = strip\n")
|
pkginfo_lines.append("makepkgopt = strip\n")
|
||||||
|
|
||||||
with open(os.path.join(build_dir, ".PKGINFO"), "w") as f:
|
with open(os.path.join(build_dir, ".PKGINFO"), "w") as f:
|
||||||
f.write("\n".join(pkginfo_lines))
|
f.write("\n".join(pkginfo_lines))
|
||||||
|
|
||||||
|
tar_members = [".PKGINFO", "usr"]
|
||||||
|
if needs_install_scriptlet:
|
||||||
|
install_script_name = f"{name}.install"
|
||||||
|
with open(os.path.join(build_dir, install_script_name), "w") as f:
|
||||||
|
f.write(build_install_scriptlet(name, installable_units, all_units, has_cron_helper))
|
||||||
|
tar_members.append(install_script_name)
|
||||||
|
|
||||||
os.makedirs("target/arch", exist_ok=True)
|
os.makedirs("target/arch", exist_ok=True)
|
||||||
output_file = os.path.abspath(f"target/arch/{name}-{version}-{pkgrel}-{arch}.pkg.tar.zst")
|
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}")
|
print(f"Arch-Paket erfolgreich erstellt: {output_file}")
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = argparse.ArgumentParser(description="Erstellt Arch Linux-Pakete (.pkg.tar.zst)")
|
parser = argparse.ArgumentParser(description="Erstellt Arch Linux-Pakete (.pkg.tar.zst)")
|
||||||
parser.add_argument("--target", help="Rust Target-Triple (z.B. x86_64-unknown-linux-gnu, aarch64-unknown-linux-gnu, i686-unknown-linux-gnu)")
|
parser.add_argument("--target", help="Rust Target-Triple (z.B. x86_64-unknown-linux-gnu, aarch64-unknown-linux-gnu)")
|
||||||
parser.add_argument("--arch", help="Architektur (z.B. x86_64, aarch64, i686)")
|
parser.add_argument("--arch", help="Architektur (z.B. x86_64, aarch64)")
|
||||||
parser.add_argument("--pkgrel", help="Release-/Build-Nummer (z.B. 1, 2, ...)")
|
parser.add_argument("--pkgrel", help="Release-/Build-Nummer (z.B. 1, 2, ...)")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
//! Erzeugt die Shell-Completion-Skripte (bash/zsh/fish) für `smart-mount` als Dateien in einem
|
||||||
|
//! Ausgabeverzeichnis - kein `smart-mount`-Subcommand mehr (siehe `smart_mount::cli::Commands`),
|
||||||
|
//! sondern ein separates Build-Hilfsprogramm, das ausschließlich während der CI-Paketierung
|
||||||
|
//! aufgerufen wird (siehe `.gitea/workflows/main.yaml`/`testing.yaml`): die erzeugten Dateien
|
||||||
|
//! werden dort als normale Paket-Assets gebündelt, sodass sie beim Installieren des .deb/.rpm/
|
||||||
|
//! .pkg.tar.zst-Pakets automatisch mit installiert und beim Entfernen automatisch mit entfernt
|
||||||
|
//! werden - ganz ohne Zutun des Nutzers.
|
||||||
|
//!
|
||||||
|
//! Nutzt `smart_mount::cli::Cli` als einzige Quelle der CLI-Definition (kein separat gepflegtes
|
||||||
|
//! Duplikat), da `cli` als `pub mod` Teil der Library ist und so auch von diesem zusätzlichen
|
||||||
|
//! Binary-Target aus der Cargo-eigenen `src/bin/`-Autodiscovery erreichbar ist.
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use clap::CommandFactory;
|
||||||
|
use clap_complete::Shell;
|
||||||
|
use smart_mount::cli::Cli;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let Some(out_dir) = std::env::args_os().nth(1).map(PathBuf::from) else {
|
||||||
|
eprintln!("usage: generate-completions <output-dir>");
|
||||||
|
std::process::exit(1);
|
||||||
|
};
|
||||||
|
std::fs::create_dir_all(&out_dir)
|
||||||
|
.unwrap_or_else(|e| panic!("could not create '{}': {e}", out_dir.display()));
|
||||||
|
|
||||||
|
let mut cmd = Cli::command();
|
||||||
|
let bin_name = cmd.get_name().to_string();
|
||||||
|
for shell in [Shell::Bash, Shell::Zsh, Shell::Fish] {
|
||||||
|
let path = clap_complete::generate_to(shell, &mut cmd, &bin_name, &out_dir)
|
||||||
|
.unwrap_or_else(|e| panic!("could not generate {shell} completions: {e}"));
|
||||||
|
println!("generated: {}", path.display());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
//! Richtet den Cron-Fallback für den periodischen `watch`-Lauf automatisch ein, sofern kein
|
||||||
|
//! systemd verfügbar ist - kein `smart-mount service`-Subcommand mehr (siehe
|
||||||
|
//! `smart_mount::cli::Commands`), sondern ein separates, nicht auf `PATH` liegendes
|
||||||
|
//! Hilfsprogramm (installiert nach `/usr/lib/smart-mount/setup-cron`), das ausschließlich vom
|
||||||
|
//! postinst-Skript der .deb/.rpm/.pkg.tar.zst-Pakete mit dem Argument `install` aufgerufen wird
|
||||||
|
//! (siehe `packaging/deb/postinst`, `[package.metadata.generate-rpm].post_install_script` in
|
||||||
|
//! `Cargo.toml`, sowie `scripts/package-arch.py`). Das Entfernen eines zuvor installierten
|
||||||
|
//! Cron-Eintrags beim Deinstallieren übernehmen die jeweiligen postrm-Skripte/Scriptlets direkt
|
||||||
|
//! (ein einfaches `rm -f`) statt dieses Programm mit `remove` aufzurufen: zum Zeitpunkt, zu dem
|
||||||
|
//! `postrm remove` bei Debian bzw. `%postun` bei RPM laufen, sind die Paketdateien - und damit
|
||||||
|
//! auch dieses Hilfsprogramm selbst - bereits von der Platte entfernt.
|
||||||
|
//!
|
||||||
|
//! Nutzt `smart_mount::systemd`/`smart_mount::config` als einzige Quelle der
|
||||||
|
//! Cron-Installationslogik (kein separat gepflegtes Duplikat in Shell), da beide als `pub mod`
|
||||||
|
//! Teil der Library sind und so auch von diesem zusätzlichen Binary-Target aus der
|
||||||
|
//! Cargo-eigenen `src/bin/`-Autodiscovery erreichbar sind.
|
||||||
|
|
||||||
|
use smart_mount::config;
|
||||||
|
use smart_mount::systemd;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
// Ein zusätzlicher Cron-Eintrag wäre redundant/unerwünscht, wenn systemd verfügbar ist - die
|
||||||
|
// paketierten systemd-Units (siehe packaging/systemd/) übernehmen dann bereits alles.
|
||||||
|
if systemd::is_available() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
config::init();
|
||||||
|
let watch_interval_secs = config::pairs::load()
|
||||||
|
.map(|cfg| cfg.settings.watch_interval_secs)
|
||||||
|
.unwrap_or(120);
|
||||||
|
|
||||||
|
match systemd::install_cron(watch_interval_secs) {
|
||||||
|
Ok(systemd::CronInstallOutcome::SystemFile(path)) => {
|
||||||
|
println!(
|
||||||
|
"smart-mount: cron fallback installed at '{}'",
|
||||||
|
path.display()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(systemd::CronInstallOutcome::Unavailable) => {
|
||||||
|
eprintln!(
|
||||||
|
"smart-mount: neither systemd nor '/etc/cron.d' found - the periodic 'watch' \
|
||||||
|
call must be set up manually, e.g.:\n{}",
|
||||||
|
systemd::crontab_equivalent(watch_interval_secs)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("smart-mount: could not install the cron fallback: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-2
@@ -1,8 +1,8 @@
|
|||||||
//! `smart-mount doctor` - prüft die im Laufe der Entwicklung angesammelten Voraussetzungen
|
//! `smart-mount doctor` - prüft die im Laufe der Entwicklung angesammelten Voraussetzungen
|
||||||
//! (Binaries, Gruppenmitgliedschaft, fstab-Setup, Scheduler) gebündelt an einer Stelle.
|
//! (Binaries, Gruppenmitgliedschaft, fstab-Setup, Scheduler) gebündelt an einer Stelle.
|
||||||
|
|
||||||
use smart_mount::config;
|
use crate::config;
|
||||||
use smart_mount::doctor::{self, CheckStatus};
|
use crate::doctor::{self, CheckStatus};
|
||||||
|
|
||||||
pub async fn run(json: bool) -> anyhow::Result<()> {
|
pub async fn run(json: bool) -> anyhow::Result<()> {
|
||||||
let cfg = config::pairs::load()?;
|
let cfg = config::pairs::load()?;
|
||||||
|
|||||||
+232
-90
@@ -6,15 +6,14 @@
|
|||||||
//! Fehler statt eines Prompts, der ohne TTY ohnehin fehlschlagen würde).
|
//! Fehler statt eines Prompts, der ohne TTY ohnehin fehlschlagen würde).
|
||||||
|
|
||||||
use std::net::Ipv4Addr;
|
use std::net::Ipv4Addr;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
use clap::{Args, Subcommand};
|
use clap::{Args, Subcommand};
|
||||||
use dialoguer::{Confirm, Input, Password, Select};
|
use dialoguer::{Confirm, Input, Password, Select};
|
||||||
|
|
||||||
use smart_mount::config::{
|
use crate::config::{self, AppConfig, CloudSide, DrivePair, LocalAddress, LocalSide, MountKind};
|
||||||
self, AppConfig, CloudSide, DrivePair, LocalAddress, LocalSide, MountContext, MountKind,
|
use crate::db::credentials::{Credential, CredentialStore, Side};
|
||||||
};
|
|
||||||
use smart_mount::db::credentials::{Credential, CredentialStore, Side};
|
|
||||||
|
|
||||||
#[derive(Subcommand)]
|
#[derive(Subcommand)]
|
||||||
pub enum DriveAction {
|
pub enum DriveAction {
|
||||||
@@ -40,8 +39,12 @@ pub enum DriveAction {
|
|||||||
pub struct DriveArgs {
|
pub struct DriveArgs {
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
name: Option<String>,
|
name: Option<String>,
|
||||||
#[arg(long, value_enum)]
|
/// Where the pair's symlink/backing directories live. Default (on 'add', derived from the
|
||||||
context: Option<MountContext>,
|
/// name): '<mount_base_dir>/<name>' (see 'settings.mount_base_dir'). Ignored on 'edit'
|
||||||
|
/// unless explicitly given - an existing pair's mount point stays fixed otherwise, so an
|
||||||
|
/// active mount doesn't get orphaned.
|
||||||
|
#[arg(long)]
|
||||||
|
mount_point: Option<PathBuf>,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
owner_user: Option<String>,
|
owner_user: Option<String>,
|
||||||
|
|
||||||
@@ -51,6 +54,8 @@ pub struct DriveArgs {
|
|||||||
local_ip: Option<Ipv4Addr>,
|
local_ip: Option<Ipv4Addr>,
|
||||||
#[arg(long, conflicts_with = "local_ip")]
|
#[arg(long, conflicts_with = "local_ip")]
|
||||||
local_mac: Option<String>,
|
local_mac: Option<String>,
|
||||||
|
/// Meaning depends on '--local-kind': the SMB share name (e.g. 'media'), the NFS export
|
||||||
|
/// path (e.g. '/export/media'), or the WebDAV URL path (e.g. '/remote.php/dav/files/user').
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
local_share: Option<String>,
|
local_share: Option<String>,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
@@ -67,6 +72,8 @@ pub struct DriveArgs {
|
|||||||
cloud_kind: Option<MountKind>,
|
cloud_kind: Option<MountKind>,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
cloud_host: Option<String>,
|
cloud_host: Option<String>,
|
||||||
|
/// Meaning depends on '--cloud-kind': the SMB share name (e.g. 'media'), the NFS export
|
||||||
|
/// path (e.g. '/export/media'), or the WebDAV URL path (e.g. '/remote.php/dav/files/user').
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
cloud_share: Option<String>,
|
cloud_share: Option<String>,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
@@ -85,6 +92,18 @@ pub struct DriveArgs {
|
|||||||
non_interactive: bool,
|
non_interactive: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Beschriftet das `share`-Feld abhängig vom bereits gewählten Mount-Typ statt des
|
||||||
|
/// generischen "share/export path" - die drei Backends legen dort inhaltlich verschiedene
|
||||||
|
/// Dinge ab (SMB-Freigabename, NFS-Export-Pfad, WebDAV-URL-Pfad), siehe
|
||||||
|
/// `mount::target::format_source`.
|
||||||
|
fn share_field_label(kind: MountKind, prefix: &str) -> String {
|
||||||
|
match kind {
|
||||||
|
MountKind::WebDav => format!("{prefix} WebDAV path (e.g. '/remote.php/dav/files/user')"),
|
||||||
|
MountKind::Smb => format!("{prefix} share name (e.g. 'media')"),
|
||||||
|
MountKind::Nfs => format!("{prefix} export path (e.g. '/export/media')"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn run(action: DriveAction) -> anyhow::Result<()> {
|
pub async fn run(action: DriveAction) -> anyhow::Result<()> {
|
||||||
match action {
|
match action {
|
||||||
DriveAction::Add(args) => add(args).await,
|
DriveAction::Add(args) => add(args).await,
|
||||||
@@ -108,10 +127,9 @@ async fn list(json: bool) -> anyhow::Result<()> {
|
|||||||
}
|
}
|
||||||
for pair in &cfg.pairs {
|
for pair in &cfg.pairs {
|
||||||
println!(
|
println!(
|
||||||
"{} \"{}\" [{:?}] lokal={} cloud={} -> {}",
|
"{} \"{}\" lokal={} cloud={} -> {}",
|
||||||
pair.id,
|
pair.id,
|
||||||
pair.name,
|
pair.name,
|
||||||
pair.context,
|
|
||||||
pair.local.kind.as_str(),
|
pair.local.kind.as_str(),
|
||||||
pair.cloud.kind.as_str(),
|
pair.cloud.kind.as_str(),
|
||||||
pair.mount_point.display()
|
pair.mount_point.display()
|
||||||
@@ -121,9 +139,11 @@ async fn list(json: bool) -> anyhow::Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn remove(id: &str) -> 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
|
// Vor dem Entfernen aus der Config nachschlagen, damit wir hinterher noch wissen, welche
|
||||||
// Mount-Typen/Adressen betroffen sind - nötig, um die passenden Klartext-Zugangsdaten
|
// Mount-Typen/Adressen betroffen sind - nötig, um die passenden Klartext-Zugangsdaten
|
||||||
// (davfs2 secrets, .cred-Datei) aufzuräumen, siehe smart_mount::mount::cleanup_credentials.
|
// (davfs2 secrets, .cred-Datei) aufzuräumen, siehe crate::mount::cleanup_credentials.
|
||||||
let cfg = config::pairs::load()?;
|
let cfg = config::pairs::load()?;
|
||||||
let pair = config::pairs::find_pair(&cfg, id)?;
|
let pair = config::pairs::find_pair(&cfg, id)?;
|
||||||
|
|
||||||
@@ -132,7 +152,7 @@ async fn remove(id: &str) -> anyhow::Result<()> {
|
|||||||
// darf das Entfernen aus der Konfiguration nicht blockieren - sonst käme der Nutzer nicht
|
// darf das Entfernen aus der Konfiguration nicht blockieren - sonst käme der Nutzer nicht
|
||||||
// mehr an sein eigenes `drive remove` heran, ohne vorher erst als root/mit den richtigen
|
// mehr an sein eigenes `drive remove` heran, ohne vorher erst als root/mit den richtigen
|
||||||
// Rechten manuell auszuhängen.
|
// Rechten manuell auszuhängen.
|
||||||
if let Err(e) = smart_mount::reconcile::unmount_pair(&pair, &cfg.settings).await {
|
if let Err(e) = crate::reconcile::unmount_pair(&pair, &cfg.settings).await {
|
||||||
logger_ctdra::warn(
|
logger_ctdra::warn(
|
||||||
"drive",
|
"drive",
|
||||||
&format!("could not unmount drive pair '{id}' before removal: {e}"),
|
&format!("could not unmount drive pair '{id}' before removal: {e}"),
|
||||||
@@ -141,22 +161,32 @@ async fn remove(id: &str) -> anyhow::Result<()> {
|
|||||||
config::pairs::remove_pair(id)?;
|
config::pairs::remove_pair(id)?;
|
||||||
let creds = CredentialStore::open().await?;
|
let creds = CredentialStore::open().await?;
|
||||||
creds.delete(id, None).await?;
|
creds.delete(id, None).await?;
|
||||||
smart_mount::mount::cleanup_credentials(&pair, &cfg.settings);
|
crate::mount::cleanup_credentials(&pair, &cfg.settings);
|
||||||
|
|
||||||
|
logger_ctdra::info("drive", &format!("drive pair '{id}' removed"));
|
||||||
println!("Drive pair '{id}' removed.");
|
println!("Drive pair '{id}' removed.");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn add(args: DriveArgs) -> anyhow::Result<()> {
|
async fn add(args: DriveArgs) -> anyhow::Result<()> {
|
||||||
|
crate::cli::require_root("drive add")?;
|
||||||
let ni = args.non_interactive;
|
let ni = args.non_interactive;
|
||||||
|
|
||||||
let name = resolve_field(args.name, None, "Drive pair name", ni, true)?.expect("required");
|
let name = resolve_field(args.name, None, "Drive pair name", ni, true)?.expect("required");
|
||||||
let context = resolve_context(args.context, None, ni)?;
|
// Der Nutzer, der das Paar einrichtet, bekommt standardmäßig vollen Zugriff darauf (siehe
|
||||||
let owner_user = resolve_owner_user(args.owner_user, context, None, ni)?;
|
// `invoking_user`) - überschreibbar per '--owner-user' oder (interaktiv) durch Ablehnen/
|
||||||
|
// einen anderen Namen im Prompt.
|
||||||
|
let owner_user = resolve_owner_user(args.owner_user, invoking_user().as_deref(), ni)?;
|
||||||
|
|
||||||
let local_kind = resolve_kind(args.local_kind, None, "Local mount type", ni)?;
|
let local_kind = resolve_kind(args.local_kind, None, "Local mount type", ni)?;
|
||||||
let local_address = resolve_local_address(args.local_ip, args.local_mac, None, ni)?;
|
let local_address = resolve_local_address(args.local_ip, args.local_mac, None, ni)?;
|
||||||
let local_share = resolve_field(args.local_share, None, "Local share/export path", ni, true)?
|
let local_share = resolve_field(
|
||||||
|
args.local_share,
|
||||||
|
None,
|
||||||
|
&share_field_label(local_kind, "Local"),
|
||||||
|
ni,
|
||||||
|
true,
|
||||||
|
)?
|
||||||
.expect("required");
|
.expect("required");
|
||||||
let local_password_flag = read_password_flag(args.local_password, args.local_password_stdin)?;
|
let local_password_flag = read_password_flag(args.local_password, args.local_password_stdin)?;
|
||||||
let (local_username, local_password) = resolve_credentials(
|
let (local_username, local_password) = resolve_credentials(
|
||||||
@@ -171,7 +201,13 @@ async fn add(args: DriveArgs) -> anyhow::Result<()> {
|
|||||||
let cloud_kind = resolve_kind(args.cloud_kind, None, "Cloud mount type", ni)?;
|
let cloud_kind = resolve_kind(args.cloud_kind, None, "Cloud mount type", ni)?;
|
||||||
let cloud_host = resolve_field(args.cloud_host, None, "Cloud server address/URL", ni, true)?
|
let cloud_host = resolve_field(args.cloud_host, None, "Cloud server address/URL", ni, true)?
|
||||||
.expect("required");
|
.expect("required");
|
||||||
let cloud_share = resolve_field(args.cloud_share, None, "Cloud share/export path", ni, true)?
|
let cloud_share = resolve_field(
|
||||||
|
args.cloud_share,
|
||||||
|
None,
|
||||||
|
&share_field_label(cloud_kind, "Cloud"),
|
||||||
|
ni,
|
||||||
|
true,
|
||||||
|
)?
|
||||||
.expect("required");
|
.expect("required");
|
||||||
let cloud_password_flag = read_password_flag(args.cloud_password, args.cloud_password_stdin)?;
|
let cloud_password_flag = read_password_flag(args.cloud_password, args.cloud_password_stdin)?;
|
||||||
let (cloud_username, cloud_password) = resolve_credentials(
|
let (cloud_username, cloud_password) = resolve_credentials(
|
||||||
@@ -192,14 +228,14 @@ async fn add(args: DriveArgs) -> anyhow::Result<()> {
|
|||||||
// `add_pair()` (das intern selbst frisch lädt) auf denselben kaputten Zustand treffen
|
// `add_pair()` (das intern selbst frisch lädt) auf denselben kaputten Zustand treffen
|
||||||
// lassen, was dort ALLE bestehenden Paare durch die eine neue Konfiguration ersetzen
|
// lassen, was dort ALLE bestehenden Paare durch die eine neue Konfiguration ersetzen
|
||||||
// würde. Besser früh mit einem klaren Fehler abbrechen.
|
// würde. Besser früh mit einem klaren Fehler abbrechen.
|
||||||
let settings = config::pairs::load()?.settings;
|
let cfg = config::pairs::load()?;
|
||||||
let mount_point = settings.mount_base_dir.join(&id);
|
let default_mount_point = default_mount_point(&cfg.settings.mount_base_dir, &name, &cfg.pairs);
|
||||||
|
let mount_point = resolve_mount_point(args.mount_point, &default_mount_point, ni)?;
|
||||||
|
|
||||||
let pair = DrivePair {
|
let pair = DrivePair {
|
||||||
id: id.clone(),
|
id: id.clone(),
|
||||||
name,
|
name,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
context,
|
|
||||||
owner_user,
|
owner_user,
|
||||||
mount_point,
|
mount_point,
|
||||||
local: LocalSide {
|
local: LocalSide {
|
||||||
@@ -232,23 +268,20 @@ async fn add(args: DriveArgs) -> anyhow::Result<()> {
|
|||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger_ctdra::info("drive", &format!("drive pair '{id}' created"));
|
||||||
println!("Drive pair '{id}' created.");
|
println!("Drive pair '{id}' created.");
|
||||||
if context == MountContext::User {
|
|
||||||
println!("Note: for user-context pairs, 'sudo smart-mount setup fstab' must be run once.");
|
|
||||||
}
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn edit(id: &str, args: DriveArgs) -> anyhow::Result<()> {
|
async fn edit(id: &str, args: DriveArgs) -> anyhow::Result<()> {
|
||||||
|
crate::cli::require_root("drive edit")?;
|
||||||
let cfg = config::pairs::load()?;
|
let cfg = config::pairs::load()?;
|
||||||
let existing = config::pairs::find_pair(&cfg, id)?;
|
let existing = config::pairs::find_pair(&cfg, id)?;
|
||||||
let ni = args.non_interactive;
|
let ni = args.non_interactive;
|
||||||
|
|
||||||
let name = resolve_field(args.name, Some(&existing.name), "Drive pair name", ni, true)?
|
let name = resolve_field(args.name, Some(&existing.name), "Drive pair name", ni, true)?
|
||||||
.expect("required");
|
.expect("required");
|
||||||
let context = resolve_context(args.context, Some(existing.context), ni)?;
|
let owner_user = resolve_owner_user(args.owner_user, existing.owner_user.as_deref(), ni)?;
|
||||||
let owner_user =
|
|
||||||
resolve_owner_user(args.owner_user, context, existing.owner_user.as_deref(), ni)?;
|
|
||||||
|
|
||||||
let local_kind = resolve_kind(
|
let local_kind = resolve_kind(
|
||||||
args.local_kind,
|
args.local_kind,
|
||||||
@@ -265,7 +298,7 @@ async fn edit(id: &str, args: DriveArgs) -> anyhow::Result<()> {
|
|||||||
let local_share = resolve_field(
|
let local_share = resolve_field(
|
||||||
args.local_share,
|
args.local_share,
|
||||||
Some(&existing.local.share),
|
Some(&existing.local.share),
|
||||||
"Local share/export path",
|
&share_field_label(local_kind, "Local"),
|
||||||
ni,
|
ni,
|
||||||
true,
|
true,
|
||||||
)?
|
)?
|
||||||
@@ -300,7 +333,7 @@ async fn edit(id: &str, args: DriveArgs) -> anyhow::Result<()> {
|
|||||||
let cloud_share = resolve_field(
|
let cloud_share = resolve_field(
|
||||||
args.cloud_share,
|
args.cloud_share,
|
||||||
Some(&existing.cloud.share),
|
Some(&existing.cloud.share),
|
||||||
"Cloud share/export path",
|
&share_field_label(cloud_kind, "Cloud"),
|
||||||
ni,
|
ni,
|
||||||
true,
|
true,
|
||||||
)?
|
)?
|
||||||
@@ -316,15 +349,29 @@ async fn edit(id: &str, args: DriveArgs) -> anyhow::Result<()> {
|
|||||||
"Cloud credentials",
|
"Cloud credentials",
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
// Anders als bei allen anderen Feldern gibt es hier bewusst KEINEN interaktiven Prompt: der
|
||||||
|
// Mountpoint (und damit die Backing-Verzeichnisse) bleibt standardmäßig unverändert, sonst
|
||||||
|
// würde ein aktiver Mount verwaisen. Nur eine explizite '--mount-point'-Angabe ändert ihn.
|
||||||
|
let mount_point = match args.mount_point {
|
||||||
|
Some(p) => {
|
||||||
|
if crate::mount::target::active_side(&existing).is_some() {
|
||||||
|
println!(
|
||||||
|
"Warning: pair '{id}' appears to be actively mounted - the old backing \
|
||||||
|
directories will be orphaned. Run 'sudo smart-mount unmount --name {id}' \
|
||||||
|
first, then 'mount --name {id}' again after this edit."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
p
|
||||||
|
}
|
||||||
|
None => existing.mount_point.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
let updated = DrivePair {
|
let updated = DrivePair {
|
||||||
id: id.to_string(),
|
id: id.to_string(),
|
||||||
name,
|
name,
|
||||||
enabled: existing.enabled,
|
enabled: existing.enabled,
|
||||||
context,
|
|
||||||
owner_user,
|
owner_user,
|
||||||
// Mountpoint (und damit die Backing-Verzeichnisse) bleiben unverändert - sonst würden
|
mount_point,
|
||||||
// eventuell noch aktive Mounts/fstab-Einträge verwaisen.
|
|
||||||
mount_point: existing.mount_point.clone(),
|
|
||||||
local: LocalSide {
|
local: LocalSide {
|
||||||
kind: local_kind,
|
kind: local_kind,
|
||||||
address: local_address,
|
address: local_address,
|
||||||
@@ -354,6 +401,7 @@ async fn edit(id: &str, args: DriveArgs) -> anyhow::Result<()> {
|
|||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger_ctdra::info("drive", &format!("drive pair '{id}' updated"));
|
||||||
println!("Drive pair '{id}' updated.");
|
println!("Drive pair '{id}' updated.");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -398,90 +446,107 @@ fn resolve_field(
|
|||||||
Ok(Some(input.interact_text()?))
|
Ok(Some(input.interact_text()?))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resolve_context(
|
/// Wandelt `name` in einen sicheren Verzeichnisnamen um (für den Standard-Mountpoint, siehe
|
||||||
flag: Option<MountContext>,
|
/// `default_mount_point`): nur ASCII-alphanumerische Zeichen sowie `-`/`_` bleiben erhalten,
|
||||||
current: Option<MountContext>,
|
/// jede Folge anderer Zeichen (Leerzeichen, `/`, Sonderzeichen, Unicode) wird zu einem einzelnen
|
||||||
non_interactive: bool,
|
/// `-`; führende/folgende `-` werden entfernt. Ein leeres Ergebnis (z. B. bei einem rein aus
|
||||||
) -> anyhow::Result<MountContext> {
|
/// Sonderzeichen bestehenden Namen) fällt auf `"pair"` zurück, damit der Mountpoint nie
|
||||||
if let Some(c) = flag {
|
/// leer/unbrauchbar wird.
|
||||||
return Ok(c);
|
fn sanitize_dir_name(name: &str) -> String {
|
||||||
|
let mut out = String::with_capacity(name.len());
|
||||||
|
for c in name.chars() {
|
||||||
|
if c.is_ascii_alphanumeric() || c == '-' || c == '_' {
|
||||||
|
out.push(c);
|
||||||
|
} else if !out.ends_with('-') {
|
||||||
|
out.push('-');
|
||||||
}
|
}
|
||||||
if let Some(c) = current
|
|
||||||
&& non_interactive
|
|
||||||
{
|
|
||||||
return Ok(c);
|
|
||||||
}
|
}
|
||||||
if non_interactive {
|
match out.trim_matches('-') {
|
||||||
anyhow::bail!(
|
"" => "pair".to_string(),
|
||||||
"Field 'context' is missing - specify it via '--context system|user' in non-interactive mode."
|
trimmed => trimmed.to_string(),
|
||||||
);
|
|
||||||
}
|
}
|
||||||
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
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Standard-Mountpoint für ein neues Paar: `<base>/<sanitierter Name>` (siehe
|
||||||
|
/// `sanitize_dir_name`). Namen sind - anders als `id` - nicht zwingend eindeutig (siehe
|
||||||
|
/// `config::pairs::find_pair`); kollidiert der Standard mit dem Mountpoint eines bestehenden
|
||||||
|
/// Paares, wird `-2`, `-3`, ... angehängt, bis er eindeutig ist.
|
||||||
|
fn default_mount_point(base: &Path, name: &str, existing: &[DrivePair]) -> PathBuf {
|
||||||
|
let sanitized = sanitize_dir_name(name);
|
||||||
|
let mut candidate = base.join(&sanitized);
|
||||||
|
let mut suffix = 2;
|
||||||
|
while existing.iter().any(|p| p.mount_point == candidate) {
|
||||||
|
candidate = base.join(format!("{sanitized}-{suffix}"));
|
||||||
|
suffix += 1;
|
||||||
|
}
|
||||||
|
candidate
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Löst den Mountpoint auf: Flag > (interaktiv: Prompt vorbelegt mit `default`, editierbar) >
|
||||||
|
/// (nicht-interaktiv: `default` ungefragt übernehmen).
|
||||||
|
fn resolve_mount_point(
|
||||||
|
flag: Option<PathBuf>,
|
||||||
|
default: &Path,
|
||||||
|
non_interactive: bool,
|
||||||
|
) -> anyhow::Result<PathBuf> {
|
||||||
|
if let Some(p) = flag {
|
||||||
|
return Ok(p);
|
||||||
|
}
|
||||||
|
if non_interactive {
|
||||||
|
return Ok(default.to_path_buf());
|
||||||
|
}
|
||||||
|
let input: String = Input::new()
|
||||||
|
.with_prompt("Mount point")
|
||||||
|
.default(default.display().to_string())
|
||||||
|
.interact_text()?;
|
||||||
|
Ok(PathBuf::from(input))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Der Nutzer, in dessen Namen `sudo` aufgerufen wurde (falls so aufgerufen) - Standardwert für
|
||||||
|
/// `owner_user` beim Anlegen eines neuen Paares (siehe `add`), damit der einrichtende Nutzer
|
||||||
|
/// ohne extra Angabe vollen Zugriff auf sein eigenes Laufwerkspaar bekommt. `None`, wenn direkt
|
||||||
|
/// als root angemeldet (kein "einrichtender Nutzer" außer root selbst) oder `SUDO_USER` leer/
|
||||||
|
/// nicht gesetzt ist.
|
||||||
|
fn invoking_user() -> Option<String> {
|
||||||
|
std::env::var("SUDO_USER").ok().filter(|u| !u.is_empty())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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 [`crate::systemd`]). `default_owner` ist der
|
||||||
|
/// vorbelegte/vorgeschlagene Wert (bei `add`: der einrichtende Nutzer, siehe `invoking_user`;
|
||||||
|
/// bei `edit`: der bisherige `owner_user`) - immer überschreibbar per Flag, im interaktiven
|
||||||
|
/// Prompt auch durch Ablehnen oder einen anderen Namen.
|
||||||
fn resolve_owner_user(
|
fn resolve_owner_user(
|
||||||
flag: Option<String>,
|
flag: Option<String>,
|
||||||
context: MountContext,
|
default_owner: Option<&str>,
|
||||||
current: Option<&str>,
|
|
||||||
non_interactive: bool,
|
non_interactive: bool,
|
||||||
) -> anyhow::Result<Option<String>> {
|
) -> anyhow::Result<Option<String>> {
|
||||||
if let Some(v) = flag {
|
if let Some(v) = flag {
|
||||||
return Ok(Some(v));
|
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 {
|
if non_interactive {
|
||||||
return current.map(str::to_string).map(Some).ok_or_else(|| {
|
return Ok(default_owner.map(str::to_string));
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
|
let prompt = match default_owner {
|
||||||
|
Some(user) => format!(
|
||||||
|
"Should '{user}' get full access to this drive (uid/gid, including script execution)?"
|
||||||
|
),
|
||||||
|
None => "Should a specific user get full access to this drive (uid/gid, including script execution)?".to_string(),
|
||||||
|
};
|
||||||
let want_owner = Confirm::new()
|
let want_owner = Confirm::new()
|
||||||
.with_prompt("Should a specific user get full access to this drive (uid/gid, including script execution)?")
|
.with_prompt(prompt)
|
||||||
.default(current.is_some())
|
.default(default_owner.is_some())
|
||||||
.interact()?;
|
.interact()?;
|
||||||
if !want_owner {
|
if !want_owner {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
let mut input = Input::<String>::new().with_prompt("Linux username");
|
let mut input = Input::<String>::new().with_prompt("Linux username");
|
||||||
if let Some(c) = current {
|
if let Some(c) = default_owner {
|
||||||
input = input.default(c.to_string());
|
input = input.default(c.to_string());
|
||||||
}
|
}
|
||||||
Ok(Some(input.interact_text()?))
|
Ok(Some(input.interact_text()?))
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn resolve_kind(
|
fn resolve_kind(
|
||||||
flag: Option<MountKind>,
|
flag: Option<MountKind>,
|
||||||
@@ -622,3 +687,80 @@ fn resolve_credentials(
|
|||||||
.interact()?;
|
.interact()?;
|
||||||
Ok((Some(username), Some(password)))
|
Ok((Some(username), Some(password)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::config::LocalAddress;
|
||||||
|
use std::net::Ipv4Addr;
|
||||||
|
|
||||||
|
fn sample_pair(mount_point: &str) -> DrivePair {
|
||||||
|
DrivePair {
|
||||||
|
id: "pair-1".into(),
|
||||||
|
name: "Test".into(),
|
||||||
|
enabled: true,
|
||||||
|
owner_user: None,
|
||||||
|
mount_point: PathBuf::from(mount_point),
|
||||||
|
local: LocalSide {
|
||||||
|
kind: MountKind::Nfs,
|
||||||
|
address: LocalAddress::Ip(Ipv4Addr::new(192, 168, 1, 5)),
|
||||||
|
share: "share".into(),
|
||||||
|
username: None,
|
||||||
|
extra_options: vec![],
|
||||||
|
},
|
||||||
|
cloud: CloudSide {
|
||||||
|
kind: MountKind::Nfs,
|
||||||
|
host_or_url: "cloud.example.com".into(),
|
||||||
|
share: "share".into(),
|
||||||
|
username: None,
|
||||||
|
extra_options: vec![],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sanitize_dir_name_keeps_simple_names_unchanged() {
|
||||||
|
assert_eq!(sanitize_dir_name("NAS"), "NAS");
|
||||||
|
assert_eq!(sanitize_dir_name("my-nas_2"), "my-nas_2");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sanitize_dir_name_collapses_runs_of_special_characters_to_one_dash() {
|
||||||
|
assert_eq!(sanitize_dir_name("My NAS / Drive"), "My-NAS-Drive");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sanitize_dir_name_trims_leading_and_trailing_dashes() {
|
||||||
|
assert_eq!(sanitize_dir_name(" NAS!!"), "NAS");
|
||||||
|
assert_eq!(sanitize_dir_name("/etc/passwd"), "etc-passwd");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sanitize_dir_name_falls_back_to_pair_for_only_special_characters() {
|
||||||
|
assert_eq!(sanitize_dir_name("!!!"), "pair");
|
||||||
|
assert_eq!(sanitize_dir_name(""), "pair");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn default_mount_point_uses_base_dir_and_sanitized_name() {
|
||||||
|
let point = default_mount_point(Path::new("/run/media"), "NAS", &[]);
|
||||||
|
assert_eq!(point, PathBuf::from("/run/media/NAS"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn default_mount_point_dedupes_against_existing_pairs() {
|
||||||
|
let existing = vec![
|
||||||
|
sample_pair("/run/media/NAS"),
|
||||||
|
sample_pair("/run/media/NAS-2"),
|
||||||
|
];
|
||||||
|
let point = default_mount_point(Path::new("/run/media"), "NAS", &existing);
|
||||||
|
assert_eq!(point, PathBuf::from("/run/media/NAS-3"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn default_mount_point_is_unaffected_by_unrelated_existing_pairs() {
|
||||||
|
let existing = vec![sample_pair("/run/media/OtherPair")];
|
||||||
|
let point = default_mount_point(Path::new("/run/media"), "NAS", &existing);
|
||||||
|
assert_eq!(point, PathBuf::from("/run/media/NAS"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+40
-29
@@ -3,13 +3,31 @@
|
|||||||
pub mod doctor;
|
pub mod doctor;
|
||||||
pub mod drive;
|
pub mod drive;
|
||||||
pub mod mount_cmd;
|
pub mod mount_cmd;
|
||||||
pub mod service;
|
|
||||||
pub mod setup;
|
|
||||||
pub mod status;
|
pub mod status;
|
||||||
pub mod watch;
|
pub mod watch;
|
||||||
|
|
||||||
use clap::{CommandFactory, Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
use clap_complete::Shell;
|
|
||||||
|
/// Startet den Prozess bei Bedarf transparent über `sudo` neu, falls nicht bereits als root
|
||||||
|
/// aufgerufen. Mounten läuft ausschließlich als root/System-Dienst (siehe
|
||||||
|
/// [`crate::systemd`]) - es gibt seit dem Wegfall des Nutzerkontexts keine
|
||||||
|
/// unprivilegierte Mount-Variante mehr, für die ein Rechte-Check hier zu früh käme.
|
||||||
|
///
|
||||||
|
/// `run_as_root()` ersetzt den aktuellen Prozess per `execve` und kehrt bei Erfolg nie zurück
|
||||||
|
/// (siehe sudo-ctdra); der `io::Error`-Rückgabewert ist ausschließlich der Fehlerfall (z. B.
|
||||||
|
/// `sudo` nicht gefunden).
|
||||||
|
pub(crate) fn require_root(context: &str) -> anyhow::Result<()> {
|
||||||
|
if !sudo_ctdra::is_run_as_root() {
|
||||||
|
logger_ctdra::info(
|
||||||
|
"cli",
|
||||||
|
&format!("'{context}' requires root - re-executing via sudo"),
|
||||||
|
);
|
||||||
|
let err = sudo_ctdra::run_as_root();
|
||||||
|
anyhow::bail!("'{context}' requires root privileges: could not re-exec via sudo: {err}");
|
||||||
|
}
|
||||||
|
logger_ctdra::debug("cli", &format!("'{context}': already running as root"));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[command(
|
#[command(
|
||||||
@@ -55,46 +73,39 @@ pub enum Commands {
|
|||||||
},
|
},
|
||||||
/// A single reconcile pass (local/cloud switching) - meant for systemd timers/cron.
|
/// A single reconcile pass (local/cloud switching) - meant for systemd timers/cron.
|
||||||
Watch,
|
Watch,
|
||||||
/// Install/remove systemd units.
|
/// Checks prerequisites (binaries, scheduler).
|
||||||
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).
|
|
||||||
Doctor {
|
Doctor {
|
||||||
/// Output as JSON instead of text - for scripts.
|
/// Output as JSON instead of text - for scripts.
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
json: bool,
|
json: bool,
|
||||||
},
|
},
|
||||||
/// Prints a shell completion script, e.g.:
|
}
|
||||||
/// `smart-mount completions bash > /etc/bash_completion.d/smart-mount`.
|
|
||||||
Completions { shell: Shell },
|
/// Kurzname eines Subcommands fürs Logging (siehe [`dispatch`]) - kein `Debug`-Derive auf
|
||||||
|
/// `Commands` nötig, das würde auch die (teils sensiblen) Argument-Felder mit abdrucken.
|
||||||
|
fn command_label(cmd: &Commands) -> &'static str {
|
||||||
|
match cmd {
|
||||||
|
Commands::Drive { .. } => "drive",
|
||||||
|
Commands::Mount { .. } => "mount",
|
||||||
|
Commands::Unmount { .. } => "unmount",
|
||||||
|
Commands::Status { .. } => "status",
|
||||||
|
Commands::Watch => "watch",
|
||||||
|
Commands::Doctor { .. } => "doctor",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Führt das per `Cli` geparste Subcommand aus.
|
/// Führt das per `Cli` geparste Subcommand aus.
|
||||||
pub async fn dispatch(cli: Cli) -> anyhow::Result<()> {
|
pub async fn dispatch(cli: Cli) -> anyhow::Result<()> {
|
||||||
|
logger_ctdra::debug(
|
||||||
|
"cli",
|
||||||
|
&format!("dispatching '{}'", command_label(&cli.command)),
|
||||||
|
);
|
||||||
match cli.command {
|
match cli.command {
|
||||||
Commands::Drive { action } => drive::run(*action).await,
|
Commands::Drive { action } => drive::run(*action).await,
|
||||||
Commands::Mount { name, all } => mount_cmd::run_mount(name, all).await,
|
Commands::Mount { name, all } => mount_cmd::run_mount(name, all).await,
|
||||||
Commands::Unmount { name, all } => mount_cmd::run_unmount(name, all).await,
|
Commands::Unmount { name, all } => mount_cmd::run_unmount(name, all).await,
|
||||||
Commands::Status { name, json } => status::run(name, json).await,
|
Commands::Status { name, json } => status::run(name, json).await,
|
||||||
Commands::Watch => watch::run().await,
|
Commands::Watch => watch::run().await,
|
||||||
Commands::Service { action } => service::run(action),
|
|
||||||
Commands::Setup { action } => setup::run(action),
|
|
||||||
Commands::Doctor { json } => doctor::run(json).await,
|
Commands::Doctor { json } => doctor::run(json).await,
|
||||||
Commands::Completions { shell } => {
|
|
||||||
clap_complete::generate(
|
|
||||||
shell,
|
|
||||||
&mut Cli::command(),
|
|
||||||
"smart-mount",
|
|
||||||
&mut std::io::stdout(),
|
|
||||||
);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+36
-24
@@ -1,11 +1,11 @@
|
|||||||
//! `smart-mount mount` / `smart-mount unmount`.
|
//! `smart-mount mount` / `smart-mount unmount`.
|
||||||
|
|
||||||
use smart_mount::config::{self, DrivePair, MountContext};
|
use crate::config::{self, DrivePair};
|
||||||
use smart_mount::db::credentials::CredentialStore;
|
use crate::db::credentials::CredentialStore;
|
||||||
use smart_mount::reconcile;
|
use crate::reconcile::{self, Action};
|
||||||
|
|
||||||
fn select_pairs(
|
fn select_pairs(
|
||||||
cfg: &smart_mount::config::AppConfig,
|
cfg: &crate::config::AppConfig,
|
||||||
name: Option<&str>,
|
name: Option<&str>,
|
||||||
all: bool,
|
all: bool,
|
||||||
) -> anyhow::Result<Vec<DrivePair>> {
|
) -> anyhow::Result<Vec<DrivePair>> {
|
||||||
@@ -15,61 +15,73 @@ fn select_pairs(
|
|||||||
if !all {
|
if !all {
|
||||||
anyhow::bail!("Please specify '--name <id>' or '--all'.");
|
anyhow::bail!("Please specify '--name <id>' or '--all'.");
|
||||||
}
|
}
|
||||||
|
Ok(cfg.pairs.clone())
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn run_mount(name: Option<String>, all: bool) -> anyhow::Result<()> {
|
pub async fn run_mount(name: Option<String>, all: bool) -> anyhow::Result<()> {
|
||||||
|
crate::cli::require_root("mount")?;
|
||||||
let cfg = config::pairs::load()?;
|
let cfg = config::pairs::load()?;
|
||||||
let pairs = select_pairs(&cfg, name.as_deref(), all)?;
|
let pairs = select_pairs(&cfg, name.as_deref(), all)?;
|
||||||
if pairs.is_empty() {
|
if pairs.is_empty() {
|
||||||
println!("No matching drive pairs found.");
|
println!("No matching drive pairs found.");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
logger_ctdra::info(
|
||||||
|
"mount",
|
||||||
|
&format!("mount: processing {} drive pair(s)", pairs.len()),
|
||||||
|
);
|
||||||
|
|
||||||
let creds = CredentialStore::open().await?;
|
let creds = CredentialStore::open().await?;
|
||||||
|
let mut had_failure = false;
|
||||||
for pair in &pairs {
|
for pair in &pairs {
|
||||||
let outcome = reconcile::reconcile_pair(pair, &cfg.settings, &creds).await;
|
let outcome = reconcile::reconcile_pair(pair, &cfg.settings, &creds).await;
|
||||||
print_outcome(&outcome);
|
print_outcome(&outcome);
|
||||||
|
if matches!(outcome.action, Action::Failed(_)) {
|
||||||
|
had_failure = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ohne dies wäre `smart-mount-mount.service` (siehe packaging/systemd/) beim Booten selbst
|
||||||
|
// dann "erfolgreich" (Exit-Code 0), wenn tatsächlich kein einziges Laufwerkspaar gemountet
|
||||||
|
// werden konnte - `systemctl status` würde den Fehlschlag also nie sichtbar machen. Analog
|
||||||
|
// zu `watch::run` unten.
|
||||||
|
if had_failure {
|
||||||
|
anyhow::bail!("at least one drive pair could not be mounted");
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn run_unmount(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 cfg = config::pairs::load()?;
|
||||||
let pairs = select_pairs(&cfg, name.as_deref(), all)?;
|
let pairs = select_pairs(&cfg, name.as_deref(), all)?;
|
||||||
if pairs.is_empty() {
|
if pairs.is_empty() {
|
||||||
println!("No matching drive pairs found.");
|
println!("No matching drive pairs found.");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
logger_ctdra::info(
|
||||||
|
"mount",
|
||||||
|
&format!("unmount: processing {} drive pair(s)", pairs.len()),
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut had_failure = false;
|
||||||
for pair in &pairs {
|
for pair in &pairs {
|
||||||
match reconcile::unmount_pair(pair, &cfg.settings).await {
|
match reconcile::unmount_pair(pair, &cfg.settings).await {
|
||||||
Ok(_) => println!("{} ({}): unmounted", pair.name, pair.id),
|
Ok(_) => println!("{} ({}): unmounted", pair.name, pair.id),
|
||||||
Err(e) => println!("{} ({}): ERROR: {e}", pair.name, pair.id),
|
Err(e) => {
|
||||||
|
println!("{} ({}): ERROR: {e}", pair.name, pair.id);
|
||||||
|
had_failure = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if had_failure {
|
||||||
|
anyhow::bail!("at least one drive pair could not be unmounted");
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn print_outcome(outcome: &reconcile::ReconcileOutcome) {
|
pub(crate) fn print_outcome(outcome: &reconcile::ReconcileOutcome) {
|
||||||
use reconcile::Action;
|
|
||||||
let action_str = match &outcome.action {
|
let action_str = match &outcome.action {
|
||||||
Action::NoOp => "no change".to_string(),
|
Action::NoOp => "no change".to_string(),
|
||||||
Action::MountedLocal => "mounted local".to_string(),
|
Action::MountedLocal => "mounted local".to_string(),
|
||||||
|
|||||||
@@ -1,152 +0,0 @@
|
|||||||
//! `smart-mount service install|uninstall --system|--user`.
|
|
||||||
|
|
||||||
use clap::{Args, Subcommand};
|
|
||||||
use smart_mount::config;
|
|
||||||
use smart_mount::fstab;
|
|
||||||
use smart_mount::systemd::{self, Scope};
|
|
||||||
|
|
||||||
#[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'."),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn run(action: ServiceAction) -> anyhow::Result<()> {
|
|
||||||
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(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+5
-5
@@ -2,10 +2,10 @@
|
|||||||
|
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
use smart_mount::config;
|
use crate::config;
|
||||||
use smart_mount::db::credentials::Side;
|
use crate::db::credentials::Side;
|
||||||
use smart_mount::mount::target;
|
use crate::mount::target;
|
||||||
use smart_mount::network;
|
use crate::network;
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
struct PairStatus {
|
struct PairStatus {
|
||||||
@@ -48,7 +48,7 @@ pub async fn run(name: Option<String>, json: bool) -> anyhow::Result<()> {
|
|||||||
// wiederverwendet, statt sie für beide Zwecke unabhängig voneinander ein zweites
|
// wiederverwendet, statt sie für beide Zwecke unabhängig voneinander ein zweites
|
||||||
// Mal aufzulösen.
|
// Mal aufzulösen.
|
||||||
let resolved_local_ip =
|
let resolved_local_ip =
|
||||||
smart_mount::network::address::resolve_ip(&pair.local.address, &cfg.settings);
|
crate::network::address::resolve_ip(&pair.local.address, &cfg.settings);
|
||||||
let local_source = resolved_local_ip
|
let local_source = resolved_local_ip
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|ip| target::format_local_source(&pair.local, *ip))
|
.map(|ip| target::format_local_source(&pair.local, *ip))
|
||||||
|
|||||||
+16
-4
@@ -1,12 +1,12 @@
|
|||||||
//! `smart-mount watch` - ein einzelner Reconcile-Durchlauf, gedacht für systemd-Timer/Cron.
|
//! `smart-mount watch` - ein einzelner Reconcile-Durchlauf, gedacht für systemd-Timer/Cron.
|
||||||
|
|
||||||
use smart_mount::config;
|
|
||||||
use smart_mount::db::credentials::CredentialStore;
|
|
||||||
use smart_mount::reconcile::{self, Action};
|
|
||||||
|
|
||||||
use crate::cli::mount_cmd::print_outcome;
|
use crate::cli::mount_cmd::print_outcome;
|
||||||
|
use crate::config;
|
||||||
|
use crate::db::credentials::CredentialStore;
|
||||||
|
use crate::reconcile::{self, Action};
|
||||||
|
|
||||||
pub async fn run() -> anyhow::Result<()> {
|
pub async fn run() -> anyhow::Result<()> {
|
||||||
|
crate::cli::require_root("watch")?;
|
||||||
let cfg = config::pairs::load()?;
|
let cfg = config::pairs::load()?;
|
||||||
let creds = CredentialStore::open().await?;
|
let creds = CredentialStore::open().await?;
|
||||||
let outcomes = reconcile::watch_once(&cfg, &creds).await;
|
let outcomes = reconcile::watch_once(&cfg, &creds).await;
|
||||||
@@ -18,6 +18,18 @@ pub async fn run() -> anyhow::Result<()> {
|
|||||||
had_failure = true;
|
had_failure = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
logger_ctdra::info(
|
||||||
|
"watch",
|
||||||
|
&format!(
|
||||||
|
"watch pass done: {} pair(s), {}",
|
||||||
|
outcomes.len(),
|
||||||
|
if had_failure {
|
||||||
|
"with failures"
|
||||||
|
} else {
|
||||||
|
"no failures"
|
||||||
|
}
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
if had_failure {
|
if had_failure {
|
||||||
anyhow::bail!("at least one drive pair could not be reconciled");
|
anyhow::bail!("at least one drive pair could not be reconciled");
|
||||||
|
|||||||
+14
-4
@@ -4,12 +4,22 @@ pub mod pairs;
|
|||||||
pub mod schema;
|
pub mod schema;
|
||||||
|
|
||||||
pub use schema::{
|
pub use schema::{
|
||||||
AppConfig, CloudSide, DrivePair, GlobalSettings, LocalAddress, LocalSide, MountContext,
|
AppConfig, CloudSide, DrivePair, GlobalSettings, LocalAddress, LocalSide, MountKind,
|
||||||
MountKind,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Initialisiert den Konfigurationsdateinamen bei `config-ctdra`. Muss vor dem ersten
|
/// Initialisiert `config-ctdra`: Dateiname und - unabhängig von den Rechten des aufrufenden
|
||||||
/// `load`/`store`/`get_config`-Aufruf laufen (globaler, prozessweiter Zustand).
|
/// 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() {
|
pub fn init() {
|
||||||
config_ctdra::set_config_name("config");
|
config_ctdra::set_config_name("config");
|
||||||
|
let program = config_ctdra::get_program_name();
|
||||||
|
let dir = std::path::PathBuf::from("/etc").join(program);
|
||||||
|
logger_ctdra::debug("config", &format!("config directory: {}", dir.display()));
|
||||||
|
config_ctdra::set_custom_dir(dir);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ pub fn load() -> Result<AppConfig> {
|
|||||||
|
|
||||||
/// Fügt ein neues Laufwerkspaar hinzu (load-mutate-store in einem atomaren Schritt).
|
/// Fügt ein neues Laufwerkspaar hinzu (load-mutate-store in einem atomaren Schritt).
|
||||||
pub fn add_pair(pair: DrivePair) -> Result<AppConfig> {
|
pub fn add_pair(pair: DrivePair) -> Result<AppConfig> {
|
||||||
|
logger_ctdra::debug(
|
||||||
|
"config",
|
||||||
|
&format!("writing new pair '{}' to config", pair.id),
|
||||||
|
);
|
||||||
Ok(config_ctdra::modify::<AppConfig, _>(|cfg| {
|
Ok(config_ctdra::modify::<AppConfig, _>(|cfg| {
|
||||||
cfg.pairs.push(pair.clone());
|
cfg.pairs.push(pair.clone());
|
||||||
})?)
|
})?)
|
||||||
@@ -18,6 +22,7 @@ pub fn add_pair(pair: DrivePair) -> Result<AppConfig> {
|
|||||||
/// Ersetzt ein bestehendes Laufwerkspaar (Vergleich über `id`).
|
/// Ersetzt ein bestehendes Laufwerkspaar (Vergleich über `id`).
|
||||||
pub fn update_pair(pair: DrivePair) -> Result<AppConfig> {
|
pub fn update_pair(pair: DrivePair) -> Result<AppConfig> {
|
||||||
let id = pair.id.clone();
|
let id = pair.id.clone();
|
||||||
|
logger_ctdra::debug("config", &format!("writing updated pair '{id}' to config"));
|
||||||
let updated = config_ctdra::modify::<AppConfig, _>(move |cfg| {
|
let updated = config_ctdra::modify::<AppConfig, _>(move |cfg| {
|
||||||
if let Some(existing) = cfg.pairs.iter_mut().find(|p| p.id == pair.id) {
|
if let Some(existing) = cfg.pairs.iter_mut().find(|p| p.id == pair.id) {
|
||||||
*existing = pair.clone();
|
*existing = pair.clone();
|
||||||
@@ -31,6 +36,7 @@ pub fn update_pair(pair: DrivePair) -> Result<AppConfig> {
|
|||||||
|
|
||||||
/// Entfernt ein Laufwerkspaar per ID.
|
/// Entfernt ein Laufwerkspaar per ID.
|
||||||
pub fn remove_pair(id: &str) -> Result<AppConfig> {
|
pub fn remove_pair(id: &str) -> Result<AppConfig> {
|
||||||
|
logger_ctdra::debug("config", &format!("removing pair '{id}' from config"));
|
||||||
// Die "vorher"-Länge wird INNERHALB desselben `modify`-Aufrufs (auf der bereits frisch
|
// Die "vorher"-Länge wird INNERHALB desselben `modify`-Aufrufs (auf der bereits frisch
|
||||||
// geladenen `cfg`) ermittelt statt über einen separaten, vorgelagerten `load()`-Aufruf:
|
// geladenen `cfg`) ermittelt statt über einen separaten, vorgelagerten `load()`-Aufruf:
|
||||||
// zwischen zwei getrennten Aufrufen könnte ein nebenläufiger Schreiber die Paarliste
|
// zwischen zwei getrennten Aufrufen könnte ein nebenläufiger Schreiber die Paarliste
|
||||||
|
|||||||
+19
-60
@@ -11,39 +11,15 @@ use serde::{Deserialize, Serialize};
|
|||||||
/// `/run/media` ist die auf diesem System bereits übliche Konvention für eingebundene
|
/// `/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
|
/// Wechseldatenträger/Netzlaufwerke (z. B. udisks2/GNOME) - tmpfs-hinterlegt, wird also bei
|
||||||
/// jedem Boot ohnehin leer neu angelegt, passend dazu, dass Mountpoints selbst nie
|
/// 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`
|
/// persistieren müssen. Jedes Paar bekommt direkt darunter sein eigenes, nach seinem Namen
|
||||||
/// (ein systemweiter Dienst, keinem einzelnen Nutzer zugeordnet); Nutzer-Kontext-Paare unter
|
/// benanntes Unterverzeichnis (`/run/media/<Name>`, siehe `cli::drive::default_mount_point`) -
|
||||||
/// `/run/media/<Nutzer>/smart-mount`, damit mehrere lokale Nutzer mit eigenen Paaren sich
|
/// kein zusätzlicher `smart-mount`-Zwischenordner, damit der Ordnername beim Durchsuchen von
|
||||||
/// nicht denselben Namensraum teilen.
|
/// `/run/media` sofort erkennbar ist. Die Trennung nach Linux-Nutzer passiert bei Bedarf über
|
||||||
|
/// die `uid=`/`gid=`-Mount-Optionen (siehe [`crate::mount::target::build_target`]), nicht über
|
||||||
|
/// unterschiedliche Mountpoint-Namensräume - smart-mount läuft ausschließlich als
|
||||||
|
/// root/System-Dienst (siehe [`crate::systemd`]).
|
||||||
fn default_mount_base_dir() -> PathBuf {
|
fn default_mount_base_dir() -> PathBuf {
|
||||||
if sudo_ctdra::is_run_as_root() {
|
PathBuf::from("/run/media")
|
||||||
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 {
|
fn default_log_level() -> String {
|
||||||
@@ -58,9 +34,8 @@ fn default_mac2ip_binary() -> String {
|
|||||||
"mac2ip".to_string()
|
"mac2ip".to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Wurzel-Konfigurationsstruktur, gespeichert via `config-ctdra` unter
|
/// Wurzel-Konfigurationsstruktur, gespeichert via `config-ctdra` immer unter
|
||||||
/// `~/.config/smart-mount/config.toml` (Nutzerkontext) bzw. `/etc/smart-mount/config.toml`
|
/// `/etc/smart-mount/config.toml` (siehe [`crate::config::init`]).
|
||||||
/// (Root-Kontext).
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
|
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
|
||||||
pub struct AppConfig {
|
pub struct AppConfig {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
@@ -76,8 +51,10 @@ pub struct GlobalSettings {
|
|||||||
pub mount_base_dir: PathBuf,
|
pub mount_base_dir: PathBuf,
|
||||||
#[serde(default = "default_log_level")]
|
#[serde(default = "default_log_level")]
|
||||||
pub log_level: String,
|
pub log_level: String,
|
||||||
/// Periode, mit der `smart-mount watch` über den generierten systemd-Timer bzw. die
|
/// Periode, mit der `smart-mount watch` über den Cron-Fallback (`smart-mount service
|
||||||
/// dokumentierte Crontab-Zeile ausgeführt werden soll.
|
/// 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")]
|
#[serde(default = "default_watch_interval_secs")]
|
||||||
pub watch_interval_secs: u64,
|
pub watch_interval_secs: u64,
|
||||||
/// Name/Pfad des `mac2ip`-Binaries (per PATH auflösbar, oder absoluter Pfad).
|
/// Name/Pfad des `mac2ip`-Binaries (per PATH auflösbar, oder absoluter Pfad).
|
||||||
@@ -101,14 +78,14 @@ impl Default for GlobalSettings {
|
|||||||
/// eingebunden - siehe [`crate::reconcile`].
|
/// eingebunden - siehe [`crate::reconcile`].
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
pub struct DrivePair {
|
pub struct DrivePair {
|
||||||
/// Stabile ID (uuid-v4), Schlüssel für DB-Zugangsdaten, Mount-Unterverzeichnis,
|
/// Stabile ID (uuid-v4), Schlüssel für DB-Zugangsdaten sowie das Mount-Unterverzeichnis.
|
||||||
/// systemd-Unit-Namen und fstab-Einträge.
|
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
#[serde(default = "default_true")]
|
#[serde(default = "default_true")]
|
||||||
pub enabled: bool,
|
pub enabled: bool,
|
||||||
pub context: MountContext,
|
/// Optional: der Linux-Benutzername, der vollen Zugriff (uid/gid) auf dieses Paar
|
||||||
/// Pflicht bei `context == User`: der Linux-Benutzername, dem dieses Paar gehört.
|
/// bekommen soll (siehe [`crate::mount::target::build_target`]). Ohne `owner_user` gehört
|
||||||
|
/// der Mount root.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub owner_user: Option<String>,
|
pub owner_user: Option<String>,
|
||||||
pub mount_point: PathBuf,
|
pub mount_point: PathBuf,
|
||||||
@@ -120,16 +97,6 @@ fn default_true() -> bool {
|
|||||||
true
|
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.
|
/// Die lokale (LAN-)Seite eines Laufwerkspaars.
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
pub struct LocalSide {
|
pub struct LocalSide {
|
||||||
@@ -198,7 +165,6 @@ mod tests {
|
|||||||
id: "pair-1".into(),
|
id: "pair-1".into(),
|
||||||
name: "NAS".into(),
|
name: "NAS".into(),
|
||||||
enabled: true,
|
enabled: true,
|
||||||
context: MountContext::User,
|
|
||||||
owner_user: Some("dragon".into()),
|
owner_user: Some("dragon".into()),
|
||||||
mount_point: PathBuf::from("/home/dragon/smart-mount/pair-1"),
|
mount_point: PathBuf::from("/home/dragon/smart-mount/pair-1"),
|
||||||
local: LocalSide {
|
local: LocalSide {
|
||||||
@@ -223,7 +189,6 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(round_tripped.pairs.len(), 1);
|
assert_eq!(round_tripped.pairs.len(), 1);
|
||||||
assert_eq!(round_tripped.pairs[0].id, "pair-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 {
|
match &round_tripped.pairs[0].local.address {
|
||||||
LocalAddress::Mac(mac) => assert_eq!(mac, "aa:bb:cc:dd:ee:ff"),
|
LocalAddress::Mac(mac) => assert_eq!(mac, "aa:bb:cc:dd:ee:ff"),
|
||||||
LocalAddress::Ip(_) => panic!("expected Mac variant"),
|
LocalAddress::Ip(_) => panic!("expected Mac variant"),
|
||||||
@@ -232,12 +197,6 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn default_mount_base_dir_uses_run_media() {
|
fn default_mount_base_dir_uses_run_media() {
|
||||||
// Tests laufen nie als root, daher greift hier immer der Nutzerkontext-Zweig.
|
assert_eq!(default_mount_base_dir(), PathBuf::from("/run/media"));
|
||||||
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));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-125
@@ -1,11 +1,8 @@
|
|||||||
//! Master-Schlüssel-Auflösung für die Zugangsdaten-Verschlüsselung.
|
//! Master-Schlüssel-Auflösung für die Zugangsdaten-Verschlüsselung.
|
||||||
//!
|
//!
|
||||||
//! Reihenfolge (wie mit dem Nutzer abgestimmt):
|
//! Immer die Schlüsseldatei: SmartMount läuft ausschließlich als root/System-Dienst (siehe
|
||||||
//! - Root-/System-Kontext: **immer** die Schlüsseldatei (kein Nutzer-Keyring im
|
//! [`crate::systemd`]), für den es kein Nutzer-Keyring (GNOME Keyring/KWallet über
|
||||||
//! Systemdienst-Kontext verfügbar).
|
//! secret-service) gibt.
|
||||||
//! - 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.
|
|
||||||
|
|
||||||
use std::fs::{self, File, OpenOptions};
|
use std::fs::{self, File, OpenOptions};
|
||||||
use std::io::{Read, Write};
|
use std::io::{Read, Write};
|
||||||
@@ -13,64 +10,13 @@ use std::path::{Path, PathBuf};
|
|||||||
|
|
||||||
use crate::error::{Error, Result};
|
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_FILE_NAME: &str = "master.key";
|
||||||
const KEY_BACKEND_MARKER_NAME: &str = "master.key.backend";
|
|
||||||
const KEY_LEN: usize = 32;
|
const KEY_LEN: usize = 32;
|
||||||
|
|
||||||
/// Ermittelt (und erzeugt bei Bedarf) den 256-Bit-Master-Schlüssel für die
|
/// 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.
|
/// Zugangsdaten-Verschlüsselung.
|
||||||
///
|
|
||||||
/// 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.
|
|
||||||
pub fn resolve_master_key() -> Result<[u8; 32]> {
|
pub fn resolve_master_key() -> Result<[u8; 32]> {
|
||||||
if sudo_ctdra::is_run_as_root() {
|
file_key::load_or_create(&key_file_path())
|
||||||
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()
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn key_file_path() -> PathBuf {
|
fn key_file_path() -> PathBuf {
|
||||||
@@ -81,21 +27,21 @@ fn key_file_path() -> PathBuf {
|
|||||||
.unwrap_or_else(|| PathBuf::from(KEY_FILE_NAME))
|
.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 {
|
mod file_key {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
pub fn load_or_create(path: &Path) -> Result<[u8; 32]> {
|
pub fn load_or_create(path: &Path) -> Result<[u8; 32]> {
|
||||||
if path.exists() {
|
if path.exists() {
|
||||||
|
logger_ctdra::debug(
|
||||||
|
"crypto",
|
||||||
|
&format!("loading master key from '{}'", path.display()),
|
||||||
|
);
|
||||||
return read(path);
|
return read(path);
|
||||||
}
|
}
|
||||||
|
logger_ctdra::info(
|
||||||
|
"crypto",
|
||||||
|
&format!("creating new master key at '{}'", path.display()),
|
||||||
|
);
|
||||||
create(path)
|
create(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,61 +96,3 @@ mod file_key {
|
|||||||
.map_err(|e| Error::Crypto(format!("Random number generator failed: {e}")))
|
.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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ pub const NONCE_LEN: usize = 12;
|
|||||||
/// aes-gcms Re-Export-Kette nicht automatisch aktiviert wird). `getrandom::fill` ist
|
/// aes-gcms Re-Export-Kette nicht automatisch aktiviert wird). `getrandom::fill` ist
|
||||||
/// unabhängig davon stabil und genau für diesen Zweck gedacht.
|
/// unabhängig davon stabil und genau für diesen Zweck gedacht.
|
||||||
pub fn encrypt(plaintext: &[u8], key: &[u8; 32]) -> Result<(Vec<u8>, Vec<u8>)> {
|
pub fn encrypt(plaintext: &[u8], key: &[u8; 32]) -> Result<(Vec<u8>, Vec<u8>)> {
|
||||||
|
logger_ctdra::debug("crypto", "encrypting credential data");
|
||||||
let cipher = Aes256Gcm::new(&Key::<Aes256Gcm>::from(*key));
|
let cipher = Aes256Gcm::new(&Key::<Aes256Gcm>::from(*key));
|
||||||
|
|
||||||
let mut nonce_bytes = [0u8; NONCE_LEN];
|
let mut nonce_bytes = [0u8; NONCE_LEN];
|
||||||
@@ -44,6 +45,7 @@ pub fn encrypt(plaintext: &[u8], key: &[u8; 32]) -> Result<(Vec<u8>, Vec<u8>)> {
|
|||||||
|
|
||||||
/// Entschlüsselt einen zuvor mit [`encrypt`] erzeugten Ciphertext.
|
/// Entschlüsselt einen zuvor mit [`encrypt`] erzeugten Ciphertext.
|
||||||
pub fn decrypt(ciphertext: &[u8], nonce: &[u8], key: &[u8; 32]) -> Result<Vec<u8>> {
|
pub fn decrypt(ciphertext: &[u8], nonce: &[u8], key: &[u8; 32]) -> Result<Vec<u8>> {
|
||||||
|
logger_ctdra::debug("crypto", "decrypting credential data");
|
||||||
if nonce.len() != NONCE_LEN {
|
if nonce.len() != NONCE_LEN {
|
||||||
return Err(Error::Crypto(format!(
|
return Err(Error::Crypto(format!(
|
||||||
"invalid nonce length: expected {NONCE_LEN}, got {}",
|
"invalid nonce length: expected {NONCE_LEN}, got {}",
|
||||||
|
|||||||
@@ -58,6 +58,13 @@ impl CredentialStore {
|
|||||||
domain: Option<&str>,
|
domain: Option<&str>,
|
||||||
password: &str,
|
password: &str,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
|
logger_ctdra::debug(
|
||||||
|
"db",
|
||||||
|
&format!(
|
||||||
|
"storing credential for pair '{pair_id}' ({})",
|
||||||
|
side.as_str()
|
||||||
|
),
|
||||||
|
);
|
||||||
let key = resolve_master_key()?;
|
let key = resolve_master_key()?;
|
||||||
let (ciphertext, nonce) = crypto::encrypt(password.as_bytes(), &key)?;
|
let (ciphertext, nonce) = crypto::encrypt(password.as_bytes(), &key)?;
|
||||||
let now = now_unix();
|
let now = now_unix();
|
||||||
@@ -85,6 +92,13 @@ impl CredentialStore {
|
|||||||
|
|
||||||
/// Liest und entschlüsselt die Zugangsdaten für `pair_id`/`side`, falls vorhanden.
|
/// Liest und entschlüsselt die Zugangsdaten für `pair_id`/`side`, falls vorhanden.
|
||||||
pub async fn get(&self, pair_id: &str, side: Side) -> Result<Option<Credential>> {
|
pub async fn get(&self, pair_id: &str, side: Side) -> Result<Option<Credential>> {
|
||||||
|
logger_ctdra::debug(
|
||||||
|
"db",
|
||||||
|
&format!(
|
||||||
|
"reading credential for pair '{pair_id}' ({})",
|
||||||
|
side.as_str()
|
||||||
|
),
|
||||||
|
);
|
||||||
let mut rows = self
|
let mut rows = self
|
||||||
.conn
|
.conn
|
||||||
.query(
|
.query(
|
||||||
@@ -117,6 +131,13 @@ impl CredentialStore {
|
|||||||
|
|
||||||
/// Löscht Zugangsdaten. `side = None` löscht beide Seiten (z. B. beim Entfernen eines Paars).
|
/// Löscht Zugangsdaten. `side = None` löscht beide Seiten (z. B. beim Entfernen eines Paars).
|
||||||
pub async fn delete(&self, pair_id: &str, side: Option<Side>) -> Result<()> {
|
pub async fn delete(&self, pair_id: &str, side: Option<Side>) -> Result<()> {
|
||||||
|
logger_ctdra::debug(
|
||||||
|
"db",
|
||||||
|
&format!(
|
||||||
|
"deleting credential(s) for pair '{pair_id}' ({})",
|
||||||
|
side.map(|s| s.as_str()).unwrap_or("both sides")
|
||||||
|
),
|
||||||
|
);
|
||||||
match side {
|
match side {
|
||||||
Some(side) => {
|
Some(side) => {
|
||||||
self.conn
|
self.conn
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ pub fn resolve_db_path() -> PathBuf {
|
|||||||
/// Öffnet (und initialisiert bei Bedarf) die lokale Datenbank am aufgelösten Pfad.
|
/// Öffnet (und initialisiert bei Bedarf) die lokale Datenbank am aufgelösten Pfad.
|
||||||
pub async fn open() -> Result<turso::Connection> {
|
pub async fn open() -> Result<turso::Connection> {
|
||||||
let path = resolve_db_path();
|
let path = resolve_db_path();
|
||||||
|
logger_ctdra::debug("db", &format!("opening database at '{}'", path.display()));
|
||||||
if let Some(dir) = path.parent() {
|
if let Some(dir) = path.parent() {
|
||||||
tokio::fs::create_dir_all(dir)
|
tokio::fs::create_dir_all(dir)
|
||||||
.await
|
.await
|
||||||
|
|||||||
+227
-143
@@ -1,13 +1,86 @@
|
|||||||
//! Diagnose-Checks für `smart-mount doctor` - prüft die im Laufe der Entwicklung
|
//! Diagnose-Checks für `smart-mount doctor` - prüft die im Laufe der Entwicklung
|
||||||
//! angesammelten Voraussetzungen (Binaries, Gruppenmitgliedschaft, fstab-Setup, Scheduler)
|
//! angesammelten Voraussetzungen (Binaries, Scheduler) gebündelt an einer Stelle, statt sie
|
||||||
//! gebündelt an einer Stelle, statt sie einzeln erst beim Mount-Fehlschlag zu entdecken.
|
//! einzeln erst beim Mount-Fehlschlag zu entdecken.
|
||||||
|
//!
|
||||||
|
//! Installationshinweise (fehlendes Paket, Scheduler-Fallback) sind bewusst je nach
|
||||||
|
//! erkannter Distribution unterschiedlich (siehe [`PackageFamily`]): auf einem Debian-System
|
||||||
|
//! soll `doctor` nur den `apt`-Hinweis zeigen, nicht zusätzlich Fedora/Arch-Varianten, die dort
|
||||||
|
//! ohnehin nicht anwendbar sind.
|
||||||
|
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::process::Command;
|
|
||||||
|
|
||||||
use crate::config::{AppConfig, DrivePair, LocalAddress, MountContext, MountKind};
|
use crate::config::{AppConfig, LocalAddress, MountKind};
|
||||||
use crate::mount;
|
use crate::mount;
|
||||||
|
|
||||||
|
/// Grob erkannte Paketmanager-Familie des laufenden Systems, allein zur Auswahl der passenden
|
||||||
|
/// Installationshinweise in `doctor` - keine vollständige Distributions-Erkennung.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum PackageFamily {
|
||||||
|
/// Debian, Ubuntu und Ableitungen (`apt`).
|
||||||
|
Debian,
|
||||||
|
/// Fedora, RHEL/CentOS und Ableitungen (`dnf`).
|
||||||
|
Fedora,
|
||||||
|
/// Arch Linux und Ableitungen (Manjaro, EndeavourOS, ...) (`pacman`).
|
||||||
|
Arch,
|
||||||
|
/// Nicht erkannt - Installationshinweise bleiben generisch (nur Paketname, kein Befehl).
|
||||||
|
Unknown,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PackageFamily {
|
||||||
|
fn install_command(self, package: &str) -> Option<String> {
|
||||||
|
match self {
|
||||||
|
PackageFamily::Debian => Some(format!("apt install {package}")),
|
||||||
|
PackageFamily::Fedora => Some(format!("dnf install {package}")),
|
||||||
|
PackageFamily::Arch => Some(format!("pacman -S {package}")),
|
||||||
|
PackageFamily::Unknown => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Liest `/etc/os-release` (auf praktisch jedem modernen Linux-System vorhanden, siehe
|
||||||
|
/// `man os-release`) und leitet daraus die [`PackageFamily`] ab.
|
||||||
|
fn detect_package_family() -> PackageFamily {
|
||||||
|
std::fs::read_to_string("/etc/os-release")
|
||||||
|
.map(|contents| parse_package_family(&contents))
|
||||||
|
.unwrap_or(PackageFamily::Unknown)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reine, testbare Parse-Funktion: sammelt `ID=`/`ID_LIKE=` aus dem `os-release`-Inhalt (deckt
|
||||||
|
/// z. B. Ubuntu `ID=ubuntu ID_LIKE=debian` oder Manjaro `ID=manjaro ID_LIKE=arch` mit ab, nicht
|
||||||
|
/// nur die jeweilige "reine" Distribution) und ordnet sie grob einer [`PackageFamily`] zu.
|
||||||
|
fn parse_package_family(os_release: &str) -> PackageFamily {
|
||||||
|
let mut fields = String::new();
|
||||||
|
for line in os_release.lines() {
|
||||||
|
if let Some(value) = line
|
||||||
|
.strip_prefix("ID=")
|
||||||
|
.or_else(|| line.strip_prefix("ID_LIKE="))
|
||||||
|
{
|
||||||
|
fields.push(' ');
|
||||||
|
fields.push_str(value.trim_matches('"'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let fields = fields.to_lowercase();
|
||||||
|
if fields.contains("debian") {
|
||||||
|
PackageFamily::Debian
|
||||||
|
} else if fields.contains("fedora") || fields.contains("rhel") {
|
||||||
|
PackageFamily::Fedora
|
||||||
|
} else if fields.contains("arch") {
|
||||||
|
PackageFamily::Arch
|
||||||
|
} else {
|
||||||
|
PackageFamily::Unknown
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Formatiert einen Installationshinweis für `package`, inkl. des passenden Befehls für die
|
||||||
|
/// erkannte [`PackageFamily`] (kein Befehl bei [`PackageFamily::Unknown`] - dann nur der
|
||||||
|
/// Paketname, ohne eine der drei Varianten zu raten).
|
||||||
|
fn install_hint(package: &str, family: PackageFamily) -> String {
|
||||||
|
match family.install_command(package) {
|
||||||
|
Some(cmd) => format!("install package '{package}' ({cmd})"),
|
||||||
|
None => format!("install package '{package}'"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum CheckStatus {
|
pub enum CheckStatus {
|
||||||
Ok,
|
Ok,
|
||||||
@@ -46,16 +119,17 @@ fn fail(label: impl Into<String>, detail: impl Into<String>) -> CheckResult {
|
|||||||
|
|
||||||
/// Führt alle Checks gegen die aktuelle Konfiguration aus.
|
/// Führt alle Checks gegen die aktuelle Konfiguration aus.
|
||||||
pub fn run_checks(cfg: &AppConfig) -> Vec<CheckResult> {
|
pub fn run_checks(cfg: &AppConfig) -> Vec<CheckResult> {
|
||||||
|
let family = detect_package_family();
|
||||||
let mut results = Vec::new();
|
let mut results = Vec::new();
|
||||||
|
|
||||||
results.push(check_timeout_binary());
|
results.push(check_timeout_binary());
|
||||||
results.push(check_scheduler());
|
results.push(check_scheduler(family));
|
||||||
results.push(check_mount_base_dir(&cfg.settings.mount_base_dir));
|
results.push(check_mount_base_dir(&cfg.settings.mount_base_dir));
|
||||||
|
|
||||||
let used_kinds = used_mount_kinds(cfg);
|
let used_kinds = used_mount_kinds(cfg);
|
||||||
for kind in [MountKind::WebDav, MountKind::Smb, MountKind::Nfs] {
|
for kind in [MountKind::WebDav, MountKind::Smb, MountKind::Nfs] {
|
||||||
if used_kinds.contains(&kind) {
|
if used_kinds.contains(&kind) {
|
||||||
results.push(check_backend(kind));
|
results.push(check_backend(kind, family));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,24 +139,6 @@ pub fn run_checks(cfg: &AppConfig) -> Vec<CheckResult> {
|
|||||||
&cfg.settings.mac2ip_binary,
|
&cfg.settings.mac2ip_binary,
|
||||||
"install mac2ip (private tool, see README)",
|
"install mac2ip (private tool, see README)",
|
||||||
));
|
));
|
||||||
results.push(check_binary("nmap", "nmap", "install package 'nmap'"));
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
results
|
||||||
@@ -125,36 +181,100 @@ fn check_binary(name: &str, binary: &str, install_hint: &str) -> CheckResult {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn check_backend(kind: MountKind) -> CheckResult {
|
fn check_backend(kind: MountKind, family: PackageFamily) -> CheckResult {
|
||||||
let backend = mount::backend_for(kind);
|
let backend = mount::backend_for(kind);
|
||||||
match backend.check_available() {
|
if backend.check_available().is_ok() {
|
||||||
Ok(()) => ok(format!("Backend: {}", backend.name()), "available"),
|
return ok(format!("Backend: {}", backend.name()), "available");
|
||||||
Err(e) => fail(format!("Backend: {}", backend.name()), e.to_string()),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn check_scheduler() -> CheckResult {
|
let (missing_binary, package) = match kind {
|
||||||
let systemd = crate::systemd::is_available();
|
MountKind::WebDav => ("mount.davfs", "davfs2"),
|
||||||
let cron_d = std::path::Path::new("/etc/cron.d").is_dir();
|
MountKind::Smb => ("mount.cifs", "cifs-utils"),
|
||||||
let crontab = mount::binary_available("crontab");
|
MountKind::Nfs => (
|
||||||
|
"mount.nfs/mount.nfs4",
|
||||||
|
match family {
|
||||||
|
PackageFamily::Debian => "nfs-common",
|
||||||
|
_ => "nfs-utils",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
};
|
||||||
|
fail(
|
||||||
|
format!("Backend: {}", backend.name()),
|
||||||
|
format!(
|
||||||
|
"'{missing_binary}' not found - {}",
|
||||||
|
install_hint(package, family)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if systemd {
|
/// Prüft, ob die periodische Ausführung tatsächlich eingerichtet ist UND läuft - nicht nur, ob
|
||||||
|
/// `systemctl`/ein Cron-Mechanismus grundsätzlich auf dem System vorhanden wäre (das allein
|
||||||
|
/// sagt nichts darüber aus, ob smart-mounts eigene Units/Cron-Eintrag auch tatsächlich
|
||||||
|
/// installiert sind, siehe [`crate::systemd::is_unit_installed`]/[`crate::systemd::is_cron_installed`]).
|
||||||
|
fn check_scheduler(family: PackageFamily) -> CheckResult {
|
||||||
|
let timer_installed = crate::systemd::is_unit_installed("smart-mount-watch.timer");
|
||||||
|
let mount_installed = crate::systemd::is_unit_installed("smart-mount-mount.service");
|
||||||
|
|
||||||
|
if timer_installed || mount_installed {
|
||||||
|
let timer_enabled = crate::systemd::is_unit_enabled("smart-mount-watch.timer");
|
||||||
|
let timer_active = crate::systemd::is_unit_active("smart-mount-watch.timer");
|
||||||
|
let mount_enabled = crate::systemd::is_unit_enabled("smart-mount-mount.service");
|
||||||
|
|
||||||
|
return if timer_enabled && timer_active && mount_enabled {
|
||||||
ok(
|
ok(
|
||||||
"Scheduler",
|
"Scheduler",
|
||||||
"systemd found - 'smart-mount service install' uses systemd timers",
|
"systemd: 'smart-mount-watch.timer' is enabled and running, 'smart-mount-mount.service' is enabled",
|
||||||
)
|
|
||||||
} else if cron_d || crontab {
|
|
||||||
warn(
|
|
||||||
"Scheduler",
|
|
||||||
"no systemd, but cron found - 'smart-mount service install' automatically falls back to cron",
|
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
warn(
|
||||||
|
"Scheduler",
|
||||||
|
format!(
|
||||||
|
"systemd units installed but not fully set up (smart-mount-watch.timer: enabled={timer_enabled} running={timer_active}, smart-mount-mount.service: enabled={mount_enabled}) - try 'sudo systemctl enable --now smart-mount-mount.service smart-mount-watch.timer'"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if crate::systemd::is_available() {
|
||||||
|
return fail(
|
||||||
|
"Scheduler",
|
||||||
|
"systemd found, but smart-mount's systemd units are not installed - was the package installed correctly?",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if crate::systemd::is_cron_installed() {
|
||||||
|
return if crate::systemd::is_cron_daemon_running() {
|
||||||
|
ok(
|
||||||
|
"Scheduler",
|
||||||
|
"cron: '/etc/cron.d/smart-mount' is installed, cron daemon is running",
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
warn(
|
||||||
|
"Scheduler",
|
||||||
|
"cron: '/etc/cron.d/smart-mount' is installed, but no cron daemon process was found - is cron running?",
|
||||||
|
)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if std::path::Path::new("/etc/cron.d").is_dir() {
|
||||||
|
return warn(
|
||||||
|
"Scheduler",
|
||||||
|
"no systemd, cron is available but smart-mount's entry ('/etc/cron.d/smart-mount') is missing - was the package installed correctly?",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let cron_package = match family {
|
||||||
|
PackageFamily::Debian => "cron",
|
||||||
|
_ => "cronie",
|
||||||
|
};
|
||||||
fail(
|
fail(
|
||||||
"Scheduler",
|
"Scheduler",
|
||||||
"neither systemd nor cron found - periodic 'watch' must be set up manually",
|
format!(
|
||||||
|
"neither systemd nor cron found - periodic 'watch' must be set up manually ({})",
|
||||||
|
install_hint(cron_package, family)
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fn check_mount_base_dir(dir: &std::path::Path) -> CheckResult {
|
fn check_mount_base_dir(dir: &std::path::Path) -> CheckResult {
|
||||||
if dir.is_dir() {
|
if dir.is_dir() {
|
||||||
@@ -178,89 +298,77 @@ 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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::config::{CloudSide, GlobalSettings, LocalSide};
|
use crate::config::{CloudSide, DrivePair, GlobalSettings, LocalSide};
|
||||||
use std::net::Ipv4Addr;
|
use std::net::Ipv4Addr;
|
||||||
|
|
||||||
fn sample_pair(context: MountContext, owner_user: Option<&str>, mac: bool) -> DrivePair {
|
#[test]
|
||||||
|
fn parse_package_family_detects_debian_and_derivatives() {
|
||||||
|
assert_eq!(
|
||||||
|
parse_package_family("ID=debian\nVERSION_ID=\"13\"\n"),
|
||||||
|
PackageFamily::Debian
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_package_family("ID=ubuntu\nID_LIKE=debian\n"),
|
||||||
|
PackageFamily::Debian
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_package_family_detects_fedora_and_rhel_derivatives() {
|
||||||
|
assert_eq!(parse_package_family("ID=fedora\n"), PackageFamily::Fedora);
|
||||||
|
assert_eq!(
|
||||||
|
parse_package_family("ID=centos\nID_LIKE=\"rhel fedora\"\n"),
|
||||||
|
PackageFamily::Fedora
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_package_family_detects_arch_and_derivatives() {
|
||||||
|
assert_eq!(parse_package_family("ID=arch\n"), PackageFamily::Arch);
|
||||||
|
assert_eq!(
|
||||||
|
parse_package_family("ID=manjaro\nID_LIKE=arch\n"),
|
||||||
|
PackageFamily::Arch
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_package_family_falls_back_to_unknown() {
|
||||||
|
assert_eq!(parse_package_family(""), PackageFamily::Unknown);
|
||||||
|
assert_eq!(parse_package_family("ID=gentoo\n"), PackageFamily::Unknown);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn install_hint_includes_package_manager_command_for_known_families() {
|
||||||
|
assert_eq!(
|
||||||
|
install_hint("davfs2", PackageFamily::Debian),
|
||||||
|
"install package 'davfs2' (apt install davfs2)"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
install_hint("davfs2", PackageFamily::Fedora),
|
||||||
|
"install package 'davfs2' (dnf install davfs2)"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
install_hint("davfs2", PackageFamily::Arch),
|
||||||
|
"install package 'davfs2' (pacman -S davfs2)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn install_hint_omits_command_for_unknown_family() {
|
||||||
|
assert_eq!(
|
||||||
|
install_hint("davfs2", PackageFamily::Unknown),
|
||||||
|
"install package 'davfs2'"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sample_pair(owner_user: Option<&str>, mac: bool) -> DrivePair {
|
||||||
DrivePair {
|
DrivePair {
|
||||||
id: "pair-1".into(),
|
id: "pair-1".into(),
|
||||||
name: "Test".into(),
|
name: "Test".into(),
|
||||||
enabled: true,
|
enabled: true,
|
||||||
context,
|
|
||||||
owner_user: owner_user.map(str::to_string),
|
owner_user: owner_user.map(str::to_string),
|
||||||
mount_point: "/media/smart-mount/pair-1".into(),
|
mount_point: "/media/smart-mount/pair-1".into(),
|
||||||
local: LocalSide {
|
local: LocalSide {
|
||||||
@@ -288,7 +396,7 @@ mod tests {
|
|||||||
fn used_mount_kinds_collects_both_sides_across_pairs() {
|
fn used_mount_kinds_collects_both_sides_across_pairs() {
|
||||||
let cfg = AppConfig {
|
let cfg = AppConfig {
|
||||||
settings: GlobalSettings::default(),
|
settings: GlobalSettings::default(),
|
||||||
pairs: vec![sample_pair(MountContext::System, None, false)],
|
pairs: vec![sample_pair(None, false)],
|
||||||
};
|
};
|
||||||
let kinds = used_mount_kinds(&cfg);
|
let kinds = used_mount_kinds(&cfg);
|
||||||
assert!(kinds.contains(&MountKind::Nfs));
|
assert!(kinds.contains(&MountKind::Nfs));
|
||||||
@@ -300,40 +408,16 @@ mod tests {
|
|||||||
fn uses_mac_addressing_detects_mac_pairs() {
|
fn uses_mac_addressing_detects_mac_pairs() {
|
||||||
let with_mac = AppConfig {
|
let with_mac = AppConfig {
|
||||||
settings: GlobalSettings::default(),
|
settings: GlobalSettings::default(),
|
||||||
pairs: vec![sample_pair(MountContext::System, None, true)],
|
pairs: vec![sample_pair(None, true)],
|
||||||
};
|
};
|
||||||
let without_mac = AppConfig {
|
let without_mac = AppConfig {
|
||||||
settings: GlobalSettings::default(),
|
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(&with_mac));
|
||||||
assert!(!uses_mac_addressing(&without_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]
|
#[test]
|
||||||
fn empty_config_still_runs_global_checks_without_panicking() {
|
fn empty_config_still_runs_global_checks_without_panicking() {
|
||||||
let cfg = AppConfig::default();
|
let cfg = AppConfig::default();
|
||||||
|
|||||||
@@ -39,9 +39,6 @@ pub enum Error {
|
|||||||
#[error("mac2ip resolution failed for MAC {mac}: {reason}")]
|
#[error("mac2ip resolution failed for MAC {mac}: {reason}")]
|
||||||
Mac2Ip { mac: String, reason: String },
|
Mac2Ip { mac: String, reason: String },
|
||||||
|
|
||||||
#[error("No root context: {0}")]
|
|
||||||
RequiresRoot(&'static str),
|
|
||||||
|
|
||||||
#[error("{0}")]
|
#[error("{0}")]
|
||||||
Other(String),
|
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"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+1
-2
@@ -1,16 +1,15 @@
|
|||||||
//! smart-mount: bindet lokale/Cloud-Laufwerkspaare (WebDAV/SMB/NFS) dynamisch ein und
|
//! smart-mount: bindet lokale/Cloud-Laufwerkspaare (WebDAV/SMB/NFS) dynamisch ein und
|
||||||
//! schaltet automatisch zwischen LAN und Cloud um.
|
//! schaltet automatisch zwischen LAN und Cloud um.
|
||||||
|
|
||||||
|
pub mod cli;
|
||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod crypto;
|
pub mod crypto;
|
||||||
pub mod db;
|
pub mod db;
|
||||||
pub mod doctor;
|
pub mod doctor;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod fstab;
|
|
||||||
pub mod mount;
|
pub mod mount;
|
||||||
pub mod network;
|
pub mod network;
|
||||||
pub mod reconcile;
|
pub mod reconcile;
|
||||||
pub mod systemd;
|
pub mod systemd;
|
||||||
pub(crate) mod util;
|
|
||||||
|
|
||||||
pub use error::{Error, Result};
|
pub use error::{Error, Result};
|
||||||
|
|||||||
+14
-2
@@ -1,18 +1,30 @@
|
|||||||
mod cli;
|
|
||||||
|
|
||||||
use std::process::ExitCode;
|
use std::process::ExitCode;
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
use smart_mount::cli;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> ExitCode {
|
async fn main() -> ExitCode {
|
||||||
smart_mount::config::init();
|
smart_mount::config::init();
|
||||||
|
|
||||||
|
// Persistentes, gut auffindbares Log-Verzeichnis statt logger-ctdras Standard-Fallback
|
||||||
|
// (ein Ordner im System-Temp-Verzeichnis, siehe logger-ctdra-Doku) - smart-mount läuft
|
||||||
|
// ausschließlich als root/System-Dienst (siehe crate::systemd), daher immer derselbe,
|
||||||
|
// feste Pfad. Muss vor dem ersten Log-Aufruf stehen (siehe logger_ctdra::set_log_dir).
|
||||||
|
logger_ctdra::set_log_dir("/var/log/smart-mount");
|
||||||
|
|
||||||
let log_level = match smart_mount::config::pairs::load() {
|
let log_level = match smart_mount::config::pairs::load() {
|
||||||
Ok(cfg) => cfg.settings.log_level,
|
Ok(cfg) => cfg.settings.log_level,
|
||||||
Err(_) => "info".to_string(),
|
Err(_) => "info".to_string(),
|
||||||
};
|
};
|
||||||
logger_ctdra::set_log_level(parse_log_level(&log_level));
|
logger_ctdra::set_log_level(parse_log_level(&log_level));
|
||||||
|
logger_ctdra::info(
|
||||||
|
"main",
|
||||||
|
&format!(
|
||||||
|
"smart-mount {} starting (log level: {log_level})",
|
||||||
|
env!("CARGO_PKG_VERSION")
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
let cli = cli::Cli::parse();
|
let cli = cli::Cli::parse();
|
||||||
match cli::dispatch(cli).await {
|
match cli::dispatch(cli).await {
|
||||||
|
|||||||
+13
-1
@@ -23,9 +23,16 @@ use crate::error::{Error, Result};
|
|||||||
/// Datei-Deskriptor), bis der Guard gedroppt wird - Schließen des Deskriptors gibt die Sperre
|
/// Datei-Deskriptor), bis der Guard gedroppt wird - Schließen des Deskriptors gibt die Sperre
|
||||||
/// implizit frei, ein explizites `unlock()` ist dafür nicht nötig.
|
/// implizit frei, ein explizites `unlock()` ist dafür nicht nötig.
|
||||||
pub struct PairLock {
|
pub struct PairLock {
|
||||||
|
pair_id: String,
|
||||||
_file: File,
|
_file: File,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Drop for PairLock {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
logger_ctdra::debug("lock", &format!("pair '{}': lock released", self.pair_id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Verzeichnis für die Sperrdateien: `/run/smart-mount/locks` im Root-/System-Kontext (root ist
|
/// Verzeichnis für die Sperrdateien: `/run/smart-mount/locks` im Root-/System-Kontext (root ist
|
||||||
/// dort ohnehin die einzige Partei, die Paare in diesem Kontext mountet), sonst
|
/// dort ohnehin die einzige Partei, die Paare in diesem Kontext mountet), sonst
|
||||||
/// `$XDG_RUNTIME_DIR` (per-Nutzer, von systemd `0700`-geschützt angelegt) mit Fallback auf das
|
/// `$XDG_RUNTIME_DIR` (per-Nutzer, von systemd `0700`-geschützt angelegt) mit Fallback auf das
|
||||||
@@ -59,8 +66,13 @@ fn acquire_blocking(pair_id: &str) -> Result<PairLock> {
|
|||||||
// Blockiert, bis die Sperre frei wird - `flock(2)` kennt keinen Async-Mechanismus, daher
|
// Blockiert, bis die Sperre frei wird - `flock(2)` kennt keinen Async-Mechanismus, daher
|
||||||
// läuft dieser gesamte Aufruf über `spawn_blocking` (siehe [`acquire`]) auf einem
|
// läuft dieser gesamte Aufruf über `spawn_blocking` (siehe [`acquire`]) auf einem
|
||||||
// Blocking-Thread statt einem Tokio-Worker-Thread.
|
// Blocking-Thread statt einem Tokio-Worker-Thread.
|
||||||
|
logger_ctdra::debug("lock", &format!("pair '{pair_id}': waiting for lock"));
|
||||||
file.lock().map_err(|e| Error::io(&path, e))?;
|
file.lock().map_err(|e| Error::io(&path, e))?;
|
||||||
Ok(PairLock { _file: file })
|
logger_ctdra::debug("lock", &format!("pair '{pair_id}': lock acquired"));
|
||||||
|
Ok(PairLock {
|
||||||
|
pair_id: pair_id.to_string(),
|
||||||
|
_file: file,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sperrt ein Laufwerkspaar prozessübergreifend für die Dauer des zurückgegebenen Guards.
|
/// Sperrt ein Laufwerkspaar prozessübergreifend für die Dauer des zurückgegebenen Guards.
|
||||||
|
|||||||
+27
-24
@@ -18,21 +18,9 @@ use crate::config::{DrivePair, GlobalSettings, MountKind};
|
|||||||
use crate::db::credentials::{Credential, Side};
|
use crate::db::credentials::{Credential, Side};
|
||||||
use crate::error::Result;
|
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-
|
/// 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 struct MountTarget {
|
||||||
pub pair_id: String,
|
pub pair_id: String,
|
||||||
/// Welche Seite des Paars (lokal/cloud) dieses Ziel betrifft - bestimmt u. a. stabile
|
/// Welche Seite des Paars (lokal/cloud) dieses Ziel betrifft - bestimmt u. a. stabile
|
||||||
@@ -41,12 +29,11 @@ pub struct MountTarget {
|
|||||||
pub mount_point: PathBuf,
|
pub mount_point: PathBuf,
|
||||||
/// Vollständiger Quellstring, z. B. `//server/share`, `https://host/path`, `server:/export`.
|
/// Vollständiger Quellstring, z. B. `//server/share`, `https://host/path`, `server:/export`.
|
||||||
pub source: String,
|
pub source: String,
|
||||||
/// `-o`-Optionen als rohe Tokens (`"uid=1000"` oder bloße Flags wie `"soft"`), nur bei
|
/// `-o`-Optionen als rohe Tokens (`"uid=1000"` oder bloße Flags wie `"soft"`) - mit Kommas
|
||||||
/// `MountInvocation::Direct` verwendet - mit Kommas verbindbar für `mount -o`.
|
/// verbindbar für `mount -o`.
|
||||||
pub options: Vec<String>,
|
pub options: Vec<String>,
|
||||||
pub invocation: MountInvocation,
|
/// Für Paare mit gesetztem `owner_user`: der Linux-Benutzername, dem Zugangsdaten-/
|
||||||
/// Für `MountContext::User`-Paare: der Linux-Benutzername, dem Zugangsdaten-/Secrets-
|
/// Secrets-Dateien gehören sollen.
|
||||||
/// Dateien gehören sollen.
|
|
||||||
pub owner_user: Option<String>,
|
pub owner_user: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,6 +93,17 @@ fn run_tolerating_already_done_with_timeout(
|
|||||||
tolerate_timeout: bool,
|
tolerate_timeout: bool,
|
||||||
timeout_secs: u64,
|
timeout_secs: u64,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
|
logger_ctdra::debug(
|
||||||
|
"mount",
|
||||||
|
&format!(
|
||||||
|
"{context}: running '{} {}' (timeout {timeout_secs}s)",
|
||||||
|
cmd.get_program().to_string_lossy(),
|
||||||
|
cmd.get_args()
|
||||||
|
.map(|a| a.to_string_lossy().into_owned())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(" ")
|
||||||
|
),
|
||||||
|
);
|
||||||
let output =
|
let output =
|
||||||
run_with_timeout(cmd, timeout_secs).map_err(|e| crate::error::Error::MountFailed {
|
run_with_timeout(cmd, timeout_secs).map_err(|e| crate::error::Error::MountFailed {
|
||||||
context: context.to_string(),
|
context: context.to_string(),
|
||||||
@@ -113,6 +111,7 @@ fn run_tolerating_already_done_with_timeout(
|
|||||||
})?;
|
})?;
|
||||||
|
|
||||||
if output.status.success() {
|
if output.status.success() {
|
||||||
|
logger_ctdra::debug("mount", &format!("{context}: succeeded"));
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,16 +185,20 @@ fn cleanup_side_credentials(
|
|||||||
match kind {
|
match kind {
|
||||||
MountKind::Smb => {
|
MountKind::Smb => {
|
||||||
let path = smb::credentials_path(&pair.id, side);
|
let path = smb::credentials_path(&pair.id, side);
|
||||||
if path.exists()
|
if path.exists() {
|
||||||
&& let Err(e) = std::fs::remove_file(&path)
|
match std::fs::remove_file(&path) {
|
||||||
{
|
Ok(()) => logger_ctdra::debug(
|
||||||
logger_ctdra::warn(
|
"mount",
|
||||||
|
&format!("deleted credentials file '{}'", path.display()),
|
||||||
|
),
|
||||||
|
Err(e) => logger_ctdra::warn(
|
||||||
"mount",
|
"mount",
|
||||||
&format!(
|
&format!(
|
||||||
"Could not delete credentials file '{}': {e}",
|
"Could not delete credentials file '{}': {e}",
|
||||||
path.display()
|
path.display()
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
MountKind::WebDav => {
|
MountKind::WebDav => {
|
||||||
|
|||||||
+1
-10
@@ -5,9 +5,7 @@ use std::process::Command;
|
|||||||
|
|
||||||
use crate::db::credentials::Credential;
|
use crate::db::credentials::Credential;
|
||||||
use crate::error::{Error, Result};
|
use crate::error::{Error, Result};
|
||||||
use crate::mount::{
|
use crate::mount::{MountBackend, MountTarget, binary_available, run_tolerating_already_done};
|
||||||
MountBackend, MountInvocation, MountTarget, binary_available, run_tolerating_already_done,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub struct NfsBackend;
|
pub struct NfsBackend;
|
||||||
|
|
||||||
@@ -34,8 +32,6 @@ impl MountBackend for NfsBackend {
|
|||||||
|
|
||||||
fn mount(&self, target: &MountTarget) -> Result<()> {
|
fn mount(&self, target: &MountTarget) -> Result<()> {
|
||||||
let mut cmd = Command::new("mount");
|
let mut cmd = Command::new("mount");
|
||||||
match target.invocation {
|
|
||||||
MountInvocation::Direct => {
|
|
||||||
cmd.arg("-t")
|
cmd.arg("-t")
|
||||||
.arg("nfs")
|
.arg("nfs")
|
||||||
.arg(&target.source)
|
.arg(&target.source)
|
||||||
@@ -43,11 +39,6 @@ impl MountBackend for NfsBackend {
|
|||||||
if !target.options.is_empty() {
|
if !target.options.is_empty() {
|
||||||
cmd.arg("-o").arg(target.options.join(","));
|
cmd.arg("-o").arg(target.options.join(","));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
MountInvocation::ViaFstab => {
|
|
||||||
cmd.arg(&target.mount_point);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
run_tolerating_already_done(cmd, "nfs mount", false)
|
run_tolerating_already_done(cmd, "nfs mount", false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+9
-13
@@ -7,9 +7,7 @@ use std::process::Command;
|
|||||||
|
|
||||||
use crate::db::credentials::Credential;
|
use crate::db::credentials::Credential;
|
||||||
use crate::error::{Error, Result};
|
use crate::error::{Error, Result};
|
||||||
use crate::mount::{
|
use crate::mount::{MountBackend, MountTarget, binary_available, run_tolerating_already_done};
|
||||||
MountBackend, MountInvocation, MountTarget, binary_available, run_tolerating_already_done,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub struct SmbBackend;
|
pub struct SmbBackend;
|
||||||
|
|
||||||
@@ -31,15 +29,18 @@ impl MountBackend for SmbBackend {
|
|||||||
|
|
||||||
fn prepare(&self, target: &MountTarget, cred: Option<&Credential>) -> Result<()> {
|
fn prepare(&self, target: &MountTarget, cred: Option<&Credential>) -> Result<()> {
|
||||||
if let Some(cred) = cred {
|
if let Some(cred) = cred {
|
||||||
write_credentials_file(&credentials_path(&target.pair_id, target.side), cred)?;
|
let path = credentials_path(&target.pair_id, target.side);
|
||||||
|
logger_ctdra::debug(
|
||||||
|
"mount",
|
||||||
|
&format!("cifs: writing credentials file '{}'", path.display()),
|
||||||
|
);
|
||||||
|
write_credentials_file(&path, cred)?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn mount(&self, target: &MountTarget) -> Result<()> {
|
fn mount(&self, target: &MountTarget) -> Result<()> {
|
||||||
let mut cmd = Command::new("mount");
|
let mut cmd = Command::new("mount");
|
||||||
match target.invocation {
|
|
||||||
MountInvocation::Direct => {
|
|
||||||
cmd.arg("-t")
|
cmd.arg("-t")
|
||||||
.arg("cifs")
|
.arg("cifs")
|
||||||
.arg(&target.source)
|
.arg(&target.source)
|
||||||
@@ -50,11 +51,6 @@ impl MountBackend for SmbBackend {
|
|||||||
credentials_path(&target.pair_id, target.side).display()
|
credentials_path(&target.pair_id, target.side).display()
|
||||||
));
|
));
|
||||||
cmd.arg("-o").arg(opts.join(","));
|
cmd.arg("-o").arg(opts.join(","));
|
||||||
}
|
|
||||||
MountInvocation::ViaFstab => {
|
|
||||||
cmd.arg(&target.mount_point);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
run_tolerating_already_done(cmd, "cifs mount", false)
|
run_tolerating_already_done(cmd, "cifs mount", false)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,8 +61,8 @@ impl MountBackend for SmbBackend {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stabiler Pfad (nicht ein Tempfile!), da bei `MountInvocation::ViaFstab` die fstab-Zeile
|
/// Stabiler Pfad (nicht ein Tempfile!) - `prepare()` schreibt hierhin, `mount()` referenziert
|
||||||
/// (von `setup fstab` einmalig geschrieben) exakt auf diesen `credentials=`-Pfad verweist.
|
/// denselben Pfad über `-o credentials=...`.
|
||||||
pub fn credentials_path(pair_id: &str, side: crate::db::credentials::Side) -> PathBuf {
|
pub fn credentials_path(pair_id: &str, side: crate::db::credentials::Side) -> PathBuf {
|
||||||
let base = config_ctdra::get_config_path()
|
let base = config_ctdra::get_config_path()
|
||||||
.parent()
|
.parent()
|
||||||
|
|||||||
+71
-8
@@ -15,12 +15,13 @@
|
|||||||
//! kurz während eines Umschaltens (siehe [`crate::reconcile`]) mehr als ein Backing-Verzeichnis
|
//! kurz während eines Umschaltens (siehe [`crate::reconcile`]) mehr als ein Backing-Verzeichnis
|
||||||
//! gemountet - im Ruhezustand immer nur eines, wie ursprünglich vorgesehen.
|
//! gemountet - im Ruhezustand immer nur eines, wie ursprünglich vorgesehen.
|
||||||
|
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
use std::path::{Path, PathBuf};
|
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::db::credentials::Side;
|
||||||
use crate::error::{Error, Result};
|
use crate::error::{Error, Result};
|
||||||
use crate::mount::{self, MountInvocation, MountTarget};
|
use crate::mount::{self, MountTarget};
|
||||||
use crate::network::address;
|
use crate::network::address;
|
||||||
|
|
||||||
/// Eindeutiges Backing-Verzeichnis für eine Seite eines Paars - hier (und nur hier) wird
|
/// Eindeutiges Backing-Verzeichnis für eine Seite eines Paars - hier (und nur hier) wird
|
||||||
@@ -60,10 +61,6 @@ pub fn build_target_with_cached_local_ip(
|
|||||||
side: Side,
|
side: Side,
|
||||||
cached_local_ip: Option<std::net::Ipv4Addr>,
|
cached_local_ip: Option<std::net::Ipv4Addr>,
|
||||||
) -> Result<MountTarget> {
|
) -> Result<MountTarget> {
|
||||||
let invocation = match pair.context {
|
|
||||||
MountContext::System => MountInvocation::Direct,
|
|
||||||
MountContext::User => MountInvocation::ViaFstab,
|
|
||||||
};
|
|
||||||
let kind = side_kind(pair, side);
|
let kind = side_kind(pair, side);
|
||||||
|
|
||||||
let (source, mut options) = match side {
|
let (source, mut options) = match side {
|
||||||
@@ -88,7 +85,6 @@ pub fn build_target_with_cached_local_ip(
|
|||||||
mount_point: backing_dir(pair, side),
|
mount_point: backing_dir(pair, side),
|
||||||
source,
|
source,
|
||||||
options,
|
options,
|
||||||
invocation,
|
|
||||||
owner_user: pair.owner_user.clone(),
|
owner_user: pair.owner_user.clone(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -164,6 +160,21 @@ fn resolve_uid_gid(username: &str) -> Result<(u32, u32)> {
|
|||||||
Ok((run_id(username, "-u")?, run_id(username, "-g")?))
|
Ok((run_id(username, "-u")?, run_id(username, "-g")?))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Macht `owner` (Nutzer + dessen primäre Gruppe) zum alleinigen Besitzer von `dir` (`0700`).
|
||||||
|
/// Für das Backing-Verzeichnis eines Paares mit gesetztem `owner_user` - unabhängig vom
|
||||||
|
/// Mount-Typ, also auch für NFS, wo es (anders als bei CIFS/WebDAV) keine `uid=`/`gid=`-
|
||||||
|
/// Mount-Option gibt, die die nach dem Mounten sichtbaren Rechte übersteuern könnte: das
|
||||||
|
/// Verzeichnis selbst gehört so wenigstens `owner` - reicht z. B. aus, um es zu betreten, bevor/
|
||||||
|
/// falls etwas gemountet ist, ersetzt aber nicht die serverseitige NFS-Export-Rechtevergabe
|
||||||
|
/// (siehe README, Abschnitt "Voller Zugriff für einen bestimmten Nutzer").
|
||||||
|
pub(crate) fn ensure_owner_only_dir(dir: &Path, owner: &str) -> Result<()> {
|
||||||
|
let (uid, gid) = resolve_uid_gid(owner)?;
|
||||||
|
std::os::unix::fs::chown(dir, Some(uid), Some(gid)).map_err(|e| Error::io(dir, e))?;
|
||||||
|
std::fs::set_permissions(dir, std::fs::Permissions::from_mode(0o700))
|
||||||
|
.map_err(|e| Error::io(dir, e))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn run_id(username: &str, flag: &str) -> Result<u32> {
|
fn run_id(username: &str, flag: &str) -> Result<u32> {
|
||||||
let output = std::process::Command::new("id")
|
let output = std::process::Command::new("id")
|
||||||
.arg(flag)
|
.arg(flag)
|
||||||
@@ -268,6 +279,13 @@ pub fn activate_symlink(pair: &DrivePair, side: Side) -> Result<()> {
|
|||||||
|
|
||||||
if let Some(parent) = link_path.parent() {
|
if let Some(parent) = link_path.parent() {
|
||||||
std::fs::create_dir_all(parent).map_err(|e| Error::io(parent, e))?;
|
std::fs::create_dir_all(parent).map_err(|e| Error::io(parent, e))?;
|
||||||
|
// `mount_base_dir` ist von allen Paaren (mit ggf. unterschiedlichem `owner_user`)
|
||||||
|
// gemeinsam genutzt - hart auf `0755` setzen statt sich auf das Umask des root-Prozesses
|
||||||
|
// zu verlassen, damit jeder lokale Nutzer bis zum eigentlichen Mountpoint durchqueren
|
||||||
|
// kann (die eigentliche Zugriffsbeschränkung passiert weiter unten je Paar, siehe
|
||||||
|
// `ensure_owner_only_dir`/die `uid=`/`gid=`-Mount-Optionen).
|
||||||
|
std::fs::set_permissions(parent, std::fs::Permissions::from_mode(0o755))
|
||||||
|
.map_err(|e| Error::io(parent, e))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let target = backing_dir(pair, side);
|
let target = backing_dir(pair, side);
|
||||||
@@ -283,6 +301,15 @@ pub fn activate_symlink(pair: &DrivePair, side: Side) -> Result<()> {
|
|||||||
let _ = std::fs::remove_file(&tmp_path);
|
let _ = std::fs::remove_file(&tmp_path);
|
||||||
std::os::unix::fs::symlink(&target, &tmp_path).map_err(|e| Error::io(&tmp_path, e))?;
|
std::os::unix::fs::symlink(&target, &tmp_path).map_err(|e| Error::io(&tmp_path, e))?;
|
||||||
std::fs::rename(&tmp_path, link_path).map_err(|e| Error::io(link_path, e))?;
|
std::fs::rename(&tmp_path, link_path).map_err(|e| Error::io(link_path, e))?;
|
||||||
|
logger_ctdra::info(
|
||||||
|
"mount",
|
||||||
|
&format!(
|
||||||
|
"pair '{}': activated '{}' -> '{}'",
|
||||||
|
pair.id,
|
||||||
|
link_path.display(),
|
||||||
|
target.display()
|
||||||
|
),
|
||||||
|
);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -296,7 +323,6 @@ mod tests {
|
|||||||
id: "pair-1".into(),
|
id: "pair-1".into(),
|
||||||
name: "Test".into(),
|
name: "Test".into(),
|
||||||
enabled: true,
|
enabled: true,
|
||||||
context: MountContext::System,
|
|
||||||
owner_user: None,
|
owner_user: None,
|
||||||
mount_point: "/media/smart-mount/pair-1".into(),
|
mount_point: "/media/smart-mount/pair-1".into(),
|
||||||
local: LocalSide {
|
local: LocalSide {
|
||||||
@@ -472,4 +498,41 @@ mod tests {
|
|||||||
let err = activate_symlink(&pair, Side::Local).unwrap_err();
|
let err = activate_symlink(&pair, Side::Local).unwrap_err();
|
||||||
assert!(err.to_string().contains("real directory"));
|
assert!(err.to_string().contains("real directory"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ensure_owner_only_dir_sets_mode_0700() {
|
||||||
|
let user = std::env::var("USER").expect("USER env var set in test environment");
|
||||||
|
let dir = tempfile::tempdir().expect("tempdir");
|
||||||
|
|
||||||
|
ensure_owner_only_dir(dir.path(), &user).expect("ensure_owner_only_dir");
|
||||||
|
|
||||||
|
let mode = std::fs::metadata(dir.path())
|
||||||
|
.expect("metadata")
|
||||||
|
.permissions()
|
||||||
|
.mode();
|
||||||
|
assert_eq!(mode & 0o777, 0o700);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ensure_owner_only_dir_fails_clearly_for_unknown_user() {
|
||||||
|
let dir = tempfile::tempdir().expect("tempdir");
|
||||||
|
let err = ensure_owner_only_dir(dir.path(), "no-such-user-xyz").unwrap_err();
|
||||||
|
assert!(err.to_string().contains("not found"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn activate_symlink_sets_shared_parent_dir_to_0755() {
|
||||||
|
let dir = tempfile::tempdir().expect("tempdir");
|
||||||
|
let parent = dir.path().join("mount-base");
|
||||||
|
let mut pair = sample_pair();
|
||||||
|
pair.mount_point = parent.join("pair-1");
|
||||||
|
|
||||||
|
activate_symlink(&pair, Side::Local).expect("activate local");
|
||||||
|
|
||||||
|
let mode = std::fs::metadata(&parent)
|
||||||
|
.expect("metadata")
|
||||||
|
.permissions()
|
||||||
|
.mode();
|
||||||
|
assert_eq!(mode & 0o777, 0o755);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-24
@@ -7,9 +7,7 @@ use std::process::Command;
|
|||||||
|
|
||||||
use crate::db::credentials::Credential;
|
use crate::db::credentials::Credential;
|
||||||
use crate::error::{Error, Result};
|
use crate::error::{Error, Result};
|
||||||
use crate::mount::{
|
use crate::mount::{MountBackend, MountTarget, binary_available, run_tolerating_already_done};
|
||||||
MountBackend, MountInvocation, MountTarget, binary_available, run_tolerating_already_done,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub struct WebDavBackend;
|
pub struct WebDavBackend;
|
||||||
|
|
||||||
@@ -48,6 +46,10 @@ impl MountBackend for WebDavBackend {
|
|||||||
"davfs2 credential has an empty username or password".to_string(),
|
"davfs2 credential has an empty username or password".to_string(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
logger_ctdra::debug(
|
||||||
|
"mount",
|
||||||
|
&format!("davfs2: writing secrets entry for '{}'", target.source),
|
||||||
|
);
|
||||||
write_secrets_entry(
|
write_secrets_entry(
|
||||||
&davfs2_secrets_path(),
|
&davfs2_secrets_path(),
|
||||||
&target.source,
|
&target.source,
|
||||||
@@ -60,8 +62,6 @@ impl MountBackend for WebDavBackend {
|
|||||||
|
|
||||||
fn mount(&self, target: &MountTarget) -> Result<()> {
|
fn mount(&self, target: &MountTarget) -> Result<()> {
|
||||||
let mut cmd = Command::new("mount");
|
let mut cmd = Command::new("mount");
|
||||||
match target.invocation {
|
|
||||||
MountInvocation::Direct => {
|
|
||||||
cmd.arg("-t")
|
cmd.arg("-t")
|
||||||
.arg("davfs")
|
.arg("davfs")
|
||||||
.arg(&target.source)
|
.arg(&target.source)
|
||||||
@@ -69,11 +69,6 @@ impl MountBackend for WebDavBackend {
|
|||||||
if !target.options.is_empty() {
|
if !target.options.is_empty() {
|
||||||
cmd.arg("-o").arg(target.options.join(","));
|
cmd.arg("-o").arg(target.options.join(","));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
MountInvocation::ViaFstab => {
|
|
||||||
cmd.arg(&target.mount_point);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
run_tolerating_already_done(cmd, "davfs2 mount", false)
|
run_tolerating_already_done(cmd, "davfs2 mount", false)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,26 +79,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 {
|
fn davfs2_conf_path() -> PathBuf {
|
||||||
if sudo_ctdra::is_run_as_root() {
|
|
||||||
PathBuf::from("/etc/davfs2/davfs2.conf")
|
PathBuf::from("/etc/davfs2/davfs2.conf")
|
||||||
} else {
|
|
||||||
home_dir().join(".davfs2/davfs2.conf")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn davfs2_secrets_path() -> PathBuf {
|
pub(crate) fn davfs2_secrets_path() -> PathBuf {
|
||||||
if sudo_ctdra::is_run_as_root() {
|
|
||||||
PathBuf::from("/etc/davfs2/secrets")
|
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
|
/// Setzt `gui_optimize 1` in `davfs2.conf` idempotent - reduziert bei grafischen
|
||||||
|
|||||||
+19
-1
@@ -38,6 +38,12 @@ enum Mac2IpOutput {
|
|||||||
/// `binary` ist der konfigurierte Binary-Name/-Pfad (`GlobalSettings::mac2ip_binary`,
|
/// `binary` ist der konfigurierte Binary-Name/-Pfad (`GlobalSettings::mac2ip_binary`,
|
||||||
/// standardmäßig `"mac2ip"`, per PATH aufgelöst).
|
/// standardmäßig `"mac2ip"`, per PATH aufgelöst).
|
||||||
pub fn resolve(mac: &str, binary: &str) -> Result<Ipv4Addr> {
|
pub fn resolve(mac: &str, binary: &str) -> Result<Ipv4Addr> {
|
||||||
|
logger_ctdra::info(
|
||||||
|
"network",
|
||||||
|
&format!(
|
||||||
|
"resolving MAC '{mac}' via '{binary}' (may take up to {MAC2IP_TIMEOUT_SECS}s, e.g. for an nmap scan)"
|
||||||
|
),
|
||||||
|
);
|
||||||
let output = Command::new("timeout")
|
let output = Command::new("timeout")
|
||||||
.arg(MAC2IP_TIMEOUT_SECS.to_string())
|
.arg(MAC2IP_TIMEOUT_SECS.to_string())
|
||||||
.arg(binary)
|
.arg(binary)
|
||||||
@@ -49,13 +55,25 @@ pub fn resolve(mac: &str, binary: &str) -> Result<Ipv4Addr> {
|
|||||||
})?;
|
})?;
|
||||||
|
|
||||||
if output.status.code() == Some(124) {
|
if output.status.code() == Some(124) {
|
||||||
|
logger_ctdra::warn(
|
||||||
|
"network",
|
||||||
|
&format!("'{binary}' did not respond within {MAC2IP_TIMEOUT_SECS}s for MAC '{mac}'"),
|
||||||
|
);
|
||||||
return Err(Error::Mac2Ip {
|
return Err(Error::Mac2Ip {
|
||||||
mac: mac.to_string(),
|
mac: mac.to_string(),
|
||||||
reason: format!("'{binary}' did not respond within {MAC2IP_TIMEOUT_SECS}s (timed out)"),
|
reason: format!("'{binary}' did not respond within {MAC2IP_TIMEOUT_SECS}s (timed out)"),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
parse_output(&output.stdout, mac)
|
let result = parse_output(&output.stdout, mac);
|
||||||
|
match &result {
|
||||||
|
Ok(ip) => logger_ctdra::info("network", &format!("MAC '{mac}' resolved to {ip}")),
|
||||||
|
Err(e) => logger_ctdra::debug(
|
||||||
|
"network",
|
||||||
|
&format!("MAC '{mac}' could not be resolved: {e}"),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Prüft, ob das konfigurierte `mac2ip`-Binary über `PATH` auffindbar ist.
|
/// Prüft, ob das konfigurierte `mac2ip`-Binary über `PATH` auffindbar ist.
|
||||||
|
|||||||
@@ -17,6 +17,15 @@ use std::process::{Command, Stdio};
|
|||||||
/// (z. B. ein kaputter Reverse-Proxy) sonst bei jedem `watch`-Durchlauf einen vollen,
|
/// (z. B. ein kaputter Reverse-Proxy) sonst bei jedem `watch`-Durchlauf einen vollen,
|
||||||
/// letztlich erfolglosen Umschaltversuch (inkl. `MOUNT_TIMEOUT_SECS`-Wartezeit) auslösen würde.
|
/// letztlich erfolglosen Umschaltversuch (inkl. `MOUNT_TIMEOUT_SECS`-Wartezeit) auslösen würde.
|
||||||
pub fn is_reachable(addr: &str) -> bool {
|
pub fn is_reachable(addr: &str) -> bool {
|
||||||
|
let reachable = is_reachable_inner(addr);
|
||||||
|
logger_ctdra::debug(
|
||||||
|
"network",
|
||||||
|
&format!("reachability check for '{addr}': {reachable}"),
|
||||||
|
);
|
||||||
|
reachable
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_reachable_inner(addr: &str) -> bool {
|
||||||
if addr.starts_with("http://") || addr.starts_with("https://") {
|
if addr.starts_with("http://") || addr.starts_with("https://") {
|
||||||
Command::new("curl")
|
Command::new("curl")
|
||||||
.args([
|
.args([
|
||||||
|
|||||||
+85
-12
@@ -50,6 +50,13 @@ pub struct ReconcileOutcome {
|
|||||||
/// Risiko für einen Effizienzgewinn, den die Reachability-Parallelisierung bereits liefert.
|
/// Risiko für einen Effizienzgewinn, den die Reachability-Parallelisierung bereits liefert.
|
||||||
pub async fn watch_once(cfg: &AppConfig, creds: &CredentialStore) -> Vec<ReconcileOutcome> {
|
pub async fn watch_once(cfg: &AppConfig, creds: &CredentialStore) -> Vec<ReconcileOutcome> {
|
||||||
let enabled: Vec<&DrivePair> = cfg.pairs.iter().filter(|p| p.enabled).collect();
|
let enabled: Vec<&DrivePair> = cfg.pairs.iter().filter(|p| p.enabled).collect();
|
||||||
|
logger_ctdra::debug(
|
||||||
|
"reconcile",
|
||||||
|
&format!(
|
||||||
|
"watch: checking reachability for {} enabled pair(s)",
|
||||||
|
enabled.len()
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
let mut checks = Vec::with_capacity(enabled.len());
|
let mut checks = Vec::with_capacity(enabled.len());
|
||||||
for pair in &enabled {
|
for pair in &enabled {
|
||||||
@@ -123,16 +130,28 @@ async fn reconcile_pair_checked(
|
|||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(action) => ReconcileOutcome {
|
Ok(action) => {
|
||||||
|
logger_ctdra::info(
|
||||||
|
"reconcile",
|
||||||
|
&format!("pair '{pair_name}' ({pair_id}): {action:?}"),
|
||||||
|
);
|
||||||
|
ReconcileOutcome {
|
||||||
pair_id,
|
pair_id,
|
||||||
pair_name,
|
pair_name,
|
||||||
action,
|
action,
|
||||||
},
|
}
|
||||||
Err(e) => ReconcileOutcome {
|
}
|
||||||
|
Err(e) => {
|
||||||
|
logger_ctdra::error(
|
||||||
|
"reconcile",
|
||||||
|
&format!("pair '{pair_name}' ({pair_id}): reconcile failed: {e}"),
|
||||||
|
);
|
||||||
|
ReconcileOutcome {
|
||||||
pair_id,
|
pair_id,
|
||||||
pair_name,
|
pair_name,
|
||||||
action: Action::Failed(e.to_string()),
|
action: Action::Failed(e.to_string()),
|
||||||
},
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,6 +166,13 @@ async fn reconcile_pair_inner(
|
|||||||
let _guard = lock::acquire(&pair.id).await?;
|
let _guard = lock::acquire(&pair.id).await?;
|
||||||
|
|
||||||
let active = target::active_side(pair);
|
let active = target::active_side(pair);
|
||||||
|
logger_ctdra::debug(
|
||||||
|
"reconcile",
|
||||||
|
&format!(
|
||||||
|
"pair '{}': local_reachable={local_reachable} cloud_reachable={cloud_reachable} active={:?}",
|
||||||
|
pair.id, active
|
||||||
|
),
|
||||||
|
);
|
||||||
cleanup_orphaned_mounts(pair, settings, active).await;
|
cleanup_orphaned_mounts(pair, settings, active).await;
|
||||||
|
|
||||||
if local_reachable {
|
if local_reachable {
|
||||||
@@ -182,7 +208,13 @@ async fn reconcile_pair_inner(
|
|||||||
fn check_local_reachable(local: &LocalSide, settings: &GlobalSettings) -> (bool, Option<Ipv4Addr>) {
|
fn check_local_reachable(local: &LocalSide, settings: &GlobalSettings) -> (bool, Option<Ipv4Addr>) {
|
||||||
match address::resolve_ip(&local.address, settings) {
|
match address::resolve_ip(&local.address, settings) {
|
||||||
Ok(ip) => (network::is_reachable(&ip.to_string()), Some(ip)),
|
Ok(ip) => (network::is_reachable(&ip.to_string()), Some(ip)),
|
||||||
Err(_) => (false, None),
|
Err(e) => {
|
||||||
|
logger_ctdra::debug(
|
||||||
|
"reconcile",
|
||||||
|
&format!("local address could not be resolved: {e}"),
|
||||||
|
);
|
||||||
|
(false, None)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -251,6 +283,15 @@ async fn switch_to(
|
|||||||
old_active: Option<Side>,
|
old_active: Option<Side>,
|
||||||
cached_local_ip: Option<Ipv4Addr>,
|
cached_local_ip: Option<Ipv4Addr>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
|
logger_ctdra::info(
|
||||||
|
"reconcile",
|
||||||
|
&format!(
|
||||||
|
"pair '{}': switching to '{}' (previously active: {:?})",
|
||||||
|
pair.id,
|
||||||
|
new_side.as_str(),
|
||||||
|
old_active
|
||||||
|
),
|
||||||
|
);
|
||||||
mount_side(pair, settings, new_side, creds, cached_local_ip).await?;
|
mount_side(pair, settings, new_side, creds, cached_local_ip).await?;
|
||||||
|
|
||||||
// Falls das Aktivieren des Symlinks fehlschlägt, muss die gerade gemountete `new_side`
|
// Falls das Aktivieren des Symlinks fehlschlägt, muss die gerade gemountete `new_side`
|
||||||
@@ -280,13 +321,38 @@ async fn mount_side(
|
|||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let mount_target =
|
let mount_target =
|
||||||
target::build_target_with_cached_local_ip(pair, settings, side, cached_local_ip)?;
|
target::build_target_with_cached_local_ip(pair, settings, side, cached_local_ip)?;
|
||||||
|
logger_ctdra::debug(
|
||||||
|
"reconcile",
|
||||||
|
&format!(
|
||||||
|
"pair '{}' ({}): resolved source '{}' -> '{}'",
|
||||||
|
pair.id,
|
||||||
|
side.as_str(),
|
||||||
|
mount_target.source,
|
||||||
|
mount_target.mount_point.display()
|
||||||
|
),
|
||||||
|
);
|
||||||
std::fs::create_dir_all(&mount_target.mount_point)
|
std::fs::create_dir_all(&mount_target.mount_point)
|
||||||
.map_err(|e| crate::error::Error::io(&mount_target.mount_point, e))?;
|
.map_err(|e| crate::error::Error::io(&mount_target.mount_point, e))?;
|
||||||
|
// Backing-Verzeichnis auf `owner_user` chownen (0700) - relevant vor allem für NFS, wo es
|
||||||
|
// (anders als CIFS/WebDAV) keine `uid=`/`gid=`-Mount-Option gibt, die den Zugriff nach dem
|
||||||
|
// Mounten sonst regeln würde (siehe `target::ensure_owner_only_dir`).
|
||||||
|
if let Some(owner) = &pair.owner_user {
|
||||||
|
target::ensure_owner_only_dir(&mount_target.mount_point, owner)?;
|
||||||
|
}
|
||||||
|
|
||||||
let backend = mount::backend_for(target::side_kind(pair, side));
|
let backend = mount::backend_for(target::side_kind(pair, side));
|
||||||
backend.check_available()?;
|
backend.check_available()?;
|
||||||
let cred = creds.get(&pair.id, side).await?;
|
let cred = creds.get(&pair.id, side).await?;
|
||||||
backend.prepare(&mount_target, cred.as_ref())?;
|
backend.prepare(&mount_target, cred.as_ref())?;
|
||||||
|
logger_ctdra::info(
|
||||||
|
"reconcile",
|
||||||
|
&format!(
|
||||||
|
"pair '{}' ({}): mounting via {} ...",
|
||||||
|
pair.id,
|
||||||
|
side.as_str(),
|
||||||
|
backend.name()
|
||||||
|
),
|
||||||
|
);
|
||||||
backend.mount(&mount_target)
|
backend.mount(&mount_target)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -295,6 +361,7 @@ async fn mount_side(
|
|||||||
/// ein leeres, ausgehängtes Backing-Verzeichnis) - der nächste `mount`/`watch`-Lauf räumt das
|
/// ein leeres, ausgehängtes Backing-Verzeichnis) - der nächste `mount`/`watch`-Lauf räumt das
|
||||||
/// beim erneuten Aktivieren automatisch auf.
|
/// beim erneuten Aktivieren automatisch auf.
|
||||||
pub async fn unmount_pair(pair: &DrivePair, settings: &GlobalSettings) -> Result<Action> {
|
pub async fn unmount_pair(pair: &DrivePair, settings: &GlobalSettings) -> Result<Action> {
|
||||||
|
logger_ctdra::info("reconcile", &format!("pair '{}': unmounting", pair.id));
|
||||||
let _guard = lock::acquire(&pair.id).await?;
|
let _guard = lock::acquire(&pair.id).await?;
|
||||||
|
|
||||||
// Beide Seiten werden unabhängig voneinander versucht - ein Fehler (auch ein
|
// Beide Seiten werden unabhängig voneinander versucht - ein Fehler (auch ein
|
||||||
@@ -354,20 +421,27 @@ async fn unmount_side(pair: &DrivePair, settings: &GlobalSettings, side: Side) -
|
|||||||
source: String::new(),
|
source: String::new(),
|
||||||
mount_point: target::backing_dir(pair, side),
|
mount_point: target::backing_dir(pair, side),
|
||||||
options: vec![],
|
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(),
|
owner_user: pair.owner_user.clone(),
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
mount::backend_for(target::side_kind(pair, side)).unmount(&mount_target)
|
let backend = mount::backend_for(target::side_kind(pair, side));
|
||||||
|
logger_ctdra::debug(
|
||||||
|
"reconcile",
|
||||||
|
&format!(
|
||||||
|
"pair '{}' ({}): unmounting via {} ({})",
|
||||||
|
pair.id,
|
||||||
|
side.as_str(),
|
||||||
|
backend.name(),
|
||||||
|
mount_target.mount_point.display()
|
||||||
|
),
|
||||||
|
);
|
||||||
|
backend.unmount(&mount_target)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::config::{CloudSide, MountContext, MountKind};
|
use crate::config::{CloudSide, MountKind};
|
||||||
use std::net::Ipv4Addr;
|
use std::net::Ipv4Addr;
|
||||||
|
|
||||||
fn sample_pair(local_kind: MountKind, cloud_kind: MountKind) -> DrivePair {
|
fn sample_pair(local_kind: MountKind, cloud_kind: MountKind) -> DrivePair {
|
||||||
@@ -375,7 +449,6 @@ mod tests {
|
|||||||
id: "pair-1".into(),
|
id: "pair-1".into(),
|
||||||
name: "Test".into(),
|
name: "Test".into(),
|
||||||
enabled: true,
|
enabled: true,
|
||||||
context: MountContext::System,
|
|
||||||
owner_user: None,
|
owner_user: None,
|
||||||
mount_point: "/media/smart-mount/pair-1".into(),
|
mount_point: "/media/smart-mount/pair-1".into(),
|
||||||
local: LocalSide {
|
local: LocalSide {
|
||||||
|
|||||||
+114
-291
@@ -1,172 +1,116 @@
|
|||||||
//! Generiert/installiert systemd-Units (System- und User-Kontext) sowie das Crontab-Äquivalent
|
//! Cron-Fallback für Systeme ohne (oder ohne genutzten) systemd.
|
||||||
//! für Systeme ohne systemd.
|
|
||||||
//!
|
//!
|
||||||
//! Die systemd-Units rufen ausschließlich einfache `smart-mount`-Subcommands auf, damit
|
//! Die eigentliche systemd-Einrichtung passiert NICHT zur Laufzeit über dieses Modul, sondern
|
||||||
//! dieselben Zeilen 1:1 als Crontab-Einträge funktionieren.
|
//! über die paketierten, statischen Unit-Dateien (siehe `packaging/systemd/` im Quellbaum)
|
||||||
|
//! sowie die postinst/postrm-Skripte der .deb/.rpm/.pkg.tar.zst-Pakete. Dieses Modul stellt die
|
||||||
|
//! Logik für den Cron-Fallback bereit - aufgerufen wird sie NICHT interaktiv über ein
|
||||||
|
//! `smart-mount`-Subcommand, sondern automatisch vom separaten `setup-cron`-Hilfsprogramm
|
||||||
|
//! (siehe `src/bin/setup-cron.rs`), das ebenfalls über die postinst-Skripte der Pakete beim
|
||||||
|
//! Installieren läuft, sofern kein systemd verfügbar ist (siehe [`is_available`]). Immer
|
||||||
|
//! systemweit (`/etc/cron.d/smart-mount`), da Mounten ohnehin immer Root-Rechte braucht (siehe
|
||||||
|
//! [`crate::cli::require_root`]); eine persönliche Nutzer-Crontab liefe ins Leere.
|
||||||
|
|
||||||
use std::io::Write;
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::process::{Command, Stdio};
|
|
||||||
|
|
||||||
use crate::error::{Error, Result};
|
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_D_PATH: &str = "/etc/cron.d/smart-mount";
|
||||||
const CRON_BEGIN_MARKER: &str = "# BEGIN smart-mount managed block";
|
const CRON_BEGIN_MARKER: &str = "# BEGIN smart-mount managed block";
|
||||||
const CRON_END_MARKER: &str = "# END smart-mount managed block";
|
const CRON_END_MARKER: &str = "# END smart-mount managed block";
|
||||||
|
|
||||||
/// System (root) oder User-Kontext für die Unit-Installation.
|
/// Fester Installationspfad der Haupt-Binary (siehe `[package.metadata.deb/generate-rpm].assets`
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
/// bzw. `scripts/package-arch.py`, die immer nach `/usr/bin/<name>` installieren) - smart-mount
|
||||||
pub enum Scope {
|
/// läuft ausschließlich als paketierter System-Dienst, es gibt keinen anderen Installationsort.
|
||||||
System,
|
/// Bewusst NICHT über `std::env::current_exe()`: der Cron-Eintrag wird vom separaten
|
||||||
User,
|
/// `setup-cron`-Hilfsprogramm geschrieben (siehe [`crate::systemd`]-Moduldoku), dessen eigener
|
||||||
|
/// Pfad hier falsch wäre.
|
||||||
|
fn binary_path() -> &'static str {
|
||||||
|
"/usr/bin/smart-mount"
|
||||||
}
|
}
|
||||||
|
|
||||||
fn binary_path() -> String {
|
/// Ob `systemctl` auf diesem System vorhanden ist - entscheidet für `doctor` UND für
|
||||||
std::env::current_exe()
|
/// `setup-cron` (siehe Moduldoku), ob der Cron-Fallback überhaupt installiert werden soll: die
|
||||||
.ok()
|
/// eigentliche systemd-Einrichtung läuft über die Paketierung, ein zusätzlicher Cron-Eintrag
|
||||||
.and_then(|p| p.to_str().map(str::to_string))
|
/// wäre dort redundant.
|
||||||
.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.
|
|
||||||
pub fn is_available() -> bool {
|
pub fn is_available() -> bool {
|
||||||
crate::mount::binary_available("systemctl")
|
crate::mount::binary_available("systemctl")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ergebnis von [`uninstall`].
|
/// `systemctl is-enabled`/`is-active <unit>` liefert bei Erfolg wie bei Misserfolg exakt eine
|
||||||
pub enum SystemdUninstallOutcome {
|
/// Ausgabezeile auf stdout (z. B. "enabled"/"disabled"/"static"/"not-found" bzw.
|
||||||
/// Mindestens eine Unit-Datei war vorhanden und wurde entfernt.
|
/// "active"/"inactive"/"failed") - der Exit-Code allein reicht nicht (z. B. ist `is-active`
|
||||||
Removed,
|
/// auch für eine deaktivierte, aber existierende Unit `1`). `None` bei fehlendem `systemctl`
|
||||||
/// Keine der Unit-Dateien war vorhanden - nichts zu tun.
|
/// oder einem sonstigen Ausführungsfehler.
|
||||||
NotPresent,
|
fn systemctl_query(subcommand: &str, unit: &str) -> Option<String> {
|
||||||
|
std::process::Command::new("systemctl")
|
||||||
|
.args([subcommand, unit])
|
||||||
|
.output()
|
||||||
|
.ok()
|
||||||
|
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Deaktiviert und entfernt die Unit-Dateien, falls vorhanden.
|
/// Ob `systemctl` die Unit überhaupt kennt (eine geladene Unit-Datei existiert), unabhängig
|
||||||
pub fn uninstall(scope: Scope) -> Result<SystemdUninstallOutcome> {
|
/// davon, ob sie aktiviert/aktiv ist - für `doctor`, um "installiert, aber nicht
|
||||||
let dir = unit_dir(scope)?;
|
/// aktiviert/gestartet" von "gar nicht installiert" unterscheiden zu können. Eine komplett
|
||||||
let units = [MOUNT_SERVICE, WATCH_SERVICE, WATCH_TIMER];
|
/// unbekannte Unit liefert bei `is-enabled` je nach systemd-Version "not-found" oder eine leere
|
||||||
|
/// Ausgabe (Fehler auf stderr) - beides ungleich einem der hier aufgeführten, eine tatsächlich
|
||||||
if !units.iter().any(|unit| dir.join(unit).exists()) {
|
/// geladene Unit-Datei voraussetzenden Zustände.
|
||||||
return Ok(SystemdUninstallOutcome::NotPresent);
|
pub fn is_unit_installed(unit: &str) -> bool {
|
||||||
|
matches!(
|
||||||
|
systemctl_query("is-enabled", unit).as_deref(),
|
||||||
|
Some(
|
||||||
|
"enabled"
|
||||||
|
| "disabled"
|
||||||
|
| "static"
|
||||||
|
| "alias"
|
||||||
|
| "linked"
|
||||||
|
| "generated"
|
||||||
|
| "transient"
|
||||||
|
| "masked"
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
let _ = systemctl(scope, &["disable", "--now", MOUNT_SERVICE, WATCH_TIMER]);
|
/// Ob die Unit dauerhaft aktiviert ist (startet automatisch, siehe `man systemctl`,
|
||||||
|
/// Abschnitt "is-enabled").
|
||||||
for unit in units {
|
pub fn is_unit_enabled(unit: &str) -> bool {
|
||||||
let path = dir.join(unit);
|
systemctl_query("is-enabled", unit).as_deref() == Some("enabled")
|
||||||
if path.exists() {
|
|
||||||
std::fs::remove_file(&path).map_err(|e| Error::io(&path, e))?;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
systemctl(scope, &["daemon-reload"])?;
|
/// Ob die Unit aktuell läuft (bei einem Timer: wartet auf die nächste Auslösung). Bei einem
|
||||||
Ok(SystemdUninstallOutcome::Removed)
|
/// `oneshot`-Service wie `smart-mount-mount.service` (läuft einmal beim Boot und beendet sich
|
||||||
|
/// danach) ist "inactive" direkt nach einem erfolgreichen Lauf der korrekte, erwartete Zustand -
|
||||||
|
/// dafür ist [`is_unit_installed`]/[`is_unit_enabled`] aussagekräftiger.
|
||||||
|
pub fn is_unit_active(unit: &str) -> bool {
|
||||||
|
systemctl_query("is-active", unit).as_deref() == Some("active")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Erzeugt die Crontab-Äquivalente zu den generierten Units, für Systeme ohne systemd.
|
/// Ob ein von [`install_cron`] verwalteter Cron-Eintrag aktuell existiert - für `doctor`, um
|
||||||
/// `watch_interval_secs` ist derselbe Wert wie `settings.watch_interval_secs`, der auch die
|
/// zwischen "kein Cron-Mechanismus vorhanden" und "vorhanden, aber smart-mounts Eintrag fehlt"
|
||||||
/// `OnUnitActiveSec`-Periode des systemd-Timers steuert - beide Wege sollen dieselbe Kadenz
|
/// (z. B. weil das Paket nicht sauber installiert wurde) unterscheiden zu können.
|
||||||
/// ergeben, statt dass die Crontab-Variante einen unabhängigen, fest eingebauten Wert hat.
|
pub fn is_cron_installed() -> bool {
|
||||||
|
Path::new(CRON_D_PATH).exists()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ob aktuell ein Cron-Daemon-Prozess läuft - rein prozessbasiert über `pgrep` geprüft (statt
|
||||||
|
/// z. B. über `systemctl is-active cron.service`), damit es unabhängig davon funktioniert, ob
|
||||||
|
/// systemd überhaupt vorhanden ist: genau im Cron-Fallback-Fall (kein/kein genutztes systemd,
|
||||||
|
/// siehe Moduldoku) ist das der einzige portable Weg. Prüft beide gängigen Prozessnamen
|
||||||
|
/// (Debian/Ubuntu: `cron`, Fedora/Arch: `crond`), ohne selbst eine Distributions-Erkennung zu
|
||||||
|
/// brauchen.
|
||||||
|
pub fn is_cron_daemon_running() -> bool {
|
||||||
|
["cron", "crond"].iter().any(|name| {
|
||||||
|
std::process::Command::new("pgrep")
|
||||||
|
.args(["-x", name])
|
||||||
|
.output()
|
||||||
|
.map(|o| o.status.success())
|
||||||
|
.unwrap_or(false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Erzeugt die Crontab-Äquivalente zu den paketierten systemd-Units, für Systeme ohne
|
||||||
|
/// (genutzten) systemd. `watch_interval_secs` ist derselbe Wert wie
|
||||||
|
/// `settings.watch_interval_secs`.
|
||||||
pub fn crontab_equivalent(watch_interval_secs: u64) -> String {
|
pub fn crontab_equivalent(watch_interval_secs: u64) -> String {
|
||||||
let bin = binary_path();
|
let bin = binary_path();
|
||||||
let schedule = cron_schedule_for_interval(watch_interval_secs);
|
let schedule = cron_schedule_for_interval(watch_interval_secs);
|
||||||
@@ -177,50 +121,22 @@ pub fn crontab_equivalent(watch_interval_secs: u64) -> String {
|
|||||||
pub enum CronInstallOutcome {
|
pub enum CronInstallOutcome {
|
||||||
/// Systemweiter Eintrag geschrieben (`/etc/cron.d/smart-mount`).
|
/// Systemweiter Eintrag geschrieben (`/etc/cron.d/smart-mount`).
|
||||||
SystemFile(PathBuf),
|
SystemFile(PathBuf),
|
||||||
/// Persönliche Crontab des aufrufenden Nutzers aktualisiert.
|
|
||||||
UserCrontab,
|
|
||||||
/// Kein Cron-Mechanismus auf diesem System gefunden - nichts geschrieben, der Aufrufer
|
/// Kein Cron-Mechanismus auf diesem System gefunden - nichts geschrieben, der Aufrufer
|
||||||
/// sollte stattdessen [`crontab_equivalent`] anzeigen.
|
/// sollte stattdessen [`crontab_equivalent`] anzeigen.
|
||||||
Unavailable,
|
Unavailable,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Richtet die periodische Ausführung direkt über Cron ein (Alternative zu [`install`] für
|
/// Richtet die periodische Ausführung direkt über `/etc/cron.d/smart-mount` ein - Fallback für
|
||||||
/// Systeme ohne systemd), sofern ein Cron-Mechanismus gefunden wird - sonst [`CronInstallOutcome::Unavailable`]
|
/// Systeme ohne (genutzten) systemd, sofern `/etc/cron.d` existiert - sonst
|
||||||
/// statt eines Fehlers, der Aufrufer zeigt dann [`crontab_equivalent`] zur manuellen Einrichtung.
|
/// [`CronInstallOutcome::Unavailable`] statt eines Fehlers, der Aufrufer (`setup-cron`, siehe
|
||||||
///
|
/// Moduldoku) zeigt dann [`crontab_equivalent`] zur manuellen Einrichtung. Erfordert Root
|
||||||
/// `Scope::System` schreibt `/etc/cron.d/smart-mount` (Standard-Konvention für
|
/// (siehe Moduldoku) - beim Aufruf aus dem postinst-Skript der Pakete ohnehin gegeben.
|
||||||
/// paketverwaltete Cron-Einträge, läuft als root; erfordert Root-Rechte, kein
|
pub fn install_cron(watch_interval_secs: u64) -> Result<CronInstallOutcome> {
|
||||||
/// 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)"));
|
|
||||||
}
|
|
||||||
if !Path::new("/etc/cron.d").is_dir() {
|
if !Path::new("/etc/cron.d").is_dir() {
|
||||||
return Ok(CronInstallOutcome::Unavailable);
|
return Ok(CronInstallOutcome::Unavailable);
|
||||||
}
|
}
|
||||||
|
|
||||||
// /etc/cron.d-Zeilen brauchen (anders als persönliche Crontabs) ein Nutzerfeld - root,
|
let contents = managed_cron_block(watch_interval_secs);
|
||||||
// passend dazu, dass System-Kontext-Paare auch sonst als root gemountet werden.
|
|
||||||
let contents = managed_cron_block(watch_interval_secs, Some("root"));
|
|
||||||
std::fs::write(CRON_D_PATH, &contents).map_err(|e| Error::io(CRON_D_PATH, e))?;
|
std::fs::write(CRON_D_PATH, &contents).map_err(|e| Error::io(CRON_D_PATH, e))?;
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
{
|
{
|
||||||
@@ -228,22 +144,18 @@ fn install_cron_system(watch_interval_secs: u64) -> Result<CronInstallOutcome> {
|
|||||||
std::fs::set_permissions(CRON_D_PATH, std::fs::Permissions::from_mode(0o644))
|
std::fs::set_permissions(CRON_D_PATH, std::fs::Permissions::from_mode(0o644))
|
||||||
.map_err(|e| Error::io(CRON_D_PATH, e))?;
|
.map_err(|e| Error::io(CRON_D_PATH, e))?;
|
||||||
}
|
}
|
||||||
|
logger_ctdra::info("systemd", &format!("cron entry written to '{CRON_D_PATH}'"));
|
||||||
Ok(CronInstallOutcome::SystemFile(PathBuf::from(CRON_D_PATH)))
|
Ok(CronInstallOutcome::SystemFile(PathBuf::from(CRON_D_PATH)))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn install_cron_user(watch_interval_secs: u64) -> Result<CronInstallOutcome> {
|
fn managed_cron_block(watch_interval_secs: u64) -> String {
|
||||||
if !crate::mount::binary_available("crontab") {
|
let bin = binary_path();
|
||||||
return Ok(CronInstallOutcome::Unavailable);
|
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.
|
||||||
let existing = read_current_user_crontab();
|
format!(
|
||||||
let without_block =
|
"{CRON_BEGIN_MARKER}\n@reboot root {bin} mount --all\n{schedule} root {bin} watch\n{CRON_END_MARKER}\n"
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ergebnis von [`uninstall_cron`].
|
/// Ergebnis von [`uninstall_cron`].
|
||||||
@@ -255,77 +167,20 @@ pub enum CronUninstallOutcome {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Gegenstück zu [`install_cron`]: entfernt einen zuvor über `install_cron` angelegten
|
/// 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` -
|
/// Cron-Eintrag wieder, sofern vorhanden.
|
||||||
/// nur den von smart-mount verwalteten Block in der persönlichen Crontab an, keine
|
pub fn uninstall_cron() -> Result<CronUninstallOutcome> {
|
||||||
/// 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)",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
let path = Path::new(CRON_D_PATH);
|
let path = Path::new(CRON_D_PATH);
|
||||||
if !path.exists() {
|
if !path.exists() {
|
||||||
return Ok(CronUninstallOutcome::NotPresent);
|
return Ok(CronUninstallOutcome::NotPresent);
|
||||||
}
|
}
|
||||||
std::fs::remove_file(path).map_err(|e| Error::io(CRON_D_PATH, e))?;
|
std::fs::remove_file(path).map_err(|e| Error::io(CRON_D_PATH, e))?;
|
||||||
|
logger_ctdra::info(
|
||||||
|
"systemd",
|
||||||
|
&format!("cron entry removed from '{CRON_D_PATH}'"),
|
||||||
|
);
|
||||||
Ok(CronUninstallOutcome::Removed)
|
Ok(CronUninstallOutcome::Removed)
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
/// 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
|
/// 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
|
/// (Cron kann nicht häufiger als minütlich auslösen). Ab 60 Minuten wird auf Stunden
|
||||||
@@ -350,44 +205,12 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn mount_service_unit_system_scope_uses_multi_user_target() {
|
fn managed_cron_block_includes_root_user_field_and_both_lines() {
|
||||||
let unit = mount_service_unit(Scope::System);
|
let block = managed_cron_block(120);
|
||||||
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);
|
|
||||||
assert!(block.starts_with(CRON_BEGIN_MARKER));
|
assert!(block.starts_with(CRON_BEGIN_MARKER));
|
||||||
assert!(block.trim_end().ends_with(CRON_END_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("@reboot root "));
|
||||||
|
assert!(block.contains("mount --all"));
|
||||||
assert!(block.contains("*/2 * * * * root "));
|
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