Add installer/script delivery metadata to the core software model

AppVersion gains sha256 and silentargs so the endpoint update_app push can pin exact installer bytes and carry per-version silent switches; Application gains installersha256 for the app-level install source. New ShopDB-hosted script library (Script model plus scripts API) backs run_script delivery, with the body fetched by digest on push rather than pre-staged or sent over the command wire. Two idempotent migrations with real downgrades. Script added to the shopdb.api contract surface for plugin use.
This commit is contained in:
benjamin.baker
2026-08-30 14:39:58 -04:00
parent dc2f4266bc
commit 0c2b816a46
12 changed files with 483 additions and 3 deletions

View File

@@ -1,6 +1,7 @@
"""Applications API endpoints."""
import glob
import hashlib
import os
from flask import Blueprint, request, current_app
@@ -47,6 +48,20 @@ def _apppackage_dir():
return os.path.join(current_app.instance_path, 'applicationpackages')
def _sha256_of(stream):
"""Hex SHA-256 of an upload stream, read in chunks, cursor left rewound.
The agent pins a version to exact bytes; computing the digest here means
the server is the one source of truth for what those bytes are.
"""
digest = hashlib.sha256()
stream.seek(0)
for chunk in iter(lambda: stream.read(1048576), b''):
digest.update(chunk)
stream.seek(0)
return digest.hexdigest()
def _replace_upload(directory, stem, ext, upload):
"""Save one file per application, replacing any prior extension.
@@ -322,7 +337,9 @@ def create_version(app_id: int):
appid=app_id,
version=data['version'],
releasedate=data.get('releasedate'),
notes=data.get('notes')
notes=data.get('notes'),
sha256=data.get('sha256'),
silentargs=data.get('silentargs')
)
db.session.add(version)
@@ -604,16 +621,37 @@ def upload_application_package(app_id: int):
filename = _replace_upload(_apppackage_dir(), f'application-{app_id}', ext, upload)
app.installpath = '{}{}'.format(APP_PACKAGE_URL_PREFIX, filename)
# The endpoint installer push pins bytes by digest; compute it once here so
# the server, not the agent, decides what the correct payload is. Stamp it
# on the app itself so an update_app push can resolve an install source by
# app alone (no AppVersion row). If the caller also names a version, stamp
# it there too; otherwise return it so the version-edit flow can persist it.
sha256 = _sha256_of(upload.stream)
app.installersha256 = sha256
stamped_version = None
appversionid = request.form.get('appversionid', type=int)
if appversionid:
stamped_version = db.session.get(AppVersion, appversionid)
if not stamped_version or stamped_version.appid != app_id:
return error_response(
ErrorCodes.VALIDATION_ERROR,
'appversionid {} does not belong to this application'.format(appversionid))
stamped_version.sha256 = sha256
db.session.commit()
AuditLog.log('updated', 'Application', entityid=app_id,
entityname=app.appname,
changes={'installpath': {'new': app.installpath},
'uploadedfilename': {'new': upload.filename},
'sha256': {'new': sha256},
'bytes': {'new': size}})
db.session.commit()
return success_response(
{**app.to_dict(), 'uploadedfilename': upload.filename, 'bytes': size},
{**app.to_dict(), 'uploadedfilename': upload.filename, 'bytes': size,
'sha256': sha256,
'appversionid': stamped_version.appversionid if stamped_version else None},
message='Installer uploaded')