Files
shopdb-flask/migrations/versions/7d45_purge_inactive_apps.py
benjamin.baker 0134eaa219 Hard delete applications, nest installer packages, detect PE versions
Soft delete left orphaned executables on disk and let a deleted name collide
with a fresh import, so delete is now a hard delete that removes the
associated files. The destructive paths (delete, revert-to-record) require a
typed OK because neither can be undone.

Installers keep their filename at import and nest per application/version,
which stops repeat imports doubling up. Version now comes from the same PE
detection the endpoints agent uses; when no version can be read the caller is
asked for one rather than silently storing a blank.

Applications also surface Version and a monitorable toggle, with launch path
and process name required only while monitorable is ticked. Fleet shows the
PC alias, matching PC Details.
2026-09-07 15:04:21 -04:00

87 lines
2.8 KiB
Python

"""Purge soft-deleted applications that still reserve their names.
Delete used to set isactive=False and leave the row in place. The uniqueness
check on create looks at every row regardless of isactive, so each of those
rows kept its name reserved permanently: creating an application with the same
name failed with "already exists" against a row no screen would display, and
nothing in the UI could clear it.
Delete is a hard delete now, but that only fixes rows deleted from here on.
This clears the ones already stranded, in every environment rather than just
the one where the bug was noticed.
Dependent rows are cleared the same way the API does it, and for the same
reasons: appversions and computerinstalledapps are NOT NULL and meaningless
without their application, while knowledgebase.appid is nullable and the
article is somebody's writing, so it is unlinked rather than destroyed.
Uploaded installers belonging to these rows are deliberately NOT touched. A
migration that deletes files cannot be rolled back and cannot be dry-run, and a
stray file is harmless next to that risk. Deletes made through the API do clean
them up.
Revision ID: 7d45_purge_inactive_apps
Revises: 7d44_app_monitorable
"""
from alembic import op
import sqlalchemy as sa
revision = '7d45_purge_inactive_apps'
down_revision = '7d44_app_monitorable'
branch_labels = None
depends_on = None
def _table_exists(bind, name):
"""True when a table is present.
The plugin tables are only there when the plugin is installed, so each is
checked before being touched.
"""
return sa.inspect(bind).has_table(name)
def upgrade():
bind = op.get_bind()
if not _table_exists(bind, 'applications'):
return
doomed = [row[0] for row in bind.execute(sa.text(
'SELECT appid FROM applications WHERE isactive = false'
))]
if not doomed:
return
for appid in doomed:
params = {'appid': appid}
if _table_exists(bind, 'knowledgebase'):
bind.execute(sa.text(
'UPDATE knowledgebase SET appid = NULL WHERE appid = :appid'
), params)
if _table_exists(bind, 'computerinstalledapps'):
bind.execute(sa.text(
'DELETE FROM computerinstalledapps WHERE appid = :appid'
), params)
if _table_exists(bind, 'appversions'):
bind.execute(sa.text(
'DELETE FROM appversions WHERE appid = :appid'
), params)
bind.execute(sa.text(
'DELETE FROM applications WHERE appid = :appid'
), params)
def downgrade():
# The rows are gone and their contents were not captured, so there is
# nothing to restore. Failing loudly is better than pretending otherwise.
raise NotImplementedError(
'Purged applications cannot be restored; recover from a backup.')