#!/usr/bin/env python3 """Build a single self-extracting installer from an explicit, secret-free allowlist.""" import argparse import gzip import hashlib import io from pathlib import Path import re import subprocess import tarfile ROOT = Path(__file__).resolve().parents[1] FILES = ['install.sh', 'update.sh', 'README.md', 'Dokumentation.md', 'UPDATE_ANLEITUNG.md', 'lib/common.sh', 'lib/locking.sh', 'lib/backup_set.py', 'lib/update-discover.py', 'usr-local-sbin/app-backup.sh', 'usr-local-sbin/app-restore.sh', 'systemd/app-backup.service', 'systemd/app-backup.timer', 'logrotate/app-backup', 'etc/app-backup/app-backup.conf'] def build(output, version): if not re.fullmatch(r'[A-Za-z0-9._-]+', version): raise ValueError('Unsafe version identifier') files = FILES + [str(p.relative_to(ROOT)) for p in sorted((ROOT/'etc/app-backup').glob('*.cnf.example'))] buffer = io.BytesIO() with gzip.GzipFile(fileobj=buffer, mode='wb', mtime=0) as zipped: with tarfile.open(fileobj=zipped, mode='w') as archive: for name in sorted(files): path = ROOT / name if path.is_symlink() or not path.is_file(): raise ValueError(f'Missing/unsafe package input: {name}') raw = path.read_bytes() info = tarfile.TarInfo(name) info.size = len(raw) info.mode = 0o700 if name.endswith('.sh') else 0o600 archive.addfile(info, io.BytesIO(raw)) raw = (version + '\n').encode() info = tarfile.TarInfo('VERSION') info.size = len(raw) info.mode = 0o600 archive.addfile(info, io.BytesIO(raw)) payload = buffer.getvalue() checksum = hashlib.sha256(payload).hexdigest() # Payload checksum catches transfer corruption. It is not a signature; obtain # this executable only through the trusted SSH/source repository channel. header = f'''#!/usr/bin/env bash set -Eeuo pipefail umask 077 if [[ ${{1:-}} == --help ]]; then echo 'Usage: sudo bash app-backup-update-{version}.run [--foreground] [--config FILE] [--rclone-config FILE] [--enable-gitea | --no-gitea] [--cron-services UNIT,...] [--mail-dir DIR | --mail-homes-only] [--install-deps]' exit 0 fi [[ $EUID == 0 ]] || {{ echo 'Bitte mit sudo bash starten.' >&2; exit 1; }} for tool in sha256sum tar mktemp tail awk; do command -v "$tool" >/dev/null; done line=$(awk '/^__APP_BACKUP_PAYLOAD__$/ {{print NR + 1; exit}}' "$0") [[ -n $line ]] || exit 1 actual=$(tail -n +"$line" "$0" | sha256sum) [[ ${{actual%% *}} == {checksum} ]] || {{ echo 'Paket-Pruefsumme falsch; keine Installation.' >&2; exit 1; }} # Root-owned persistent extraction survives SSH disconnect and worker startup. package_dir=$(mktemp -d /var/tmp/app-backup-package-{version}-XXXXXXXX) tail -n +"$line" "$0" | tar -xz --no-same-owner -C "$package_dir" exec bash "$package_dir/update.sh" "$@" exit 1 __APP_BACKUP_PAYLOAD__ '''.encode() output.mkdir(parents=True, exist_ok=True) package = output / f'app-backup-update-{version}.run' package.write_bytes(header + payload) package.chmod(0o700) (output / (package.name+'.sha256')).write_text(hashlib.sha256(package.read_bytes()).hexdigest()+' '+package.name+'\n') return package if __name__ == '__main__': parser = argparse.ArgumentParser() parser.add_argument('--output', type=Path, default=ROOT/'dist') parser.add_argument('--version', help='Override for development/testing only') args = parser.parse_args() version = args.version if not version: if subprocess.check_output(['git','status','--porcelain'],cwd=ROOT).strip(): parser.error('Commit source changes before creating a release package') version = subprocess.check_output(['git','rev-parse','--short=12','HEAD'],cwd=ROOT,text=True).strip() print(build(args.output, version))