Files
verdaccio-cache-manager/backend/server.js
T
yinjun.chen1 b250ecbf55 perf(packages): 优化 /api/packages 7000+ 条响应性能
- buildPackages 缓存全量结果(rawCache/ignored/threshold 不变时复用)

- gzip 压缩大响应(Node 内置 zlib,减少传输体积 60-80%)
2026-09-28 11:04:21 +08:00

116 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.
/**
* 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()
})