v0.3.22: 存储刷新改用 /member API 秒级精准获取,删除文件遍历

根因: /1/clouddrive/capacity/detail 只返回总容量不含已用空间。
      之前的方案遍历所有文件计算已用空间,根目录估算只有 2.38 GB,
      全量遍历走后台回调但不被 refreshAllStorageInfo 接收。

修复: 发现 /1/clouddrive/member API 直接返回 use_capacity + total_capacity。
      getStorageInfoQuick/getStorageInfo 统一改为 member API 单次调用,
      返回 2.76 TB / 6 TB 精准值。删除昂贵的文件遍历逻辑。
      refreshAllStorageInfo 移除后台回调复杂度
This commit is contained in:
2026-05-17 17:52:06 +08:00
parent d87bc6fd5a
commit f9338e5906
4 changed files with 61 additions and 97 deletions

View File

@@ -1 +1 @@
0.3.21 0.3.22

View File

@@ -1 +1 @@
0.3.21 0.3.22

View File

@@ -352,7 +352,7 @@ export async function refreshAllStorageInfo(): Promise<void> {
const driver = new Driver({ cookie: cfg.cookie, nickname: cfg.nickname }); const driver = new Driver({ cookie: cfg.cookie, nickname: cfg.nickname });
// Try getStorageInfo first (quark cleanup API), fallback to getStorageInfoQuick // Try getStorageInfo (now uses fast /member API for accurate data)
let storage: any; let storage: any;
try { try {
storage = await driver.getStorageInfo(); storage = await driver.getStorageInfo();

View File

@@ -16,76 +16,75 @@ const storageCache = { bytes: 0, hourBlock: -1 };
* (夸克目录的 size 字段 = 该目录内所有文件总大小,无需递归). * (夸克目录的 size 字段 = 该目录内所有文件总大小,无需递归).
* If the API fails (e.g. missing sign params), falls back to fallbackTotal if provided. * If the API fails (e.g. missing sign params), falls back to fallbackTotal if provided.
*/ */
export async function getStorageInfoQuick(cookie, fallbackTotal) { /**
* Get storage info from /member API — single fast call returns both used & total capacity.
* Falls back to capacity/detail + root file sum if member API fails.
*/
export async function getStorageInfoQuick(cookie, fallbackTotal?) {
try {
const memberParams = new URLSearchParams({ pr: 'ucpro', fr: 'pc', uc_param_str: '', __t: String(Date.now()), __dt: '1000' });
const resp = await fetch(`https://pan.quark.cn/1/clouddrive/member?${memberParams.toString()}`, {
headers: quark_api.getHeaders(cookie),
signal: AbortSignal.timeout(10000),
});
if (resp.ok) {
const data = await resp.json();
if (data.status === 200 && data.data) {
const usedBytes = data.data.use_capacity ?? 0;
const totalBytes = data.data.total_capacity ?? 0;
if (totalBytes > 0) {
// Cache for calculateUsedSpace compatibility
const currentHourBlock = Math.floor(new Date().getHours() / 3);
storageCache.bytes = usedBytes;
storageCache.hourBlock = currentHourBlock;
return {
total: quark_api.formatBytes(totalBytes),
totalBytes,
used: quark_api.formatBytes(usedBytes),
usedBytes,
};
}
}
}
} catch {}
// Fallback: capacity/detail for total + root file sum for used
try { try {
const params = new URLSearchParams(quark_api.getCommonParams()); const params = new URLSearchParams(quark_api.getCommonParams());
const capResponse = await fetch(`${BASE_URL}/1/clouddrive/capacity/detail?${params.toString()}`, { const capResp = await fetch(`${BASE_URL}/1/clouddrive/capacity/detail?${params.toString()}`, {
headers: quark_api.getHeaders(cookie), headers: quark_api.getHeaders(cookie),
signal: AbortSignal.timeout(10000), signal: AbortSignal.timeout(10000),
}); });
let totalBytes = 0; let totalBytes = 0;
if (capResponse.ok) { if (capResp.ok) {
const data = await capResponse.json(); const data = await capResp.json();
if (data.status === 200 && data.data) { if (data.status === 200 && data.data) {
totalBytes = data.data.capacity_summary?.sum_capacity || 0; totalBytes = data.data.capacity_summary?.sum_capacity || 0;
if (totalBytes === 0) { if (totalBytes === 0) {
const memberships = [...(data.data.effect || []), ...(data.data.expired || [])]; const memberships = [...(data.data.effect || []), ...(data.data.expired || [])];
totalBytes = memberships.reduce((max, m) => Math.max(max, m.capacity || 0), 0); totalBytes = memberships.reduce((max: number, m: any) => Math.max(max, m.capacity || 0), 0);
} }
} }
} }
// 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; let usedBytes = 0;
try { try {
const memberParams = new URLSearchParams({ pr: 'ucpro', fr: 'pc', uc_param_str: '', __t: String(Date.now()), __dt: '1000' }); const rootFiles = await quark_api.listRootDir(cookie);
const memberResp = await fetch(`https://pan.quark.cn/1/clouddrive/member?${memberParams.toString()}`, { for (const f of rootFiles) usedBytes += f.size || 0;
headers: quark_api.getHeaders(cookie), } catch {}
signal: AbortSignal.timeout(10000),
});
if (memberResp.ok) {
const memberData = await memberResp.json();
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 quark_api.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;
storageCache.hourBlock = currentHourBlock;
if (totalBytes > 0) { if (totalBytes > 0) {
return { return { total: quark_api.formatBytes(totalBytes), totalBytes, used: quark_api.formatBytes(usedBytes), usedBytes };
total: quark_api.formatBytes(totalBytes),
totalBytes,
used: quark_api.formatBytes(usedBytes),
usedBytes,
};
} }
} } catch {}
catch { }
// Fallback: try to parse from a human-readable string like "6 TB" // Last resort: parse fallbackTotal string
if (fallbackTotal) { if (fallbackTotal) {
const match = fallbackTotal.match(/^([\d.]+)\s*([KMGT]B?)/i); const match = String(fallbackTotal).match(/^([\d.]+)\s*([KMGT]B?)/i);
if (match) { if (match) {
const num = parseFloat(match[1]); const num = parseFloat(match[1]);
const unit = match[2].toUpperCase(); const unit = match[2].toUpperCase();
const multipliers = { B: 1, KB: 1024, MB: 1024 ** 2, GB: 1024 ** 3, TB: 1024 ** 4, PB: 1024 ** 5 }; const multipliers: Record<string,number> = { B:1, KB:1024, MB:1048576, GB:1073741824, TB:1099511627776 };
const multiplier = multipliers[unit] || multipliers[unit.replace('B', '') + 'B'] || 0; const m = multipliers[unit] || multipliers[unit.replace('B','')+'B'] || 0;
if (multiplier > 0) { if (m > 0) return { total: String(fallbackTotal), totalBytes: Math.round(num*m), used: '-', usedBytes: 0 };
return { total: fallbackTotal, totalBytes: Math.round(num * multiplier), used: '-', usedBytes: 0 };
}
} }
} }
return { total: '-', totalBytes: 0, used: '-', usedBytes: 0 }; return { total: '-', totalBytes: 0, used: '-', usedBytes: 0 };
@@ -98,52 +97,17 @@ export async function getStorageInfoQuick(cookie, fallbackTotal) {
* First call returns quickly; full traversal runs async and updates DB later. * First call returns quickly; full traversal runs async and updates DB later.
* `onBackgroundComplete` is called when traversal finishes. * `onBackgroundComplete` is called when traversal finishes.
*/ */
export async function getStorageInfo(cookie, onBackgroundComplete) { /**
try { * Get accurate storage info via /member API (fast, no file traversal needed).
const params = new URLSearchParams(quark_api.getCommonParams()); * Returns { total, totalBytes, used, usedBytes } in ~1s.
const response = await fetch(`${BASE_URL}/1/clouddrive/capacity/detail?${params.toString()}`, { * onBackgroundComplete is kept for backward compat (called synchronously).
headers: quark_api.getHeaders(cookie), */
signal: AbortSignal.timeout(10000), export async function getStorageInfo(cookie, onBackgroundComplete?) {
}); const result = await getStorageInfoQuick(cookie);
let totalBytes = 0; if (onBackgroundComplete && result.used !== '-' && result.usedBytes > 0) {
if (response.ok) { try { onBackgroundComplete(result.used, result.total); } catch {}
const data = await response.json();
if (data.status === 200 && data.data) {
totalBytes = data.data.capacity_summary?.sum_capacity || 0;
if (totalBytes === 0) {
const memberships = [...(data.data.effect || []), ...(data.data.expired || [])];
totalBytes = memberships.reduce((max, m) => Math.max(max, m.capacity || 0), 0);
}
}
}
const totalFormatted = totalBytes > 0 ? quark_api.formatBytes(totalBytes) : '-';
// Quick estimation: sum root-level files only
let quickUsed = 0;
try {
const rootFiles = await quark_api.listRootDir(cookie);
for (const f of rootFiles) {
quickUsed += f.size || 0;
}
}
catch { }
// Budget full traversal in background (no await)
calculateUsedSpace(cookie).then(fullUsed => {
if (onBackgroundComplete) {
onBackgroundComplete(quark_api.formatBytes(fullUsed), totalFormatted);
}
}).catch(err => {
console.error('[Storage] Background full traversal failed:', err.message);
});
return {
total: totalFormatted,
totalBytes,
used: quark_api.formatBytes(quickUsed),
usedBytes: quickUsed,
};
}
catch {
return { used: '-', total: '-', usedBytes: 0, totalBytes: 0 };
} }
return result;
} }
/** /**
* Calculate total used space by recursively traversing all files * Calculate total used space by recursively traversing all files