v0.2.7: 修复Redis连接 + 启动管理后台
- 修复Redis认证 (配置密码) - 启动Python管理后台 (端口9531, 15个功能开关) - 统一版本号 0.2.7 - 更新docker-compose.yml (镜像版本/Redis URL/Admin服务)
This commit is contained in:
254
source_clean/src/cloud/cleanup.service.ts
Executable file
254
source_clean/src/cloud/cleanup.service.ts
Executable 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}`);
|
||||
}
|
||||
Reference in New Issue
Block a user