Files
verdaccio-cache-manager/backend/db.js
T
yinjun.chen1 bb7d45efcf
CI / Backend tests (push) Canceled after 0s
CI / Frontend build (push) Canceled after 0s
feat: 新增热门 npm 包清单,定时收录榜单并支持按清单拉取缓存
- 内置热门包池(248条/12品类) ∪ npm-search 榜单(14组关键词top300),按包名去重增量收录
- 榜单源优先私服 /-/v1/search,不可用时回退官方 registry,仅提示来源不阻塞
- 清单 CRUD + 手动/定时拉取(失败重试3次) + 进度轮询 + 失败包红色角标
- 清单全部完成即失效并联动停止定时拉取,写入新 pending 包自动恢复
- 前端:我的缓存页包清单抽屉、用户名下拉定时配置弹窗、mock 分支同步
2026-09-21 15:47:22 +08:00

146 lines
5.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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