feat(server): modularize backend and admin console
This commit is contained in:
@@ -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()
|
||||
@@ -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"],
|
||||
),
|
||||
)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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),
|
||||
)
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user