import { randomBytes } from 'node:crypto' import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs' import path from 'node:path' export interface ServerConfig { port: number host: string dataDir: string imagesDir: string databaseFile: string /** 构建后的前端目录;为空表示不托管静态文件(开发时由 Vite 负责) */ staticDir: string | null password: string secret: string sessionDays: number maxImageBytes: number production: boolean } export function loadConfig(env: NodeJS.ProcessEnv = process.env): ServerConfig { const production = env.NODE_ENV === 'production' const dataDir = path.resolve(env.DATA_DIR || './data') const imagesDir = path.join(dataDir, 'images') mkdirSync(imagesDir, { recursive: true }) let password = env.BOUNDLESS_PASSWORD ?? '' if (!password) { if (production) { throw new Error('请通过环境变量 BOUNDLESS_PASSWORD 设置登录口令') } password = 'boundless' console.warn('[boundless] 未设置 BOUNDLESS_PASSWORD,开发模式下使用默认口令 "boundless"') } // 默认托管构建好的 dist/;开发时页面由 Vite 提供,这里有没有都不影响 const staticDir = path.resolve(env.STATIC_DIR || 'dist') return { port: Number(env.PORT) || 3000, host: env.HOST || '0.0.0.0', dataDir, imagesDir, databaseFile: path.join(dataDir, 'boundless.sqlite'), staticDir: staticDir && existsSync(staticDir) ? staticDir : null, password, secret: env.BOUNDLESS_SECRET || loadOrCreateSecret(dataDir), sessionDays: Number(env.SESSION_DAYS) || 90, maxImageBytes: (Number(env.MAX_IMAGE_MB) || 20) * 1024 * 1024, production, } } /** 未显式提供密钥时,生成一份并保存在数据目录,重启后登录状态仍然有效。 */ function loadOrCreateSecret(dataDir: string) { const file = path.join(dataDir, 'session.secret') if (existsSync(file)) { const saved = readFileSync(file, 'utf8').trim() if (saved) return saved } const secret = randomBytes(32).toString('base64url') writeFileSync(file, secret, { mode: 0o600 }) return secret }