88 lines
4.3 KiB
Python
88 lines
4.3 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/usage.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']
|
|
FILES += ['RESTORE_TEST.md', 'SPEICHER.md', 'REMOTE_READBACK.md', 'usr-local-sbin/app-restore-test.sh', 'lib/restore_test.py', 'lib/restore-test-remote.sh',
|
|
'lib/space.sh', 'lib/space.py', 'lib/prepare-restore-test.sh',
|
|
'systemd/app-restore-test.service', 'systemd/app-restore-test.timer',
|
|
'systemd/app-restore-test-cleanup.service']
|
|
|
|
|
|
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] [--enable-restore-test] [--enable-ai-rem]'
|
|
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))
|