586 lines
23 KiB
TypeScript
586 lines
23 KiB
TypeScript
import { getDb } from '../database/database';
|
||
import { localTimestamp, formatLocalDate, formatLocalDateTime } from '../utils/time';
|
||
import { encrypt, decrypt, isEncrypted } from '../utils/crypto';
|
||
|
||
// ── Background Used-Space Calculation ──────────────────────────
|
||
|
||
/**
|
||
* Fire-and-forget: recursively calculate used space for a quark drive
|
||
* and update the database when done.
|
||
*/
|
||
async function calculateUsedSpaceAsync(cookie: string, configId: number): Promise<void> {
|
||
const { calculateUsedSpace } = require('./drivers/quark-cleanup');
|
||
const usedBytes = await calculateUsedSpace(cookie);
|
||
if (usedBytes > 0) {
|
||
const usedFormatted = usedBytes >= 1024 ** 4
|
||
? (usedBytes / 1024 ** 4).toFixed(1) + ' TB'
|
||
: usedBytes >= 1024 ** 3
|
||
? (usedBytes / 1024 ** 3).toFixed(1) + ' GB'
|
||
: (usedBytes / 1024 ** 2).toFixed(1) + ' MB';
|
||
const db = getDb();
|
||
db.prepare(
|
||
`UPDATE cloud_configs SET storage_used = ?, updated_at = ? WHERE id = ?`
|
||
).run(usedFormatted, localTimestamp(), configId);
|
||
console.log(`[UsedSpace] Updated config #${configId}: used=${usedFormatted}`);
|
||
}
|
||
}
|
||
|
||
export interface CloudConfig {
|
||
id: number;
|
||
cloud_type: string;
|
||
cookie?: string;
|
||
nickname?: string;
|
||
is_active: number;
|
||
promotion_account?: string;
|
||
is_transfer_enabled: number;
|
||
is_primary: number;
|
||
storage_used?: string;
|
||
storage_total?: string;
|
||
checkin_status: string; // 'none'|'success'|'failed'|'pending'|'skipped'
|
||
last_checkin_at?: string;
|
||
checkin_message?: string;
|
||
consecutive_failures: number;
|
||
last_used_at?: string;
|
||
total_saves: number;
|
||
created_at: string;
|
||
updated_at: string;
|
||
verification_status?: string;
|
||
cloud_type_uid?: string;
|
||
}
|
||
|
||
// ── Cookie Encryption Helper ──────────────────────────────────────
|
||
/** Decrypt cookie. Handles legacy plaintext data transparently. */
|
||
function decryptCookie(encrypted: string | null | undefined): string {
|
||
if (!encrypted) return '';
|
||
// If already plaintext (legacy data), return as-is
|
||
if (!isEncrypted(encrypted)) return encrypted;
|
||
return decrypt(encrypted);
|
||
}
|
||
|
||
/**
|
||
* Extract Quark __uid from cookie string.
|
||
* Used for dedup: same cloud_type + same __uid = same account.
|
||
*/
|
||
function extractQuarkUid(cookie: string): string | null {
|
||
const match = cookie.match(/(?:^|;\s*)__uid=([^;]+)/);
|
||
return match ? match[1] : null;
|
||
}
|
||
|
||
// ── Config CRUD ──────────────────────────────────────────────────
|
||
|
||
export function getCloudConfigs(): CloudConfig[] {
|
||
const db = getDb();
|
||
return db.prepare(
|
||
`SELECT id, cloud_type, cookie, nickname, is_active, promotion_account, is_transfer_enabled, is_primary, storage_used, storage_total,
|
||
cloud_type_uid,
|
||
checkin_status, last_checkin_at, checkin_message, consecutive_failures,
|
||
last_used_at, total_saves, created_at, updated_at, verification_status
|
||
FROM cloud_configs ORDER BY id ASC`
|
||
).all() as CloudConfig[];
|
||
}
|
||
|
||
export function getAvailableClouds(): CloudConfig[] {
|
||
const db = getDb();
|
||
return db.prepare(
|
||
`SELECT id, cloud_type, nickname, is_active, promotion_account, is_transfer_enabled, is_primary, storage_used, storage_total,
|
||
cloud_type_uid,
|
||
checkin_status, last_checkin_at, checkin_message, consecutive_failures,
|
||
last_used_at, total_saves, created_at, updated_at
|
||
FROM cloud_configs WHERE is_active = 1 ORDER BY id ASC`
|
||
).all() as CloudConfig[];
|
||
}
|
||
|
||
/** Returns the first active config matching the given cloud type. */
|
||
export function getCloudConfigByType(cloudType: string): CloudConfig | undefined {
|
||
const db = getDb();
|
||
return db.prepare(
|
||
`SELECT id, cloud_type, cookie, nickname, is_active, promotion_account, is_transfer_enabled, is_primary, storage_used, storage_total,
|
||
cloud_type_uid,
|
||
checkin_status, last_checkin_at, checkin_message, consecutive_failures,
|
||
last_used_at, total_saves, created_at, updated_at, verification_status
|
||
FROM cloud_configs WHERE cloud_type = ? AND is_active = 1
|
||
ORDER BY id ASC LIMIT 1`
|
||
).get(cloudType) as CloudConfig | undefined;
|
||
}
|
||
|
||
export function getCloudConfigById(id: number): CloudConfig | undefined {
|
||
const db = getDb();
|
||
return db.prepare(
|
||
`SELECT id, cloud_type, cookie, nickname, is_active, promotion_account, is_transfer_enabled, is_primary, storage_used, storage_total,
|
||
cloud_type_uid,
|
||
checkin_status, last_checkin_at, checkin_message, consecutive_failures,
|
||
last_used_at, total_saves, created_at, updated_at, verification_status
|
||
FROM cloud_configs WHERE id = ?`
|
||
).get(id) as CloudConfig | undefined;
|
||
}
|
||
|
||
/** Returns all active cloud configs (used by save flow for cloud type switching). */
|
||
export function getActiveCloudConfigs(): CloudConfig[] {
|
||
const db = getDb();
|
||
return db.prepare(
|
||
`SELECT id, cloud_type, cookie, nickname, is_active, promotion_account, is_transfer_enabled, is_primary, storage_used, storage_total,
|
||
cloud_type_uid,
|
||
checkin_status, last_checkin_at, checkin_message, consecutive_failures,
|
||
last_used_at, total_saves, created_at, updated_at
|
||
FROM cloud_configs WHERE is_active = 1
|
||
ORDER BY cloud_type ASC, id ASC`
|
||
).all() as CloudConfig[];
|
||
}
|
||
|
||
/**
|
||
* Toggle the is_primary flag for a cloud config.
|
||
* Enforces max 2 primary accounts per cloud type.
|
||
*/
|
||
export function togglePrimary(id: number, setPrimary: boolean): CloudConfig {
|
||
const db = getDb();
|
||
const config = getCloudConfigById(id);
|
||
if (!config) throw new Error(`Cloud config ${id} not found`);
|
||
|
||
if (setPrimary) {
|
||
// Check how many primary accounts already exist for this cloud type
|
||
const primaryCount = db.prepare(
|
||
`SELECT COUNT(*) as c FROM cloud_configs WHERE cloud_type = ? AND is_primary = 1 AND id != ?`
|
||
).get(config.cloud_type, id) as { c: number };
|
||
if (primaryCount.c >= 2) {
|
||
throw new Error(`同类型网盘最多只能设置 2 个默认账号(已存在 ${primaryCount.c} 个)`);
|
||
}
|
||
}
|
||
|
||
db.prepare(`UPDATE cloud_configs SET is_primary = ?, updated_at = datetime('now', 'localtime') WHERE id = ?`)
|
||
.run(setPrimary ? 1 : 0, id);
|
||
|
||
return getCloudConfigById(id)!;
|
||
}
|
||
|
||
export function saveCloudConfig(data: {
|
||
id?: number;
|
||
cloud_type: string;
|
||
cookie?: string;
|
||
nickname?: string;
|
||
is_active?: number;
|
||
promotion_account?: string;
|
||
is_transfer_enabled?: number;
|
||
storage_used?: string;
|
||
storage_total?: string;
|
||
}): CloudConfig {
|
||
const db = getDb();
|
||
// Encrypt cookie before storing
|
||
const encryptedCookie = data.cookie ? encrypt(data.cookie) : null;
|
||
|
||
// Extract cloud_type_uid from cookie (Quark __uid)
|
||
let cloudTypeUid: string | null = null;
|
||
if (data.cookie) {
|
||
cloudTypeUid = extractQuarkUid(data.cookie);
|
||
}
|
||
|
||
if (data.id) {
|
||
// Update by ID — always succeeds
|
||
db.prepare(
|
||
`UPDATE cloud_configs SET
|
||
cloud_type = COALESCE(?, cloud_type),
|
||
cookie = COALESCE(?, cookie),
|
||
nickname = COALESCE(?, nickname),
|
||
is_active = COALESCE(?, is_active),
|
||
promotion_account = COALESCE(?, promotion_account),
|
||
is_transfer_enabled = COALESCE(?, is_transfer_enabled),
|
||
storage_used = COALESCE(?, storage_used),
|
||
storage_total = COALESCE(?, storage_total),
|
||
cloud_type_uid = COALESCE(?, cloud_type_uid),
|
||
consecutive_failures = 0,
|
||
updated_at = ?
|
||
WHERE id = ?`
|
||
).run(data.cloud_type, encryptedCookie || null, data.nickname || null, data.is_active == null ? 1 : Number(data.is_active), data.promotion_account ?? '', data.is_transfer_enabled == null ? 1 : Number(data.is_transfer_enabled), data.storage_used || null, data.storage_total || null, cloudTypeUid || null, localTimestamp(), data.id);
|
||
} else {
|
||
// Try to find existing config by cloud_type + cloud_type_uid
|
||
let existing: any = null;
|
||
if (cloudTypeUid) {
|
||
existing = db.prepare(
|
||
`SELECT id FROM cloud_configs WHERE cloud_type = ? AND cloud_type_uid = ? LIMIT 1`
|
||
).get(data.cloud_type, cloudTypeUid);
|
||
}
|
||
|
||
// Fallback: match by cloud_type alone (legacy records without cloud_type_uid)
|
||
if (!existing) {
|
||
existing = db.prepare(
|
||
'SELECT id FROM cloud_configs WHERE cloud_type = ? AND is_active = 1 LIMIT 1'
|
||
).get(data.cloud_type) as any;
|
||
}
|
||
|
||
if (existing) {
|
||
db.prepare(
|
||
`UPDATE cloud_configs SET
|
||
cookie = COALESCE(?, cookie),
|
||
nickname = COALESCE(?, nickname),
|
||
is_active = COALESCE(?, is_active),
|
||
promotion_account = COALESCE(?, promotion_account),
|
||
is_transfer_enabled = COALESCE(?, is_transfer_enabled),
|
||
storage_used = COALESCE(?, storage_used),
|
||
storage_total = COALESCE(?, storage_total),
|
||
cloud_type_uid = COALESCE(?, cloud_type_uid),
|
||
consecutive_failures = 0,
|
||
updated_at = ?
|
||
WHERE id = ?`
|
||
).run(encryptedCookie || null, data.nickname || null, data.is_active == null ? 1 : Number(data.is_active), data.promotion_account ?? '', data.is_transfer_enabled == null ? 1 : Number(data.is_transfer_enabled), data.storage_used || null, data.storage_total || null, cloudTypeUid || null, localTimestamp(), existing.id);
|
||
|
||
// Re-read savedId for return
|
||
const savedId = existing.id;
|
||
return db.prepare(
|
||
`SELECT id, cloud_type, cookie, nickname, is_active, promotion_account, is_transfer_enabled, is_primary, storage_used, storage_total,
|
||
cloud_type_uid,
|
||
checkin_status, last_checkin_at, checkin_message, consecutive_failures,
|
||
last_used_at, total_saves, created_at, updated_at
|
||
FROM cloud_configs WHERE id = ?`
|
||
).get(savedId) as CloudConfig;
|
||
}
|
||
|
||
// No existing config found — insert new
|
||
db.prepare(
|
||
'INSERT INTO cloud_configs (cloud_type, cookie, nickname, is_active, promotion_account, is_transfer_enabled, storage_used, storage_total, cloud_type_uid, consecutive_failures) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 0)'
|
||
).run(data.cloud_type, encryptedCookie || null, data.nickname || null, data.is_active == null ? 1 : Number(data.is_active), data.promotion_account ?? '', data.is_transfer_enabled == null ? 1 : Number(data.is_transfer_enabled), data.storage_used || null, data.storage_total || null, cloudTypeUid || null);
|
||
}
|
||
|
||
const savedId = data.id || (db.prepare('SELECT last_insert_rowid() as id').get() as any).id;
|
||
return db.prepare(
|
||
`SELECT id, cloud_type, cookie, nickname, is_active, promotion_account, is_transfer_enabled, is_primary, storage_used, storage_total,
|
||
cloud_type_uid,
|
||
checkin_status, last_checkin_at, checkin_message, consecutive_failures,
|
||
last_used_at, total_saves, created_at, updated_at
|
||
FROM cloud_configs WHERE id = ?`
|
||
).get(savedId) as CloudConfig;
|
||
}
|
||
|
||
export function deleteCloudConfig(id: number): boolean {
|
||
const db = getDb();
|
||
const result = db.prepare('DELETE FROM cloud_configs WHERE id = ?').run(id);
|
||
return result.changes > 0;
|
||
}
|
||
|
||
// ── Cookie Validation ────────────────────────────────────────────
|
||
|
||
async function fetchQuarkNickname(cookie: string): Promise<string | null> {
|
||
const MAX_RETRIES = 2;
|
||
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
||
try {
|
||
const response = await fetch('https://pan.quark.cn/account/info?fr=pc&platform=pc', {
|
||
headers: {
|
||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) quark-cloud-drive/3.14.2 Chrome/112.0.5615.165 Electron/24.1.3.8 Safari/537.36 Channel/pckk_other_ch',
|
||
'Cookie': cookie,
|
||
'Accept': 'application/json',
|
||
'Referer': 'https://pan.quark.cn/',
|
||
},
|
||
signal: AbortSignal.timeout(15000),
|
||
});
|
||
if (!response.ok) return null;
|
||
const data = await response.json() as any;
|
||
if (data?.data?.nickname) return data.data.nickname;
|
||
} catch {
|
||
if (attempt < MAX_RETRIES) {
|
||
await new Promise(r => setTimeout(r, 1500));
|
||
continue;
|
||
}
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
export async function testCloudConnection(id: number): Promise<{
|
||
success: boolean;
|
||
message: string;
|
||
nickname?: string;
|
||
storage_used?: string;
|
||
storage_total?: string;
|
||
}> {
|
||
const config = getCloudConfigById(id);
|
||
if (!config) {
|
||
return { success: false, message: 'Cloud config not found' };
|
||
}
|
||
|
||
if (!config.cookie) {
|
||
return { success: false, message: 'Cookie not configured' };
|
||
}
|
||
|
||
try {
|
||
let valid = false;
|
||
let nickname = '';
|
||
let storageUsed = config.storage_used || '';
|
||
let storageTotal = config.storage_total || '';
|
||
|
||
if (config.cloud_type === 'baidu') {
|
||
const { BaiduDriver } = require('./drivers/baidu.driver');
|
||
const driver = new BaiduDriver({ cookie: config.cookie, nickname: config.nickname });
|
||
valid = await driver.validate();
|
||
if (valid) {
|
||
const info = await driver.getUserInfo();
|
||
if (info) {
|
||
nickname = config.nickname || info.nickname || '百度网盘';
|
||
const fmt = (b: number) => b >= 1024**3 ? (b/1024**3).toFixed(2)+' GB' : (b/1024**2).toFixed(2)+' MB';
|
||
storageUsed = fmt(info.usedBytes);
|
||
storageTotal = fmt(info.totalBytes);
|
||
}
|
||
}
|
||
} else {
|
||
const decodedCookie = decrypt(config.cookie);
|
||
const { QuarkDriver } = require('./drivers/quark.driver');
|
||
const driver = new QuarkDriver({ cookie: decodedCookie, nickname: config.nickname });
|
||
valid = await driver.validate();
|
||
if (valid) {
|
||
nickname = config.nickname || (await fetchQuarkNickname(decodedCookie)) || '夸克网盘';
|
||
const storage = await driver.getStorageInfoQuick(config.storage_total);
|
||
storageTotal = (storage.total !== '-' && storage.total !== '0 B') ? storage.total : (config.storage_total || '');
|
||
storageUsed = (storage.used && storage.used !== '-' && storage.used !== '0 B') ? storage.used : (config.storage_used || '');
|
||
}
|
||
}
|
||
|
||
const db = getDb();
|
||
if (!valid) {
|
||
db.prepare(
|
||
`UPDATE cloud_configs SET verification_status = 'invalid', updated_at = ? WHERE id = ?`
|
||
).run(localTimestamp(), id);
|
||
return { success: false, message: '连接失败:Cookie 无效或已过期,或网络暂时异常' };
|
||
}
|
||
|
||
db.prepare(
|
||
`UPDATE cloud_configs SET nickname = ?, storage_total = ?, storage_used = ?, is_active = 1, verification_status = 'valid', updated_at = ? WHERE id = ?`
|
||
).run(nickname, storageTotal, storageUsed, localTimestamp(), id);
|
||
|
||
// Fire-and-forget: recalculate used space in background (slow for big drives)
|
||
if (config.cloud_type === 'quark') {
|
||
calculateUsedSpaceAsync(decrypt(config.cookie), id).catch(err => console.error(`[UsedSpace] Background calc failed for #${id}:`, err.message));
|
||
}
|
||
|
||
return {
|
||
success: true,
|
||
message: '连接成功',
|
||
nickname,
|
||
storage_used: storageUsed,
|
||
storage_total: storageTotal,
|
||
};
|
||
} catch (err: any) {
|
||
try {
|
||
const db = getDb();
|
||
db.prepare(
|
||
`UPDATE cloud_configs SET verification_status = 'invalid', updated_at = ? WHERE id = ?`
|
||
).run(localTimestamp(), id);
|
||
} catch {}
|
||
return { success: false, message: `连接失败:${err.message || '未知错误'}` };
|
||
}
|
||
}
|
||
|
||
export async function testCloudConnectionWithCookie(cloudType: string, cookie: string): Promise<{
|
||
success: boolean;
|
||
message: string;
|
||
nickname?: string;
|
||
storage_used?: string;
|
||
storage_total?: string;
|
||
}> {
|
||
try {
|
||
const { QuarkDriver } = require('./drivers/quark.driver');
|
||
const driver = new QuarkDriver({ cookie, nickname: '' });
|
||
const valid = await driver.validate();
|
||
if (!valid) {
|
||
return { success: false, message: '连接失败:Cookie 无效或已过期' };
|
||
}
|
||
const nickname = (await fetchQuarkNickname(cookie)) || cloudType;
|
||
// getStorageInfo may timeout from overseas servers, don't fail if it does
|
||
let storage: { used: string; total: string } = { used: '-', total: '-' };
|
||
try {
|
||
const s = await driver.getStorageInfoQuick();
|
||
if (s) {
|
||
storage = { used: s.used || '-', total: s.total || '-' };
|
||
}
|
||
} catch {
|
||
// storage info is optional
|
||
}
|
||
return {
|
||
success: true,
|
||
message: '连接成功',
|
||
nickname,
|
||
storage_used: storage.used,
|
||
storage_total: storage.total,
|
||
};
|
||
} catch (err: any) {
|
||
return { success: false, message: `连接失败:${err.message || '未知错误'}` };
|
||
}
|
||
}
|
||
|
||
// ── Unified Credential Validation ─────────────────────────────────
|
||
|
||
export interface CredentialValidationResult {
|
||
valid: boolean;
|
||
config?: CloudConfig;
|
||
errorCode?: string;
|
||
message: string;
|
||
}
|
||
|
||
/**
|
||
* Get and validate a credential for the given cloud type.
|
||
*
|
||
* This is the unified entry point for all save/transfer operations.
|
||
* It handles:
|
||
* 1. Finding an active config with < 5 consecutive failures (round-robin)
|
||
* 2. Validating cookie freshness via driver.validate()
|
||
* 3. Returning structured result with error codes
|
||
*
|
||
* Reference: search-ucmao get_and_validate_credential() pattern.
|
||
*/
|
||
export async function getAndValidateCredential(cloudType: string, ipAddress?: string): Promise<CredentialValidationResult> {
|
||
const db = getDb();
|
||
|
||
let config: CloudConfig | undefined;
|
||
|
||
|
||
|
||
if (!ipAddress) {
|
||
// No IP info — fallback to simple LUR
|
||
config = db.prepare(
|
||
`SELECT * FROM cloud_configs
|
||
WHERE cloud_type = ? AND is_active = 1
|
||
AND consecutive_failures < 5
|
||
ORDER BY is_primary DESC, last_used_at ASC NULLS FIRST
|
||
LIMIT 1`
|
||
).get(cloudType) as CloudConfig | undefined;
|
||
} else {
|
||
// Get today's date in Shanghai time
|
||
const today = (() => {
|
||
const now = new Date();
|
||
const shanghai = new Date(now.toLocaleString('en-US', { timeZone: 'Asia/Shanghai' }));
|
||
return shanghai.toISOString().slice(0, 10);
|
||
})();
|
||
|
||
// Count how many times this IP has saved today for this cloud type
|
||
const ipCountRow = db.prepare(
|
||
`SELECT COALESCE(SUM(save_count), 0) as total
|
||
FROM ip_daily_save_counts
|
||
WHERE ip_address = ? AND date = ? AND cloud_type = ?`
|
||
).get(ipAddress, today, cloudType) as { total: number };
|
||
|
||
const ipTodayCount = ipCountRow?.total || 0;
|
||
|
||
// How many primary accounts does this cloud type have?
|
||
const primaryCountRow = db.prepare(
|
||
`SELECT COUNT(*) as c FROM cloud_configs WHERE cloud_type = ? AND is_primary = 1 AND is_active = 1`
|
||
).get(cloudType) as { c: number };
|
||
const primaryCount = primaryCountRow?.c || 0;
|
||
const primaryThreshold = primaryCount * 2; // Each primary account gets 2 uses per IP
|
||
|
||
if (ipTodayCount < primaryThreshold) {
|
||
// First N saves (primaryCount × 2) — use primary accounts (is_primary=1), fallback to any healthy
|
||
config = db.prepare(
|
||
`SELECT * FROM cloud_configs
|
||
WHERE cloud_type = ? AND is_active = 1
|
||
AND consecutive_failures < 5
|
||
ORDER BY is_primary DESC, last_used_at ASC NULLS FIRST
|
||
LIMIT 1`
|
||
).get(cloudType) as CloudConfig | undefined;
|
||
} else {
|
||
// After primary threshold — exclude accounts this IP has already used today,
|
||
// fall back to other available accounts round-robin
|
||
const usedConfigIds = db.prepare(
|
||
`SELECT DISTINCT config_id FROM ip_daily_save_counts
|
||
WHERE ip_address = ? AND date = ? AND cloud_type = ?
|
||
ORDER BY config_id`
|
||
).all(ipAddress, today, cloudType) as { config_id: number }[];
|
||
|
||
const usedIds = usedConfigIds.map(r => r.config_id);
|
||
const placeholders = usedIds.length > 0 ? usedIds.map(() => '?').join(',') : '-1';
|
||
|
||
config = db.prepare(
|
||
`SELECT * FROM cloud_configs
|
||
WHERE cloud_type = ? AND is_active = 1
|
||
AND consecutive_failures < 5
|
||
AND id NOT IN (${placeholders})
|
||
ORDER BY last_used_at ASC NULLS FIRST
|
||
LIMIT 1`
|
||
).get(cloudType, ...usedIds) as CloudConfig | undefined;
|
||
|
||
// If all accounts have been used by this IP, fall back to primary
|
||
if (!config) {
|
||
config = db.prepare(
|
||
`SELECT * FROM cloud_configs
|
||
WHERE cloud_type = ? AND is_active = 1
|
||
AND consecutive_failures < 5
|
||
ORDER BY is_primary DESC, last_used_at ASC NULLS FIRST
|
||
LIMIT 1`
|
||
).get(cloudType) as CloudConfig | undefined;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!config) {
|
||
return {
|
||
valid: false,
|
||
errorCode: 'NO_AVAILABLE_DRIVE',
|
||
message: `Cloud type "${cloudType}" is not configured or no available drives`,
|
||
};
|
||
}
|
||
|
||
if (!config.cookie) {
|
||
return {
|
||
valid: false,
|
||
errorCode: 'COOKIE_MISSING',
|
||
message: `Cookie not configured for ${cloudType} drive #${config.id}`,
|
||
};
|
||
}
|
||
|
||
try {
|
||
// Decrypt cookie before validation
|
||
const decryptedCookie = decryptCookie(config.cookie);
|
||
if (!decryptedCookie) {
|
||
return {
|
||
valid: false,
|
||
errorCode: 'COOKIE_MISSING',
|
||
message: `Cookie not configured for ${cloudType} drive #${config.id}`,
|
||
};
|
||
}
|
||
|
||
let cookieValid = false;
|
||
if (cloudType === 'baidu') {
|
||
const { BaiduDriver } = require('./drivers/baidu.driver');
|
||
const driver = new BaiduDriver({ cookie: decryptedCookie, nickname: config.nickname });
|
||
cookieValid = await driver.validate();
|
||
} else {
|
||
const { QuarkDriver } = require('./drivers/quark.driver');
|
||
const driver = new QuarkDriver({ cookie: decryptedCookie, nickname: config.nickname });
|
||
cookieValid = await driver.validate();
|
||
}
|
||
|
||
if (!cookieValid) {
|
||
db.prepare(
|
||
`UPDATE cloud_configs SET verification_status = 'invalid', updated_at = ? WHERE id = ?`
|
||
).run(localTimestamp(), config.id);
|
||
return {
|
||
valid: false,
|
||
errorCode: 'COOKIE_EXPIRED',
|
||
message: `Cookie expired or invalid for ${cloudType} drive #${config.id}`,
|
||
};
|
||
}
|
||
|
||
// Track IP daily usage count (if ipAddress provided)
|
||
if (ipAddress && config) {
|
||
const today = (() => {
|
||
const now = new Date();
|
||
const shanghai = new Date(now.toLocaleString('en-US', { timeZone: 'Asia/Shanghai' }));
|
||
return shanghai.toISOString().slice(0, 10);
|
||
})();
|
||
db.prepare(
|
||
`INSERT INTO ip_daily_save_counts (ip_address, date, cloud_type, config_id, save_count)
|
||
VALUES (?, ?, ?, ?, 1)
|
||
ON CONFLICT(ip_address, date, cloud_type, config_id)
|
||
DO UPDATE SET save_count = save_count + 1`
|
||
).run(ipAddress, today, cloudType, config.id);
|
||
}
|
||
|
||
return {
|
||
valid: true,
|
||
config: { ...config, cookie: decryptedCookie },
|
||
message: 'ok',
|
||
};
|
||
} catch (err: any) {
|
||
return {
|
||
valid: false,
|
||
errorCode: 'VALIDATION_ERROR',
|
||
message: `Credential validation failed: ${err.message}`,
|
||
};
|
||
}
|
||
}
|