first release
This commit is contained in:
parent
4b9359ebe2
commit
9f88beed48
4 changed files with 543 additions and 0 deletions
21
LICENSE
Normal file
21
LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025 Peter Knauer
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
25
packaging/PKGBUILD
Normal file
25
packaging/PKGBUILD
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
# Maintainer: Peter Knauer <zuzu@quantweave.ca>
|
||||||
|
pkgname=zuzu-system-backup
|
||||||
|
pkgver=1.0.0
|
||||||
|
pkgrel=1
|
||||||
|
pkgdesc="SSH/rsync snapshot + mirror backups to NAS for Linux based systems (Python-based)"
|
||||||
|
arch=(any)
|
||||||
|
depends=(python python-yaml rsync openssh coreutils tar zstd)
|
||||||
|
source=(
|
||||||
|
'zuzu-system-backup.py'
|
||||||
|
'zuzu-system-backup.yaml'
|
||||||
|
'zuzu-system-backup.service'
|
||||||
|
'zuzu-system-backup.timer'
|
||||||
|
)
|
||||||
|
sha256sums=('SKIP' 'SKIP' 'SKIP' 'SKIP')
|
||||||
|
|
||||||
|
package() {
|
||||||
|
install -d "$pkgdir/usr/local/sbin/zuzu-system-backup"
|
||||||
|
install -m 0755 "backup.py" "$pkgdir/usr/local/sbin/zuzu-system-backup/zuzu-system-backup.py"
|
||||||
|
# Template config; edit per-host after install
|
||||||
|
install -m 0644 "backup.yaml" "$pkgdir/usr/local/sbin/zuzu-system-backup/zuzu-system-backup.yaml"
|
||||||
|
|
||||||
|
install -d "$pkgdir/usr/lib/systemd/system"
|
||||||
|
install -m 0644 "zuzu-system-backup.service" "$pkgdir/usr/lib/systemd/system/zuzu-system-backup.service"
|
||||||
|
install -m 0644 "zuzu-system-backup.timer" "$pkgdir/usr/lib/systemd/system/zuzu-system-backup.timer"
|
||||||
|
}
|
||||||
417
zuzu-system-backup.py
Normal file
417
zuzu-system-backup.py
Normal file
|
|
@ -0,0 +1,417 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
zuzu-system-backup
|
||||||
|
|
||||||
|
Snapshot + mirror backups from Arch hosts to NAS over SSH/rsync.
|
||||||
|
|
||||||
|
- Snapshot sources:
|
||||||
|
INCLUDE_PATHS
|
||||||
|
USER_INCLUDE_DIRS
|
||||||
|
USER_INCLUDE_FILES
|
||||||
|
|
||||||
|
-> rsync’d into a local temp tree per category
|
||||||
|
-> compressed locally (tar + optional zstd)
|
||||||
|
-> archives uploaded to NAS snapshot dir
|
||||||
|
|
||||||
|
- Single-copy mirrors:
|
||||||
|
SINGLE_COPY_MAPPINGS
|
||||||
|
|
||||||
|
-> rsync directly to NAS with --delete
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import shlex
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
BASE_DIR = Path(__file__).resolve().parent
|
||||||
|
CONFIG_PATH = BASE_DIR / "backup.yaml"
|
||||||
|
|
||||||
|
SNAPSHOT_FMT = "%Y-%m-%d_%H-%M-%S"
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Load config (backup.yaml)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _fatal(msg: str) -> None:
|
||||||
|
print(f"[FATAL] {msg}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
try:
|
||||||
|
with CONFIG_PATH.open("r", encoding="utf-8") as f:
|
||||||
|
cfg = yaml.safe_load(f) or {}
|
||||||
|
except FileNotFoundError:
|
||||||
|
_fatal(f"Config file not found: {CONFIG_PATH}")
|
||||||
|
|
||||||
|
remote_cfg = cfg.get("remote", {})
|
||||||
|
SSH_USER = remote_cfg.get("user")
|
||||||
|
SSH_HOST = remote_cfg.get("host")
|
||||||
|
SSH_PORT = int(remote_cfg.get("port", 22))
|
||||||
|
SSH_KEY = remote_cfg.get("key")
|
||||||
|
REMOTE_BASE = remote_cfg.get("base")
|
||||||
|
REMOTE_HOST_DIR = remote_cfg.get("host_dir")
|
||||||
|
|
||||||
|
if not all([SSH_USER, SSH_HOST, SSH_KEY, REMOTE_BASE, REMOTE_HOST_DIR]):
|
||||||
|
_fatal("remote.{user,host,key,base,host_dir} must all be set in backup.yaml")
|
||||||
|
|
||||||
|
# retention
|
||||||
|
RETENTION_DAYS = int(cfg.get("retention", {}).get("snapshots", 7))
|
||||||
|
|
||||||
|
# compression
|
||||||
|
compression_cfg = cfg.get("compression", {})
|
||||||
|
COMPRESSION_MODE = (compression_cfg.get("mode") or "high").lower()
|
||||||
|
if COMPRESSION_MODE not in ("high", "light", "none"):
|
||||||
|
COMPRESSION_MODE = "high"
|
||||||
|
|
||||||
|
COMPRESSION_PATH = compression_cfg.get("path")
|
||||||
|
|
||||||
|
# rsync
|
||||||
|
RSYNC_EXTRA_OPTS = cfg.get("rsync", {}).get("extra_opts", [])
|
||||||
|
|
||||||
|
user_cfg = cfg.get("user", {})
|
||||||
|
USER_HOME = user_cfg.get("home")
|
||||||
|
|
||||||
|
|
||||||
|
def expand_user_path(p: str) -> str:
|
||||||
|
if not p:
|
||||||
|
return p
|
||||||
|
if p.startswith("/"):
|
||||||
|
return p
|
||||||
|
if USER_HOME:
|
||||||
|
return str(Path(USER_HOME) / p)
|
||||||
|
return p
|
||||||
|
|
||||||
|
|
||||||
|
def expand_home_var(s: str) -> str:
|
||||||
|
if not isinstance(s, str):
|
||||||
|
return s
|
||||||
|
if USER_HOME:
|
||||||
|
return (
|
||||||
|
s.replace("${USER_HOME}", USER_HOME)
|
||||||
|
.replace("${HOME}", USER_HOME)
|
||||||
|
)
|
||||||
|
return s
|
||||||
|
|
||||||
|
# System trees
|
||||||
|
INCLUDE_PATHS = cfg.get("system", {}).get("include_paths", [])
|
||||||
|
|
||||||
|
# User trees
|
||||||
|
USER_INCLUDE_DIRS = [expand_user_path(p) for p in user_cfg.get("include_dirs", [])]
|
||||||
|
USER_INCLUDE_FILES = [expand_user_path(p) for p in user_cfg.get("include_files", [])]
|
||||||
|
|
||||||
|
# Exclude patterns
|
||||||
|
raw_excl = cfg.get("exclude_patterns", [])
|
||||||
|
EXCLUDE_PATTERNS = [expand_home_var(p) for p in raw_excl]
|
||||||
|
|
||||||
|
# Single-copy mappings
|
||||||
|
SINGLE_COPY_MAPPINGS = cfg.get("single_copy_mappings", [])
|
||||||
|
|
||||||
|
# Derived remote paths
|
||||||
|
REMOTE_ROOT = f"{REMOTE_BASE.rstrip('/')}/{REMOTE_HOST_DIR}"
|
||||||
|
REMOTE_SNAPSHOTS_DIR = f"{REMOTE_ROOT}/snapshots"
|
||||||
|
|
||||||
|
# Decide where local temp/compression work happens
|
||||||
|
if COMPRESSION_PATH:
|
||||||
|
compression_root = Path(COMPRESSION_PATH)
|
||||||
|
try:
|
||||||
|
compression_root.mkdir(parents=True, exist_ok=True)
|
||||||
|
except Exception as e:
|
||||||
|
print(
|
||||||
|
f"[WARN] COMPRESSION_PATH {compression_root} not usable ({e}); "
|
||||||
|
"falling back to system temp",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
compression_root = Path(tempfile.gettempdir())
|
||||||
|
else:
|
||||||
|
compression_root = Path(tempfile.gettempdir())
|
||||||
|
|
||||||
|
COMPRESSION_ROOT = compression_root
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Helpers
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def log(msg: str) -> None:
|
||||||
|
ts = datetime.now().strftime(SNAPSHOT_FMT)
|
||||||
|
print(f"[{ts}] {msg}", file=sys.stderr)
|
||||||
|
|
||||||
|
|
||||||
|
def run(cmd, check: bool = True, capture_output: bool = False, text: bool = True):
|
||||||
|
if isinstance(cmd, list):
|
||||||
|
debug_cmd = " ".join(shlex.quote(str(c)) for c in cmd)
|
||||||
|
else:
|
||||||
|
debug_cmd = cmd
|
||||||
|
log(f"run: {debug_cmd}")
|
||||||
|
return subprocess.run(
|
||||||
|
cmd,
|
||||||
|
check=check,
|
||||||
|
capture_output=capture_output,
|
||||||
|
text=text,
|
||||||
|
)
|
||||||
|
|
||||||
|
def remote_shell(cmd_str: str, **kwargs):
|
||||||
|
ssh_cmd = [
|
||||||
|
"ssh",
|
||||||
|
"-i", SSH_KEY,
|
||||||
|
"-p", str(SSH_PORT),
|
||||||
|
"-o", "BatchMode=yes",
|
||||||
|
"-o", "StrictHostKeyChecking=accept-new",
|
||||||
|
f"{SSH_USER}@{SSH_HOST}",
|
||||||
|
cmd_str,
|
||||||
|
]
|
||||||
|
return run(ssh_cmd, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def build_excludes_file() -> str:
|
||||||
|
fd, path = tempfile.mkstemp(prefix="arch-rsync-backup-excludes-", text=True)
|
||||||
|
with os.fdopen(fd, "w") as fh:
|
||||||
|
for pat in EXCLUDE_PATTERNS:
|
||||||
|
fh.write(str(pat) + "\n")
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_remote_snapshot_root() -> None:
|
||||||
|
remote_shell(f"mkdir -p {shlex.quote(REMOTE_SNAPSHOTS_DIR)}")
|
||||||
|
|
||||||
|
|
||||||
|
def snapshot_name() -> str:
|
||||||
|
return datetime.now().strftime(SNAPSHOT_FMT)
|
||||||
|
|
||||||
|
|
||||||
|
def list_remote_snapshots():
|
||||||
|
cmd = f"ls -1 {shlex.quote(REMOTE_SNAPSHOTS_DIR)} || true"
|
||||||
|
result = remote_shell(cmd, capture_output=True)
|
||||||
|
names = []
|
||||||
|
if result.stdout:
|
||||||
|
for line in result.stdout.splitlines():
|
||||||
|
line = line.strip()
|
||||||
|
if line:
|
||||||
|
names.append(line)
|
||||||
|
return names
|
||||||
|
|
||||||
|
|
||||||
|
def prune_old_snapshots() -> None:
|
||||||
|
"""
|
||||||
|
Retention policy: keep at most RETENTION_DAYS snapshots
|
||||||
|
(by timestamp order). Older ones are deleted.
|
||||||
|
"""
|
||||||
|
max_keep = RETENTION_DAYS
|
||||||
|
if max_keep <= 0:
|
||||||
|
log("RETENTION_DAYS <= 0; skipping pruning")
|
||||||
|
return
|
||||||
|
|
||||||
|
names = list_remote_snapshots()
|
||||||
|
parsed = []
|
||||||
|
for name in names:
|
||||||
|
try:
|
||||||
|
dt = datetime.strptime(name, SNAPSHOT_FMT)
|
||||||
|
except ValueError:
|
||||||
|
# non-standard dirs, ignore
|
||||||
|
continue
|
||||||
|
parsed.append((dt, name))
|
||||||
|
|
||||||
|
parsed.sort()
|
||||||
|
if len(parsed) <= max_keep:
|
||||||
|
log(f"prune: {len(parsed)} snapshots <= {max_keep}; nothing to delete")
|
||||||
|
return
|
||||||
|
|
||||||
|
to_delete = [name for _, name in parsed[:-max_keep]]
|
||||||
|
base_q = shlex.quote(REMOTE_SNAPSHOTS_DIR)
|
||||||
|
del_str = " ".join(shlex.quote(n) for n in to_delete)
|
||||||
|
log(f"prune: deleting old snapshots: {', '.join(to_delete)}")
|
||||||
|
remote_shell(f"cd {base_q} && rm -rf -- {del_str}")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Snapshot: build local tree per category, compress locally, upload archive
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def rsync_to_local_category(category_root: Path, sources, excludes_file: str) -> None:
|
||||||
|
category_root.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
for src in sources:
|
||||||
|
src = str(src).rstrip()
|
||||||
|
if not src:
|
||||||
|
continue
|
||||||
|
if not os.path.exists(src):
|
||||||
|
log(f"skip missing snapshot source: {src}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
cmd = [
|
||||||
|
"rsync",
|
||||||
|
"-aHAXR",
|
||||||
|
"--relative",
|
||||||
|
"--human-readable",
|
||||||
|
f"--exclude-from={excludes_file}",
|
||||||
|
src,
|
||||||
|
str(category_root) + "/",
|
||||||
|
]
|
||||||
|
# insert extra options after -aHAXR
|
||||||
|
cmd[5:5] = RSYNC_EXTRA_OPTS
|
||||||
|
run(cmd)
|
||||||
|
|
||||||
|
def compress_category_local(category_name: str, category_root: Path, tmp_root: Path,
|
||||||
|
remote_snapshot_dir: str) -> None:
|
||||||
|
# If directory is empty, nothing to do
|
||||||
|
if not category_root.exists():
|
||||||
|
log(f"category {category_name}: no data, skipping")
|
||||||
|
return
|
||||||
|
|
||||||
|
any_content = False
|
||||||
|
for _ in category_root.rglob("*"):
|
||||||
|
any_content = True
|
||||||
|
break
|
||||||
|
if not any_content:
|
||||||
|
log(f"category {category_name}: empty tree, skipping")
|
||||||
|
return
|
||||||
|
|
||||||
|
mode = COMPRESSION_MODE
|
||||||
|
if mode == "none":
|
||||||
|
archive_name = f"{category_name}.tar"
|
||||||
|
else:
|
||||||
|
archive_name = f"{category_name}.tar.zst"
|
||||||
|
|
||||||
|
archive_path = tmp_root / archive_name
|
||||||
|
|
||||||
|
if mode == "none":
|
||||||
|
# plain tar, no compression
|
||||||
|
cmd = [
|
||||||
|
"tar",
|
||||||
|
"-cf", str(archive_path),
|
||||||
|
"--ignore-failed-read",
|
||||||
|
"-C", str(category_root),
|
||||||
|
".",
|
||||||
|
]
|
||||||
|
run(cmd)
|
||||||
|
else:
|
||||||
|
level = 19 if mode == "high" else 3
|
||||||
|
# Use shell for tar | zstd pipeline
|
||||||
|
shell_cmd = (
|
||||||
|
f"cd {shlex.quote(str(category_root))} && "
|
||||||
|
f"tar -cf - --ignore-failed-read . "
|
||||||
|
f"| zstd -T0 -{level} -o {shlex.quote(str(archive_path))}"
|
||||||
|
)
|
||||||
|
run(["sh", "-c", shell_cmd])
|
||||||
|
|
||||||
|
log(f"category {category_name}: archive created at {archive_path}")
|
||||||
|
|
||||||
|
# Upload archive to remote snapshot dir
|
||||||
|
rsync_cmd = [
|
||||||
|
"rsync",
|
||||||
|
"-a",
|
||||||
|
"--human-readable",
|
||||||
|
"-e",
|
||||||
|
f"ssh -i {SSH_KEY} -p {SSH_PORT} -oBatchMode=yes -oStrictHostKeyChecking=accept-new",
|
||||||
|
str(archive_path),
|
||||||
|
f"{SSH_USER}@{SSH_HOST}:{remote_snapshot_dir}/",
|
||||||
|
]
|
||||||
|
# insert extra opts after -a
|
||||||
|
rsync_cmd[3:3] = RSYNC_EXTRA_OPTS
|
||||||
|
run(rsync_cmd)
|
||||||
|
|
||||||
|
log(f"category {category_name}: archive uploaded to {remote_snapshot_dir}")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Single-copy mirrors (unchanged)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def rsync_single_copy(src: str, dest_remote: str, excludes_file: str) -> None:
|
||||||
|
src = src.rstrip("/")
|
||||||
|
if not os.path.exists(src):
|
||||||
|
log(f"skip missing single-copy src: {src}")
|
||||||
|
return
|
||||||
|
|
||||||
|
dest_remote = dest_remote.rstrip("/")
|
||||||
|
remote_shell(f"mkdir -p {shlex.quote(dest_remote)}")
|
||||||
|
|
||||||
|
cmd = [
|
||||||
|
"rsync",
|
||||||
|
"-aHAX",
|
||||||
|
"--delete",
|
||||||
|
"--human-readable",
|
||||||
|
f"--exclude-from={excludes_file}",
|
||||||
|
"-e",
|
||||||
|
f"ssh -i {SSH_KEY} -p {SSH_PORT} -oBatchMode=yes -oStrictHostKeyChecking=accept-new",
|
||||||
|
f"{src}/",
|
||||||
|
f"{SSH_USER}@{SSH_HOST}:{dest_remote}/",
|
||||||
|
]
|
||||||
|
cmd[4:4] = RSYNC_EXTRA_OPTS
|
||||||
|
run(cmd)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Main
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
log("==== arch-rsync-backup (local compression): start ====")
|
||||||
|
ensure_remote_snapshot_root()
|
||||||
|
|
||||||
|
snap = snapshot_name()
|
||||||
|
remote_snapshot_dir = f"{REMOTE_SNAPSHOTS_DIR}/{snap}"
|
||||||
|
remote_shell(f"mkdir -p {shlex.quote(remote_snapshot_dir)}")
|
||||||
|
log(f"snapshot: {snap} -> {remote_snapshot_dir}")
|
||||||
|
|
||||||
|
excludes_file = build_excludes_file()
|
||||||
|
|
||||||
|
tmp_root = Path(
|
||||||
|
tempfile.mkdtemp(
|
||||||
|
prefix=f"arch-rsync-backup-{snap}-",
|
||||||
|
dir=str(COMPRESSION_ROOT),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 1) Build local category trees
|
||||||
|
system_root = tmp_root / "system"
|
||||||
|
user_dirs_root = tmp_root / "user-dirs"
|
||||||
|
user_files_root = tmp_root / "user-files"
|
||||||
|
|
||||||
|
rsync_to_local_category(system_root, INCLUDE_PATHS, excludes_file)
|
||||||
|
rsync_to_local_category(user_dirs_root, USER_INCLUDE_DIRS, excludes_file)
|
||||||
|
rsync_to_local_category(user_files_root, USER_INCLUDE_FILES, excludes_file)
|
||||||
|
|
||||||
|
# 2) Compress locally and upload archives
|
||||||
|
compress_category_local("system", system_root, tmp_root, remote_snapshot_dir)
|
||||||
|
compress_category_local("user-dirs", user_dirs_root, tmp_root, remote_snapshot_dir)
|
||||||
|
compress_category_local("user-files", user_files_root, tmp_root, remote_snapshot_dir)
|
||||||
|
|
||||||
|
# 3) Single-copy mirrors (unchanged)
|
||||||
|
for mapping in SINGLE_COPY_MAPPINGS:
|
||||||
|
if not mapping:
|
||||||
|
continue
|
||||||
|
if "|" not in mapping:
|
||||||
|
log(f"skip malformed mapping (no '|'): {mapping}")
|
||||||
|
continue
|
||||||
|
src, dest = mapping.split("|", 1)
|
||||||
|
src = src.strip()
|
||||||
|
dest = dest.strip()
|
||||||
|
if not src or not dest:
|
||||||
|
log(f"skip malformed mapping (empty src/dest): {mapping}")
|
||||||
|
continue
|
||||||
|
rsync_single_copy(src, dest, excludes_file)
|
||||||
|
|
||||||
|
# 4) Retention
|
||||||
|
prune_old_snapshots()
|
||||||
|
|
||||||
|
log(f"Backup complete: {snap}")
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
os.remove(excludes_file)
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
# clean up local temp tree
|
||||||
|
shutil.rmtree(tmp_root, ignore_errors=True)
|
||||||
|
log("==== arch-rsync-backup: end ====")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
80
zuzu-system-backup.yaml
Normal file
80
zuzu-system-backup.yaml
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
remote:
|
||||||
|
user: backupuser
|
||||||
|
host: backup-nas.local
|
||||||
|
port: 22
|
||||||
|
key: /home/backupuser/.ssh/id_ed25519-orion
|
||||||
|
base: /srv/backup/automated
|
||||||
|
host_dir: system-orion
|
||||||
|
|
||||||
|
retention:
|
||||||
|
# Max number of snapshot directories to keep on NAS
|
||||||
|
snapshots: 7
|
||||||
|
|
||||||
|
compression:
|
||||||
|
# high | light | none
|
||||||
|
mode: high
|
||||||
|
# Optional: where local temp trees and archives live
|
||||||
|
path: /srv/tmp/backups
|
||||||
|
|
||||||
|
rsync:
|
||||||
|
extra_opts:
|
||||||
|
- --numeric-ids
|
||||||
|
- --info=progress2
|
||||||
|
- --protect-args
|
||||||
|
|
||||||
|
system:
|
||||||
|
include_paths:
|
||||||
|
- /etc/nftables.conf
|
||||||
|
- /etc/snapper/configs
|
||||||
|
- /etc/NetworkManager/system-connections
|
||||||
|
- /etc/chromium/policies/managed
|
||||||
|
- /etc/fstab
|
||||||
|
- /etc/systemd/system/*.mount
|
||||||
|
- /etc/systemd/system/*.automount
|
||||||
|
- /etc/nut/nut.conf
|
||||||
|
- /etc/nut/upsmon.conf
|
||||||
|
|
||||||
|
user:
|
||||||
|
home: /home/devuser
|
||||||
|
|
||||||
|
include_dirs:
|
||||||
|
- .ssh
|
||||||
|
- .gnupg
|
||||||
|
- .local/share/wallpapers
|
||||||
|
- projects
|
||||||
|
- pkgbuilds
|
||||||
|
- venvs
|
||||||
|
|
||||||
|
include_files:
|
||||||
|
- .config/chromium/Default/Preferences
|
||||||
|
- .config/chromium/Default/Bookmarks
|
||||||
|
- .config/vlc/vlcrc
|
||||||
|
- .gitconfig
|
||||||
|
- .bashrc
|
||||||
|
- .bash_profile
|
||||||
|
- .local/share/user-places.xbel
|
||||||
|
|
||||||
|
exclude_patterns:
|
||||||
|
# Caches (generic)
|
||||||
|
- "**/Cache/**"
|
||||||
|
- "**/GPUCache/**"
|
||||||
|
- "**/shadercache/**"
|
||||||
|
- "**/ShaderCache/**"
|
||||||
|
- "**/Code Cache/**"
|
||||||
|
|
||||||
|
# SSH ControlMaster sockets
|
||||||
|
- "${USER_HOME}/.ssh/ctl-*"
|
||||||
|
- "**/.ssh/ctl-*"
|
||||||
|
|
||||||
|
# JetBrains bulk (plugins + Toolbox app bundles)
|
||||||
|
- "${USER_HOME}/.local/share/JetBrains/**/plugins/**"
|
||||||
|
- "${USER_HOME}/.local/share/JetBrains/Toolbox/apps/**"
|
||||||
|
- "${USER_HOME}/.cache/JetBrains/**"
|
||||||
|
|
||||||
|
# Chromium bulk (we include only specific files above)
|
||||||
|
- "${USER_HOME}/.config/chromium/**"
|
||||||
|
|
||||||
|
single_copy_mappings:
|
||||||
|
# Example mirrors:
|
||||||
|
- "/srv/data/postgres|/srv/backup/automated/sync/system-orion-postgres"
|
||||||
|
- "/srv/data/models|/srv/backup/automated/sync/system-orion-models"
|
||||||
Loading…
Reference in a new issue