#!/usr/bin/env bash
# Back up committed tracked source only. Does not push, deploy or delete.
set -euo pipefail
if [[ $# != 2 ]]; then echo 'Usage: bash backup-source-v99.sh ABS_REPOSITORY ABS_BACKUP_DIRECTORY' >&2; exit 2; fi
repo="$1"; dest="$2"
[[ "$repo" = /* && "$dest" = /* ]] || { echo 'Use absolute paths' >&2; exit 2; }
repo=$(realpath "$repo");mkdir -p "$dest";dest=$(realpath "$dest")
[[ "$dest" != "$repo" && "$dest" != "$repo/"* ]] || { echo 'Backup directory must be outside source' >&2;exit 2; }
umask 077
[[ -z "$(git -C "$repo" status --porcelain --untracked-files=all)" ]] || { echo 'Working tree has unsaved/untracked changes; save and review first. No backup claimed.' >&2; exit 3; }
commit=$(git -C "$repo" rev-parse HEAD)
stamp=$(date -u +%Y%m%dT%H%M%SZ)
name="source-${stamp}-${commit:0:12}"
tmp=$(mktemp -d "$dest/.backup.XXXXXXXX")
trap 'rm -rf -- "$tmp"' EXIT
git -C "$repo" archive --format=zip --output="$tmp/$name.zip" "$commit"
python3 - "$tmp/$name.zip" "$repo" "$commit" <<'PY'
import sys,zipfile,hashlib,json,subprocess
path,repo,commit=sys.argv[1:]
with zipfile.ZipFile(path) as z:
 if z.testzip():raise SystemExit('ZIP integrity failure')
 names=[n for n in z.namelist() if not n.endswith('/')]
 expected=subprocess.check_output(['git','-C',repo,'ls-tree','-r','--name-only',commit],text=True).splitlines()
 if sorted(names)!=sorted(expected):raise SystemExit('File list mismatch; submodule or archive exclusion needs review')
 records=[{'path':n,'sha256':hashlib.sha256(z.read(n)).hexdigest()} for n in names]
with open(path+'.manifest.json','w') as f:json.dump({'commit':commit,'count':len(records),'files':records},f,ensure_ascii=False,indent=2)
print('Verified archive entries:',len(records))
PY
(cd "$tmp" && sha256sum "$name.zip" > "$name.zip.sha256")
[[ ! -e "$dest/$name.zip" ]] || { echo 'Same backup already exists; not overwritten' >&2;exit 4; }
mv "$tmp/$name.zip" "$tmp/$name.zip.sha256" "$tmp/$name.zip.manifest.json" "$dest/"
echo "Backup completed: $dest/$name.zip"
