/** * SQLite 数据层(§4.1) * 表:settings / ignored_packages / logs */ import Database from 'better-sqlite3' import fs from 'node:fs' import path from 'node:path' import { fileURLToPath } from 'node:url' const __dirname = path.dirname(fileURLToPath(import.meta.url)) // 数据目录:环境变量 DATA_DIR 可覆盖(Docker 挂载 manager-data) const DATA_DIR = process.env.DATA_DIR || path.join(__dirname, 'data') fs.mkdirSync(DATA_DIR, { recursive: true }) const db = new Database(path.join(DATA_DIR, 'manager.db')) db.pragma('journal_mode = WAL') db.exec(` CREATE TABLE IF NOT EXISTS settings ( key TEXT PRIMARY KEY, value TEXT ); CREATE TABLE IF NOT EXISTS ignored_packages ( pkg_name TEXT PRIMARY KEY, ignored_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS logs ( id INTEGER PRIMARY KEY AUTOINCREMENT, action TEXT DEFAULT 'clean', pkg_name TEXT, deleted_versions TEXT, deleted_count INTEGER, freed_space TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS pkglist ( id INTEGER PRIMARY KEY AUTOINCREMENT, pkg_name TEXT NOT NULL UNIQUE, version TEXT, category TEXT DEFAULT '', status TEXT DEFAULT 'pending', auto INTEGER DEFAULT 0, created_at DATETIME ); `) // 北京时间(Asia/Shanghai 固定 UTC+8,无夏令时):SQLite DEFAULT CURRENT_TIMESTAMP 是 UTC, // 与前端按本地时间展示相差 8 小时,故写入与迁移统一显式使用北京时间墙上时间字符串 // (格式与前端 slice 兼容:YYYY-MM-DD HH:MM:SS,不带时区后缀,避免被二次误解) const pad2 = n => String(n).padStart(2, '0') export function nowIso() { const d = new Date(Date.now() + 8 * 3600_000) return `${d.getUTCFullYear()}-${pad2(d.getUTCMonth() + 1)}-${pad2(d.getUTCDate())} ${pad2(d.getUTCHours())}:${pad2(d.getUTCMinutes())}:${pad2(d.getUTCSeconds())}` } function utcToShanghai(iso) { try { const ts = Date.parse(iso) if (!Number.isFinite(ts)) return null const d = new Date(ts + 8 * 3600_000) return `${d.getUTCFullYear()}-${pad2(d.getUTCMonth() + 1)}-${pad2(d.getUTCDate())} ${pad2(d.getUTCHours())}:${pad2(d.getUTCMinutes())}:${pad2(d.getUTCSeconds())}` } catch { return null } } // 旧库迁移:早期 logs 表无 action 列,运行时补齐(忽略/取消忽略等操作依赖该列区分类型) const logCols = db.prepare('PRAGMA table_info(logs)').all().map(c => c.name) if (!logCols.includes('action')) { db.exec("ALTER TABLE logs ADD COLUMN action TEXT DEFAULT 'clean'") } // 旧库迁移 v1:历史日志/忽略记录的时间戳是 UTC,统一转成北京时间;settings 标志位防重复执行 function migrateTimezone() { if (getSetting('timezone_migrated_v1')) return db.transaction(() => { for (const { id, created_at: t } of db.prepare('SELECT id, created_at FROM logs').all()) { if (t) { const local = utcToShanghai(t) if (local) db.prepare('UPDATE logs SET created_at = ? WHERE id = ?').run(local, id) } } for (const { pkg_name: n, ignored_at: t } of db.prepare('SELECT pkg_name, ignored_at FROM ignored_packages').all()) { if (t) { const local = utcToShanghai(t) if (local) db.prepare('UPDATE ignored_packages SET ignored_at = ? WHERE pkg_name = ?').run(local, n) } } setSetting('timezone_migrated_v1', '1') })() } // —— settings —— export function getSetting(key, def = null) { const row = db.prepare('SELECT value FROM settings WHERE key = ?').get(key) return row ? row.value : def } export function setSetting(key, value) { db.prepare( 'INSERT INTO settings (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value', ).run(key, String(value)) } // —— ignored —— export function getIgnored() { return db.prepare('SELECT pkg_name FROM ignored_packages ORDER BY ignored_at DESC').all().map(r => r.pkg_name) } export function getIgnoredRows() { return db.prepare('SELECT pkg_name, ignored_at FROM ignored_packages ORDER BY ignored_at DESC').all() } export function isIgnored(pkgName) { return !!db.prepare('SELECT 1 FROM ignored_packages WHERE pkg_name = ?').get(pkgName) } export function addIgnored(pkgName) { db.prepare( 'INSERT INTO ignored_packages (pkg_name, ignored_at) VALUES (?, ?) ON CONFLICT(pkg_name) DO NOTHING', ).run(pkgName, nowIso()) } export function removeIgnored(pkgName) { db.prepare('DELETE FROM ignored_packages WHERE pkg_name = ?').run(pkgName) } // —— logs —— export function addLog({ action = 'clean', pkgName, deletedVersions = null, deletedCount = 0, freedSpace = null }) { const info = db.prepare( 'INSERT INTO logs (action, pkg_name, deleted_versions, deleted_count, freed_space, created_at) VALUES (?, ?, ?, ?, ?, ?)', ).run(action, pkgName, deletedVersions, deletedCount, freedSpace, nowIso()) return info.lastInsertRowid } export function getLogs(limit = 50) { return db.prepare( 'SELECT * FROM logs ORDER BY id DESC LIMIT ?', ).all(limit) } // 模块加载时执行时间迁移(幂等) migrateTimezone() export default db