Files
CloudSearch/source_clean/src/cloud/cleanup.service.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

267 lines
11 KiB
TypeScript
Executable File

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 { CloudConfig, getActiveCloudConfigs } from './credential.service';
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, whitelistDirs?: string[]): 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, whitelistDirs?: string[]): 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() {
const configs = getActiveCloudConfigs();
return configs.filter((c): c is CloudConfig & { cookie: string } => !!c.cookie);
}
/**
* 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;
// Read whitelist from system config
let whitelist: string[] = [];
try {
const raw = getSystemConfig('cleanup_whitelist_dirs');
if (raw) whitelist = JSON.parse(raw);
} catch {}
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, whitelist);
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;
// Read whitelist from system config
let whitelist: string[] = [];
try {
const raw = getSystemConfig('cleanup_whitelist_dirs');
if (raw) whitelist = JSON.parse(raw);
} catch {}
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, whitelist);
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}`);
}