Initial commit; base scripting and systemd user event code for borg based backup
This commit is contained in:
@@ -0,0 +1,11 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
.DS_Store
|
||||||
|
*.log
|
||||||
|
/.cache/
|
||||||
|
/tmp/
|
||||||
@@ -0,0 +1,283 @@
|
|||||||
|
# Dokumentation: nextcloud-borg-backup
|
||||||
|
|
||||||
|
## Ziel
|
||||||
|
|
||||||
|
Diese Anwendung sichert den via rclone gemounteten Ordner `/home/johannes/Nextcloud` regelmäßig auf den via autofs erreichbaren Synology-Pfad `/net/thor/volume1/NetBackup/Nextcloud`.
|
||||||
|
|
||||||
|
Die Sicherung verwendet Borg, weil Borg inkrementelle, deduplizierte und verschlüsselte Archive erstellt. Dadurch bleiben tägliche Backups platzsparend und einzelne Dateien können aus älteren Ständen wiederhergestellt werden.
|
||||||
|
|
||||||
|
## Architektur
|
||||||
|
|
||||||
|
Bestandteile:
|
||||||
|
|
||||||
|
- `scripts/nextcloud-borg-backup`: Haupt-CLI für Backup, Restore, Suche, Mount, Check und Retention.
|
||||||
|
- `etc/config.example`: Beispielkonfiguration mit den produktiven Standardpfaden.
|
||||||
|
- `systemd/user/nextcloud-borg-backup.service`: systemd User Service für einen Backup-Lauf.
|
||||||
|
- `systemd/user/nextcloud-borg-backup.timer`: täglicher Timer.
|
||||||
|
- `scripts/install.sh`: Installation in das Home-Verzeichnis des Users.
|
||||||
|
- `scripts/uninstall.sh`: Entfernt Service, Timer und Executable, lässt Daten bewusst bestehen.
|
||||||
|
|
||||||
|
Die Anwendung läuft als User `johannes`. Das ist für diesen Fall sinnvoll, weil die Quelle ein rclone/FUSE-Mount unterhalb des Home-Verzeichnisses ist. Ein root-Service könnte je nach FUSE-Konfiguration keine stabilen Leserechte auf diesen Mount haben.
|
||||||
|
|
||||||
|
## Pfade
|
||||||
|
|
||||||
|
Standardkonfiguration:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
SOURCE_DIR="${HOME}/Nextcloud"
|
||||||
|
REPO_DIR="/net/thor/volume1/NetBackup/Nextcloud/borg-repo"
|
||||||
|
ARCHIVE_PREFIX="nextcloud"
|
||||||
|
RETENTION_WITHIN="4w"
|
||||||
|
```
|
||||||
|
|
||||||
|
Lokale Laufzeitdaten:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
~/.config/nextcloud-borg-backup/config
|
||||||
|
~/.config/nextcloud-borg-backup/passphrase
|
||||||
|
~/.config/nextcloud-borg-backup/excludes
|
||||||
|
~/.local/state/nextcloud-borg-backup/logs/
|
||||||
|
~/Nextcloud-Restore/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/johannes/localdev/nextcloud-borg-backup
|
||||||
|
./scripts/install.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Der Installer:
|
||||||
|
|
||||||
|
1. kopiert die CLI nach `~/.local/bin/nextcloud-borg-backup`,
|
||||||
|
2. legt die Konfiguration an, falls sie noch nicht existiert,
|
||||||
|
3. erzeugt eine Borg-Passphrase, falls noch keine existiert,
|
||||||
|
4. installiert systemd-User-Service und Timer,
|
||||||
|
5. aktiviert den Timer.
|
||||||
|
|
||||||
|
Für automatische Läufe ohne aktive Anmeldung:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo loginctl enable-linger johannes
|
||||||
|
```
|
||||||
|
|
||||||
|
## Erster Backup-Lauf
|
||||||
|
|
||||||
|
```bash
|
||||||
|
systemctl --user start nextcloud-borg-backup.service
|
||||||
|
```
|
||||||
|
|
||||||
|
Logs:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
journalctl --user -u nextcloud-borg-backup.service -f
|
||||||
|
ls -lah ~/.local/state/nextcloud-borg-backup/logs/
|
||||||
|
```
|
||||||
|
|
||||||
|
Status:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nextcloud-borg-backup status
|
||||||
|
nextcloud-borg-backup list
|
||||||
|
```
|
||||||
|
|
||||||
|
## Zeitplanung
|
||||||
|
|
||||||
|
Der Timer läuft täglich:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
OnCalendar=*-*-* 03:15:00
|
||||||
|
Persistent=true
|
||||||
|
RandomizedDelaySec=30m
|
||||||
|
```
|
||||||
|
|
||||||
|
`Persistent=true` sorgt dafür, dass ein verpasster Lauf nachgeholt wird, sobald der User-systemd-Manager wieder aktiv ist.
|
||||||
|
|
||||||
|
## Retention
|
||||||
|
|
||||||
|
Die Aufbewahrung ist auf vier Wochen ausgelegt:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
RETENTION_WITHIN="4w"
|
||||||
|
```
|
||||||
|
|
||||||
|
Nach jedem Backup läuft:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
borg prune --keep-within 4w
|
||||||
|
borg compact
|
||||||
|
borg check --repository-only
|
||||||
|
```
|
||||||
|
|
||||||
|
Damit bleiben alle Archive der letzten vier Wochen erhalten. Bei einem täglichen Timer sind dadurch mindestens tägliche Wiederherstellungspunkte über rund einen Monat verfügbar. Manuelle Zusatzläufe innerhalb der vier Wochen werden ebenfalls behalten.
|
||||||
|
|
||||||
|
## Resilienzmaßnahmen
|
||||||
|
|
||||||
|
Die Anwendung bricht bewusst ab, wenn eine Sicherheitsannahme nicht erfüllt ist:
|
||||||
|
|
||||||
|
- Quelle ist nicht erreichbar oder nicht lesbar.
|
||||||
|
- Quelle wirkt leer und `ALLOW_EMPTY_SOURCE=false`.
|
||||||
|
- Synology-Ziel ist nicht erreichbar oder nicht beschreibbar.
|
||||||
|
- Freier Platz am Ziel liegt unter `MIN_FREE_REPO_GB`.
|
||||||
|
- Borg-Passphrase fehlt bei verschlüsseltem Repository.
|
||||||
|
- Ein anderer Backup-Lauf hält bereits den Lock.
|
||||||
|
|
||||||
|
Weitere Maßnahmen:
|
||||||
|
|
||||||
|
- Borg-Repo wird bei Bedarf einmalig initialisiert.
|
||||||
|
- Borg verwendet deduplizierte Archive statt Vollkopien.
|
||||||
|
- Nach dem Backup läuft Retention und Kompaktierung.
|
||||||
|
- Nach dem Backup läuft ein Repository-Check.
|
||||||
|
- Optional kann regelmäßig ein vollständiger Datencheck aktiviert werden:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
FULL_CHECK_VERIFY_DATA="true"
|
||||||
|
FULL_CHECK_INTERVAL_DAYS="7"
|
||||||
|
```
|
||||||
|
|
||||||
|
Ein vollständiger `borg check --verify-data` kann bei großen Repositories lange dauern.
|
||||||
|
|
||||||
|
## Dateien suchen
|
||||||
|
|
||||||
|
Über alle Archive suchen:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nextcloud-borg-backup search "Steuer"
|
||||||
|
```
|
||||||
|
|
||||||
|
Nur in einem bestimmten Archiv suchen:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nextcloud-borg-backup search "Steuer" nextcloud-2026-07-10_03-15-00
|
||||||
|
```
|
||||||
|
|
||||||
|
Die Ausgabe enthält bei archivübergreifender Suche den Archivnamen und den Pfad.
|
||||||
|
|
||||||
|
## Archive mounten
|
||||||
|
|
||||||
|
Alle Archive als lesbare Struktur mounten:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nextcloud-borg-backup mount
|
||||||
|
```
|
||||||
|
|
||||||
|
Standard-Mountpunkt:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
${XDG_RUNTIME_DIR}/nextcloud-borg-backup-mount
|
||||||
|
```
|
||||||
|
|
||||||
|
Ein einzelnes Archiv mounten:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nextcloud-borg-backup mount nextcloud-2026-07-10_03-15-00
|
||||||
|
```
|
||||||
|
|
||||||
|
Unmount:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nextcloud-borg-backup umount
|
||||||
|
```
|
||||||
|
|
||||||
|
Falls `borg mount` nicht verfügbar ist, muss auf Arch Linux das passende FUSE-Paket installiert sein.
|
||||||
|
|
||||||
|
## Restore
|
||||||
|
|
||||||
|
Neuestes Archiv komplett wiederherstellen:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nextcloud-borg-backup restore
|
||||||
|
```
|
||||||
|
|
||||||
|
Ziel:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
~/Nextcloud-Restore/<Archivname>/
|
||||||
|
```
|
||||||
|
|
||||||
|
Bestimmten Pfad aus dem neuesten Archiv wiederherstellen:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nextcloud-borg-backup restore "" "Dokumente/Projekt"
|
||||||
|
```
|
||||||
|
|
||||||
|
Bestimmten Pfad aus einem bestimmten Archiv wiederherstellen:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nextcloud-borg-backup restore nextcloud-2026-07-10_03-15-00 "Dokumente/Projekt" ~/Restore-Test
|
||||||
|
```
|
||||||
|
|
||||||
|
Wichtig: Die Anwendung extrahiert standardmäßig nicht direkt zurück in den produktiven Nextcloud-Ordner. Das reduziert das Risiko, versehentlich aktuelle Dateien zu überschreiben. Nach Prüfung können die wiederhergestellten Dateien gezielt zurückkopiert werden.
|
||||||
|
|
||||||
|
## Disaster Recovery
|
||||||
|
|
||||||
|
Für eine Wiederherstellung auf einem neuen System werden benötigt:
|
||||||
|
|
||||||
|
1. das Borg-Repository unter `/net/thor/volume1/NetBackup/Nextcloud/borg-repo`,
|
||||||
|
2. die Borg-Passphrase aus `~/.config/nextcloud-borg-backup/passphrase`,
|
||||||
|
3. Borg selbst.
|
||||||
|
|
||||||
|
Minimaler Restore:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export BORG_PASSCOMMAND='cat /pfad/zur/passphrase'
|
||||||
|
borg list /net/thor/volume1/NetBackup/Nextcloud/borg-repo
|
||||||
|
borg extract /net/thor/volume1/NetBackup/Nextcloud/borg-repo::ARCHIVNAME
|
||||||
|
```
|
||||||
|
|
||||||
|
Die Passphrase sollte separat sicher abgelegt werden. Ohne Passphrase ist ein verschlüsseltes Repository nicht nutzbar.
|
||||||
|
|
||||||
|
## Wartung
|
||||||
|
|
||||||
|
Manueller Repository-Check:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nextcloud-borg-backup check
|
||||||
|
```
|
||||||
|
|
||||||
|
Vollständiger Datencheck:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nextcloud-borg-backup check --verify-data
|
||||||
|
```
|
||||||
|
|
||||||
|
Borg-Lock lösen, falls ein Lauf hart abgebrochen wurde und sicher kein Backup mehr läuft:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nextcloud-borg-backup break-lock
|
||||||
|
```
|
||||||
|
|
||||||
|
Timer deaktivieren:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
systemctl --user disable --now nextcloud-borg-backup.timer
|
||||||
|
```
|
||||||
|
|
||||||
|
Deinstallation der lokalen Anwendung:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/uninstall.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Die Deinstallation löscht bewusst nicht:
|
||||||
|
|
||||||
|
- Borg-Repository,
|
||||||
|
- Passphrase,
|
||||||
|
- Konfiguration,
|
||||||
|
- Logs,
|
||||||
|
- Restore-Verzeichnis.
|
||||||
|
|
||||||
|
## Gitea-Upload
|
||||||
|
|
||||||
|
Das Verzeichnis `/home/johannes/localdev/nextcloud-borg-backup` ist als eigenständiges Projekt vorbereitet. Vor dem Push:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/johannes/localdev/nextcloud-borg-backup
|
||||||
|
git init
|
||||||
|
git add .
|
||||||
|
git commit -m "Initial nextcloud borg backup application"
|
||||||
|
git remote add origin <gitea-url>
|
||||||
|
git push -u origin main
|
||||||
|
```
|
||||||
|
|
||||||
|
Keine lokalen Geheimnisse werden im Projekt abgelegt. Die produktive Passphrase entsteht erst bei der Installation unter `~/.config/nextcloud-borg-backup/passphrase`.
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
.PHONY: check install uninstall
|
||||||
|
|
||||||
|
check:
|
||||||
|
bash -n scripts/nextcloud-borg-backup scripts/install.sh scripts/uninstall.sh
|
||||||
|
systemd-analyze --user verify systemd/user/nextcloud-borg-backup.service systemd/user/nextcloud-borg-backup.timer
|
||||||
|
|
||||||
|
install:
|
||||||
|
./scripts/install.sh
|
||||||
|
|
||||||
|
uninstall:
|
||||||
|
./scripts/uninstall.sh
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
# nextcloud-borg-backup
|
||||||
|
|
||||||
|
Tägliches Borg-Backup des lokalen Nextcloud-Mounts nach Synology:
|
||||||
|
|
||||||
|
- Quelle: `/home/johannes/Nextcloud`
|
||||||
|
- Ziel: `/net/thor/volume1/NetBackup/Nextcloud/borg-repo`
|
||||||
|
- Historie: `4w`, also vier Wochen wiederherstellbare Archive
|
||||||
|
- Betrieb: systemd User Timer, täglich um 03:15 Uhr mit Zufallsverzögerung
|
||||||
|
- Restore: über CLI, Suche, Borg-Mount oder gezieltes Extrahieren
|
||||||
|
|
||||||
|
Das Projekt ist bewusst klein gehalten: eine Bash-CLI, Konfiguration, systemd-User-Units und Dokumentation. Es eignet sich direkt für ein Gitea-Repository.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/johannes/localdev/nextcloud-borg-backup
|
||||||
|
./scripts/install.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Der Installer legt an:
|
||||||
|
|
||||||
|
- `~/.local/bin/nextcloud-borg-backup`
|
||||||
|
- `~/.config/nextcloud-borg-backup/config`
|
||||||
|
- `~/.config/nextcloud-borg-backup/passphrase`
|
||||||
|
- `~/.config/systemd/user/nextcloud-borg-backup.service`
|
||||||
|
- `~/.config/systemd/user/nextcloud-borg-backup.timer`
|
||||||
|
|
||||||
|
Der Installer überschreibt eine vorhandene Konfiguration nicht.
|
||||||
|
|
||||||
|
Danach den ersten Lauf manuell starten:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
systemctl --user start nextcloud-borg-backup.service
|
||||||
|
journalctl --user -u nextcloud-borg-backup.service -f
|
||||||
|
```
|
||||||
|
|
||||||
|
Timer prüfen:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
systemctl --user status nextcloud-borg-backup.timer
|
||||||
|
systemctl --user list-timers nextcloud-borg-backup.timer
|
||||||
|
```
|
||||||
|
|
||||||
|
Damit der User-Timer auch ohne aktive Anmeldung läuft:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo loginctl enable-linger johannes
|
||||||
|
```
|
||||||
|
|
||||||
|
## Bedienung
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nextcloud-borg-backup backup
|
||||||
|
nextcloud-borg-backup list
|
||||||
|
nextcloud-borg-backup status
|
||||||
|
nextcloud-borg-backup search Rechnung
|
||||||
|
nextcloud-borg-backup mount
|
||||||
|
nextcloud-borg-backup restore
|
||||||
|
nextcloud-borg-backup check --verify-data
|
||||||
|
```
|
||||||
|
|
||||||
|
## Restore-Beispiele
|
||||||
|
|
||||||
|
Neuestes Archiv komplett nach `~/Nextcloud-Restore/<Archivname>` wiederherstellen:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nextcloud-borg-backup restore
|
||||||
|
```
|
||||||
|
|
||||||
|
Bestimmte Datei im neuesten Archiv wiederherstellen:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nextcloud-borg-backup restore "" "Dokumente/Beispiel.pdf"
|
||||||
|
```
|
||||||
|
|
||||||
|
Bestimmtes Archiv und bestimmten Pfad wiederherstellen:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nextcloud-borg-backup restore nextcloud-2026-07-10_03-15-00 "Dokumente"
|
||||||
|
```
|
||||||
|
|
||||||
|
Archive bequem lesbar mounten:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nextcloud-borg-backup mount
|
||||||
|
xdg-open "${XDG_RUNTIME_DIR}/nextcloud-borg-backup-mount"
|
||||||
|
nextcloud-borg-backup umount
|
||||||
|
```
|
||||||
|
|
||||||
|
## Wichtige Hinweise
|
||||||
|
|
||||||
|
Die Borg-Passphrase liegt lokal in `~/.config/nextcloud-borg-backup/passphrase`. Ohne diese Datei kann ein verschlüsseltes Borg-Repository im Notfall nicht wiederhergestellt werden. Eine Kopie gehört daher separat und sicher abgelegt.
|
||||||
|
|
||||||
|
Weitere Details stehen in [Dokumentation.md](Dokumentation.md).
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
# nextcloud-borg-backup configuration
|
||||||
|
#
|
||||||
|
# Copy to:
|
||||||
|
# ~/.config/nextcloud-borg-backup/config
|
||||||
|
|
||||||
|
SOURCE_DIR="${HOME}/Nextcloud"
|
||||||
|
REPO_DIR="/net/thor/volume1/NetBackup/Nextcloud/borg-repo"
|
||||||
|
ARCHIVE_PREFIX="nextcloud"
|
||||||
|
|
||||||
|
# Borg repository encryption. repokey-blake2 needs a passphrase file for
|
||||||
|
# unattended systemd timer runs. The installer creates this file if missing.
|
||||||
|
BORG_ENCRYPTION="repokey-blake2"
|
||||||
|
BORG_PASSPHRASE_FILE="${HOME}/.config/nextcloud-borg-backup/passphrase"
|
||||||
|
BORG_COMPRESSION="zstd,6"
|
||||||
|
|
||||||
|
# Four weeks of restorable history. With the daily timer this keeps at least
|
||||||
|
# one month of archives; manual extra runs inside the window are also retained.
|
||||||
|
RETENTION_WITHIN="4w"
|
||||||
|
|
||||||
|
# Safety checks.
|
||||||
|
ALLOW_EMPTY_SOURCE="false"
|
||||||
|
MIN_FREE_REPO_GB="5"
|
||||||
|
PRECHECK_TIMEOUT_SECONDS="60"
|
||||||
|
|
||||||
|
# Runtime paths.
|
||||||
|
LOG_DIR="${HOME}/.local/state/nextcloud-borg-backup/logs"
|
||||||
|
STATE_DIR="${HOME}/.local/state/nextcloud-borg-backup"
|
||||||
|
RESTORE_DIR="${HOME}/Nextcloud-Restore"
|
||||||
|
MOUNT_DIR="${XDG_RUNTIME_DIR:-/tmp}/nextcloud-borg-backup-mount"
|
||||||
|
EXCLUDE_FILE="${HOME}/.config/nextcloud-borg-backup/excludes"
|
||||||
|
LOCK_FILE="${HOME}/.local/state/nextcloud-borg-backup/nextcloud-borg-backup.lock"
|
||||||
|
|
||||||
|
# Borg behavior.
|
||||||
|
PRUNE_AFTER_BACKUP="true"
|
||||||
|
COMPACT_AFTER_PRUNE="true"
|
||||||
|
REPOSITORY_CHECK_AFTER_BACKUP="true"
|
||||||
|
FULL_CHECK_INTERVAL_DAYS="7"
|
||||||
|
FULL_CHECK_VERIFY_DATA="false"
|
||||||
|
EXCLUDE_CACHES="true"
|
||||||
|
ONE_FILE_SYSTEM="true"
|
||||||
|
|
||||||
|
# Leave empty unless the rclone mount exposes unstable metadata and Borg rereads
|
||||||
|
# too much. Conservative alternative: BORG_FILES_CACHE="mtime,size"
|
||||||
|
BORG_FILES_CACHE=""
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# Optional Borg exclude patterns, one per line.
|
||||||
|
# The backup runs from inside SOURCE_DIR, so patterns are relative.
|
||||||
|
#
|
||||||
|
# Examples:
|
||||||
|
# cache directories:
|
||||||
|
# */.cache
|
||||||
|
#
|
||||||
|
# thumbnails:
|
||||||
|
# */thumbnails
|
||||||
Executable
+50
@@ -0,0 +1,50 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -Eeuo pipefail
|
||||||
|
umask 077
|
||||||
|
|
||||||
|
APP_NAME="nextcloud-borg-backup"
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
|
||||||
|
BIN_DIR="${HOME}/.local/bin"
|
||||||
|
CONFIG_DIR="${XDG_CONFIG_HOME:-${HOME}/.config}/${APP_NAME}"
|
||||||
|
STATE_DIR="${XDG_STATE_HOME:-${HOME}/.local/state}/${APP_NAME}"
|
||||||
|
SYSTEMD_USER_DIR="${XDG_CONFIG_HOME:-${HOME}/.config}/systemd/user"
|
||||||
|
|
||||||
|
mkdir -p "$BIN_DIR" "$CONFIG_DIR" "$STATE_DIR/logs" "$SYSTEMD_USER_DIR"
|
||||||
|
|
||||||
|
install -m 0750 "$ROOT_DIR/scripts/nextcloud-borg-backup" "$BIN_DIR/nextcloud-borg-backup"
|
||||||
|
|
||||||
|
if [[ ! -f "$CONFIG_DIR/config" ]]; then
|
||||||
|
install -m 0600 "$ROOT_DIR/etc/config.example" "$CONFIG_DIR/config"
|
||||||
|
printf 'Installed config: %s\n' "$CONFIG_DIR/config"
|
||||||
|
else
|
||||||
|
printf 'Config exists, not overwriting: %s\n' "$CONFIG_DIR/config"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -f "$CONFIG_DIR/excludes" ]]; then
|
||||||
|
install -m 0600 "$ROOT_DIR/etc/excludes.example" "$CONFIG_DIR/excludes"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -f "$CONFIG_DIR/passphrase" ]]; then
|
||||||
|
if command -v openssl >/dev/null 2>&1; then
|
||||||
|
openssl rand -base64 48 > "$CONFIG_DIR/passphrase"
|
||||||
|
else
|
||||||
|
od -An -tx1 -N48 /dev/urandom | tr -d ' \n' > "$CONFIG_DIR/passphrase"
|
||||||
|
printf '\n' >> "$CONFIG_DIR/passphrase"
|
||||||
|
fi
|
||||||
|
chmod 0600 "$CONFIG_DIR/passphrase"
|
||||||
|
printf 'Created Borg passphrase file: %s\n' "$CONFIG_DIR/passphrase"
|
||||||
|
printf 'Store a copy of this passphrase somewhere safe for disaster recovery.\n'
|
||||||
|
fi
|
||||||
|
|
||||||
|
install -m 0644 "$ROOT_DIR/systemd/user/nextcloud-borg-backup.service" "$SYSTEMD_USER_DIR/nextcloud-borg-backup.service"
|
||||||
|
install -m 0644 "$ROOT_DIR/systemd/user/nextcloud-borg-backup.timer" "$SYSTEMD_USER_DIR/nextcloud-borg-backup.timer"
|
||||||
|
|
||||||
|
systemctl --user daemon-reload
|
||||||
|
systemctl --user enable --now nextcloud-borg-backup.timer
|
||||||
|
|
||||||
|
printf '\nInstalled %s.\n' "$APP_NAME"
|
||||||
|
printf 'Next steps:\n'
|
||||||
|
printf ' 1. Check config: %s\n' "$CONFIG_DIR/config"
|
||||||
|
printf ' 2. Run first backup: systemctl --user start nextcloud-borg-backup.service\n'
|
||||||
|
printf ' 3. Watch logs: journalctl --user -u nextcloud-borg-backup.service -f\n'
|
||||||
Executable
+438
@@ -0,0 +1,438 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -Eeuo pipefail
|
||||||
|
umask 077
|
||||||
|
|
||||||
|
APP_NAME="nextcloud-borg-backup"
|
||||||
|
APP_VERSION="1.0.0"
|
||||||
|
|
||||||
|
CONFIG_FILE="${NCB_CONFIG:-${XDG_CONFIG_HOME:-${HOME}/.config}/${APP_NAME}/config}"
|
||||||
|
|
||||||
|
die() {
|
||||||
|
printf 'ERROR: %s\n' "$*" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
warn() {
|
||||||
|
printf 'WARN: %s\n' "$*" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
info() {
|
||||||
|
printf '%s\n' "$*"
|
||||||
|
}
|
||||||
|
|
||||||
|
have() {
|
||||||
|
command -v "$1" >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
shell_quote() {
|
||||||
|
printf '%q' "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
nextcloud-borg-backup - Borg backup helper for a mounted Nextcloud folder
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
nextcloud-borg-backup backup
|
||||||
|
nextcloud-borg-backup init
|
||||||
|
nextcloud-borg-backup list
|
||||||
|
nextcloud-borg-backup info [ARCHIVE]
|
||||||
|
nextcloud-borg-backup search PATTERN [ARCHIVE]
|
||||||
|
nextcloud-borg-backup mount [ARCHIVE] [MOUNT_DIR]
|
||||||
|
nextcloud-borg-backup umount [MOUNT_DIR]
|
||||||
|
nextcloud-borg-backup restore [ARCHIVE] [PATH] [DEST_DIR]
|
||||||
|
nextcloud-borg-backup check [--verify-data]
|
||||||
|
nextcloud-borg-backup prune
|
||||||
|
nextcloud-borg-backup status
|
||||||
|
nextcloud-borg-backup break-lock
|
||||||
|
|
||||||
|
Environment:
|
||||||
|
NCB_CONFIG=/path/to/config
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
load_config() {
|
||||||
|
[[ -r "$CONFIG_FILE" ]] || die "Config not readable: $CONFIG_FILE"
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
source "$CONFIG_FILE"
|
||||||
|
|
||||||
|
: "${SOURCE_DIR:=${HOME}/Nextcloud}"
|
||||||
|
: "${REPO_DIR:=/net/thor/volume1/NetBackup/Nextcloud/borg-repo}"
|
||||||
|
: "${ARCHIVE_PREFIX:=nextcloud}"
|
||||||
|
: "${BORG_BIN:=borg}"
|
||||||
|
: "${BORG_ENCRYPTION:=repokey-blake2}"
|
||||||
|
: "${BORG_PASSPHRASE_FILE:=${XDG_CONFIG_HOME:-${HOME}/.config}/${APP_NAME}/passphrase}"
|
||||||
|
: "${BORG_COMPRESSION:=zstd,6}"
|
||||||
|
: "${BORG_REMOTE_PATH:=borg}"
|
||||||
|
: "${BORG_FILES_CACHE:=}"
|
||||||
|
: "${RETENTION_WITHIN:=4w}"
|
||||||
|
: "${PRUNE_AFTER_BACKUP:=true}"
|
||||||
|
: "${COMPACT_AFTER_PRUNE:=true}"
|
||||||
|
: "${REPOSITORY_CHECK_AFTER_BACKUP:=true}"
|
||||||
|
: "${FULL_CHECK_INTERVAL_DAYS:=7}"
|
||||||
|
: "${FULL_CHECK_VERIFY_DATA:=false}"
|
||||||
|
: "${ALLOW_EMPTY_SOURCE:=false}"
|
||||||
|
: "${MIN_FREE_REPO_GB:=5}"
|
||||||
|
: "${PRECHECK_TIMEOUT_SECONDS:=60}"
|
||||||
|
: "${LOG_DIR:=${XDG_STATE_HOME:-${HOME}/.local/state}/${APP_NAME}/logs}"
|
||||||
|
: "${STATE_DIR:=${XDG_STATE_HOME:-${HOME}/.local/state}/${APP_NAME}}"
|
||||||
|
: "${RESTORE_DIR:=${HOME}/Nextcloud-Restore}"
|
||||||
|
: "${MOUNT_DIR:=${XDG_RUNTIME_DIR:-/tmp}/${APP_NAME}-mount}"
|
||||||
|
: "${EXCLUDE_FILE:=${XDG_CONFIG_HOME:-${HOME}/.config}/${APP_NAME}/excludes}"
|
||||||
|
: "${EXCLUDE_CACHES:=true}"
|
||||||
|
: "${ONE_FILE_SYSTEM:=true}"
|
||||||
|
: "${LOCK_FILE:=${STATE_DIR}/${APP_NAME}.lock}"
|
||||||
|
|
||||||
|
export BORG_RELOCATED_REPO_ACCESS_IS_OK="${BORG_RELOCATED_REPO_ACCESS_IS_OK:-yes}"
|
||||||
|
export BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK="${BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK:-yes}"
|
||||||
|
export BORG_REMOTE_PATH
|
||||||
|
[[ -n "$BORG_FILES_CACHE" ]] && export BORG_FILES_CACHE
|
||||||
|
|
||||||
|
if [[ -f "$BORG_PASSPHRASE_FILE" ]]; then
|
||||||
|
export BORG_PASSCOMMAND="cat $(shell_quote "$BORG_PASSPHRASE_FILE")"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
require_command() {
|
||||||
|
have "$1" || die "Required command missing: $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_dirs() {
|
||||||
|
mkdir -p "$LOG_DIR" "$STATE_DIR"
|
||||||
|
}
|
||||||
|
|
||||||
|
log_file_for_run() {
|
||||||
|
local timestamp
|
||||||
|
timestamp="$(date '+%Y-%m-%d_%H-%M-%S')"
|
||||||
|
printf '%s/%s_%s.log' "$LOG_DIR" "$APP_NAME" "$timestamp"
|
||||||
|
}
|
||||||
|
|
||||||
|
start_logging() {
|
||||||
|
ensure_dirs
|
||||||
|
local log_file
|
||||||
|
log_file="$(log_file_for_run)"
|
||||||
|
exec > >(tee -a "$log_file") 2>&1
|
||||||
|
info "== ${APP_NAME} ${APP_VERSION} =="
|
||||||
|
info "-- Time: $(date -Is)"
|
||||||
|
info "-- Config: $CONFIG_FILE"
|
||||||
|
info "-- Log: $log_file"
|
||||||
|
}
|
||||||
|
|
||||||
|
with_lock() {
|
||||||
|
exec 9>"$LOCK_FILE"
|
||||||
|
flock -n 9 || die "Another ${APP_NAME} run is already active: $LOCK_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
repo_parent() {
|
||||||
|
dirname "$REPO_DIR"
|
||||||
|
}
|
||||||
|
|
||||||
|
check_passphrase_for_encryption() {
|
||||||
|
case "$BORG_ENCRYPTION" in
|
||||||
|
none|authenticated*) return 0 ;;
|
||||||
|
esac
|
||||||
|
[[ -r "$BORG_PASSPHRASE_FILE" ]] || die "BORG_PASSPHRASE_FILE missing/read-protected: $BORG_PASSPHRASE_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
borg_repo_exists() {
|
||||||
|
[[ -d "$REPO_DIR" && -f "$REPO_DIR/config" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
init_repo() {
|
||||||
|
require_command "$BORG_BIN"
|
||||||
|
check_passphrase_for_encryption
|
||||||
|
mkdir -p "$(repo_parent)"
|
||||||
|
if borg_repo_exists; then
|
||||||
|
info "-- Borg repository already exists: $REPO_DIR"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
info "-- Initializing Borg repository: $REPO_DIR"
|
||||||
|
"$BORG_BIN" init --encryption="$BORG_ENCRYPTION" "$REPO_DIR"
|
||||||
|
}
|
||||||
|
|
||||||
|
timeout_cmd() {
|
||||||
|
if have timeout; then
|
||||||
|
timeout "$PRECHECK_TIMEOUT_SECONDS" "$@"
|
||||||
|
else
|
||||||
|
"$@"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
count_source_entries() {
|
||||||
|
timeout_cmd find "$SOURCE_DIR" -mindepth 1 -maxdepth 1 -print -quit | wc -l
|
||||||
|
}
|
||||||
|
|
||||||
|
free_repo_gb() {
|
||||||
|
df -PBG "$(repo_parent)" | awk 'NR==2 { gsub(/G/, "", $4); print $4 }'
|
||||||
|
}
|
||||||
|
|
||||||
|
preflight() {
|
||||||
|
require_command "$BORG_BIN"
|
||||||
|
require_command flock
|
||||||
|
require_command df
|
||||||
|
require_command find
|
||||||
|
|
||||||
|
info "-- Source: $SOURCE_DIR"
|
||||||
|
info "-- Repository: $REPO_DIR"
|
||||||
|
|
||||||
|
timeout_cmd test -d "$SOURCE_DIR" || die "Source directory is not reachable: $SOURCE_DIR"
|
||||||
|
timeout_cmd test -r "$SOURCE_DIR" || die "Source directory is not readable: $SOURCE_DIR"
|
||||||
|
|
||||||
|
local entry_count
|
||||||
|
entry_count="$(count_source_entries | tr -d '[:space:]')"
|
||||||
|
if [[ "$ALLOW_EMPTY_SOURCE" != "true" && "${entry_count:-0}" -eq 0 ]]; then
|
||||||
|
die "Source directory looks empty. Refusing to create an empty backup."
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$(repo_parent)"
|
||||||
|
timeout_cmd test -w "$(repo_parent)" || die "Repository parent is not writable/reachable: $(repo_parent)"
|
||||||
|
|
||||||
|
local free_gb
|
||||||
|
free_gb="$(free_repo_gb)"
|
||||||
|
info "-- Free space at repository parent: ${free_gb}G"
|
||||||
|
[[ "${free_gb:-0}" -ge "$MIN_FREE_REPO_GB" ]] || die "Not enough free space at repository parent; need ${MIN_FREE_REPO_GB}G"
|
||||||
|
|
||||||
|
if ! borg_repo_exists; then
|
||||||
|
init_repo
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
create_archive() {
|
||||||
|
local timestamp archive
|
||||||
|
timestamp="$(date '+%Y-%m-%d_%H-%M-%S')"
|
||||||
|
archive="${ARCHIVE_PREFIX}-${timestamp}"
|
||||||
|
|
||||||
|
local borg_args
|
||||||
|
borg_args=(
|
||||||
|
create
|
||||||
|
--stats
|
||||||
|
--show-rc
|
||||||
|
--compression "$BORG_COMPRESSION"
|
||||||
|
)
|
||||||
|
|
||||||
|
[[ "$EXCLUDE_CACHES" == "true" ]] && borg_args+=(--exclude-caches)
|
||||||
|
[[ "$ONE_FILE_SYSTEM" == "true" ]] && borg_args+=(--one-file-system)
|
||||||
|
[[ -f "$EXCLUDE_FILE" ]] && borg_args+=(--exclude-from "$EXCLUDE_FILE")
|
||||||
|
|
||||||
|
info "-- Creating archive: $archive"
|
||||||
|
(
|
||||||
|
cd "$SOURCE_DIR"
|
||||||
|
"$BORG_BIN" "${borg_args[@]}" "${REPO_DIR}::${archive}" .
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
archive_names() {
|
||||||
|
"$BORG_BIN" list --short --glob-archives "${ARCHIVE_PREFIX}-*" "$REPO_DIR"
|
||||||
|
}
|
||||||
|
|
||||||
|
latest_archive() {
|
||||||
|
archive_names | tail -n 1
|
||||||
|
}
|
||||||
|
|
||||||
|
prune_repo() {
|
||||||
|
info "-- Pruning archives with retention: keep-within $RETENTION_WITHIN"
|
||||||
|
"$BORG_BIN" prune --list --show-rc --glob-archives "${ARCHIVE_PREFIX}-*" --keep-within "$RETENTION_WITHIN" "$REPO_DIR"
|
||||||
|
}
|
||||||
|
|
||||||
|
compact_repo() {
|
||||||
|
info "-- Compacting repository"
|
||||||
|
"$BORG_BIN" compact "$REPO_DIR"
|
||||||
|
}
|
||||||
|
|
||||||
|
days_since_stamp() {
|
||||||
|
local stamp="$1"
|
||||||
|
[[ -f "$stamp" ]] || { printf '999999\n'; return 0; }
|
||||||
|
local now then
|
||||||
|
now="$(date +%s)"
|
||||||
|
then="$(stat -c %Y "$stamp")"
|
||||||
|
printf '%s\n' "$(((now - then) / 86400))"
|
||||||
|
}
|
||||||
|
|
||||||
|
check_repo() {
|
||||||
|
local verify_data="${1:-false}"
|
||||||
|
if [[ "$verify_data" == "true" ]]; then
|
||||||
|
info "-- Running full Borg check with data verification"
|
||||||
|
"$BORG_BIN" check --verify-data "$REPO_DIR"
|
||||||
|
else
|
||||||
|
info "-- Running Borg repository check"
|
||||||
|
"$BORG_BIN" check --repository-only "$REPO_DIR"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
maybe_periodic_full_check() {
|
||||||
|
[[ "$FULL_CHECK_VERIFY_DATA" == "true" ]] || return 0
|
||||||
|
local stamp="${STATE_DIR}/last-full-check"
|
||||||
|
local age_days
|
||||||
|
age_days="$(days_since_stamp "$stamp")"
|
||||||
|
[[ "$age_days" -ge "$FULL_CHECK_INTERVAL_DAYS" ]] || return 0
|
||||||
|
check_repo true
|
||||||
|
date -Is > "$stamp"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_backup() {
|
||||||
|
load_config
|
||||||
|
start_logging
|
||||||
|
with_lock
|
||||||
|
preflight
|
||||||
|
create_archive
|
||||||
|
if [[ "$PRUNE_AFTER_BACKUP" == "true" ]]; then
|
||||||
|
prune_repo
|
||||||
|
[[ "$COMPACT_AFTER_PRUNE" == "true" ]] && compact_repo
|
||||||
|
fi
|
||||||
|
[[ "$REPOSITORY_CHECK_AFTER_BACKUP" == "true" ]] && check_repo false
|
||||||
|
maybe_periodic_full_check
|
||||||
|
info "-- Backup finished successfully"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_list() {
|
||||||
|
load_config
|
||||||
|
require_command "$BORG_BIN"
|
||||||
|
"$BORG_BIN" list "$REPO_DIR"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_info() {
|
||||||
|
load_config
|
||||||
|
require_command "$BORG_BIN"
|
||||||
|
local archive="${1:-}"
|
||||||
|
[[ -n "$archive" ]] || archive="$(latest_archive)"
|
||||||
|
[[ -n "$archive" ]] || die "No archive found"
|
||||||
|
"$BORG_BIN" info "${REPO_DIR}::${archive}"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_search() {
|
||||||
|
load_config
|
||||||
|
require_command "$BORG_BIN"
|
||||||
|
local pattern="${1:-}"
|
||||||
|
local archive="${2:-}"
|
||||||
|
[[ -n "$pattern" ]] || die "Missing search pattern"
|
||||||
|
|
||||||
|
if [[ -n "$archive" ]]; then
|
||||||
|
"$BORG_BIN" list --short "${REPO_DIR}::${archive}" | grep -i -- "$pattern" || true
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local found=0
|
||||||
|
while IFS= read -r archive; do
|
||||||
|
[[ -n "$archive" ]] || continue
|
||||||
|
while IFS= read -r path; do
|
||||||
|
printf '%s\t%s\n' "$archive" "$path"
|
||||||
|
found=1
|
||||||
|
done < <("$BORG_BIN" list --short "${REPO_DIR}::${archive}" | grep -i -- "$pattern" || true)
|
||||||
|
done < <(archive_names)
|
||||||
|
[[ "$found" -eq 1 ]] || warn "No matches found for: $pattern"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_mount() {
|
||||||
|
load_config
|
||||||
|
require_command "$BORG_BIN"
|
||||||
|
local archive="${1:-}"
|
||||||
|
local mount_dir="${2:-$MOUNT_DIR}"
|
||||||
|
mkdir -p "$mount_dir"
|
||||||
|
if [[ -n "$archive" ]]; then
|
||||||
|
info "-- Mounting archive $archive at $mount_dir"
|
||||||
|
"$BORG_BIN" mount "${REPO_DIR}::${archive}" "$mount_dir"
|
||||||
|
else
|
||||||
|
info "-- Mounting repository archive overview at $mount_dir"
|
||||||
|
"$BORG_BIN" mount "$REPO_DIR" "$mount_dir"
|
||||||
|
fi
|
||||||
|
info "-- Mounted at: $mount_dir"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_umount() {
|
||||||
|
load_config
|
||||||
|
local mount_dir="${1:-$MOUNT_DIR}"
|
||||||
|
if have borg; then
|
||||||
|
"$BORG_BIN" umount "$mount_dir" 2>/dev/null && return 0
|
||||||
|
fi
|
||||||
|
if have fusermount3; then
|
||||||
|
fusermount3 -u "$mount_dir"
|
||||||
|
elif have fusermount; then
|
||||||
|
fusermount -u "$mount_dir"
|
||||||
|
else
|
||||||
|
umount "$mount_dir"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
normalize_restore_path() {
|
||||||
|
local path="$1"
|
||||||
|
path="${path#/}"
|
||||||
|
path="${path#./}"
|
||||||
|
printf '%s\n' "$path"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_restore() {
|
||||||
|
load_config
|
||||||
|
require_command "$BORG_BIN"
|
||||||
|
local archive="${1:-}"
|
||||||
|
local restore_path="${2:-}"
|
||||||
|
local dest_dir="${3:-}"
|
||||||
|
|
||||||
|
[[ -n "$archive" ]] || archive="$(latest_archive)"
|
||||||
|
[[ -n "$archive" ]] || die "No archive found"
|
||||||
|
[[ -n "$dest_dir" ]] || dest_dir="${RESTORE_DIR}/${archive}"
|
||||||
|
mkdir -p "$dest_dir"
|
||||||
|
|
||||||
|
info "-- Restoring from archive: $archive"
|
||||||
|
info "-- Destination: $dest_dir"
|
||||||
|
(
|
||||||
|
cd "$dest_dir"
|
||||||
|
if [[ -n "$restore_path" ]]; then
|
||||||
|
restore_path="$(normalize_restore_path "$restore_path")"
|
||||||
|
"$BORG_BIN" extract "${REPO_DIR}::${archive}" "$restore_path"
|
||||||
|
else
|
||||||
|
"$BORG_BIN" extract "${REPO_DIR}::${archive}"
|
||||||
|
fi
|
||||||
|
)
|
||||||
|
info "-- Restore finished: $dest_dir"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_check() {
|
||||||
|
load_config
|
||||||
|
require_command "$BORG_BIN"
|
||||||
|
local verify_data=false
|
||||||
|
[[ "${1:-}" == "--verify-data" ]] && verify_data=true
|
||||||
|
check_repo "$verify_data"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_status() {
|
||||||
|
load_config
|
||||||
|
require_command "$BORG_BIN"
|
||||||
|
info "Config: $CONFIG_FILE"
|
||||||
|
info "Source: $SOURCE_DIR"
|
||||||
|
info "Repository: $REPO_DIR"
|
||||||
|
info "Retention: $RETENTION_WITHIN"
|
||||||
|
info "Latest archive: $(latest_archive || true)"
|
||||||
|
info
|
||||||
|
"$BORG_BIN" info "$REPO_DIR"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_break_lock() {
|
||||||
|
load_config
|
||||||
|
require_command "$BORG_BIN"
|
||||||
|
"$BORG_BIN" break-lock "$REPO_DIR"
|
||||||
|
}
|
||||||
|
|
||||||
|
main() {
|
||||||
|
local cmd="${1:-}"
|
||||||
|
[[ $# -gt 0 ]] && shift || true
|
||||||
|
|
||||||
|
case "$cmd" in
|
||||||
|
backup) cmd_backup "$@" ;;
|
||||||
|
init) load_config; init_repo "$@" ;;
|
||||||
|
list) cmd_list "$@" ;;
|
||||||
|
info) cmd_info "$@" ;;
|
||||||
|
search) cmd_search "$@" ;;
|
||||||
|
mount) cmd_mount "$@" ;;
|
||||||
|
umount|unmount) cmd_umount "$@" ;;
|
||||||
|
restore) cmd_restore "$@" ;;
|
||||||
|
check) cmd_check "$@" ;;
|
||||||
|
prune) load_config; prune_repo "$@"; compact_repo ;;
|
||||||
|
status) cmd_status "$@" ;;
|
||||||
|
break-lock|unlock) cmd_break_lock "$@" ;;
|
||||||
|
-h|--help|help|"") usage ;;
|
||||||
|
--version|version) printf '%s %s\n' "$APP_NAME" "$APP_VERSION" ;;
|
||||||
|
*) usage >&2; die "Unknown command: $cmd" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
Executable
+16
@@ -0,0 +1,16 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
APP_NAME="nextcloud-borg-backup"
|
||||||
|
BIN_DIR="${HOME}/.local/bin"
|
||||||
|
SYSTEMD_USER_DIR="${XDG_CONFIG_HOME:-${HOME}/.config}/systemd/user"
|
||||||
|
|
||||||
|
systemctl --user disable --now "${APP_NAME}.timer" 2>/dev/null || true
|
||||||
|
systemctl --user daemon-reload
|
||||||
|
|
||||||
|
rm -f "${SYSTEMD_USER_DIR}/${APP_NAME}.service" "${SYSTEMD_USER_DIR}/${APP_NAME}.timer"
|
||||||
|
rm -f "${BIN_DIR}/${APP_NAME}"
|
||||||
|
systemctl --user daemon-reload
|
||||||
|
|
||||||
|
printf 'Removed user service, timer, and executable.\n'
|
||||||
|
printf 'Kept config, passphrase, logs, restore directory, and Borg repository.\n'
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Back up mounted Nextcloud folder to Synology with Borg
|
||||||
|
Documentation=man:borg(1)
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart=%h/.local/bin/nextcloud-borg-backup backup
|
||||||
|
TimeoutStartSec=12h
|
||||||
|
Nice=10
|
||||||
|
IOSchedulingClass=best-effort
|
||||||
|
IOSchedulingPriority=6
|
||||||
|
Environment=LANG=C.UTF-8
|
||||||
|
Environment=LC_ALL=C.UTF-8
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Daily Nextcloud Borg backup timer
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnCalendar=*-*-* 03:15:00
|
||||||
|
Persistent=true
|
||||||
|
RandomizedDelaySec=30m
|
||||||
|
Unit=nextcloud-borg-backup.service
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
Reference in New Issue
Block a user