mirror of
https://github.com/wahyd4/home-docker.git
synced 2026-08-09 04:15:52 +10:00
ALTER TABLE ADD COLUMN fails silently if column already exists (rather than crashing on startup).
189 lines
6.1 KiB
Python
189 lines
6.1 KiB
Python
import os
|
|
import json
|
|
import sqlite3
|
|
import threading
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
|
|
DB_PATH = Path(os.environ.get("DEMO_DB_PATH", "/data/demos/.meta/sites.db"))
|
|
_lock = threading.Lock()
|
|
_conn: Optional[sqlite3.Connection] = None
|
|
|
|
VALID_VISIBILITY = frozenset({"disabled", "internal", "public"})
|
|
|
|
|
|
def get_conn() -> sqlite3.Connection:
|
|
global _conn
|
|
if _conn is None:
|
|
DB_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
_conn = sqlite3.connect(str(DB_PATH), check_same_thread=False)
|
|
_conn.row_factory = sqlite3.Row
|
|
_init_schema(_conn)
|
|
return _conn
|
|
|
|
|
|
def _init_schema(conn: sqlite3.Connection):
|
|
conn.execute("""
|
|
CREATE TABLE IF NOT EXISTS sites (
|
|
name TEXT PRIMARY KEY,
|
|
enabled INTEGER NOT NULL DEFAULT 1,
|
|
published INTEGER NOT NULL DEFAULT 0,
|
|
visibility TEXT NOT NULL DEFAULT 'internal',
|
|
description TEXT NOT NULL DEFAULT '',
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL
|
|
)
|
|
""")
|
|
# Migration: add visibility column to existing tables (from v1 schema)
|
|
try:
|
|
conn.execute("ALTER TABLE sites ADD COLUMN visibility TEXT NOT NULL DEFAULT 'internal'")
|
|
except sqlite3.OperationalError:
|
|
pass # column already exists
|
|
# Migrate old enabled/published data to visibility
|
|
conn.execute("""
|
|
UPDATE sites SET visibility = 'disabled' WHERE enabled = 0 AND visibility = 'internal'
|
|
""")
|
|
conn.execute("""
|
|
UPDATE sites SET visibility = 'public' WHERE enabled = 1 AND published = 1 AND visibility = 'internal'
|
|
""")
|
|
# Future-proof: ensure no invalid visibility values
|
|
conn.execute("""
|
|
UPDATE sites SET visibility = 'internal' WHERE visibility NOT IN ('disabled', 'internal', 'public')
|
|
""")
|
|
conn.commit()
|
|
|
|
|
|
# ─── Converters ───────────────────────────
|
|
|
|
|
|
def _visibility_from_old(enabled: bool, published: bool) -> str:
|
|
"""Convert old enabled+published to new visibility."""
|
|
if not enabled:
|
|
return "disabled"
|
|
if published:
|
|
return "public"
|
|
return "internal"
|
|
|
|
|
|
def _row_to_dict(r) -> dict:
|
|
"""Convert a SQLite Row to dict with both old and new fields."""
|
|
return {
|
|
"name": r["name"],
|
|
"enabled": r["visibility"] != "disabled",
|
|
"published": r["visibility"] == "public",
|
|
"visibility": r["visibility"],
|
|
"description": r["description"],
|
|
"created_at": r["created_at"],
|
|
"updated_at": r["updated_at"],
|
|
}
|
|
|
|
|
|
# ─── Migration ────────────────────────────
|
|
|
|
|
|
def migrate_from_json():
|
|
"""One-time migration from old sites.json to SQLite DB."""
|
|
json_path = DB_PATH.parent / "sites.json"
|
|
if not json_path.exists():
|
|
return False
|
|
conn = get_conn()
|
|
cur = conn.execute("SELECT COUNT(*) FROM sites")
|
|
if cur.fetchone()[0] > 0:
|
|
# Already migrated
|
|
return False
|
|
try:
|
|
data = json.loads(json_path.read_text())
|
|
for name, info in data.get("sites", {}).items():
|
|
published = info.get("published", True)
|
|
enabled = info.get("enabled", True)
|
|
visibility = _visibility_from_old(enabled, published)
|
|
conn.execute(
|
|
"""INSERT OR REPLACE INTO sites
|
|
(name, enabled, published, visibility, description, created_at, updated_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)""",
|
|
(name, 1 if enabled else 0, 1 if published else 0, visibility,
|
|
info.get("description", ""),
|
|
info.get("created_at", ""),
|
|
info.get("updated_at", "")),
|
|
)
|
|
conn.commit()
|
|
json_path.rename(json_path.with_suffix(".json.migrated"))
|
|
return True
|
|
except Exception as e:
|
|
print(f"Migration failed: {e}")
|
|
return False
|
|
|
|
|
|
# ─── CRUD ────────────────────────────────
|
|
|
|
|
|
def load_all() -> dict:
|
|
"""Return sites as {name: info_dict} for backward compat."""
|
|
conn = get_conn()
|
|
rows = conn.execute(
|
|
"SELECT name, visibility, description, created_at, updated_at FROM sites"
|
|
).fetchall()
|
|
sites = {}
|
|
for r in rows:
|
|
sites[r["name"]] = _row_to_dict(r)
|
|
return {"sites": sites}
|
|
|
|
|
|
def get(name: str) -> Optional[dict]:
|
|
conn = get_conn()
|
|
r = conn.execute(
|
|
"SELECT name, visibility, description, created_at, updated_at FROM sites WHERE name = ?",
|
|
(name,),
|
|
).fetchone()
|
|
if not r:
|
|
return None
|
|
return _row_to_dict(r)
|
|
|
|
|
|
def insert(name: str, visibility: str, description: str, now: str):
|
|
if visibility not in VALID_VISIBILITY:
|
|
raise ValueError(f"Invalid visibility: {visibility}")
|
|
enabled = 1 if visibility != "disabled" else 0
|
|
published = 1 if visibility == "public" else 0
|
|
with _lock:
|
|
conn = get_conn()
|
|
conn.execute(
|
|
"""INSERT INTO sites (name, enabled, published, visibility, description, created_at, updated_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)""",
|
|
(name, enabled, published, visibility, description, now, now),
|
|
)
|
|
conn.commit()
|
|
|
|
|
|
def update(name: str, **kwargs):
|
|
if not kwargs:
|
|
return
|
|
# Handle visibility specially — sync enabled/published
|
|
if "visibility" in kwargs:
|
|
v = kwargs["visibility"]
|
|
if v not in VALID_VISIBILITY:
|
|
raise ValueError(f"Invalid visibility: {v}")
|
|
kwargs["enabled"] = 1 if v != "disabled" else 0
|
|
kwargs["published"] = 1 if v == "public" else 0
|
|
cols = []
|
|
vals = []
|
|
for k, v in kwargs.items():
|
|
cols.append(f"{k} = ?")
|
|
vals.append(v)
|
|
vals.append(name)
|
|
with _lock:
|
|
conn = get_conn()
|
|
conn.execute(f"UPDATE sites SET {', '.join(cols)} WHERE name = ?", vals)
|
|
conn.commit()
|
|
|
|
|
|
def delete(name: str):
|
|
with _lock:
|
|
conn = get_conn()
|
|
conn.execute("DELETE FROM sites WHERE name = ?", (name,))
|
|
conn.commit()
|
|
|
|
|
|
# Run migration on import
|
|
migrate_from_json()
|