补全前端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从仅前端 → 全部有后端读取模块
267 lines
11 KiB
TypeScript
Executable File
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}`);
|
|
}
|