feat(server): modularize backend and admin console

This commit is contained in:
2026-07-14 01:38:41 +00:00
parent 9a6ac1e4ed
commit 95dd32c75a
261 changed files with 39289 additions and 2156 deletions
+1
View File
@@ -0,0 +1 @@
"""Repository package placeholder for the template migration."""
@@ -0,0 +1,93 @@
import db
DEFAULT_CHANNELS = [
("stable", "正式版", 1, 10),
("preview", "预览版", 1, 20),
("dev", "开发版", 1, 30),
]
def list_apps() -> list[dict]:
conn = db.get_conn()
rows = conn.execute("SELECT app_id, app_name FROM apps").fetchall()
conn.close()
return [{"app_id": row["app_id"], "app_name": row["app_name"]} for row in rows]
def add_app_with_default_channels(app_id: str, app_name: str):
conn = db.get_conn()
try:
conn.execute("INSERT INTO apps(app_id, app_name) VALUES (?, ?)", (app_id, app_name))
conn.executemany(
"INSERT INTO channels(app_id,channel_code,display_name,enabled,sort_order) VALUES(?,?,?,?,?)",
[(app_id, code, name, enabled, order) for code, name, enabled, order in DEFAULT_CHANNELS],
)
conn.commit()
except Exception:
conn.rollback()
raise
finally:
conn.close()
def app_exists(app_id: str) -> bool:
conn = db.get_conn()
row = conn.execute("SELECT 1 FROM apps WHERE app_id=?", (app_id,)).fetchone()
conn.close()
return bool(row)
def get_app(app_id: str):
conn = db.get_conn()
row = conn.execute("SELECT app_id, app_name FROM apps WHERE app_id=?", (app_id,)).fetchone()
conn.close()
return row
def list_channels(app_id: str, include_disabled: bool = True) -> list[dict]:
conn = db.get_conn()
sql = (
"SELECT channel_code,display_name,enabled,sort_order FROM channels WHERE app_id=?"
+ ("" if include_disabled else " AND enabled=1")
+ " ORDER BY sort_order,channel_code"
)
rows = conn.execute(sql, (app_id,)).fetchall()
conn.close()
return [
{
"channel_code": row["channel_code"],
"display_name": row["display_name"],
"enabled": bool(row["enabled"]),
"sort_order": row["sort_order"],
}
for row in rows
]
def get_channel(app_id: str, channel: str):
conn = db.get_conn()
row = conn.execute("SELECT * FROM channels WHERE app_id=? AND channel_code=?", (app_id, channel)).fetchone()
conn.close()
return row
def save_channel(app_id: str, code: str, name: str, enabled: bool, sort_order: int):
conn = db.get_conn()
try:
conn.execute(
"""INSERT INTO channels(app_id,channel_code,display_name,enabled,sort_order)
VALUES(?,?,?,?,?)
ON CONFLICT(app_id,channel_code) DO UPDATE SET
display_name=excluded.display_name,
enabled=excluded.enabled,
sort_order=excluded.sort_order,
updated_at=datetime('now')""",
(app_id, code, name, int(enabled), sort_order),
)
conn.commit()
except Exception:
conn.rollback()
raise
finally:
conn.close()
@@ -0,0 +1,168 @@
from datetime import datetime, timezone, timedelta
import secrets
import db
def get_device(device_id: str):
conn = db.get_conn()
row = conn.execute("SELECT * FROM devices WHERE device_id=?", (device_id,)).fetchone()
conn.close()
return row
def get_license(license_id: str):
conn = db.get_conn()
row = conn.execute("SELECT * FROM licenses WHERE license_id=?", (license_id,)).fetchone()
conn.close()
return row
def touch_device(device_id: str):
conn = db.get_conn()
conn.execute("UPDATE devices SET last_seen_at=datetime('now') WHERE device_id=?", (device_id,))
conn.commit()
conn.close()
def issue_device(app_id: str, channel: str, license_key_hash: str, installation_id: str, machine_hash: str, ip: str):
now = datetime.now(timezone.utc)
conn = db.get_conn()
conn.execute("BEGIN IMMEDIATE")
try:
channel_row = conn.execute("SELECT * FROM channels WHERE app_id=? AND channel_code=?", (app_id, channel)).fetchone()
if not channel_row:
conn.rollback()
return {"status": "channel_not_found"}
if not channel_row["enabled"]:
conn.rollback()
return {"status": "channel_disabled"}
license_row = conn.execute("SELECT * FROM licenses WHERE license_key_hash=?", (license_key_hash,)).fetchone()
try:
expiry = datetime.fromisoformat((license_row["valid_until"] if license_row else "").replace("Z", "+00:00"))
except ValueError:
expiry = now - timedelta(seconds=1)
if (
not license_row
or license_row["status"] != "active"
or expiry <= now
or license_row["app_id"] != app_id
or license_row["channel_code"] != channel
):
conn.rollback()
return {"status": "license_invalid"}
device_row = conn.execute("SELECT * FROM devices WHERE app_id=? AND installation_id=?", (app_id, installation_id)).fetchone()
if device_row and device_row["disabled"]:
conn.rollback()
return {"status": "device_disabled", "reason": device_row["disabled_reason"] or "设备已被禁用"}
if device_row and device_row["license_id"] not in ("", license_row["license_id"]):
conn.rollback()
return {"status": "license_conflict"}
device_id = device_row["device_id"] if device_row else "dev_" + secrets.token_hex(16)
seq = int(device_row["credential_seq"]) if device_row else 1
bound = conn.execute("SELECT 1 FROM license_devices WHERE license_id=? AND device_id=?", (license_row["license_id"], device_id)).fetchone()
used = conn.execute("SELECT COUNT(*) FROM license_devices WHERE license_id=?", (license_row["license_id"],)).fetchone()[0]
if not bound and used >= int(license_row["max_devices"]):
conn.rollback()
return {"status": "license_device_limit"}
if device_row:
conn.execute(
"UPDATE devices SET machine_hash=?,license_id=?,channel=?,last_seen_at=datetime('now'),last_ip=? WHERE device_id=?",
(machine_hash, license_row["license_id"], channel, ip, device_id),
)
else:
conn.execute(
"INSERT INTO devices(device_id,app_id,installation_id,machine_hash,credential_seq,last_ip,license_id,channel) VALUES(?,?,?,?,?,?,?,?)",
(device_id, app_id, installation_id, machine_hash, seq, ip, license_row["license_id"], channel),
)
conn.execute("INSERT OR IGNORE INTO license_devices(license_id,device_id) VALUES(?,?)", (license_row["license_id"], device_id))
conn.commit()
return {"status": "ok", "device_id": device_id, "license": license_row, "credential_seq": seq, "issued_at": now}
except Exception:
conn.rollback()
raise
finally:
conn.close()
def get_update_context(app_id: str, channel: str):
conn = db.get_conn()
try:
channel_row = conn.execute("SELECT * FROM channels WHERE app_id=? AND channel_code=?", (app_id, channel)).fetchone()
if not channel_row:
return {"status": "channel_not_found"}
if not channel_row["enabled"]:
return {"status": "channel_disabled"}
version = conn.execute(
"SELECT * FROM versions WHERE app_id=? AND channel=? AND latest=1",
(app_id, channel),
).fetchone()
policy = conn.execute(
"SELECT * FROM version_policies WHERE app_id=? AND channel=?",
(app_id, channel),
).fetchone()
return {"status": "ok", "version": version, "policy": policy}
finally:
conn.close()
def get_download_files(version_id: int, app_id: str, channel: str, version: str, identity: dict, ip: str, user_agent: str):
conn = db.get_conn()
version_row = conn.execute("SELECT app_id,channel,version FROM versions WHERE id=?", (version_id,)).fetchone()
if not version_row or version_row["app_id"] != app_id or version_row["channel"] != channel or version_row["version"] != version:
conn.close()
return {"status": "not_found"}
rows = conn.execute("SELECT path, sha256, size FROM version_files WHERE version_id=?", (version_id,)).fetchall()
conn.executemany(
"""INSERT INTO download_logs(device_id,license_id,app_id,version,channel_code,file_path,file_size,result,ip,user_agent)
VALUES(?,?,?,?,?,?,?,?,?,?)""",
[
(identity["device_id"], identity["license_id"], app_id, version, channel, row["path"], row["size"], "authorized", ip, user_agent)
for row in rows
],
)
conn.commit()
conn.close()
return {"status": "ok", "files": rows}
def insert_download_report_logs(values: list[tuple]) -> int:
conn = db.get_conn()
if values:
conn.executemany(
"INSERT INTO download_logs(device_id,license_id,app_id,version,channel_code,file_path,file_size,result,ip,user_agent) VALUES(?,?,?,?,?,?,?,?,?,?)",
values,
)
conn.commit()
conn.close()
return len(values)
def get_manifest_version_files(version_id: int):
conn = db.get_conn()
version = conn.execute(
"SELECT app_id, channel, version, create_time FROM versions WHERE id=?",
(version_id,),
).fetchone()
if not version:
conn.close()
return None, []
rows = conn.execute("SELECT path, sha256, size FROM version_files WHERE version_id=?", (version_id,)).fetchall()
conn.close()
return version, rows
def insert_upgrade_log(device_id: str, from_version: str, to_version: str, result: str):
conn = db.get_conn()
conn.execute(
"""
INSERT INTO upgrade_logs(device_id,from_ver,to_ver,result,create_time)
VALUES (?,?,?,?,datetime('now'))
""",
(device_id, from_version, to_version, result),
)
conn.commit()
conn.close()
+133
View File
@@ -0,0 +1,133 @@
from datetime import datetime, timezone
import secrets
import db
def list_crash_reports(limit: int = 300) -> list[dict]:
conn = db.get_conn()
rows = conn.execute(
"""
SELECT
id,report_id,client_report_id,status,symbolication_status,product,app_version,
git_commit,build_type,channel,exception_code,crash_time_utc,received_at_utc,
remote_address,content_length,metadata_size,minidump_size,attachments_size
FROM crash_reports
ORDER BY id DESC
LIMIT ?
""",
(limit,),
).fetchall()
conn.close()
return [dict(row) for row in rows]
def get_crash_report(report_id: str):
conn = db.get_conn()
row = conn.execute("SELECT * FROM crash_reports WHERE report_id=?", (report_id,)).fetchone()
conn.close()
return row
def log_file_access(report_id: str, file_name: str, actor_hash: str, ip: str, user_agent: str):
conn = db.get_conn()
conn.execute(
"INSERT INTO crash_file_access_logs(report_id,file_name,actor_hash,ip,user_agent) VALUES(?,?,?,?,?)",
(report_id, file_name, actor_hash, ip, user_agent),
)
conn.commit()
conn.close()
def generate_prefixed_id(conn, table: str, column: str, prefix: str) -> str | None:
day = datetime.now(timezone.utc).strftime("%Y%m%d")
for _ in range(20):
value = f"{prefix}_{day}_{secrets.token_hex(4)}"
if not conn.execute(f"SELECT 1 FROM {table} WHERE {column}=?", (value,)).fetchone():
return value
return None
def get_report_by_client_report_id(conn, client_report_id: str):
return conn.execute("SELECT * FROM crash_reports WHERE client_report_id=?", (client_report_id,)).fetchone()
def get_report_by_id(conn, report_id: str):
return conn.execute("SELECT * FROM crash_reports WHERE report_id=?", (report_id,)).fetchone()
def insert_crash_report(conn, values: dict):
conn.execute(
"""
INSERT INTO crash_reports(
report_id,client_report_id,content_hash,status,symbolication_status,product,app_version,
git_commit,build_type,channel,exception_code,crash_time_utc,received_at_utc,remote_address,
content_length,metadata_sha256,metadata_size,minidump_sha256,minidump_size,attachments_sha256,
attachments_size,storage_path,metadata_json,server_json
) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
""",
(
values["report_id"],
values["client_report_id"],
values["content_hash"],
values["status"],
values["symbolication_status"],
values["product"],
values["app_version"],
values["git_commit"],
values["build_type"],
values["channel"],
values["exception_code"],
values["crash_time_utc"],
values["received_at_utc"],
values["remote_address"],
values["content_length"],
values["metadata_sha256"],
values["metadata_size"],
values["minidump_sha256"],
values["minidump_size"],
values["attachments_sha256"],
values["attachments_size"],
values["storage_path"],
values["metadata_json"],
values["server_json"],
),
)
def get_symbol_upload_by_identity_hash(conn, identity_hash: str):
return conn.execute("SELECT * FROM crash_symbol_uploads WHERE identity_hash=?", (identity_hash,)).fetchone()
def get_symbol_upload_by_id(conn, symbol_upload_id: str):
return conn.execute("SELECT * FROM crash_symbol_uploads WHERE symbol_upload_id=?", (symbol_upload_id,)).fetchone()
def insert_symbol_upload(conn, values: dict):
conn.execute(
"""
INSERT INTO crash_symbol_uploads(
symbol_upload_id,identity_hash,status,product,app_version,git_commit,build_type,platform,toolchain,
created_at_utc,received_at_utc,metadata_sha256,metadata_size,symbols_sha256,symbols_size,storage_path,metadata_json
) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
""",
(
values["symbol_upload_id"],
values["identity_hash"],
values["status"],
values["product"],
values["app_version"],
values["git_commit"],
values["build_type"],
values["platform"],
values["toolchain"],
values["created_at_utc"],
values["received_at_utc"],
values["metadata_sha256"],
values["metadata_size"],
values["symbols_sha256"],
values["symbols_size"],
values["storage_path"],
values["metadata_json"],
),
)
+36
View File
@@ -0,0 +1,36 @@
import db
def list_devices(app_id: str = "") -> list[dict]:
conn = db.get_conn()
if app_id:
rows = conn.execute("SELECT * FROM devices WHERE app_id=? ORDER BY last_seen_at DESC", (app_id,)).fetchall()
else:
rows = conn.execute("SELECT * FROM devices ORDER BY last_seen_at DESC LIMIT 500").fetchall()
conn.close()
return [
{
"device_id": row["device_id"],
"app_id": row["app_id"],
"installation_id": row["installation_id"],
"disabled": bool(row["disabled"]),
"disabled_reason": row["disabled_reason"],
"credential_seq": row["credential_seq"],
"first_seen_at": row["first_seen_at"],
"last_seen_at": row["last_seen_at"],
"last_ip": row["last_ip"],
}
for row in rows
]
def set_device_disabled(device_id: str, disabled: bool, reason: str) -> int:
conn = db.get_conn()
cur = conn.execute(
"UPDATE devices SET disabled=?,disabled_reason=? WHERE device_id=?",
(int(disabled), reason if disabled else "", device_id),
)
conn.commit()
updated = cur.rowcount
conn.close()
return updated
+75
View File
@@ -0,0 +1,75 @@
import db
def insert_license(
license_id: str,
license_key_hash: str,
license_key_cipher: str,
customer_name: str,
app_id: str,
channel: str,
max_devices: int,
valid_until: str,
):
conn = db.get_conn()
try:
channel_row = conn.execute("SELECT * FROM channels WHERE app_id=? AND channel_code=?", (app_id, channel)).fetchone()
if not channel_row:
return "channel_not_found"
if not channel_row["enabled"]:
return "channel_disabled"
conn.execute(
"""INSERT INTO licenses
(license_id,license_key_hash,license_key_cipher,customer_name,app_id,channel_code,max_devices,valid_until,status)
VALUES(?,?,?,?,?,?,?,?,'active')""",
(license_id, license_key_hash, license_key_cipher, customer_name, app_id, channel, max_devices, valid_until),
)
conn.commit()
return "ok"
except Exception:
conn.rollback()
raise
finally:
conn.close()
def list_licenses(app_id: str = "", include_deleted: bool = False) -> list:
deleted_filter = "" if include_deleted else " AND status<>'deleted'"
conn = db.get_conn()
if app_id:
rows = conn.execute(
f"""SELECT l.*,(SELECT COUNT(*) FROM license_devices d WHERE d.license_id=l.license_id) used_devices
FROM licenses l WHERE app_id=?{deleted_filter} ORDER BY created_at DESC""",
(app_id,),
).fetchall()
else:
rows = conn.execute(
f"""SELECT l.*,(SELECT COUNT(*) FROM license_devices d WHERE d.license_id=l.license_id) used_devices
FROM licenses l WHERE 1=1{deleted_filter} ORDER BY created_at DESC"""
).fetchall()
conn.close()
return rows
def set_license_status(license_id: str, status: str) -> int:
conn = db.get_conn()
cur = conn.execute(
"UPDATE licenses SET status=?,updated_at=datetime('now') WHERE license_id=? AND status<>'deleted'",
(status, license_id),
)
conn.commit()
updated = cur.rowcount
conn.close()
return updated
def soft_delete_license(license_id: str) -> int:
conn = db.get_conn()
cur = conn.execute(
"UPDATE licenses SET status='deleted',updated_at=datetime('now') WHERE license_id=? AND status<>'deleted'",
(license_id,),
)
conn.commit()
updated = cur.rowcount
conn.close()
return updated
+99
View File
@@ -0,0 +1,99 @@
import db
def list_upgrade_logs(limit: int = 200) -> list[dict]:
conn = db.get_conn()
rows = conn.execute(
"""
SELECT id,device_id,from_ver,to_ver,result,create_time
FROM upgrade_logs ORDER BY create_time DESC LIMIT ?
""",
(limit,),
).fetchall()
conn.close()
return [
{
"id": row["id"],
"device_id": row["device_id"],
"app_id": "",
"from_version": row["from_ver"],
"to_version": row["to_ver"],
"result": row["result"],
"create_time": row["create_time"],
}
for row in rows
]
def delete_upgrade_log(log_id: int) -> int:
conn = db.get_conn()
cur = conn.execute("DELETE FROM upgrade_logs WHERE id=?", (log_id,))
conn.commit()
deleted = cur.rowcount
conn.close()
return deleted
def clear_upgrade_logs() -> int:
conn = db.get_conn()
cur = conn.execute("DELETE FROM upgrade_logs")
conn.commit()
deleted = cur.rowcount
conn.close()
return deleted
def list_download_logs(app_id: str = "", limit: int = 300) -> list[dict]:
conn = db.get_conn()
if app_id:
rows = conn.execute("SELECT * FROM download_logs WHERE app_id=? ORDER BY id DESC LIMIT ?", (app_id, limit)).fetchall()
else:
rows = conn.execute("SELECT * FROM download_logs ORDER BY id DESC LIMIT ?", (limit,)).fetchall()
conn.close()
return [dict(row) for row in rows]
def delete_download_log(log_id: int) -> int:
conn = db.get_conn()
cur = conn.execute("DELETE FROM download_logs WHERE id=?", (log_id,))
conn.commit()
deleted = cur.rowcount
conn.close()
return deleted
def clear_download_logs(app_id: str = "") -> int:
conn = db.get_conn()
if app_id:
cur = conn.execute("DELETE FROM download_logs WHERE app_id=?", (app_id,))
else:
cur = conn.execute("DELETE FROM download_logs")
conn.commit()
deleted = cur.rowcount
conn.close()
return deleted
def list_audit_logs(limit: int = 300) -> list[dict]:
conn = db.get_conn()
rows = conn.execute("SELECT * FROM admin_audit_logs ORDER BY id DESC LIMIT ?", (limit,)).fetchall()
conn.close()
return [dict(row) for row in rows]
def delete_audit_log(log_id: int) -> int:
conn = db.get_conn()
cur = conn.execute("DELETE FROM admin_audit_logs WHERE id=?", (log_id,))
conn.commit()
deleted = cur.rowcount
conn.close()
return deleted
def clear_audit_logs() -> int:
conn = db.get_conn()
cur = conn.execute("DELETE FROM admin_audit_logs")
conn.commit()
deleted = cur.rowcount
conn.close()
return deleted
+62
View File
@@ -0,0 +1,62 @@
import db
def get_policy(app_id: str, channel: str):
conn = db.get_conn()
row = conn.execute("SELECT * FROM version_policies WHERE app_id=? AND channel=?", (app_id, channel)).fetchone()
conn.close()
return row
def save_policy(
app_id: str,
channel: str,
force_update: bool,
allow_rollback: bool,
offline_allowed: bool,
valid_until: str,
min_supported_version: str,
disabled_versions_json: str,
message: str,
) -> tuple[str, int | None]:
conn = db.get_conn()
try:
channel_row = conn.execute("SELECT * FROM channels WHERE app_id=? AND channel_code=?", (app_id, channel)).fetchone()
if not channel_row:
return "channel_not_found", None
if not channel_row["enabled"]:
return "channel_disabled", None
row = conn.execute("SELECT policy_seq FROM version_policies WHERE app_id=? AND channel=?", (app_id, channel)).fetchone()
next_seq = (int(row["policy_seq"]) + 1) if row else 1
values = (
next_seq,
int(bool(force_update)),
int(bool(allow_rollback)),
int(bool(offline_allowed)),
valid_until,
min_supported_version,
disabled_versions_json,
message,
app_id,
channel,
)
conn.execute(
"""
INSERT INTO version_policies(policy_seq,force_update,allow_rollback,offline_allowed,valid_until,
min_supported_version,disabled_versions,message,app_id,channel)
VALUES(?,?,?,?,?,?,?,?,?,?)
ON CONFLICT(app_id,channel) DO UPDATE SET policy_seq=excluded.policy_seq,
force_update=excluded.force_update,allow_rollback=excluded.allow_rollback,
offline_allowed=excluded.offline_allowed,valid_until=excluded.valid_until,
min_supported_version=excluded.min_supported_version,disabled_versions=excluded.disabled_versions,
message=excluded.message,updated_at=datetime('now')
""",
values,
)
conn.commit()
return "ok", next_seq
except Exception:
conn.rollback()
raise
finally:
conn.close()
+24
View File
@@ -0,0 +1,24 @@
import sqlite3
def version_exists(conn: sqlite3.Connection, app_id: str, channel: str, version: str) -> bool:
return conn.execute(
"SELECT 1 FROM versions WHERE app_id=? AND channel=? AND version=?",
(app_id, channel, version),
).fetchone() is not None
def create_version(conn: sqlite3.Connection, app_id: str, channel: str, version: str, client_protocol: int) -> int:
conn.execute("UPDATE versions SET latest=0 WHERE app_id=? AND channel=?", (app_id, channel))
cur = conn.execute(
"INSERT INTO versions(app_id, channel, version, latest, client_protocol) VALUES (?,?,?,1,?)",
(app_id, channel, version, client_protocol),
)
return int(cur.lastrowid)
def insert_version_file(conn: sqlite3.Connection, version_id: int, path: str, sha256: str, size: int):
conn.execute(
"INSERT INTO version_files(version_id, path, sha256, size) VALUES (?,?,?,?)",
(version_id, path, sha256, size),
)
+91
View File
@@ -0,0 +1,91 @@
import db
def get_version_with_files(version_id: int):
conn = db.get_conn()
version = conn.execute("SELECT app_id,channel,version,create_time FROM versions WHERE id=?", (version_id,)).fetchone()
files = conn.execute("SELECT path,sha256,size FROM version_files WHERE version_id=? ORDER BY id", (version_id,)).fetchall()
conn.close()
return version, files
def list_versions(app_id: str) -> list[dict]:
conn = db.get_conn()
rows = conn.execute(
"""
SELECT id, version, channel, latest, client_protocol, create_time
FROM versions WHERE app_id=? ORDER BY create_time DESC
""",
(app_id,),
).fetchall()
conn.close()
return [
{
"id": row["id"],
"version": row["version"],
"channel": row["channel"],
"latest": bool(row["latest"]),
"client_protocol": int(row["client_protocol"] or 1),
"create_time": row["create_time"],
}
for row in rows
]
def set_version_protocol(version_id: int, client_protocol: int) -> int:
conn = db.get_conn()
cur = conn.execute("UPDATE versions SET client_protocol=? WHERE id=?", (client_protocol, version_id))
conn.commit()
updated = cur.rowcount
conn.close()
return updated
def set_latest_version(version_id: int) -> str:
conn = db.get_conn()
try:
cur = conn.cursor()
version_info = cur.execute("SELECT app_id, channel FROM versions WHERE id=?", (version_id,)).fetchone()
if not version_info:
return "not_found"
app_id, channel = version_info["app_id"], version_info["channel"]
cur.execute("UPDATE versions SET latest=0 WHERE app_id=? AND channel=?", (app_id, channel))
cur.execute("UPDATE versions SET latest=1 WHERE id=?", (version_id,))
conn.commit()
return "ok"
except Exception:
conn.rollback()
raise
finally:
conn.close()
def get_version_for_delete(version_id: int):
conn = db.get_conn()
row = conn.execute("SELECT app_id, channel, version, latest FROM versions WHERE id=?", (version_id,)).fetchone()
conn.close()
return row
def delete_version_and_promote(version_id: int, app_id: str, channel: str, was_latest: bool) -> str | None:
conn = db.get_conn()
try:
cur = conn.cursor()
cur.execute("DELETE FROM version_files WHERE version_id=?", (version_id,))
cur.execute("DELETE FROM versions WHERE id=?", (version_id,))
promoted_version = None
if was_latest:
replacement = cur.execute(
"SELECT id, version FROM versions WHERE app_id=? AND channel=? ORDER BY create_time DESC, id DESC LIMIT 1",
(app_id, channel),
).fetchone()
if replacement:
cur.execute("UPDATE versions SET latest=1 WHERE id=?", (replacement["id"],))
promoted_version = replacement["version"]
conn.commit()
return promoted_version
except Exception:
conn.rollback()
raise
finally:
conn.close()