This commit is contained in:
135
scripts/backup_restore.sh
Executable file
135
scripts/backup_restore.sh
Executable file
@@ -0,0 +1,135 @@
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
MODE="${1:-}"
|
||||
ARCHIVE_PATH="${2:-}"
|
||||
COUCH_HOME="${COUCH_HOME:-/home/couch}"
|
||||
STAMP="$(date +%Y%m%d-%H%M%S)"
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
./scripts/backup_restore.sh backup [archive.tar.gz]
|
||||
./scripts/backup_restore.sh restore <archive.tar.gz>
|
||||
EOF
|
||||
}
|
||||
|
||||
if [[ -z "${MODE}" ]]; then
|
||||
usage >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
build_backup_manifest() {
|
||||
python3 - <<'PY' "${COUCH_HOME}"
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
couch_home = Path(sys.argv[1])
|
||||
roots = [
|
||||
Path(".config/couchd/runtime.json"),
|
||||
Path(".config/dolphin-emu"),
|
||||
Path(".config/Slippi"),
|
||||
Path(".local/share/dolphin-emu"),
|
||||
Path("Slippi"),
|
||||
]
|
||||
excluded_dirs = tuple(
|
||||
item.lower()
|
||||
for item in (
|
||||
".config/Slippi Launcher",
|
||||
".local/share/accounts",
|
||||
".local/share/sessions",
|
||||
"Slippi/Replay",
|
||||
"Slippi/Replays",
|
||||
)
|
||||
)
|
||||
excluded_suffixes = (".slp", ".iso", ".gcm", ".wbfs", ".nkit.iso")
|
||||
|
||||
for root in roots:
|
||||
source = couch_home / root
|
||||
if not source.exists():
|
||||
continue
|
||||
if source.is_file():
|
||||
print(str(root), end="\0")
|
||||
continue
|
||||
for current_root, dir_names, file_names in os.walk(source):
|
||||
rel_root = Path(current_root).relative_to(couch_home)
|
||||
dir_names[:] = [
|
||||
name
|
||||
for name in dir_names
|
||||
if str(rel_root / name).lower() not in excluded_dirs
|
||||
]
|
||||
for file_name in file_names:
|
||||
rel_path = rel_root / file_name
|
||||
rel_lower = str(rel_path).lower()
|
||||
if any(rel_lower == item or rel_lower.startswith(f"{item}/") for item in excluded_dirs):
|
||||
continue
|
||||
if rel_lower.endswith(excluded_suffixes):
|
||||
continue
|
||||
print(str(rel_path), end="\0")
|
||||
PY
|
||||
}
|
||||
|
||||
validate_restore_archive() {
|
||||
python3 - <<'PY' "${ARCHIVE_PATH}" "${COUCH_HOME}"
|
||||
import sys
|
||||
import tarfile
|
||||
from pathlib import Path
|
||||
|
||||
archive_path = Path(sys.argv[1])
|
||||
couch_home = Path(sys.argv[2]).resolve(strict=False)
|
||||
|
||||
with tarfile.open(archive_path, "r:*") as archive:
|
||||
for member in archive.getmembers():
|
||||
member_path = Path(member.name)
|
||||
if member_path.is_absolute():
|
||||
raise SystemExit(f"unsafe archive entry: {member.name}")
|
||||
if any(part == ".." for part in member_path.parts):
|
||||
raise SystemExit(f"unsafe archive entry: {member.name}")
|
||||
if member.isdev():
|
||||
raise SystemExit(f"unsafe archive entry: {member.name}")
|
||||
target_path = (couch_home / member_path).resolve(strict=False)
|
||||
if couch_home != target_path and couch_home not in target_path.parents:
|
||||
raise SystemExit(f"unsafe archive entry: {member.name}")
|
||||
if member.issym() or member.islnk():
|
||||
link_target = Path(member.linkname)
|
||||
if link_target.is_absolute():
|
||||
raise SystemExit(f"unsafe archive entry: {member.name}")
|
||||
resolved_link = (target_path.parent / link_target).resolve(strict=False)
|
||||
if couch_home != resolved_link and couch_home not in resolved_link.parents:
|
||||
raise SystemExit(f"unsafe archive entry: {member.name}")
|
||||
PY
|
||||
}
|
||||
|
||||
case "${MODE}" in
|
||||
backup)
|
||||
ARCHIVE_PATH="${ARCHIVE_PATH:-${COUCH_HOME}/Slippi/couchos-backup-${STAMP}.tar.gz}"
|
||||
umask 077
|
||||
mapfile -d '' manifest < <(build_backup_manifest)
|
||||
if [[ "${#manifest[@]}" -eq 0 ]]; then
|
||||
printf 'no backup sources found under %s\n' "${COUCH_HOME}" >&2
|
||||
exit 1
|
||||
fi
|
||||
tar -C "${COUCH_HOME}" -czf "${ARCHIVE_PATH}" \
|
||||
--exclude='.config/couchd/token' \
|
||||
"${manifest[@]}"
|
||||
chmod 0600 "${ARCHIVE_PATH}"
|
||||
printf 'backup written to %s\n' "${ARCHIVE_PATH}"
|
||||
;;
|
||||
restore)
|
||||
if [[ -z "${ARCHIVE_PATH}" || ! -f "${ARCHIVE_PATH}" ]]; then
|
||||
printf 'restore archive is required\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! validation_error="$(validate_restore_archive 2>&1)"; then
|
||||
printf '%s\n' "${validation_error}" >&2
|
||||
exit 1
|
||||
fi
|
||||
tar -C "${COUCH_HOME}" -xzf "${ARCHIVE_PATH}"
|
||||
printf 'restored from %s\n' "${ARCHIVE_PATH}"
|
||||
;;
|
||||
*)
|
||||
usage >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
Reference in New Issue
Block a user