my-homelab-configs/infra/heimdall/seed-heimdall.py

177 lines
5.2 KiB
Python

#!/usr/bin/env python3
import json
import os
import sqlite3
import time
from datetime import datetime, timezone
DB_CANDIDATES = (
"/config/www/app.sqlite",
"/config/app.sqlite",
)
LINKS_PATH = os.environ.get("HEIMDALL_LINKS_PATH", "/seed/links.json")
RESYNC_SECONDS = int(os.environ.get("HEIMDALL_LINK_RESYNC_SECONDS", "3600"))
def find_database():
while True:
for path in DB_CANDIDATES:
if os.path.exists(path):
return path
time.sleep(5)
def table_exists(conn, name):
row = conn.execute(
"select name from sqlite_master where type = 'table' and name = ?",
(name,),
).fetchone()
return row is not None
def columns(conn, table):
return {row[1] for row in conn.execute(f"pragma table_info({table})")}
def quote_identifier(name):
return '"' + name.replace('"', '""') + '"'
def ensure_public_dashboard(conn, user_id):
if not table_exists(conn, "users"):
return
user_columns = columns(conn, "users")
if "public_front" not in user_columns:
return
conn.execute(
'update "users" set "public_front" = 1 where "id" = ?',
(user_id,),
)
def ensure_dashboard_tag(conn, item_id, tag_id, now):
if not table_exists(conn, "item_tag"):
return
item_tag_columns = columns(conn, "item_tag")
if not {"item_id", "tag_id"}.issubset(item_tag_columns):
return
existing = conn.execute(
'select 1 from "item_tag" where "item_id" = ? and "tag_id" = ?',
(item_id, tag_id),
).fetchone()
if existing:
return
values = {
"item_id": item_id,
"tag_id": tag_id,
"created_at": now,
"updated_at": now,
}
insert_columns = [name for name in values if name in item_tag_columns]
placeholders = ", ".join("?" for _ in insert_columns)
quoted_insert_columns = ", ".join(quote_identifier(name) for name in insert_columns)
conn.execute(
f'insert into "item_tag" ({quoted_insert_columns}) values ({placeholders})',
[values[name] for name in insert_columns],
)
def home_dashboard_tag_id(conn):
item_columns = columns(conn, "items")
if not {"id", "title"}.issubset(item_columns):
return 0
row = conn.execute(
'select "id" from "items" where "title" = ? order by "id" limit 1',
("app.dashboard",),
).fetchone()
if row:
return row[0]
return 0
def wait_for_items_table(db_path):
while True:
try:
conn = sqlite3.connect(db_path)
if table_exists(conn, "items"):
return conn
conn.close()
except sqlite3.Error:
pass
time.sleep(5)
def upsert_links(conn, links):
item_columns = columns(conn, "items")
seed_user_id = 1
home_dashboard_tag = home_dashboard_tag_id(conn)
now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
ensure_public_dashboard(conn, seed_user_id)
for order, link in enumerate(links):
existing = conn.execute(
'select "id" from "items" where "title" = ?',
(link["title"],),
).fetchone()
values = {
"title": link["title"],
"url": link["url"],
"description": None,
"colour": link.get("colour"),
"pinned": 1,
"order": order,
"type": 0,
"user_id": seed_user_id,
"class": None,
"appid": None,
"appdescription": link.get("description"),
"created_at": now,
"updated_at": now,
"deleted_at": None,
}
if existing:
update_columns = [
name
for name in values
if name in item_columns and name not in ("id", "title", "created_at")
]
assignments = ", ".join(f"{quote_identifier(name)} = ?" for name in update_columns)
params = [values[name] for name in update_columns]
params.append(existing[0])
conn.execute(f'update "items" set {assignments} where "id" = ?', params)
ensure_dashboard_tag(conn, existing[0], home_dashboard_tag, now)
continue
insert_columns = [name for name in values if name in item_columns and name != "deleted_at"]
placeholders = ", ".join("?" for _ in insert_columns)
quoted_insert_columns = ", ".join(quote_identifier(name) for name in insert_columns)
conn.execute(
f'insert into "items" ({quoted_insert_columns}) values ({placeholders})',
[values[name] for name in insert_columns],
)
new_item_id = conn.execute("select last_insert_rowid()").fetchone()[0]
ensure_dashboard_tag(conn, new_item_id, home_dashboard_tag, now)
conn.commit()
def load_links():
with open(LINKS_PATH, encoding="utf-8") as links_file:
return json.load(links_file)
while True:
db_path = find_database()
connection = wait_for_items_table(db_path)
try:
upsert_links(connection, load_links())
time.sleep(RESYNC_SECONDS)
except (OSError, sqlite3.Error, json.JSONDecodeError) as error:
print(f"Failed to seed Heimdall links: {error}", flush=True)
time.sleep(10)
finally:
connection.close()