mirror of
https://github.com/Hommy-master/capcut-mate.git
synced 2026-11-03 22:43:43 +08:00
765 lines
23 KiB
JavaScript
765 lines
23 KiB
JavaScript
const path = require("path");
|
||
const axios = require("axios");
|
||
const { app, dialog, shell } = require("electron");
|
||
const { createWriteStream } = require("fs");
|
||
const fs = require("fs").promises; // 使用 fs.promises 进行异步文件操作
|
||
const logger = require("./logger");
|
||
const { v4: uuidv4 } = require('uuid');
|
||
|
||
const RECORD_MAX = 500;
|
||
|
||
const LOG_MAX = 2000;
|
||
|
||
const axiosConfig = {
|
||
method: "GET",
|
||
timeout: 30000, // 30秒超时
|
||
headers: {
|
||
// 添加常见的浏览器User-Agent
|
||
"User-Agent":
|
||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
|
||
},
|
||
};
|
||
|
||
function getConfigPath() {
|
||
return path.join(app.getPath("userData"), "app-config.json");
|
||
}
|
||
|
||
async function readConfig() {
|
||
const configPath = getConfigPath();
|
||
logger.info("[log] Config path:", configPath);
|
||
try {
|
||
const data = await fs.readFile(configPath, "utf8");
|
||
return JSON.parse(data) || {};
|
||
} catch (error) {
|
||
return {};
|
||
}
|
||
}
|
||
|
||
async function writeConfig(config) {
|
||
const configPath = getConfigPath();
|
||
try {
|
||
await fs.writeFile(configPath, JSON.stringify(config, null, 2), "utf8");
|
||
return true;
|
||
} catch (error) {
|
||
logger.error("写入配置文件失败:", error);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
function getDownloadLogPath() {
|
||
return path.join(app.getPath("userData"), "download-log.json");
|
||
}
|
||
|
||
async function readDownloadLog() {
|
||
const logPath = getDownloadLogPath();
|
||
logger.info("[log] Log path:", logPath);
|
||
try {
|
||
const data = await fs.readFile(logPath, "utf8");
|
||
return JSON.parse(data);
|
||
} catch (error) {
|
||
return [];
|
||
}
|
||
}
|
||
|
||
/**
|
||
*
|
||
* @param {*} entry {time: Date, level: 'error', message: '日志内容' }
|
||
*/
|
||
async function appendDownloadLog(entry, parentWindow) {
|
||
const logPath = getDownloadLogPath();
|
||
let logs = [];
|
||
try {
|
||
logs = await readDownloadLog();
|
||
} catch (error) {
|
||
// 如果文件不存在或无法读取,初始化为空数组
|
||
logs = [];
|
||
}
|
||
|
||
entry.time = new Date();
|
||
console.log(`appendDownloadLog: ${JSON.stringify(entry)}`);
|
||
await parentWindow.webContents.send("file-operation-log", entry);
|
||
logs.push(entry);
|
||
if (logs.length > LOG_MAX) {
|
||
logs.shift();
|
||
}
|
||
try {
|
||
await fs.writeFile(logPath, JSON.stringify(logs, null, 2), "utf8");
|
||
} catch (writeErr) {
|
||
logger.error("写入日志文件失败:", writeErr);
|
||
}
|
||
}
|
||
|
||
async function clearDownloadLog() {
|
||
const logPath = getDownloadLogPath();
|
||
try {
|
||
await fs.writeFile(logPath, JSON.stringify([], null, 2), "utf8");
|
||
return true;
|
||
} catch (error) {
|
||
logger.error("清空日志文件失败:", error);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
function getHistoryRecordPath() {
|
||
return path.join(app.getPath("userData"), "history-record.json");
|
||
}
|
||
|
||
async function readHistoryRecord() {
|
||
const recordPath = getHistoryRecordPath();
|
||
console.info("[History] Record path:", recordPath);
|
||
try {
|
||
const data = await fs.readFile(recordPath, "utf8");
|
||
return JSON.parse(data);
|
||
} catch (error) {
|
||
return [];
|
||
}
|
||
}
|
||
|
||
/**
|
||
*
|
||
* @param {*} entry {id: 'uuid', time: Date, draft_id: 'draft_id' draft_url: 'draft_url' }
|
||
*/
|
||
async function appendHistoryRecord(entry) {
|
||
const recordPath = getHistoryRecordPath();
|
||
let records = [];
|
||
try {
|
||
records = await readHistoryRecord();
|
||
} catch (error) {
|
||
// 如果文件不存在或无法读取,初始化为空数组
|
||
records = [];
|
||
}
|
||
|
||
console.log(`appendHistoryRecord: ${JSON.stringify(entry)}`);
|
||
records.push(entry);
|
||
if (records.length > RECORD_MAX) {
|
||
records.shift();
|
||
}
|
||
try {
|
||
await fs.writeFile(recordPath, JSON.stringify(records, null, 2), "utf8");
|
||
} catch (writeErr) {
|
||
console.error("写入草稿历史记录文件失败:", writeErr);
|
||
}
|
||
}
|
||
|
||
// 更精确的错误处理
|
||
function errorHandler(error = {}, url = "") {
|
||
if (error.code === "ECONNREFUSED") {
|
||
throw new Error(`[error] not connect to server: ${url}`);
|
||
} else if (error.code === "ENOTFOUND") {
|
||
throw new Error(`[error] domain not found: ${url}`);
|
||
} else if (error.response) {
|
||
// 服务器返回了错误状态码(如4xx, 5xx)
|
||
throw new Error(`[error] server error (${error.response.status}): ${url}`);
|
||
} else {
|
||
throw error; // 重新抛出其他未知错误
|
||
}
|
||
}
|
||
|
||
async function getDraftUrls(remoteUrl, parentWindow) {
|
||
logger.info("[info] get draft url");
|
||
try {
|
||
const response = await axios({
|
||
...axiosConfig,
|
||
url: remoteUrl,
|
||
responseType: "json",
|
||
});
|
||
|
||
// 检查HTTP状态码
|
||
if (response.status !== 200) {
|
||
await appendDownloadLog(
|
||
{ level: "error", message: `获取草稿地址信息失败` },
|
||
parentWindow
|
||
);
|
||
throw new Error(
|
||
`[error] [draft url] request failed, status code: ${response.status}`
|
||
);
|
||
}
|
||
logger.info("[success] get draft url");
|
||
return response.data;
|
||
} catch (error) {
|
||
errorHandler(error, remoteUrl);
|
||
}
|
||
}
|
||
|
||
async function updateDraftPath(parentWindow) {
|
||
const targetDir = await getTargetDirectory(parentWindow, true);
|
||
if (!targetDir) {
|
||
return { success: false, error: "用户取消了目录选择" };
|
||
}
|
||
try {
|
||
// 验证目录权限
|
||
try {
|
||
await fs.access(targetDir, fs.constants.R_OK | fs.constants.W_OK);
|
||
} catch (accessError) {
|
||
logger.error('所选目录无读写权限:', accessError);
|
||
// 尝试使用 dialog 显示错误消息
|
||
if (parentWindow) {
|
||
const { dialog } = require('electron');
|
||
await dialog.showMessageBox(parentWindow, {
|
||
type: 'error',
|
||
title: '权限不足',
|
||
message: '所选目录没有足够的读写权限,请选择其他目录。',
|
||
buttons: ['确定']
|
||
});
|
||
}
|
||
return { success: false, error: '所选目录没有足够的读写权限' };
|
||
}
|
||
|
||
const configPath = getConfigPath();
|
||
let config = {};
|
||
|
||
// 尝试读取现有配置
|
||
try {
|
||
const data = await fs.readFile(configPath, "utf8");
|
||
config = JSON.parse(data);
|
||
} catch (error) {
|
||
// 如果文件不存在,保持config为空对象
|
||
}
|
||
|
||
config.targetDirectory = targetDir;
|
||
|
||
// 写回配置文件
|
||
await fs.writeFile(configPath, JSON.stringify(config, null, 2), 'utf8');
|
||
logger.info('默认草稿路径已更新为:', targetDir);
|
||
return { success: true, targetDir };
|
||
} catch (error) {
|
||
logger.error('更新默认草稿路径失败:', error);
|
||
return { success: false, error: error.message };
|
||
}
|
||
}
|
||
|
||
// 提取出来的函数,可选参数parentWindow用于显示对话框时附加到对话框
|
||
async function getTargetDirectory(parentWindow = null, isUpdate = false) {
|
||
let config = await readConfig();
|
||
if (!isUpdate && config.targetDirectory) {
|
||
try {
|
||
await fs.access(config.targetDirectory, fs.constants.R_OK | fs.constants.W_OK);
|
||
return config.targetDirectory;
|
||
} catch (accessErr) {
|
||
logger.warn("配置的目录已不存在或无访问权限,将重新选择。", accessErr.message);
|
||
}
|
||
}
|
||
|
||
const dialogOptions = {
|
||
properties: ["openDirectory", "createDirectory"], // 允许创建新目录
|
||
title: "请选择目标目录",
|
||
buttonLabel: "选择此目录",
|
||
defaultPath: isUpdate ? config.targetDirectory : undefined
|
||
};
|
||
|
||
// 如果有父窗口,则附加到父窗口
|
||
if (parentWindow) {
|
||
dialogOptions.window = parentWindow;
|
||
}
|
||
|
||
const result = await dialog.showOpenDialog(dialogOptions);
|
||
|
||
if (!result.canceled && result.filePaths.length > 0) {
|
||
const selectedDir = result.filePaths[0];
|
||
|
||
// 再次验证目录权限
|
||
try {
|
||
await fs.access(selectedDir, fs.constants.R_OK | fs.constants.W_OK);
|
||
} catch (accessErr) {
|
||
logger.error('所选目录无读写权限:', accessErr);
|
||
if (parentWindow) {
|
||
const { dialog } = require('electron');
|
||
await dialog.showMessageBox(parentWindow, {
|
||
type: 'error',
|
||
title: '权限不足',
|
||
message: '所选目录没有足够的读写权限,请重新选择。',
|
||
buttons: ['确定']
|
||
});
|
||
}
|
||
return ''; // 返回空字符串表示失败
|
||
}
|
||
|
||
config.targetDirectory = selectedDir;
|
||
await writeConfig(config);
|
||
return selectedDir;
|
||
} else {
|
||
return '';
|
||
}
|
||
}
|
||
|
||
function updateValue(current, finalKey, targetDir, oldVal, targetId) {
|
||
if (oldVal) {
|
||
// 找到ID在路径中的位置
|
||
const idIndex = oldVal.indexOf(targetId);
|
||
if (idIndex === -1) return;
|
||
|
||
// 提取ID及之后的部分作为将要下载的路径
|
||
const relativePath = oldVal.substring(idIndex).replaceAll("/", path.sep); // 替换为系统路径分隔符
|
||
// targetDir 已包含 targetId 目录,所以relativePath中的targetId要去重
|
||
const newRelativePath = relativePath.replace(`${targetId}${path.sep}`, "");
|
||
const newValue = path.join(targetDir, newRelativePath);
|
||
current[finalKey] = newValue;
|
||
|
||
logger.info(`✅ newValue to:`, newValue);
|
||
}
|
||
}
|
||
|
||
// 递归遍历对象,更新所有名为path的属性
|
||
function recursivelyUpdatePaths(obj, targetDir, targetId) {
|
||
// 处理数组
|
||
if (Array.isArray(obj)) {
|
||
obj.forEach((item) => {
|
||
recursivelyUpdatePaths(item, targetDir, targetId);
|
||
});
|
||
return;
|
||
}
|
||
|
||
// 处理对象
|
||
if (obj && typeof obj === "object") {
|
||
// 检查是否有path属性
|
||
if (obj.path && typeof obj.path === "string") {
|
||
updateValue(obj, "path", targetDir, obj.path, targetId);
|
||
}
|
||
|
||
// 递归处理所有属性
|
||
for (const key in obj) {
|
||
if (obj.hasOwnProperty(key)) {
|
||
recursivelyUpdatePaths(obj[key], targetDir, targetId);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 带错误处理的JSON文件下载
|
||
async function downloadJsonFile(
|
||
{ fileUrl, filePath, targetDir, targetId },
|
||
parentWindow
|
||
) {
|
||
// 1. 使用 Axios 下载 JSON 文件
|
||
try {
|
||
const response = await axios({
|
||
method: "GET",
|
||
url: fileUrl,
|
||
responseType: "json", // 直接告诉 Axios 解析 JSON
|
||
});
|
||
|
||
// 检查HTTP状态码
|
||
if (response.status !== 200) {
|
||
await appendDownloadLog(
|
||
{ level: "error", message: `下载草稿内容文件失败` },
|
||
parentWindow
|
||
);
|
||
throw new Error(
|
||
`[error] [json] request failed, status code: ${response.status}`
|
||
);
|
||
}
|
||
|
||
// 2. 解析获取到的数据(Axios 会根据 responseType: 'json' 自动解析)
|
||
const jsonData = response.data;
|
||
|
||
// 3. 修改 JSON 数据中指定键的值
|
||
if (jsonData?.materials) {
|
||
logger.info(`[log] start modifyJsonValue: materials`);
|
||
recursivelyUpdatePaths(jsonData.materials, targetDir, targetId);
|
||
}
|
||
|
||
await appendDownloadLog(
|
||
{
|
||
level: "loading",
|
||
message: `正在将草稿内容文件写入本地草稿目录 ${targetDir}`,
|
||
},
|
||
parentWindow
|
||
);
|
||
|
||
// 4. 将修改后的 JSON 对象转换为格式化的字符串并写入本地文件
|
||
const jsonString = JSON.stringify(jsonData, null, 2); // 使用 2 个空格进行缩进,美化输出
|
||
await fs.writeFile(filePath, jsonString, "utf8"); // 指定编码为 utf8
|
||
} catch (error) {
|
||
logger.error(`下载JSON文件失败: ${fileUrl}`, error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
async function downloadNotJsonFile(
|
||
{ fileUrl, filePath, targetDir },
|
||
parentWindow
|
||
) {
|
||
try {
|
||
// 1. 使用 Axios 下载非 JSON 文件
|
||
const response = await axios({
|
||
...axiosConfig,
|
||
url: fileUrl,
|
||
responseType: "stream", // 设置响应类型为 'stream' 以处理大文件
|
||
});
|
||
|
||
// 检查HTTP状态码
|
||
if (response.status !== 200) {
|
||
await appendDownloadLog(
|
||
{ level: "error", message: `下载草稿内容文件失败` },
|
||
parentWindow
|
||
);
|
||
throw new Error(
|
||
`[error] [stream] request failed, status code: ${response.status}`
|
||
);
|
||
}
|
||
|
||
logger.info(`[log] start create writable stream: ${filePath}`);
|
||
|
||
await appendDownloadLog(
|
||
{
|
||
level: "loading",
|
||
message: `正在将草稿内容文件写入本地草稿目录 ${targetDir}`,
|
||
},
|
||
parentWindow
|
||
);
|
||
|
||
// 创建可写流
|
||
// 显式指定 flags 和 mode,避免 Windows 下文件句柄共享模式异常
|
||
const writer = response.data.pipe(createWriteStream(filePath, { flags: "w", mode: 0o666 }));
|
||
|
||
return new Promise((resolve, reject) => {
|
||
// 监听 close 而非 finish:finish 仅表示数据写完,close 才表示文件句柄已释放
|
||
// 在 Windows 上,句柄未释放时其他进程访问该文件会出现权限异常(EACCES)
|
||
writer.on("close", resolve);
|
||
writer.on("error", (err) => {
|
||
// 尝试删除可能不完整的文件
|
||
fs.unlink(filePath).catch(() => { });
|
||
reject(new Error(`[error] write file failed: ${err.message}`));
|
||
});
|
||
response.data.on("error", (err) => {
|
||
reject(new Error(`[error] download stream error: ${err.message}`));
|
||
});
|
||
});
|
||
} catch (error) {
|
||
logger.error(`下载非JSON文件失败: ${fileUrl}`, error);
|
||
// 不使用errorHandler,直接抛出错误以便上层进行重试
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 下载单个文件并保存到指定路径的辅助函数
|
||
* @param {string} url 远程文件的URL
|
||
* @param {string} filePath 要保存到的本地文件路径
|
||
*/
|
||
async function downloadSingleFile(config, parentWindow) {
|
||
const filePath = config.filePath;
|
||
const fileUrl = config.fileUrl;
|
||
|
||
if (fileUrl.endsWith(".json")) {
|
||
logger.info(`[log] start download json file : ${filePath}`);
|
||
await downloadJsonFile(config, parentWindow);
|
||
} else {
|
||
logger.info(`[log] start download non-json file : ${filePath}`);
|
||
await downloadNotJsonFile(config, parentWindow);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 触发目录扫描,激活剪映的目录发现机制
|
||
* 原理:将草稿目录复制到临时目录,触发文件系统变更通知,让剪映无需重启即可感知到新草稿
|
||
* - Windows:使用 robocopy(内置工具,返回码 0-7 均为成功)
|
||
* - macOS:使用 rsync(触发 FSEvents 变更通知)
|
||
* @param {string} targetDir 草稿目录路径
|
||
*/
|
||
async function triggerDirectoryScan(targetDir) {
|
||
if (!targetDir) return;
|
||
|
||
try {
|
||
await fs.access(targetDir);
|
||
} catch {
|
||
// 目录不存在则跳过
|
||
return;
|
||
}
|
||
|
||
const tmpDir = targetDir + ".tmp";
|
||
const { execFile } = require("child_process");
|
||
const platform = process.platform;
|
||
|
||
await new Promise((resolve) => {
|
||
if (platform === "win32") {
|
||
// Windows:使用 robocopy 触发 ReadDirectoryChangesW 通知
|
||
const args = [
|
||
targetDir,
|
||
tmpDir,
|
||
"/E", // 递归复制所有子目录
|
||
"/COPY:DAT", // 复制数据、属性和时间戳(无需管理员权限)
|
||
"/R:1", // 失败重试1次
|
||
"/W:1", // 重试等待1秒
|
||
"/NP", // 不显示进度百分比
|
||
"/NJH", // 不显示作业头
|
||
"/NJS", // 不显示作业摘要
|
||
];
|
||
execFile("robocopy", args, { windowsHide: true }, (err) => {
|
||
// robocopy 返回码 0-7 均表示成功或正常状态,8+ 才是错误
|
||
const code = err ? err.code : 0;
|
||
if (typeof code === "number" && code >= 8) {
|
||
logger.warn(`[scan] Windows 触发目录扫描失败,robocopy 返回码: ${code}`);
|
||
} else {
|
||
logger.info(`[scan] Windows 触发目录扫描完成,robocopy 返回码: ${code}`);
|
||
}
|
||
resolve();
|
||
});
|
||
} else if (platform === "darwin") {
|
||
// macOS:使用 rsync 触发 FSEvents 变更通知
|
||
// -a: 归档模式(递归+保留属性),触发目录写入事件
|
||
execFile("rsync", ["-a", targetDir + "/", tmpDir], (err) => {
|
||
if (err) {
|
||
logger.warn(`[scan] macOS 触发目录扫描失败: ${err.message}`);
|
||
} else {
|
||
logger.info(`[scan] macOS 触发目录扫描完成`);
|
||
}
|
||
resolve();
|
||
});
|
||
} else {
|
||
logger.info(`[scan] 当前平台 ${platform} 不支持触发目录扫描,跳过`);
|
||
resolve();
|
||
}
|
||
});
|
||
|
||
// 清理临时目录
|
||
try {
|
||
await fs.rm(tmpDir, { recursive: true, force: true });
|
||
} catch (e) {
|
||
logger.warn(`[scan] 清理临时目录失败 ${tmpDir}: ${e.message}`);
|
||
}
|
||
}
|
||
|
||
// 打开目录
|
||
async function openDraftDirectory(dirPath) {
|
||
try {
|
||
const errorMsg = await shell.openPath(dirPath);
|
||
if (errorMsg) {
|
||
logger.error(`[error] Failed to open path: ${errorMsg}`);
|
||
return { success: false, error: errorMsg };
|
||
}
|
||
return { success: true };
|
||
} catch (error) {
|
||
logger.error(`[error] Error opening path: ${error}`);
|
||
return { success: false, error: error.message };
|
||
}
|
||
}
|
||
|
||
// 获取目标文件路径
|
||
// 解析URL并创建必要的目录结构
|
||
async function getTargetFilePath(fileUrl, baseTargetDir, targetId) {
|
||
const urlObj = new URL(fileUrl);
|
||
let fullPath = urlObj.pathname;
|
||
|
||
// 找到ID在路径中的位置
|
||
const idIndex = fullPath.indexOf(targetId);
|
||
if (idIndex === -1) return null;
|
||
|
||
// 提取ID及之后的部分作为将要下载的路径
|
||
const relativePath = fullPath.substring(idIndex).replaceAll("/", path.sep); // 替换为系统路径分隔符
|
||
const fullTargetPath = path.join(baseTargetDir, relativePath);
|
||
const targetDir = path.dirname(fullTargetPath);
|
||
|
||
logger.info("[log] fullTargetPath: " + fullTargetPath);
|
||
logger.info("[log] targetDir: " + targetDir);
|
||
|
||
// 确保目标目录存在
|
||
try {
|
||
await fs.mkdir(targetDir, { recursive: true }); // recursive: true 可以创建多级目录
|
||
} catch (mkdirError) {
|
||
logger.error(`创建目录失败: ${targetDir}`, mkdirError);
|
||
throw mkdirError;
|
||
}
|
||
|
||
return { fullTargetPath, targetDir };
|
||
}
|
||
|
||
// 带重试机制的单个文件下载
|
||
// 实现最多3次重试,失败后跳过该文件的功能
|
||
async function downloadFileWithRetry(config, parentWindow, fileIndex) {
|
||
const maxRetries = 3;
|
||
let retryCount = 0;
|
||
|
||
// 获取文件名用于日志显示
|
||
const fileName = path.basename(config.filePath);
|
||
|
||
while (retryCount <= maxRetries) {
|
||
try {
|
||
await appendDownloadLog(
|
||
{
|
||
level: "loading",
|
||
message: `正在下载草稿内容文件: ${fileName} (第${fileIndex}个文件) ${retryCount > 0 ? `(重试第${retryCount}次)` : ''}`,
|
||
},
|
||
parentWindow
|
||
);
|
||
|
||
logger.info(`[log] start get file context : ${config.fileUrl}, retry: ${retryCount}`);
|
||
|
||
await downloadSingleFile(config, parentWindow);
|
||
|
||
logger.info(`[log] file saved to : ${config.filePath}`);
|
||
await appendDownloadLog(
|
||
{ level: "success", message: `第 ${fileIndex} 个草稿信息文件保存成功` },
|
||
parentWindow
|
||
);
|
||
return true; // 下载成功
|
||
} catch (error) {
|
||
retryCount++;
|
||
logger.error(`[error] download file ${config.fileUrl} failed (attempt ${retryCount}/${maxRetries}):`, error);
|
||
|
||
if (retryCount >= maxRetries) {
|
||
// 达到最大重试次数,记录失败并跳过
|
||
await appendDownloadLog(
|
||
{ level: "error", message: `第 ${fileIndex} 个草稿信息文件下载失败,已达到最大重试次数(${maxRetries})` },
|
||
parentWindow
|
||
);
|
||
return false; // 下载失败
|
||
} else {
|
||
// 等待一段时间再重试
|
||
await new Promise(resolve => setTimeout(resolve, 1000)); // 等待1秒再重试
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 批量下载文件主函数
|
||
async function downloadFiles(
|
||
{ sourceUrl, remoteFileUrls, targetId, isOpenDir },
|
||
parentWindow
|
||
) {
|
||
try {
|
||
let baseTargetDir = "";
|
||
// 然后获取目标目录,将主窗口作为父窗口传递
|
||
try {
|
||
baseTargetDir = await getTargetDirectory(parentWindow);
|
||
} catch (error) {
|
||
logger.error("[log] get target dir fail:", error);
|
||
baseTargetDir = '';
|
||
}
|
||
|
||
if (!baseTargetDir) {
|
||
await appendDownloadLog(
|
||
{ level: "error", message: `获取目录失败` },
|
||
parentWindow
|
||
);
|
||
return;
|
||
}
|
||
|
||
logger.info("[log] get target dir:", baseTargetDir);
|
||
|
||
await appendDownloadLog(
|
||
{ level: "info", message: `创建剪映草稿目录:${targetId}` },
|
||
parentWindow
|
||
);
|
||
|
||
let successCount = 0;
|
||
let failureCount = 0;
|
||
|
||
// 2. 遍历远程文件URL数组
|
||
for (let i = 0; i < remoteFileUrls.length; i++) {
|
||
const fileUrl = remoteFileUrls[i];
|
||
const fileIndex = i + 1; // 文件索引从1开始
|
||
|
||
try {
|
||
// 获取目标文件路径
|
||
const targetPaths = await getTargetFilePath(fileUrl, baseTargetDir, targetId);
|
||
|
||
if (!targetPaths) {
|
||
logger.error(`[error] 无法获取第 ${fileIndex} 个文件的目标路径: ${fileUrl}`);
|
||
await appendDownloadLog(
|
||
{ level: "error", message: `第 ${fileIndex} 个草稿信息文件路径解析失败` },
|
||
parentWindow
|
||
);
|
||
failureCount++;
|
||
continue; // 跳过当前文件
|
||
}
|
||
|
||
const { fullTargetPath, targetDir } = targetPaths;
|
||
|
||
// 带重试机制的下载文件
|
||
const downloadSuccess = await downloadFileWithRetry(
|
||
{ fileUrl, filePath: fullTargetPath, targetDir, targetId },
|
||
parentWindow,
|
||
fileIndex
|
||
);
|
||
|
||
if (downloadSuccess) {
|
||
successCount++;
|
||
} else {
|
||
failureCount++;
|
||
}
|
||
} catch (error) {
|
||
logger.error(`[error] 处理第 ${fileIndex} 个文件时发生错误:`, error);
|
||
await appendDownloadLog(
|
||
{ level: "error", message: `第 ${fileIndex} 个草稿信息文件处理失败` },
|
||
parentWindow
|
||
);
|
||
failureCount++;
|
||
}
|
||
}
|
||
|
||
// 输出最终统计结果
|
||
await appendDownloadLog(
|
||
{
|
||
level: "all",
|
||
message: `下载完成:共 ${remoteFileUrls.length} 个文件,成功 ${successCount} 个,失败 ${failureCount} 个`
|
||
},
|
||
parentWindow
|
||
);
|
||
|
||
// {id: 'uuid', time: Date, draft_id: 'draft_id', draft_url: 'draft_url' }
|
||
await appendHistoryRecord({
|
||
id: uuidv4(),
|
||
time: new Date(),
|
||
draft_id: targetId,
|
||
draft_url: sourceUrl,
|
||
});
|
||
const jointPath = path.join(baseTargetDir, targetId);
|
||
logger.info(`[finish] all download: ${jointPath}`);
|
||
|
||
// 触发剪映目录扫描,使剪映无需重启即可识别新草稿
|
||
await triggerDirectoryScan(jointPath);
|
||
|
||
if (isOpenDir) await openDraftDirectory(jointPath);
|
||
|
||
return {
|
||
success: true,
|
||
message: `文件批量保存完成,保存至目录: ${jointPath},成功 ${successCount} 个,失败 ${failureCount} 个`,
|
||
};
|
||
} catch (error) {
|
||
logger.error(`[error] 批量保存过程发生错误:`, error);
|
||
|
||
await appendDownloadLog(
|
||
{
|
||
level: "error",
|
||
message: `下载失败:批量保存 ${targetId} 中的剪映草稿过程发生错误!`,
|
||
},
|
||
parentWindow
|
||
);
|
||
return { success: false, message: `保存失败: ${error.message} ` };
|
||
}
|
||
}
|
||
|
||
async function checkUrlAccessRight(url) {
|
||
try {
|
||
const response = await axios({
|
||
...axiosConfig,
|
||
method: 'HEAD',
|
||
url: url,
|
||
timeout: 5000
|
||
});
|
||
logger.info(`URL Accessibility Check Result: ${url} - ${response.status}`);
|
||
return { accessible: response.status < 400 };
|
||
} catch (error) {
|
||
logger.error(`URL Accessibility Check Failed: ${url}`, error);
|
||
return { accessible: false, error: error.message };
|
||
}
|
||
}
|
||
|
||
module.exports = {
|
||
readDownloadLog,
|
||
clearDownloadLog,
|
||
|
||
updateDraftPath,
|
||
|
||
readConfig,
|
||
|
||
getDraftUrls,
|
||
|
||
downloadFiles,
|
||
|
||
checkUrlAccessRight,
|
||
|
||
readHistoryRecord
|
||
};
|