Files
verdaccio-cache-manager/backend/db.js
T
yinjun.chen1 84c9a11e6f
CI / Backend tests (push) Canceled after 0s
CI / Frontend build (push) Canceled after 0s
feat(pkglist): 去重 + 状态持久 + UI 弹窗风格统一
后端:
- seedIfNeeded/addPkg/updateFromAllSources 插入前检测 storage 是否已缓存同名包,已缓存直接置 done
- 新增 markDoneByName + dedupPkgList 函数
- pkglist 表新增 cached 列区分「已在缓存」vs「清单拉取完成」
- 新增 POST /api/pkglist/dedup 接口

前端:
- api/index.js 新增 dedupPkgList()
- store 暴露 dedupPkgList action,doRefresh 初始化时检测 sync-latest 运行状态并恢复轮询
- PkgListDrawer.vue 增加「去重」按钮,状态列区分「已在缓存」/「已完成」
- SearchView.vue 一键拉取弹窗改为与整体一致的自定义风格(确认+结果弹窗)
- 按钮已按 syncProgress.running 显示旋转动画
2026-09-21 22:45:12 +08:00

153 lines
5.5 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'")
}
// 旧库迁移 v2:pkglist 增加 cached 列——标记「写入清单时 storage 已缓存该包」(done 的来源之一,
// 与「通过清单拉取完成」区分,前端据此展示「已在缓存 / 已完成」两种 done)
const pkglistCols = db.prepare('PRAGMA table_info(pkglist)').all().map(c => c.name)
if (!pkglistCols.includes('cached')) {
db.exec("ALTER TABLE pkglist ADD COLUMN cached INTEGER DEFAULT 0")
}
// 旧库迁移 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