v0.2.7: 修复Redis连接 + 启动管理后台

- 修复Redis认证 (配置密码)
- 启动Python管理后台 (端口9531, 15个功能开关)
- 统一版本号 0.2.7
- 更新docker-compose.yml (镜像版本/Redis URL/Admin服务)
This commit is contained in:
2026-05-17 02:22:18 +08:00
commit 83cbfaf03f
164 changed files with 25195 additions and 0 deletions

View File

@@ -0,0 +1,254 @@
import { getDb } from '../database/database';
import { getSystemConfig, updateSystemConfig } from '../admin/system-config.service';
import { formatLocalDate, formatLocalDateTime } from '../utils/time';
import { QuarkDriver } from './drivers/quark.driver';
import { BaiduDriver } from './drivers/baidu.driver';
// ═══════════════════════════════════════════════════════════════════════════
// CloudCleanupDriver — contract that each cloud driver must fulfill
// to participate in the cleanup cycle.
//
// To add a new cloud type (e.g. Baidu, Aliyun), implement these three
// methods in the driver and register it in getDriverForCleanup() below.
// The controller (this file) handles WHEN and WITH WHAT parameters;
// the driver handles HOW.
// ═══════════════════════════════════════════════════════════════════════════
/** Each cleanup operation returns { trashed: number; errors: string[] } */
interface CleanupOpResult { trashed: number; errors: string[] }
interface CloudCleanupDriver {
/** Trash date folders (YYYY-MM-DD) older than `days`. */
cleanupOldDateFolders(days: number): Promise<CleanupOpResult>;
/**
* If used space exceeds thresholdPercent% of TOTAL capacity,
* delete oldest date folders until totalBytes * deletePercent/100
* of TOTAL capacity is freed.
* @param thresholdPercent — trigger when usage >= this % of total
* @param deletePercent — free this % of total capacity
*/
cleanupBySpaceThreshold(thresholdPercent: number, deletePercent: number): Promise<CleanupOpResult>;
/** Permanently empty the recycle bin. */
emptyTrash(): Promise<boolean>;
}
// ═══════════════════════════════════════════════════════════════════════════
// Driver factory — create the right driver for a given cloud config.
// When adding a new cloud type, add a case here.
// ═══════════════════════════════════════════════════════════════════════════
function getDriverForCleanup(config: { cloud_type: string; cookie: string }): CloudCleanupDriver | null {
switch (config.cloud_type) {
case 'quark':
return new QuarkDriver({ cookie: config.cookie });
case 'baidu':
return new BaiduDriver({ cookie: config.cookie });
// case 'aliyun': return new AliyunDriver({ cookie: config.cookie });
default:
return null;
}
}
// ═══════════════════════════════════════════════════════════════════════════
// Cleanup controller — reads system configs and dispatches to each
// active cloud driver. Every driver receives the same parameters;
// the driver decides whether/how to act.
// ═══════════════════════════════════════════════════════════════════════════
interface CleanupStats {
filesTrashed: number;
logsDeleted: number;
trashEmptied: boolean;
errors: string[];
}
/** Get all active cloud configs (any type). Used by the orchestrator. */
function getActiveCleanupConfigs(): Array<{ id: number; cloud_type: string; cookie: string; nickname?: string }> {
const db = getDb();
return db.prepare(
`SELECT id, cloud_type, cookie, nickname FROM cloud_configs
WHERE is_active = 1 AND cookie IS NOT NULL AND cookie != ''`
).all() as Array<{ id: number; cloud_type: string; cookie: string; nickname?: string }>;
}
/**
* Dispatch cleanupOldDateFolders to every active driver.
* Each driver receives the same `days` parameter.
*/
async function cleanupCloudFiles(days: number): Promise<CleanupOpResult> {
const configs = getActiveCleanupConfigs();
const errors: string[] = [];
let totalTrashed = 0;
for (const cfg of configs) {
const driver = getDriverForCleanup(cfg);
if (!driver) {
console.log(`[Cleanup] No driver for cloud_type="${cfg.cloud_type}", skipping config #${cfg.id}`);
continue;
}
try {
const result = await driver.cleanupOldDateFolders(days);
totalTrashed += result.trashed;
errors.push(...result.errors.map(e => `[${cfg.cloud_type}#${cfg.id}] ${e}`));
} catch (err: any) {
errors.push(`[${cfg.cloud_type}#${cfg.id}] cleanupOldDateFolders: ${err.message}`);
}
await new Promise(r => setTimeout(r, 1000));
}
return { trashed: totalTrashed, errors };
}
/**
* Dispatch cleanupBySpaceThreshold to every active driver.
* Each driver receives the same threshold/delete percentages.
*/
async function cleanupAllBySpaceThreshold(
thresholdPercent: number,
deletePercent: number,
): Promise<CleanupOpResult> {
const configs = getActiveCleanupConfigs();
const errors: string[] = [];
let totalTrashed = 0;
for (const cfg of configs) {
const driver = getDriverForCleanup(cfg);
if (!driver) {
console.log(`[Cleanup] No driver for cloud_type="${cfg.cloud_type}", skipping config #${cfg.id}`);
continue;
}
try {
const result = await driver.cleanupBySpaceThreshold(thresholdPercent, deletePercent);
totalTrashed += result.trashed;
errors.push(...result.errors.map(e => `[${cfg.cloud_type}#${cfg.id}] ${e}`));
} catch (err: any) {
errors.push(`[${cfg.cloud_type}#${cfg.id}] cleanupBySpaceThreshold: ${err.message}`);
}
await new Promise(r => setTimeout(r, 1000));
}
return { trashed: totalTrashed, errors };
}
/**
* Dispatch emptyTrash to every active driver.
*/
export async function emptyAllTrash(): Promise<{ emptied: boolean; errors: string[] }> {
const configs = getActiveCleanupConfigs();
const errors: string[] = [];
let emptied = false;
for (const cfg of configs) {
const driver = getDriverForCleanup(cfg);
if (!driver) continue;
try {
const ok = await driver.emptyTrash();
if (ok) {
emptied = true;
console.log(`[Cleanup] ✅ Emptied trash for [${cfg.cloud_type}#${cfg.id}]`);
} else {
errors.push(`[${cfg.cloud_type}#${cfg.id}] empty trash failed`);
}
} catch (err: any) {
errors.push(`[${cfg.cloud_type}#${cfg.id}]: ${err.message}`);
}
}
return { emptied, errors };
}
/**
* Delete save_records older than the specified number of days.
*/
function cleanupLogs(days: number): number {
const db = getDb();
const cutoffStr = formatLocalDateTime(new Date(Date.now() - days * 24 * 60 * 60 * 1000));
const result = db.prepare('DELETE FROM save_records WHERE created_at < ?').run(cutoffStr);
console.log(`[Cleanup] Deleted ${result.changes} save records older than ${days} days (before ${cutoffStr})`);
return result.changes;
}
/**
* Run full cleanup cycle:
* 0. Force-clean by space threshold (if enabled & exceeded) — priority highest
* 1. Delete old save_records
* 2. Trash old date folders by retention days
* 3. Empty recycle bin (permanently free space)
*/
export async function runFullCleanup(): Promise<CleanupStats> {
const fileDays = parseInt(getSystemConfig('cleanup_file_retention_days') || '7', 10);
const logDays = parseInt(getSystemConfig('cleanup_log_retention_days') || '30', 10);
const emptyTrashEnabled = getSystemConfig('cleanup_empty_trash') !== 'false';
const stats: CleanupStats = { filesTrashed: 0, logsDeleted: 0, trashEmptied: false, errors: [] };
// 0. Space threshold (highest priority)
const thresholdEnabled = getSystemConfig('cleanup_space_threshold_enabled');
if (thresholdEnabled === 'true') {
const thresholdPercent = parseInt(getSystemConfig('cleanup_space_threshold_percent') || '90', 10);
const deletePercent = parseInt(getSystemConfig('cleanup_space_threshold_delete_percent') || '10', 10);
if (thresholdPercent > 0 && thresholdPercent < 100) {
try {
const result = await cleanupAllBySpaceThreshold(thresholdPercent, deletePercent);
stats.filesTrashed += result.trashed;
stats.errors.push(...result.errors);
} catch (err: any) {
stats.errors.push(`空间阈值清理失败: ${err.message}`);
}
}
}
// 1. Delete old save_records
try {
stats.logsDeleted = cleanupLogs(logDays);
} catch (err: any) {
stats.errors.push(`日志清理失败: ${err.message}`);
}
// 2. Trash old files from cloud drives
try {
const result = await cleanupCloudFiles(fileDays);
stats.filesTrashed += result.trashed;
stats.errors.push(...result.errors);
} catch (err: any) {
stats.errors.push(`文件清理失败: ${err.message}`);
}
// 3. Empty recycle bin (only if enabled, and only if we trashed something)
if (emptyTrashEnabled && stats.filesTrashed > 0) {
try {
const result = await emptyAllTrash();
stats.trashEmptied = result.emptied;
stats.errors.push(...result.errors);
} catch (err: any) {
stats.errors.push(`清空回收站失败: ${err.message}`);
}
}
// Save last run timestamp and stats
updateSystemConfig('cleanup_last_run', formatLocalDateTime());
updateSystemConfig('cleanup_last_stats',
JSON.stringify({ filesTrashed: stats.filesTrashed, logsDeleted: stats.logsDeleted, trashEmptied: stats.trashEmptied, errors: stats.errors.length })
);
return stats;
}
/**
* Check if a daily cleanup is due and run it.
* Called periodically by the scheduler (setInterval).
*/
export async function checkAndRunScheduledCleanup(): Promise<void> {
const enabled = getSystemConfig('cleanup_enabled');
if (enabled !== 'true') return;
const lastRun = getSystemConfig('cleanup_last_run');
const todayStr = formatLocalDate();
if (lastRun && lastRun.startsWith(todayStr)) return;
console.log(`[Cleanup] Scheduled cleanup starting at ${new Date().toISOString()}...`);
const stats = await runFullCleanup();
console.log(`[Cleanup] Done: trashed ${stats.filesTrashed} folders, deleted ${stats.logsDeleted} logs, emptied trash: ${stats.trashEmptied}, errors: ${stats.errors.length}`);
}