Implement Linastorage project backups
This commit is contained in:
Executable
+220
@@ -0,0 +1,220 @@
|
||||
#!/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 "$@"
|
||||
Reference in New Issue
Block a user