- buildPackages 缓存全量结果(rawCache/ignored/threshold 不变时复用) - gzip 压缩大响应(Node 内置 zlib,减少传输体积 60-80%)
116 lines
5.1 KiB
JavaScript
116 lines
5.1 KiB
JavaScript
/**
|
||
* 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()
|
||
}) |