/** * Verdaccio 缓存可视化管理面板 — 后端服务入口 * Express + better-sqlite3,§4.2 REST API * * 环境变量: * VERDACCIO_STORAGE Verdaccio 缓存目录(优先于 UI 设置) * PORT 监听端口(默认 3000) * DATA_DIR SQLite 数据目录(默认 backend/data) * VCM_USERNAME 面板登录账号(默认 admin) * VCM_PASSWORD 面板登录密码(默认 666666,Docker 部署务必通过 compose 覆盖) * UV_THREADPOOL_SIZE libuv IO 线程池大小(默认 4 → 8:扫描/stat/readFile 并发更高,NAS 四核安全) * * 模块划分: * - scan-manager.js 扫描状态机与业务编排(缓存/进度/状态构建) * - schedules.js 每日扫描 + 每周抓榜定时任务 * - routes/* REST 路由(按领域拆分) */ // 必须在 libuv 线程池首次使用前设置:扫描大量目录/文件时默认 4 线程是主要瓶颈 process.env.UV_THREADPOOL_SIZE = process.env.UV_THREADPOOL_SIZE || '8' import express from 'express' import path from 'node:path' import fs from 'node:fs' import zlib from 'node:zlib' import { fileURLToPath } from 'node:url' import { resolveStorageDir } from './scanner.js' import { isDemoMode, scanState, runScanAsync } from './scan-manager.js' import { scheduleDailyScan, scheduleWeeklyTopList } from './schedules.js' import { authRouter, authMiddleware } from './routes/auth.js' import { thresholdsRouter } from './routes/thresholds.js' import { ignoredRouter } from './routes/ignored.js' import { packagesRouter } from './routes/packages.js' import { syncRouter } from './routes/sync.js' import { toplistRouter } from './routes/toplist.js' import { cleanRouter } from './routes/clean.js' import { logsRouter } from './routes/logs.js' import { themeRouter } from './routes/theme.js' import { verdaccioRouter } from './routes/verdaccio.js' import { apiNotFound, errorHandler } from './http.js' const __dirname = path.dirname(fileURLToPath(import.meta.url)) const app = express() app.use(express.json({ limit: '1mb' })) // gzip 压缩:大响应(如 /api/packages 7000+ 条)减少传输体积 60-80%,Node 内置 zlib 无需新依赖 app.use((req, res, next) => { const accept = req.headers['accept-encoding'] || '' if (!accept.includes('gzip')) return next() const originalJson = res.json.bind(res) res.json = (data) => { try { const json = JSON.stringify(data) if (json.length < 1024) return originalJson(data) // 小响应不压缩,避免开销 const compressed = zlib.gzipSync(json) res.setHeader('Content-Encoding', 'gzip') res.setHeader('Content-Type', 'application/json') res.send(compressed) } catch { originalJson(data) } } next() }) const PORT = Number(process.env.PORT || 3000) // —— 路由装配:登录/登出/主题公开,其余 /api/* 需鉴权 —— app.use('/api', themeRouter) // 公开:登录页加载时也需要主题 app.use('/api', authRouter) app.use('/api', authMiddleware) app.use('/api', verdaccioRouter) // Verdaccio Web API 透传(读取类;面板侧需登录) app.use('/api', thresholdsRouter) app.use('/api', ignoredRouter) app.use('/api', packagesRouter) app.use('/api', syncRouter) app.use('/api', toplistRouter) app.use('/api', cleanRouter) app.use('/api', logsRouter) // —— /api 未匹配任何路由 → JSON 404(Express 默认对非 GET 返回 HTML) —— app.use('/api', apiNotFound) // —— 生产:托管前端 dist —— const distDir = path.join(__dirname, '..', 'frontend', 'dist') if (fs.existsSync(distDir)) { app.use(express.static(distDir)) app.get('*', (req, res) => { if (req.path.startsWith('/api/')) return res.status(404).json({ ok: false, message: 'not found' }) res.sendFile(path.join(distDir, 'index.html')) }) } // —— 统一错误处理:Express4 同步 throw 会跳到这里,返回 JSON 而非 HTML 500 —— app.use(errorHandler) app.listen(PORT, () => { const storage = resolveStorageDir() const mode = isDemoMode() ? 'demo' : 'storage' if (!process.env.VCM_PASSWORD) { console.warn( '[auth] ⚠ 未设置 VCM_PASSWORD 环境变量,正在使用默认口令 666666。' + '生产部署请务必通过 docker-compose 的环境变量覆盖,否则面板可被任意登录!', ) } console.log(`[verdaccio-manager] listening on :${PORT} (mode=${mode})`) if (mode === 'storage') console.log(`[verdaccio-manager] storage: ${storage}`) else console.log('[verdaccio-manager] 未配置 VERDACCIO_STORAGE,运行于演示模式(设置 VCM_DEMO=0 + 真实目录可关闭)') // 启动即扫描:容器部署后无需打开前端即可预置缓存(demo 模式跳过; // 启动时 rawCache 恒为 null,直接触发即可,无需 invalidateScan) if (mode === 'storage' && !scanState.scanning) { scanState._triggerSource = 'startup' scanState.startupScanning = true // 部署后启动扫描:供前端显示首次扫描遮罩,扫描结束即清零 runScanAsync() console.log(`[auto-scan] 启动后立即扫描缓存目录:${storage}`) } scheduleDailyScan() scheduleWeeklyTopList() })