Files
app-backup/tools/build-update-package.py
T

84 lines
3.9 KiB
Python

#!/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/report.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))