chore: v0.1.6 UI优化 - 两列网格布局、暗色适配、系统配置浮窗保存、退出登录统一到侧边栏
This commit is contained in:
1191
packages/backend/src/cloud/baidu.driver.ts
Normal file
1191
packages/backend/src/cloud/baidu.driver.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -19,7 +19,7 @@ interface CleanupOpResult { trashed: number; errors: string[] }
|
||||
|
||||
interface CloudCleanupDriver {
|
||||
/** Trash date folders (YYYY-MM-DD) older than `days`. */
|
||||
cleanupOldDateFolders(days: number): Promise<CleanupOpResult>;
|
||||
cleanupOldDateFolders(days: number, whitelistDirs?: string[]): Promise<CleanupOpResult>;
|
||||
/**
|
||||
* If used space exceeds thresholdPercent% of TOTAL capacity,
|
||||
* delete oldest date folders until totalBytes * deletePercent/100
|
||||
@@ -27,7 +27,7 @@ interface CloudCleanupDriver {
|
||||
* @param thresholdPercent — trigger when usage >= this % of total
|
||||
* @param deletePercent — free this % of total capacity
|
||||
*/
|
||||
cleanupBySpaceThreshold(thresholdPercent: number, deletePercent: number): Promise<CleanupOpResult>;
|
||||
cleanupBySpaceThreshold(thresholdPercent: number, deletePercent: number, whitelistDirs?: string[]): Promise<CleanupOpResult>;
|
||||
/** Permanently empty the recycle bin. */
|
||||
emptyTrash(): Promise<boolean>;
|
||||
}
|
||||
@@ -62,6 +62,18 @@ interface CleanupStats {
|
||||
errors: string[];
|
||||
}
|
||||
|
||||
/** Read whitelist dirs from system_configs (cleanup_whitelist_dirs). */
|
||||
export function getWhitelistDirs(): string[] {
|
||||
const raw = getSystemConfig('cleanup_whitelist_dirs');
|
||||
if (!raw) return [];
|
||||
try {
|
||||
const parsed = JSON.parse(raw);
|
||||
return Array.isArray(parsed) ? parsed.filter((d: any) => typeof d === 'string') : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/** 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();
|
||||
@@ -77,6 +89,7 @@ function getActiveCleanupConfigs(): Array<{ id: number; cloud_type: string; cook
|
||||
*/
|
||||
async function cleanupCloudFiles(days: number): Promise<CleanupOpResult> {
|
||||
const configs = getActiveCleanupConfigs();
|
||||
const whitelistDirs = getWhitelistDirs();
|
||||
const errors: string[] = [];
|
||||
let totalTrashed = 0;
|
||||
|
||||
@@ -87,7 +100,7 @@ async function cleanupCloudFiles(days: number): Promise<CleanupOpResult> {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const result = await driver.cleanupOldDateFolders(days);
|
||||
const result = await driver.cleanupOldDateFolders(days, whitelistDirs);
|
||||
totalTrashed += result.trashed;
|
||||
errors.push(...result.errors.map(e => `[${cfg.cloud_type}#${cfg.id}] ${e}`));
|
||||
} catch (err: any) {
|
||||
@@ -108,6 +121,7 @@ async function cleanupAllBySpaceThreshold(
|
||||
deletePercent: number,
|
||||
): Promise<CleanupOpResult> {
|
||||
const configs = getActiveCleanupConfigs();
|
||||
const whitelistDirs = getWhitelistDirs();
|
||||
const errors: string[] = [];
|
||||
let totalTrashed = 0;
|
||||
|
||||
@@ -118,7 +132,7 @@ async function cleanupAllBySpaceThreshold(
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const result = await driver.cleanupBySpaceThreshold(thresholdPercent, deletePercent);
|
||||
const result = await driver.cleanupBySpaceThreshold(thresholdPercent, deletePercent, whitelistDirs);
|
||||
totalTrashed += result.trashed;
|
||||
errors.push(...result.errors.map(e => `[${cfg.cloud_type}#${cfg.id}] ${e}`));
|
||||
} catch (err: any) {
|
||||
|
||||
@@ -322,31 +322,86 @@ export function cleanupOldSaveRecords(): void {
|
||||
// ── Storage Refresh ───────────────────────────────────────────────
|
||||
|
||||
export async function refreshAllStorageInfo(): Promise<void> {
|
||||
const configs = getActiveCloudConfigs().filter(c => c.cloud_type === 'quark' && c.cookie);
|
||||
const configs = getActiveCloudConfigs().filter(c => c.cookie);
|
||||
if (configs.length === 0) return;
|
||||
|
||||
const verifyCookies = getSystemConfig('cleanup_verify_enabled') === 'true';
|
||||
|
||||
for (const cfg of configs) {
|
||||
try {
|
||||
const { QuarkDriver } = require('./drivers/quark.driver');
|
||||
const driver = new QuarkDriver({ cookie: decrypt(cfg.cookie!), nickname: cfg.nickname });
|
||||
const storage = await driver.getStorageInfo(
|
||||
decrypt(cfg.cookie!),
|
||||
(fullUsed: string, total: string) => {
|
||||
const db = getDb();
|
||||
db.prepare(
|
||||
`UPDATE cloud_configs SET storage_used = ?, storage_total = ? WHERE id = ?`
|
||||
).run(fullUsed, total, cfg.id);
|
||||
console.log(`[Storage] Background calibration done for quark#${cfg.id}: ${fullUsed} / ${total}`);
|
||||
const db = getDb();
|
||||
const decryptedCookie = decrypt(cfg.cookie!);
|
||||
|
||||
switch (cfg.cloud_type) {
|
||||
case 'quark': {
|
||||
const driver = new QuarkDriver({ cookie: decryptedCookie, nickname: cfg.nickname });
|
||||
|
||||
// Get storage info (includes background calibration callback)
|
||||
const storage = await driver.getStorageInfo(
|
||||
(fullUsed: string, total: string) => {
|
||||
const dbInner = getDb();
|
||||
dbInner.prepare(
|
||||
`UPDATE cloud_configs SET storage_used = ?, storage_total = ? WHERE id = ?`
|
||||
).run(fullUsed, total, cfg.id);
|
||||
console.log(`[Storage] Background calibration done for quark#${cfg.id}: ${fullUsed} / ${total}`);
|
||||
}
|
||||
);
|
||||
if (storage.totalBytes > 0 || storage.usedBytes > 0) {
|
||||
db.prepare(
|
||||
`UPDATE cloud_configs SET storage_used = ?, storage_total = ? WHERE id = ?`
|
||||
).run(storage.used, storage.total, cfg.id);
|
||||
console.log(`[Storage] Updated quark#${cfg.id}: ${storage.used} / ${storage.total}`);
|
||||
}
|
||||
|
||||
// Cookie verification
|
||||
if (verifyCookies) {
|
||||
const valid = await driver.validate();
|
||||
db.prepare(
|
||||
`UPDATE cloud_configs SET verification_status = ?, updated_at = ? WHERE id = ?`
|
||||
).run(valid ? 'valid' : 'invalid', localTimestamp(), cfg.id);
|
||||
console.log(`[Storage] Verification for quark#${cfg.id}: ${valid ? 'valid' : 'invalid'}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
);
|
||||
if (storage.totalBytes > 0 || storage.usedBytes > 0) {
|
||||
const db = getDb();
|
||||
db.prepare(
|
||||
`UPDATE cloud_configs SET storage_used = ?, storage_total = ? WHERE id = ?`
|
||||
).run(storage.used, storage.total, cfg.id);
|
||||
|
||||
case 'baidu': {
|
||||
const driver = new BaiduDriver({ cookie: decryptedCookie, nickname: cfg.nickname });
|
||||
|
||||
// Get storage info
|
||||
const storage = await driver.getStorageInfo();
|
||||
if (storage.used !== '0 B' && storage.total !== '0 B') {
|
||||
db.prepare(
|
||||
`UPDATE cloud_configs SET storage_used = ?, storage_total = ? WHERE id = ?`
|
||||
).run(storage.used, storage.total, cfg.id);
|
||||
console.log(`[Storage] Updated baidu#${cfg.id}: ${storage.used} / ${storage.total}`);
|
||||
}
|
||||
|
||||
// Cookie verification
|
||||
if (verifyCookies) {
|
||||
const valid = await driver.validate();
|
||||
db.prepare(
|
||||
`UPDATE cloud_configs SET verification_status = ?, updated_at = ? WHERE id = ?`
|
||||
).run(valid ? 'valid' : 'invalid', localTimestamp(), cfg.id);
|
||||
console.log(`[Storage] Verification for baidu#${cfg.id}: ${valid ? 'valid' : 'invalid'}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
console.log(`[Storage] Skipping ${cfg.cloud_type}#${cfg.id} — unsupported cloud type for storage refresh`);
|
||||
break;
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error(`[Storage] Failed to refresh quark#${cfg.id}:`, err.message);
|
||||
console.error(`[Storage] Failed to refresh ${cfg.cloud_type}#${cfg.id}:`, err.message);
|
||||
// On error, mark as invalid if verification is enabled
|
||||
if (verifyCookies) {
|
||||
try {
|
||||
const db = getDb();
|
||||
db.prepare(
|
||||
`UPDATE cloud_configs SET verification_status = 'invalid', updated_at = ? WHERE id = ?`
|
||||
).run(localTimestamp(), cfg.id);
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -306,6 +306,11 @@ function seedSystemConfigs(db: Database.Database): void {
|
||||
{ key: 'cleanup_space_threshold_percent', value: '90', description: '空间使用阈值百分比(超过此值时触发强制清理)' },
|
||||
{ key: 'cleanup_space_threshold_delete_percent', value: '10', description: '触发阈值清理时释放总空间的百分比(如 10 表示累计删除最旧文件直到达到总空间的 10%,6TB 总空间 → 释放 ~600GB)' },
|
||||
{ key: 'save_reuse_enabled', value: 'true', description: '启用分享链接复用(相同原始链接不再重复转存,直接复用之前的分享链接)' },
|
||||
{ key: 'cleanup_whitelist_dirs', value: '[]', description: '清理白名单目录名称列表(JSON数组),这些目录不会被自动清理' },
|
||||
{ key: 'storage_refresh_interval', value: '60', description: '存储空间刷新间隔(分钟),0=不自动刷新' },
|
||||
{ key: 'cleanup_auto_refresh_storage', value: 'true', description: '启用自动刷新存储空间信息' },
|
||||
{ key: 'cleanup_verify_enabled', value: 'true', description: '定期验证网盘 Cookie 有效性(随存储刷新一起执行)' },
|
||||
{ key: 'cleanup_verify_interval', value: '30', description: 'Cookie 有效性检测间隔(分钟)' },
|
||||
{ key: 'cleanup_last_run', value: '', description: '上次自动清理时间' },
|
||||
{ key: 'cleanup_last_stats', value: '', description: '上次清理结果统计(JSON)' },
|
||||
];
|
||||
|
||||
@@ -38,15 +38,33 @@ export async function getStorageInfoQuick(cookie: string, fallbackTotal?: string
|
||||
}
|
||||
}
|
||||
|
||||
// Quick used-space estimate: sum root-level file sizes + subdir sizes
|
||||
// Accurate used space via /member API (1 call, no full traversal needed)
|
||||
// Ref: pan.quark.cn/1/clouddrive/member returns use_capacity + total_capacity
|
||||
let usedBytes = 0;
|
||||
try {
|
||||
const rootFiles = await listRootDir(cookie);
|
||||
for (const f of rootFiles) {
|
||||
usedBytes += f.size || 0;
|
||||
const memberParams = new URLSearchParams({ pr: 'ucpro', fr: 'pc', uc_param_str: '', __t: String(Date.now()), __dt: '1000' });
|
||||
const memberResp = await fetch(`https://pan.quark.cn/1/clouddrive/member?${memberParams.toString()}`, {
|
||||
headers: getHeaders(cookie),
|
||||
signal: AbortSignal.timeout(10000),
|
||||
});
|
||||
if (memberResp.ok) {
|
||||
const memberData = await memberResp.json() as any;
|
||||
if (memberData.status === 200 && memberData.data?.use_capacity != null) {
|
||||
usedBytes = memberData.data.use_capacity;
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
|
||||
// Fallback: sum root-level file sizes (夸克 folders return size=0)
|
||||
if (usedBytes === 0) {
|
||||
try {
|
||||
const rootFiles = await listRootDir(cookie);
|
||||
for (const f of rootFiles) {
|
||||
usedBytes += f.size || 0;
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// Cache the result (3h window)
|
||||
const currentHourBlock = Math.floor(new Date().getHours() / 3);
|
||||
storageCache.bytes = usedBytes;
|
||||
@@ -231,7 +249,7 @@ export async function emptyTrash(cookie: string): Promise<boolean> {
|
||||
/**
|
||||
* Cleanup: trash date-named folders (YYYY-MM-DD) older than `days`.
|
||||
*/
|
||||
export async function cleanupOldDateFolders(cookie: string, days: number): Promise<{ trashed: number; errors: string[] }> {
|
||||
export async function cleanupOldDateFolders(cookie: string, days: number, whitelistDirs?: string[]): Promise<{ trashed: number; errors: string[] }> {
|
||||
const errors: string[] = [];
|
||||
const cutoff = new Date();
|
||||
cutoff.setDate(cutoff.getDate() - days);
|
||||
@@ -242,6 +260,7 @@ export async function cleanupOldDateFolders(cookie: string, days: number): Promi
|
||||
const oldFolders = rootItems.filter(item => {
|
||||
if (!item.dir) return false;
|
||||
if (!/^\d{4}-\d{2}-\d{2}$/.test(item.file_name)) return false;
|
||||
if (whitelistDirs && whitelistDirs.includes(item.file_name)) return false;
|
||||
return item.file_name < cutoffStr;
|
||||
});
|
||||
|
||||
@@ -270,6 +289,7 @@ export async function cleanupBySpaceThreshold(
|
||||
cookie: string,
|
||||
thresholdPercent: number,
|
||||
deletePercent: number,
|
||||
whitelistDirs?: string[],
|
||||
): Promise<{ trashed: number; errors: string[] }> {
|
||||
const errors: string[] = [];
|
||||
|
||||
@@ -288,6 +308,7 @@ export async function cleanupBySpaceThreshold(
|
||||
const rootItems = await listRootDir(cookie);
|
||||
const dateFolders = rootItems
|
||||
.filter(item => item.dir && /^\d{4}-\d{2}-\d{2}$/.test(item.file_name))
|
||||
.filter(item => !whitelistDirs || !whitelistDirs.includes(item.file_name))
|
||||
.sort((a, b) => a.file_name.localeCompare(b.file_name));
|
||||
|
||||
if (dateFolders.length === 0) return { trashed: 0, errors: [] };
|
||||
|
||||
@@ -177,10 +177,20 @@ async function start(): Promise<void> {
|
||||
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 — every 60 minutes
|
||||
const STORAGE_REFRESH_INTERVAL = 60 * 60 * 1000;
|
||||
setInterval(() => { refreshAllStorageInfo().catch(err => console.error('[Storage] Refresh error:', err.message)); }, STORAGE_REFRESH_INTERVAL);
|
||||
setTimeout(() => { refreshAllStorageInfo().catch(err => console.error('[Storage] Initial refresh error:', err.message)); }, 60000);
|
||||
// Storage info refresh scheduler — configurable via system_configs
|
||||
const { getSystemConfig: getSysCfg } = require('./admin/system-config.service');
|
||||
const storageRefreshEnabled = getSysCfg('cleanup_auto_refresh_storage') !== 'false';
|
||||
const storageRefreshMin = parseInt(getSysCfg('storage_refresh_interval') || '60', 10) || 60;
|
||||
const storageRefreshMs = storageRefreshMin * 60 * 1000;
|
||||
const verifyCookies = getSysCfg('cleanup_verify_enabled') === 'true';
|
||||
|
||||
if (!storageRefreshEnabled || storageRefreshMin === 0) {
|
||||
console.log(`[Storage] Auto-refresh disabled (enabled=${storageRefreshEnabled}, interval=${storageRefreshMin} min)`);
|
||||
} else {
|
||||
console.log(`[Storage] Auto-refresh every ${storageRefreshMin} minutes (verifyCookies=${verifyCookies})`);
|
||||
setInterval(() => { refreshAllStorageInfo().catch(err => console.error('[Storage] Refresh error:', err.message)); }, storageRefreshMs);
|
||||
setTimeout(() => { refreshAllStorageInfo().catch(err => console.error('[Storage] Initial refresh error:', err.message)); }, 60000);
|
||||
}
|
||||
|
||||
const server = app.listen(config.port, () => {
|
||||
console.log(`[Server] CloudSearch Backend running on port ${config.port} (${config.nodeEnv})`);
|
||||
|
||||
@@ -9,4 +9,4 @@
|
||||
* 修改此文件的同时请同步更新后端 package.json 中的 version 字段。
|
||||
*/
|
||||
|
||||
export const APP_VERSION = "0.1.1";
|
||||
export const APP_VERSION = "0.1.6";
|
||||
|
||||
Reference in New Issue
Block a user