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:
@@ -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')
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user