feat: build CouchOS Melee appliance
Some checks failed
ci / verify (push) Has been cancelled

This commit is contained in:
jackmhny
2026-08-03 02:52:28 +02:00
commit 04ef2a0d74
42 changed files with 3671 additions and 0 deletions

135
scripts/backup_restore.sh Executable file
View 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