Initial commit: QSafe Android 1.1.0 and Windows 1.0.4
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Publish a reviewed QSafe APK into the existing Argus catalog, with rollback and identity guards."""
|
||||
import argparse
|
||||
from contextlib import closing
|
||||
from datetime import datetime, timezone
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shutil
|
||||
import sqlite3
|
||||
import tempfile
|
||||
import uuid
|
||||
|
||||
ROOT = Path('/mnt/data/argus').resolve()
|
||||
SLUG, PACKAGE = 'qsafe', 'ru.kusoft.qsafe'
|
||||
|
||||
|
||||
def digest(path):
|
||||
h = hashlib.sha256()
|
||||
with path.open('rb') as f:
|
||||
for chunk in iter(lambda: f.read(1024 * 1024), b''):
|
||||
h.update(chunk)
|
||||
return h.hexdigest()
|
||||
|
||||
|
||||
def check(db):
|
||||
if db.execute('PRAGMA integrity_check').fetchall() != [('ok',)]:
|
||||
raise RuntimeError('SQLite integrity check failed')
|
||||
if db.execute('PRAGMA foreign_key_check').fetchall():
|
||||
raise RuntimeError('SQLite foreign key check failed')
|
||||
|
||||
|
||||
def publish(source, metadata):
|
||||
version, code, size, sha = (metadata[k] for k in ['version', 'versionCode', 'size', 'sha256'])
|
||||
if not re.fullmatch(r'\d+\.\d+\.\d+', version) or not isinstance(code, int) or code < 1 or not isinstance(size, int) or size < 1 or not re.fullmatch('[0-9a-f]{64}', sha):
|
||||
raise ValueError('Invalid reviewed metadata')
|
||||
if not source.is_file() or source.stat().st_size != size or digest(source) != sha:
|
||||
raise ValueError('Reviewed APK does not match source')
|
||||
with closing(sqlite3.connect(ROOT / 'argus.db', timeout=30)) as db:
|
||||
db.execute('PRAGMA foreign_keys=ON')
|
||||
check(db)
|
||||
existing = db.execute('SELECT Id,AndroidPackageName FROM Apps WHERE Slug=?', (SLUG,)).fetchone()
|
||||
if existing and existing[1] != PACKAGE:
|
||||
raise RuntimeError('Slug is owned by another package')
|
||||
if db.execute('SELECT Slug FROM Apps WHERE AndroidPackageName=? AND Slug<>?', (PACKAGE, SLUG)).fetchall():
|
||||
raise RuntimeError('Another slug owns this Android package')
|
||||
if existing:
|
||||
same = db.execute('SELECT Id,StoredRelativePath FROM Releases WHERE CatalogAppId=? AND Version=? AND AndroidVersionCode=? AND Sha256=?', (existing[0], version, code, sha)).fetchone()
|
||||
if same:
|
||||
file = (ROOT / 'Packages' / same[1]).resolve()
|
||||
file.relative_to(ROOT / 'Packages')
|
||||
if not file.is_file() or digest(file) != sha:
|
||||
raise RuntimeError('Existing publication has an invalid artifact')
|
||||
return {'alreadyPublished': True, 'slug': SLUG, 'releaseId': same[0], 'sha256': sha}
|
||||
previous = db.execute('SELECT MAX(AndroidVersionCode) FROM Releases WHERE CatalogAppId=?', (existing[0],)).fetchone()[0]
|
||||
if metadata.get('expectedPreviousVersionCode') != previous or (previous is not None and code <= previous):
|
||||
raise RuntimeError('Catalog changed or versionCode did not increase')
|
||||
elif metadata.get('expectedPreviousVersionCode') is not None:
|
||||
raise RuntimeError('Expected previous release is missing')
|
||||
stamp = datetime.now(timezone.utc)
|
||||
now = stamp.isoformat(timespec='microseconds')
|
||||
backup_dir = ROOT / 'backups' / ('qsafe-' + version + '-' + stamp.strftime('%Y%m%dT%H%M%S%fZ'))
|
||||
backup_dir.mkdir(mode=0o700)
|
||||
with closing(sqlite3.connect(backup_dir / 'argus.db')) as saved:
|
||||
db.backup(saved)
|
||||
check(saved)
|
||||
if existing:
|
||||
for relative, in db.execute('SELECT StoredRelativePath FROM Releases WHERE CatalogAppId=?', (existing[0],)):
|
||||
old = (ROOT / 'Packages' / relative).resolve()
|
||||
old.relative_to(ROOT / 'Packages')
|
||||
if old.is_file():
|
||||
dest = backup_dir / 'Packages' / relative
|
||||
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(old, dest)
|
||||
app_id = existing[0] if existing else str(uuid.uuid4()).upper()
|
||||
release_id = str(uuid.uuid4()).upper()
|
||||
package_dir = ROOT / 'Packages' / SLUG
|
||||
if package_dir.is_symlink():
|
||||
raise RuntimeError('Package directory may not be a symlink')
|
||||
package_dir.mkdir(exist_ok=True)
|
||||
package_dir.resolve().relative_to(ROOT / 'Packages')
|
||||
relative = SLUG + '/' + stamp.strftime('%Y%m%d%H%M%S') + '-' + version + '-' + release_id.replace('-', '') + '.apk'
|
||||
target = ROOT / 'Packages' / relative
|
||||
with tempfile.NamedTemporaryFile(dir=package_dir, prefix='.upload-', delete=False) as output:
|
||||
temp = Path(output.name)
|
||||
with source.open('rb') as src:
|
||||
shutil.copyfileobj(src, output)
|
||||
output.flush()
|
||||
os.fsync(output.fileno())
|
||||
temp.chmod(0o644)
|
||||
if digest(temp) != sha:
|
||||
temp.unlink()
|
||||
raise RuntimeError('Staging hash mismatch')
|
||||
temp.replace(target)
|
||||
|
||||
def unrelated():
|
||||
rows = [db.execute('SELECT * FROM Apps WHERE Id<>? ORDER BY Id', (app_id,)).fetchall(), db.execute('SELECT * FROM Releases WHERE CatalogAppId<>? ORDER BY Id', (app_id,)).fetchall()]
|
||||
return hashlib.sha256(json.dumps(rows, ensure_ascii=False).encode()).hexdigest()
|
||||
|
||||
committed = False
|
||||
try:
|
||||
db.execute('BEGIN IMMEDIATE')
|
||||
if db.execute('SELECT Id,AndroidPackageName FROM Apps WHERE Slug=?', (SLUG,)).fetchone() != existing:
|
||||
raise RuntimeError('Application catalog changed during preparation')
|
||||
before = unrelated()
|
||||
if existing:
|
||||
previous = db.execute('SELECT MAX(AndroidVersionCode) FROM Releases WHERE CatalogAppId=?', (app_id,)).fetchone()[0]
|
||||
if metadata.get('expectedPreviousVersionCode') != previous:
|
||||
raise RuntimeError('Previous release changed during preparation')
|
||||
db.execute('UPDATE Apps SET Name=?,Summary=?,Description=?,IsListed=1,UpdatedAt=? WHERE Id=?', ('QSafe', metadata['summary'], metadata['description'], now, app_id))
|
||||
else:
|
||||
db.execute('INSERT INTO Apps VALUES(?,?,?,?,?,NULL,NULL,?,1,?,?)', (app_id, SLUG, 'QSafe', metadata['summary'], metadata['description'], PACKAGE, now, now))
|
||||
db.execute('INSERT INTO Releases VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?)', (release_id, app_id, version, 'stable', 'android', 'apk', code, source.name, relative, 'application/vnd.android.package-archive', size, sha, metadata['notes'], now))
|
||||
if unrelated() != before:
|
||||
raise RuntimeError('Unrelated catalog data changed')
|
||||
if db.execute('SELECT COUNT(*) FROM Releases WHERE CatalogAppId=?', (app_id,)).fetchone()[0] != 1:
|
||||
raise RuntimeError('Expected a single retained release')
|
||||
if db.execute('PRAGMA foreign_key_check').fetchall():
|
||||
raise RuntimeError('Foreign key check failed before commit')
|
||||
db.commit()
|
||||
committed = True
|
||||
finally:
|
||||
if not committed:
|
||||
db.rollback()
|
||||
target.unlink(missing_ok=True)
|
||||
check(db)
|
||||
report = {'slug': SLUG, 'version': version, 'versionCode': code, 'size': size, 'sha256': sha, 'releaseId': release_id,
|
||||
'backup': str(backup_dir), 'integrity': 'ok', 'unrelatedCatalogRowsUnchanged': True,
|
||||
'catalogUrl': 'https://argus.kusoft.xyz/?app=qsafe',
|
||||
'manifestUrl': 'https://argus.kusoft.xyz/api/apps/qsafe/manifest?platform=android&channel=stable',
|
||||
'downloadUrl': f'https://argus.kusoft.xyz/api/apps/qsafe/releases/{release_id}/download'}
|
||||
(backup_dir / 'publication.json').write_text(json.dumps(report, ensure_ascii=False, indent=2))
|
||||
return report
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
p = argparse.ArgumentParser()
|
||||
p.add_argument('apk', type=Path)
|
||||
p.add_argument('metadata', type=Path)
|
||||
args = p.parse_args()
|
||||
print(json.dumps(publish(args.apk, json.loads(args.metadata.read_text())), ensure_ascii=False))
|
||||
Reference in New Issue
Block a user