- quark/baidu 驱动 saveFromShare 返回 fileSize 总大小 - cloud.service.ts 写入 file_size 字段(非null时转字符串) - 详情页新增文件大小展示(formatFileSize 自动格式化) - 详情页时间改为 formatTime(yyyy-MM-dd HH:mm:ss) - SaveRecords 时间格式: 05-15 → 2026-05-15
315 lines
11 KiB
TypeScript
315 lines
11 KiB
TypeScript
import { getHeaders, getCommonParams, makeQuery, getMparam, humanDelay, dailyFolderName, formatBytes, apiFetch, listDir, listDirAllPages, listRootDir, QuarkFile } from './quark-api';
|
||
import { acquireStoken, getShareFiles, saveFiles, waitForTask } from './quark-share';
|
||
|
||
/**
|
||
* 转存 & 存储管理模块。
|
||
* 处理分享链接解析 → 转存 → 查/创建目标文件夹 → 文件重命名 → 递归统计。
|
||
*/
|
||
|
||
// ==================== saveFromShare — 核心转存流水线 ====================
|
||
|
||
/**
|
||
* Save files from a share link → magic rename → create shared link.
|
||
*
|
||
* Flow: token → detail → save → wait_task → rename → share
|
||
*/
|
||
export async function saveFromShare(
|
||
cookie: string,
|
||
nickname: string | undefined,
|
||
shareUrl: string,
|
||
sourceTitle?: string,
|
||
): Promise<{
|
||
success: boolean;
|
||
message: string;
|
||
shareUrl?: string;
|
||
sharePwd?: string;
|
||
folderName?: string;
|
||
taskId?: string;
|
||
renamed?: string[];
|
||
fileCount?: number;
|
||
folderCount?: number;
|
||
fileSize?: number;
|
||
originalFolderName?: string;
|
||
}> {
|
||
try {
|
||
// Parse share token from URL
|
||
const urlObj = new URL(shareUrl);
|
||
const pwdId = urlObj.pathname.split('/').filter(Boolean).pop();
|
||
if (!pwdId) {
|
||
return { success: false, message: 'Invalid share URL: could not extract share token' };
|
||
}
|
||
|
||
// Step 1: Acquire stoken
|
||
const stoken = await acquireStoken(cookie, pwdId);
|
||
if (!stoken) {
|
||
return { success: false, message: '😅 Oops!资源好像偷偷溜走了,换个链接试试吧~' };
|
||
}
|
||
|
||
// Step 2: Get share detail
|
||
const shareInfo = await getShareFiles(cookie, pwdId, stoken);
|
||
if (!shareInfo || !shareInfo.files || shareInfo.files.length === 0) {
|
||
return { success: false, message: '🌚 空的!这个分享里啥都没有…' };
|
||
}
|
||
|
||
const { files: topFiles, topDir, childFiles } = shareInfo;
|
||
const originalFolderName = topFiles[0]?.file_name || '';
|
||
const fids = topFiles.map(f => f.fid);
|
||
const fidTokens = topFiles.map(f => f.share_fid_token);
|
||
|
||
// 按日期创建/查找文件夹,每天的转存存入当天文件夹
|
||
await humanDelay();
|
||
const saveDirName = dailyFolderName();
|
||
console.log(`[Quark] saveFromShare: looking for/create dir "${saveDirName}"`);
|
||
const saveDirFid = await findOrCreateDir(cookie, saveDirName);
|
||
const targetPdirFid = saveDirFid || '0';
|
||
if (saveDirFid) {
|
||
console.log(`[Quark] Using save directory: ${saveDirName} (fid: ${saveDirFid})`);
|
||
} else {
|
||
console.log(`[Quark] WARNING: failed to create/find dir "${saveDirName}", saving to root`);
|
||
}
|
||
|
||
// Step 3: Save top-level item(s) to the target directory
|
||
const saveResult = await saveFiles(cookie, pwdId, stoken, fids, fidTokens.filter(Boolean) as string[], targetPdirFid);
|
||
if (!saveResult.success) {
|
||
return saveResult;
|
||
}
|
||
|
||
const taskId = saveResult.taskId!;
|
||
|
||
// Step 4: Wait for save task to complete (poll up to 30s)
|
||
const savedFids = await waitForTask(cookie, taskId, 30000);
|
||
if (!savedFids || savedFids.length === 0) {
|
||
return { success: true, message: '文件已保存,但获取保存结果超时' };
|
||
}
|
||
|
||
// Step 5: Magic rename files — with random delay to avoid detection
|
||
await humanDelay();
|
||
const renamed: Array<{ original: string; renamed: string }> = [];
|
||
let shareFid = '';
|
||
let savedFolderName = '';
|
||
let newInnerDirName = '';
|
||
|
||
if (topDir && childFiles && childFiles.length > 0) {
|
||
// ── Single folder share ──
|
||
const savedDirFid = savedFids[0];
|
||
shareFid = savedDirFid;
|
||
savedFolderName = topFiles[0]?.file_name || '';
|
||
} else {
|
||
// ── Multiple files at top level ──
|
||
shareFid = savedFids[0];
|
||
savedFolderName = topFiles[0]?.file_name || '';
|
||
}
|
||
|
||
// Step 6: Create share link FIRST (before rename), so all files are guaranteed to be shared
|
||
await humanDelay();
|
||
let shareUrlResult = '';
|
||
let sharePwdResult = '';
|
||
let shareMsg = '';
|
||
let successCount = 0; // total items (files + folders) actually saved
|
||
|
||
const { createShareLink } = await import('./quark-share');
|
||
if (shareFid) {
|
||
const shareResult = await createShareLink(cookie, shareFid);
|
||
if (shareResult.success && shareResult.shareUrl) {
|
||
shareUrlResult = shareResult.shareUrl;
|
||
if (shareResult.sharePwd) sharePwdResult = shareResult.sharePwd;
|
||
} else {
|
||
shareMsg = `(分享失败:${shareResult.message})`;
|
||
}
|
||
}
|
||
|
||
const { magicRenameDir, magicRename } = await import('./quark-rename');
|
||
const { renameFile } = await import('./quark-share');
|
||
|
||
// Step 7: Rename files AFTER creating the share link (anti-harmony, won't affect the share)
|
||
if (topDir && childFiles && childFiles.length > 0) {
|
||
// ── Single folder share ──
|
||
const savedDirFid = savedFids[0];
|
||
|
||
// List files inside the saved directory
|
||
const dirFiles = await listDir(cookie, savedDirFid);
|
||
if (dirFiles && dirFiles.length > 0) {
|
||
for (const file of dirFiles) {
|
||
if (file.dir) continue;
|
||
const newName = magicRename(file.file_name);
|
||
const renameOk = await renameFile(cookie, file.fid, newName);
|
||
if (renameOk) {
|
||
renamed.push({ original: file.file_name, renamed: newName });
|
||
}
|
||
}
|
||
}
|
||
|
||
// Also rename the inner folder itself (the actual shared folder)
|
||
const innerDirOriginalName = sourceTitle || topFiles[0]?.file_name || '';
|
||
if (innerDirOriginalName) {
|
||
newInnerDirName = magicRenameDir(innerDirOriginalName);
|
||
const innerDirRenameOk = await renameFile(cookie, savedDirFid, newInnerDirName);
|
||
if (innerDirRenameOk) {
|
||
console.log(`[Quark] Renamed inner folder: ${innerDirOriginalName} → ${newInnerDirName}`);
|
||
}
|
||
}
|
||
} else {
|
||
// ── Multiple files at top level ──
|
||
for (let i = 0; i < savedFids.length && i < topFiles.length; i++) {
|
||
const originalName = topFiles[i].file_name;
|
||
if (topFiles[i].dir) continue;
|
||
const newName = magicRename(originalName);
|
||
const renameOk = await renameFile(cookie, savedFids[i], newName);
|
||
if (renameOk) {
|
||
renamed.push({ original: originalName, renamed: newName });
|
||
}
|
||
}
|
||
}
|
||
|
||
// Step 7.5: 广告关键词清理 + 创建警示文件夹
|
||
if (shareFid) {
|
||
try {
|
||
const { runAdCleanup } = await import('./quark-ad-cleanup');
|
||
const adResult = await runAdCleanup(cookie, shareFid);
|
||
if (adResult.adDeleted > 0) {
|
||
console.log(`[Quark] 广告清理完成: 删除了 ${adResult.adDeleted} 个广告文件/文件夹`);
|
||
}
|
||
if (adResult.warningDirs > 0) {
|
||
console.log(`[Quark] 已创建 ${adResult.warningDirs} 个警示文件夹`);
|
||
}
|
||
} catch (err: any) {
|
||
console.log(`[Quark] 广告清理/警示文件夹创建失败(非致命): ${err.message}`);
|
||
}
|
||
}
|
||
|
||
// Step 8: DAY FOLDER STAYS AS-IS (e.g. "2026-05-03")
|
||
// DO NOT rename the date folder — it serves as the organizational container.
|
||
savedFolderName = newInnerDirName ? `${saveDirName}/${newInnerDirName}` : saveDirName;
|
||
|
||
// Recursively count files and folders from saved cloud directory
|
||
let fileCount = 0;
|
||
let folderCount = 0;
|
||
if (shareFid) {
|
||
try {
|
||
const counts = await countRecursive(cookie, shareFid);
|
||
fileCount = counts.fileCount;
|
||
folderCount = counts.folderCount;
|
||
} catch {
|
||
console.log('[Quark] Recursive count failed, using fallback');
|
||
}
|
||
}
|
||
// If recursive count returned nothing, try fallback
|
||
if (fileCount === 0 && folderCount === 0) {
|
||
if (topDir && childFiles) {
|
||
folderCount = 1 + childFiles.filter(f => f.dir).length;
|
||
fileCount = childFiles.filter(f => !f.dir).length;
|
||
} else {
|
||
folderCount = topFiles.filter(f => f.dir).length;
|
||
fileCount = topFiles.filter(f => !f.dir).length;
|
||
}
|
||
}
|
||
|
||
// Calculate total file size
|
||
const allFiles = topDir && childFiles ? childFiles : topFiles;
|
||
const fileSize = allFiles.reduce((sum, f) => sum + (Number(f.size) || 0), 0);
|
||
|
||
const renameMsg = renamed.length > 0
|
||
? `,已重命名 ${renamed.length} 个文件`
|
||
: '';
|
||
const folderMsg = savedFolderName ? `到文件夹「${savedFolderName}」` : '';
|
||
|
||
return {
|
||
success: true,
|
||
message: `已保存${folderMsg}${renameMsg}${shareMsg}`,
|
||
shareUrl: shareUrlResult || undefined,
|
||
sharePwd: sharePwdResult || undefined,
|
||
folderName: savedFolderName,
|
||
taskId,
|
||
renamed: renamed.map(r => `${r.original} → ${r.renamed}`),
|
||
fileCount,
|
||
folderCount,
|
||
fileSize,
|
||
originalFolderName,
|
||
};
|
||
} catch (err: any) {
|
||
return { success: false, message: err.message || 'Network error' };
|
||
}
|
||
}
|
||
|
||
// ==================== Dir Management ====================
|
||
|
||
/**
|
||
* Create a new directory at root.
|
||
*/
|
||
export async function createDir(cookie: string, dirName: string): Promise<string | null> {
|
||
try {
|
||
const resp = await fetch(
|
||
`https://drive-pc.quark.cn/1/clouddrive/file?${makeQuery()}`,
|
||
{
|
||
method: 'POST',
|
||
headers: { ...getHeaders(cookie), 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
pdir_fid: '0',
|
||
file_name: dirName,
|
||
dir: true,
|
||
dir_path: '',
|
||
}),
|
||
signal: AbortSignal.timeout(10000),
|
||
},
|
||
);
|
||
const data = await resp.json() as any;
|
||
if (data.status === 200 && data.data?.fid) {
|
||
console.log(`[Quark] Created dir "${dirName}" (fid: ${data.data.fid})`);
|
||
return data.data.fid;
|
||
}
|
||
console.log(`[Quark] createDir API returned non-200: status=${data.status} msg=${data.message}`);
|
||
return null;
|
||
} catch (err: any) {
|
||
console.log(`[Quark] createDir error: ${err.message}`);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Find an existing directory by name, or create it if not found.
|
||
*/
|
||
export async function findOrCreateDir(cookie: string, dirName: string): Promise<string | null> {
|
||
try {
|
||
const rootFiles = await listDirAllPages(cookie, '0');
|
||
const existing = rootFiles.find(f => f.dir && f.file_name === dirName);
|
||
if (existing?.fid) {
|
||
console.log(`[Quark] Found existing daily folder: ${dirName} (fid: ${existing.fid})`);
|
||
return existing.fid;
|
||
}
|
||
console.log(`[Quark] Daily folder "${dirName}" not found, creating...`);
|
||
} catch (err: any) {
|
||
console.log(`[Quark] findOrCreateDir list error: ${err.message}`);
|
||
}
|
||
const fid = await createDir(cookie, dirName);
|
||
console.log(`[Quark] createDir result for "${dirName}": ${fid || 'null'}`);
|
||
return fid;
|
||
}
|
||
|
||
// ==================== Recursive Count ====================
|
||
|
||
/**
|
||
* Recursively count files and folders for a saved cloud directory.
|
||
*/
|
||
export async function countRecursive(cookie: string, pdirFid: string): Promise<{ fileCount: number; folderCount: number }> {
|
||
let fileCount = 0;
|
||
let folderCount = 0;
|
||
const stack = [pdirFid];
|
||
const visited = new Set<string>();
|
||
while (stack.length > 0) {
|
||
const fid = stack.pop()!;
|
||
if (visited.has(fid)) continue;
|
||
visited.add(fid);
|
||
const files = await listDir(cookie, fid);
|
||
if (!files) continue;
|
||
for (const f of files) {
|
||
if (f.dir) {
|
||
folderCount++;
|
||
stack.push(f.fid);
|
||
} else {
|
||
fileCount++;
|
||
}
|
||
}
|
||
}
|
||
return { fileCount, folderCount };
|
||
}
|