Files
CloudSearch/source_clean/src/main.ts
admin 32dcc44524 v0.3.24: 清理设置4个后端接线 — 白名单/cookie检测/空间校准开关
补全前端4个控制项的后端实现:
- cleanup_whitelist_dirs → cleanupCloudFiles + cleanupAllBySpaceThreshold 读取并传递
- cleanup_auto_refresh_storage → scheduleStorageRefresh 读取开关,false时跳过
- cleanup_verify_enabled + cleanup_verify_interval → 新增Cookie验证调度器
- CloudCleanupDriver 接口 + baidu.driver 签名同步支持 whitelistDirs 可选参数

验证: 4个key从仅前端 → 全部有后端读取模块
2026-05-17 18:10:12 +08:00

274 lines
11 KiB
TypeScript
Executable File
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.
import express from 'express';
import path from 'path';
import cors from 'cors';
import helmet from 'helmet';
import morgan from 'morgan';
import config from './config';
import { VERSION as version } from "./version";
import { checkStartup } from './config/startup-validator';
import { getDb } from './database/database';
import { connectRedis, disconnectRedis, reconnectRedis, testRedisConnection } from './middleware/cache';
import rateLimiter from './middleware/rate-limit';
import routes from './routes';
import { pansouWebProxy } from './proxy/pansou-web';
import { checkAndRunScheduledCleanup } from './cloud/cleanup.service';
import { refreshAllStorageInfo } from './cloud/cloud.service';
const app = express();
// ============ Middleware ============
app.set('trust proxy', true);
// CORS — 生产环境必须配置真实域名(空值或占位符用 * 并打警告日志)
const corsOrigin = config.corsOrigin;
const isPlaceholder = !corsOrigin || corsOrigin === 'https://your-domain.com';
if (config.nodeEnv === 'production' && isPlaceholder) {
console.error('[FATAL] CORS_ORIGIN 未配置或使用了占位符 https://your-domain.com生产环境必须设置真实域名。应用拒绝启动。');
process.exit(1);
}
if (config.nodeEnv === 'production' && !isPlaceholder) {
app.use(cors({ origin: corsOrigin, credentials: true }));
} else {
app.use(cors({ origin: '*', credentials: false }));
}
app.use(helmet({ contentSecurityPolicy: false }));
// morgan 日志格式:不记录 IP避免隐私合规问题
app.use(morgan(':method :url :status :res[content-length] - :response-time ms'));
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
// ============ 前端静态文件 ============
const frontendDist = path.join(__dirname, 'frontend');
// Cache control: HTML no-cache, hashed assets immutable
app.use((req, res, next) => {
if (req.path.endsWith('.html')) {
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
} else if (req.path.startsWith('/assets/')) {
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
}
next();
});
app.use(express.static(frontendDist));
// ============ Rate Limiting (after static files — only API routes are limited)
app.use(rateLimiter);
// ============ Routes ============
app.use('/api/uploads', express.static(config.uploadDir));
app.use('/api', routes);
// ============ Health Check增强版覆盖 Redis / PanSou / VideoParser 状态) ============
app.get('/health', async (_req, res) => {
const dbOk = (() => {
try { getDb(); return true; } catch { return false; }
})();
const redisStatus = await (async () => {
try {
const { getRedis } = require('./middleware/cache');
const redis = getRedis();
if (!redis) return 'disconnected';
const pong = await redis.ping().catch(() => null);
return pong === 'PONG' ? 'connected' : 'error';
// eslint-disable-next-line no-unused-vars
} catch { return 'unknown'; }
})();
const pansouStatus = await (async () => {
try {
// Native fetch available in Node 20+
const url = (config.pansouUrl || 'http://pansou:80').replace(/\/+$/, '') + '/api/search';
const controller = new AbortController();
const t = setTimeout(() => controller.abort(), 3000);
const r = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ kw: 'health', page: 1 }), signal: controller.signal });
clearTimeout(t);
return r.ok ? 'ok' : 'degraded';
// eslint-disable-next-line no-unused-vars
} catch { return 'unreachable'; }
})();
const videoParserStatus = await (async () => {
try {
// Native fetch available in Node 20+
const url = (config.videoParserUrl || 'http://video-parser:3001').replace(/\/+$/, '');
const controller = new AbortController();
const t = setTimeout(() => controller.abort(), 3000);
const r = await fetch(url, { signal: controller.signal });
clearTimeout(t);
return r.ok ? 'ok' : 'degraded';
// eslint-disable-next-line no-unused-vars
} catch { return 'unreachable'; }
})();
const overall = dbOk && redisStatus === 'connected' && pansouStatus === 'ok' && videoParserStatus === 'ok'
? 'ok'
: dbOk && redisStatus !== 'unknown' && pansouStatus !== 'unreachable'
? 'degraded'
: 'unhealthy';
res.json({
version,
status: overall,
timestamp: new Date().toISOString(),
uptime: Math.floor(process.uptime()),
memory: process.memoryUsage().rss,
components: {
db: dbOk ? 'connected' : 'error',
redis: redisStatus,
pansou: pansouStatus,
videoParser: videoParserStatus,
},
});
});
// ============ PanSou Web UI Proxy ============
app.use('/pansou', pansouWebProxy);
// SPA fallback
app.use((req, res, next) => {
if (req.path.startsWith('/api/') || req.path === '/health') return next();
res.sendFile(path.join(frontendDist, 'index.html'), (err) => { if (err) next(); });
});
// Global error handler
app.use((err: any, _req: express.Request, res: express.Response, _next: express.NextFunction) => {
console.error('[Error]', err);
res.status(err.status || 500).json({
error: config.nodeEnv === 'production' ? 'Internal server error' : err.message,
code: err.status || 500,
});
});
// ============ Server Start ============
async function start(): Promise<void> {
// Startup config validation
if (!checkStartup()) {
console.error('[Server] 配置校验未通过,进程退出');
process.exit(1);
}
try {
getDb();
console.log('[DB] SQLite database initialized');
try {
const { getSystemConfig } = require('./admin/system-config.service');
const tz = getSystemConfig('timezone');
if (tz) { process.env.TZ = tz; console.log(`[Config] Timezone set to: ${tz}`); }
} catch { console.warn('[Config] Could not set timezone, using default'); }
} catch (err) {
console.error('[DB] Failed to initialize database:', err);
process.exit(1);
}
try {
const { getSystemConfig } = require('./admin/system-config.service');
const redisUrl = process.env.REDIS_URL || getSystemConfig('redis_url');
if (redisUrl) {
const ok = await reconnectRedis(redisUrl);
if (ok) console.log('[Redis] Connected to', redisUrl);
} else {
await connectRedis();
}
} catch { console.warn('[Redis] Redis not available, continuing without cache'); }
// Cleanup scheduler
const CLEANUP_INTERVAL = 10 * 60 * 1000;
setInterval(() => { checkAndRunScheduledCleanup().catch(err => console.error('[Cleanup] Scheduler error:', err.message)); }, CLEANUP_INTERVAL);
setTimeout(() => { checkAndRunScheduledCleanup().catch(err => console.error('[Cleanup] Initial check error:', err.message)); }, 30000);
// Storage info refresh scheduler — dynamic interval from system_configs.storage_refresh_interval
// Uses setTimeout chain so interval changes take effect without restart
let storageRefreshTimer: NodeJS.Timeout | null = null;
const scheduleStorageRefresh = () => {
if (storageRefreshTimer) clearTimeout(storageRefreshTimer);
// Check if auto refresh is enabled
let enabled = true;
let intervalMinutes = 180; // default
try {
const { getSystemConfig } = require('./admin/system-config.service');
const autoVal = getSystemConfig('cleanup_auto_refresh_storage');
if (autoVal === 'false' || autoVal === '0') enabled = false;
const val = getSystemConfig('storage_refresh_interval');
if (val) { const n = parseInt(val, 10); if (n >= 5 && n <= 1440) intervalMinutes = n; }
} catch {}
if (!enabled) { console.log('[Storage] Auto refresh disabled, skipping'); return; }
const ms = intervalMinutes * 60 * 1000;
console.log(`[Storage] Next refresh in ${intervalMinutes} minutes`);
storageRefreshTimer = setTimeout(() => {
refreshAllStorageInfo().catch(err => console.error('[Storage] Refresh error:', err.message));
scheduleStorageRefresh(); // re-schedule with possibly updated interval
}, ms);
};
// First run 60s after startup
setTimeout(() => { refreshAllStorageInfo().catch(err => console.error('[Storage] Initial refresh error:', err.message)); }, 60000);
// Schedule first periodic refresh (delay = configured interval)
setTimeout(() => scheduleStorageRefresh(), 60000);
// Cookie verification scheduler — periodically test cloud connections
let verifyTimer: NodeJS.Timeout | null = null;
const scheduleVerify = () => {
if (verifyTimer) clearTimeout(verifyTimer);
let enabled = false;
let intervalMinutes = 60; // default every hour
try {
const { getSystemConfig } = require('./admin/system-config.service');
const enabledVal = getSystemConfig('cleanup_verify_enabled');
if (enabledVal === 'true' || enabledVal === '1') enabled = true;
const intVal = getSystemConfig('cleanup_verify_interval');
if (intVal) { const n = parseInt(intVal, 10); if (n >= 5 && n <= 1440) intervalMinutes = n; }
} catch {}
if (!enabled) return;
const ms = intervalMinutes * 60 * 1000;
verifyTimer = setTimeout(async () => {
try {
const { getActiveCloudConfigs, testCloudConnection } = require('./cloud/credential.service');
const configs = getActiveCloudConfigs();
for (const c of configs) {
try { await testCloudConnection(c.id); } catch {}
}
} catch (e: any) { console.error('[Verify] Error:', e.message); }
scheduleVerify();
}, ms);
};
// Start verification 2 minutes after startup
setTimeout(() => scheduleVerify(), 120000);
// Daily Report scheduler — check every 60 seconds
const DAILY_REPORT_CHECK_INTERVAL = 60 * 1000;
setInterval(() => {
try {
const { runDailyReportIfScheduled } = require('./services/daily-report.service');
runDailyReportIfScheduled().catch(err => console.error('[DailyReport] Scheduler error:', err.message));
} catch {}
}, DAILY_REPORT_CHECK_INTERVAL);
// Also run once 30s after startup (catch case where server starts at 08:00-08:05)
setTimeout(() => {
try {
const { runDailyReportIfScheduled } = require('./services/daily-report.service');
runDailyReportIfScheduled().catch(err => console.error('[DailyReport] Initial check error:', err.message));
} catch {}
}, 30000);
const server = app.listen(config.port, () => {
console.log(`[Server] CloudSearch Backend running on port ${config.port} (${config.nodeEnv})`);
});
const shutdown = async (signal: string) => {
console.log(`\n[Server] Received ${signal}, shutting down gracefully...`);
server.close(async () => { await disconnectRedis(); console.log('[Server] Closed'); process.exit(0); });
setTimeout(() => { console.error('[Server] Force shutdown'); process.exit(1); }, 10000);
};
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));
process.on('uncaughtException', (err) => { console.error('[FATAL] Uncaught Exception:', err); setTimeout(() => process.exit(1), 1000); });
process.on('unhandledRejection', (reason) => { const msg = reason instanceof Error ? reason.message : String(reason); console.error('[FATAL] Unhandled Rejection:', msg); });
}
start().catch((err) => { console.error('[Server] Failed to start:', err); process.exit(1); });
export default app;