This repository has been archived on 2026-05-24. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
AgrarianGameArchive/Scripts/agrarian_project_backup.sh

221 lines
5.9 KiB
Bash
Executable File

#!/usr/bin/env bash
set -Eeuo pipefail
umask 077
SOURCE_DIR="${AGRARIAN_PROJECT_SOURCE:-/mnt/projects/AgrarianGameBulid}"
MOUNT_POINT="${AGRARIAN_BACKUP_MOUNT:-/mnt/backups/linastorage}"
BACKUP_ROOT="${AGRARIAN_BACKUP_ROOT:-$MOUNT_POINT/backups/agrarian-game/project}"
RESTIC_REPOSITORY="${AGRARIAN_RESTIC_REPOSITORY:-$BACKUP_ROOT/restic-repository}"
RESTIC_PASSWORD_FILE="${AGRARIAN_RESTIC_PASSWORD_FILE:-/root/.backup-secrets/agrarian-project-restic.password}"
STATE_DIR="${AGRARIAN_BACKUP_STATE_DIR:-$BACKUP_ROOT/state}"
LOCK_FILE="${AGRARIAN_BACKUP_LOCK:-/run/lock/agrarian-project-backup.lock}"
LOG_TAG="${AGRARIAN_BACKUP_LOG_TAG:-agrarian-project-backup}"
DRY_RUN=0
FORCE=0
MANUAL=0
EXCLUDES=(
--exclude "$SOURCE_DIR/DerivedDataCache"
--exclude "$SOURCE_DIR/Intermediate"
--exclude "$SOURCE_DIR/Saved"
--exclude "$SOURCE_DIR/Binaries"
--exclude "$SOURCE_DIR/Builds"
--exclude "$SOURCE_DIR/.git/index.lock"
--exclude "$SOURCE_DIR/.git/lfs/tmp"
--exclude "$SOURCE_DIR/.git/logs"
--exclude "$SOURCE_DIR/.vs"
--exclude "$SOURCE_DIR/AGRARIAN_BACKUP_MANIFEST.txt"
--exclude "$SOURCE_DIR/AGRARIAN_CRITICAL_SHA256SUMS"
--exclude "$SOURCE_DIR/AGRARIAN_RSYNC_LOG.txt"
)
log() {
printf '[%s] %s\n' "$(date -Is)" "$*"
}
die() {
log "ERROR: $*"
exit 1
}
usage() {
cat <<'USAGE'
Usage: agrarian_project_backup.sh [--force] [--manual] [--dry-run]
Creates an incremental deduplicated restic backup of the Agrarian Unreal project
on Linastorage. Generated Unreal cache/build folders are excluded; Git metadata
and local source data are included.
USAGE
}
parse_args() {
while [[ $# -gt 0 ]]; do
case "$1" in
--force)
FORCE=1
;;
--manual)
MANUAL=1
FORCE=1
;;
--dry-run)
DRY_RUN=1
;;
-h|--help)
usage
exit 0
;;
*)
die "Unknown argument: $1"
;;
esac
shift
done
}
restic_cmd() {
RESTIC_PASSWORD_FILE="$RESTIC_PASSWORD_FILE" \
restic -r "$RESTIC_REPOSITORY" "$@"
}
require_environment() {
[[ -d "$SOURCE_DIR" ]] || die "Project source directory not found: $SOURCE_DIR"
[[ -f "$SOURCE_DIR/AgrarianGame.uproject" ]] || die "Missing AgrarianGame.uproject under $SOURCE_DIR"
findmnt -rn "$MOUNT_POINT" >/dev/null || die "$MOUNT_POINT is not mounted"
mkdir -p "$BACKUP_ROOT" "$STATE_DIR" "$(dirname "$RESTIC_PASSWORD_FILE")"
[[ -w "$BACKUP_ROOT" ]] || die "$BACKUP_ROOT is not writable"
command -v restic >/dev/null || die "restic is required"
}
ensure_password_file() {
if [[ -f "$RESTIC_PASSWORD_FILE" ]]; then
chmod 0600 "$RESTIC_PASSWORD_FILE"
return 0
fi
log "Creating root-owned restic password file: $RESTIC_PASSWORD_FILE"
openssl rand -base64 48 > "$RESTIC_PASSWORD_FILE"
chmod 0600 "$RESTIC_PASSWORD_FILE"
}
ensure_repository() {
if [[ -f "$RESTIC_REPOSITORY/config" ]]; then
return 0
fi
log "Initializing restic repository: $RESTIC_REPOSITORY"
mkdir -p "$RESTIC_REPOSITORY"
restic_cmd init
}
source_signature() {
local tmp
tmp="$(mktemp)"
{
git -C "$SOURCE_DIR" rev-parse HEAD 2>/dev/null || true
git -C "$SOURCE_DIR" status --porcelain=v1 2>/dev/null || true
find "$SOURCE_DIR" \
\( -path "$SOURCE_DIR/DerivedDataCache" \
-o -path "$SOURCE_DIR/Intermediate" \
-o -path "$SOURCE_DIR/Saved" \
-o -path "$SOURCE_DIR/Binaries" \
-o -path "$SOURCE_DIR/Builds" \
-o -path "$SOURCE_DIR/.git/lfs/tmp" \
-o -path "$SOURCE_DIR/.git/logs" \
-o -path "$SOURCE_DIR/.vs" \) -prune \
-o -type f -printf '%P\t%s\t%T@\n' 2>/dev/null \
| sort
} > "$tmp"
sha256sum "$tmp" | awk '{print $1}'
rm -f -- "$tmp"
}
write_manifest() {
local signature="$1"
local manifest="$STATE_DIR/LATEST_MANIFEST.txt"
{
echo "backup_timestamp=$(date -Is)"
echo "host=$(hostname -f 2>/dev/null || hostname)"
echo "source=$SOURCE_DIR"
echo "mount=$MOUNT_POINT"
echo "repository=$RESTIC_REPOSITORY"
echo "signature=$signature"
echo "backup_mode=$([[ "$MANUAL" == "1" ]] && echo manual || echo scheduled)"
echo "git_head=$(git -C "$SOURCE_DIR" rev-parse HEAD 2>/dev/null || echo unavailable)"
echo
echo "[excluded]"
printf '%s\n' "${EXCLUDES[@]}"
} > "$manifest"
}
backup_project() {
local signature="$1"
local mode_tag="scheduled"
[[ "$MANUAL" == "1" ]] && mode_tag="manual"
log "Starting restic backup for Agrarian project"
restic_cmd backup \
--one-file-system \
--tag agrarian-game \
--tag project \
--tag "$mode_tag" \
"${EXCLUDES[@]}" \
"$SOURCE_DIR"
write_manifest "$signature"
printf '%s\n' "$signature" > "$STATE_DIR/LAST_SOURCE_SIGNATURE"
log "Applying restic retention policy"
restic_cmd forget \
--tag agrarian-game \
--tag project \
--keep-within 7d \
--keep-daily 30 \
--keep-weekly 12 \
--keep-monthly 12 \
--prune
restic_cmd snapshots --tag agrarian-game --tag project > "$STATE_DIR/LATEST_SNAPSHOTS.txt"
log "Project backup completed"
logger -t "$LOG_TAG" "restic project backup completed successfully"
}
main() {
parse_args "$@"
exec 9>"$LOCK_FILE"
if ! flock -n 9; then
log "Another Agrarian project backup is already running; skipping this trigger"
logger -t "$LOG_TAG" "another project backup is already running; skipped"
exit 0
fi
require_environment
ensure_password_file
ensure_repository
local signature previous_signature
signature="$(source_signature)"
previous_signature="$(cat "$STATE_DIR/LAST_SOURCE_SIGNATURE" 2>/dev/null || true)"
if [[ "$FORCE" != "1" && "$signature" == "$previous_signature" ]]; then
log "No project changes detected since latest restic backup; skipping"
logger -t "$LOG_TAG" "no project changes detected; skipped"
exit 0
fi
if [[ "$DRY_RUN" == "1" ]]; then
log "Dry run: project changes detected; restic snapshot would be created"
exit 0
fi
backup_project "$signature"
}
main "$@"