前端部分无用按钮删除,文件处理优化
This commit is contained in:
@@ -4,6 +4,7 @@ const path = require('path');
|
|||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
const ConfigManager = require('../config/config-manager');
|
const ConfigManager = require('../config/config-manager');
|
||||||
const FileUtils = require('../utils/file-utils');
|
const FileUtils = require('../utils/file-utils');
|
||||||
|
const ErrorHandler = require('../utils/error-handler');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 文件管理器 - 负责文件下载、检查和目录管理
|
* 文件管理器 - 负责文件下载、检查和目录管理
|
||||||
@@ -92,6 +93,27 @@ class FileManager {
|
|||||||
* 简单的文件下载方法
|
* 简单的文件下载方法
|
||||||
*/
|
*/
|
||||||
async downloadFile(url, filePath) {
|
async downloadFile(url, filePath) {
|
||||||
|
const maxRetries = this.downloadConfig.retryAttempts;
|
||||||
|
let lastError = null;
|
||||||
|
|
||||||
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||||
|
try {
|
||||||
|
// 使用增强的文件工具类确保目录存在
|
||||||
|
const dirPath = path.dirname(filePath);
|
||||||
|
const dirCreated = await FileUtils.safeEnsureDirEnhanced(dirPath);
|
||||||
|
|
||||||
|
if (!dirCreated) {
|
||||||
|
throw new Error(`无法创建目录: ${dirPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查文件是否被占用
|
||||||
|
if (await fs.pathExists(filePath)) {
|
||||||
|
const fileReleased = await FileUtils.waitForFileRelease(filePath);
|
||||||
|
if (!fileReleased) {
|
||||||
|
throw new Error(`文件被占用,无法写入: ${filePath}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const response = await axios({
|
const response = await axios({
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: url,
|
url: url,
|
||||||
@@ -103,11 +125,20 @@ class FileManager {
|
|||||||
timeout: 60000
|
timeout: 60000
|
||||||
});
|
});
|
||||||
|
|
||||||
const writer = fs.createWriteStream(filePath);
|
// 使用增强的写入流创建方法
|
||||||
|
const writer = await FileUtils.safeCreateWriteStream(filePath);
|
||||||
response.data.pipe(writer);
|
response.data.pipe(writer);
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
writer.on('finish', resolve);
|
writer.on('finish', () => {
|
||||||
|
// 验证文件是否写入成功
|
||||||
|
fs.access(filePath)
|
||||||
|
.then(() => resolve())
|
||||||
|
.catch(error => {
|
||||||
|
console.error(`文件写入验证失败: ${filePath}`, error.message);
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
writer.on('error', async (error) => {
|
writer.on('error', async (error) => {
|
||||||
// 下载失败时删除文件
|
// 下载失败时删除文件
|
||||||
try {
|
try {
|
||||||
@@ -118,6 +149,34 @@ class FileManager {
|
|||||||
reject(error);
|
reject(error);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
} catch (error) {
|
||||||
|
lastError = error;
|
||||||
|
|
||||||
|
// 处理文件系统错误
|
||||||
|
const errorResult = ErrorHandler.handleFileSystemError(error, filePath, 'download');
|
||||||
|
|
||||||
|
console.error(`下载文件失败 (尝试 ${attempt}/${maxRetries}): ${filePath}`, error.message);
|
||||||
|
|
||||||
|
// 如果不是可重试的错误,直接抛出
|
||||||
|
if (!errorResult.retryable) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果是最后一次尝试,抛出错误
|
||||||
|
if (attempt === maxRetries) {
|
||||||
|
console.error(`下载文件最终失败: ${filePath}`, error.message);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 等待后重试
|
||||||
|
const retryDelay = ErrorHandler.getRetryDelay(error, attempt);
|
||||||
|
console.log(`等待 ${retryDelay}ms 后重试下载: ${filePath}`);
|
||||||
|
await new Promise(resolve => setTimeout(resolve, retryDelay));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果所有重试都失败了
|
||||||
|
throw lastError;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -157,7 +216,19 @@ class FileManager {
|
|||||||
if (!name) return 'Untitled';
|
if (!name) return 'Untitled';
|
||||||
|
|
||||||
// 移除或替换Windows文件系统不允许的字符
|
// 移除或替换Windows文件系统不允许的字符
|
||||||
let safeName = name.replace(/[<>:"/\\|?*]/g, '_');
|
let safeName = name
|
||||||
|
// 替换Windows文件系统不允许的字符
|
||||||
|
.replace(/[<>:"/\\|?*]/g, '_')
|
||||||
|
// 替换波浪号和其他可能导致问题的字符
|
||||||
|
.replace(/[~`!@#$%^&*()+=\[\]{};',]/g, '_')
|
||||||
|
// 替换控制字符
|
||||||
|
.replace(/[\x00-\x1f\x7f]/g, '_')
|
||||||
|
// 替换Unicode控制字符
|
||||||
|
.replace(/[\u0000-\u001F\u007F-\u009F]/g, '_')
|
||||||
|
// 替换零宽字符
|
||||||
|
.replace(/[\u200B-\u200D\uFEFF]/g, '_')
|
||||||
|
// 替换其他可能导致问题的Unicode字符
|
||||||
|
.replace(/[\uFFFD]/g, '_');
|
||||||
|
|
||||||
// 移除前后空格和点
|
// 移除前后空格和点
|
||||||
safeName = safeName.trim().replace(/^\.+|\.+$/g, '');
|
safeName = safeName.trim().replace(/^\.+|\.+$/g, '');
|
||||||
@@ -167,11 +238,22 @@ class FileManager {
|
|||||||
safeName = 'Untitled';
|
safeName = 'Untitled';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 限制长度,避免路径过长
|
// 限制长度,避免路径过长(Windows路径限制为260字符)
|
||||||
if (safeName.length > 100) {
|
if (safeName.length > 100) {
|
||||||
safeName = safeName.substring(0, 100);
|
safeName = safeName.substring(0, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 确保不以数字开头(避免与Windows保留名称冲突)
|
||||||
|
if (/^\d/.test(safeName)) {
|
||||||
|
safeName = 'artwork_' + safeName;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否为Windows保留名称
|
||||||
|
const reservedNames = ['CON', 'PRN', 'AUX', 'NUL', 'COM1', 'COM2', 'COM3', 'COM4', 'COM5', 'COM6', 'COM7', 'COM8', 'COM9', 'LPT1', 'LPT2', 'LPT3', 'LPT4', 'LPT5', 'LPT6', 'LPT7', 'LPT8', 'LPT9'];
|
||||||
|
if (reservedNames.includes(safeName.toUpperCase())) {
|
||||||
|
safeName = 'artwork_' + safeName;
|
||||||
|
}
|
||||||
|
|
||||||
return safeName;
|
return safeName;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,7 +261,7 @@ class FileManager {
|
|||||||
* 确保目录存在
|
* 确保目录存在
|
||||||
*/
|
*/
|
||||||
async ensureDirectory(dirPath) {
|
async ensureDirectory(dirPath) {
|
||||||
const success = await FileUtils.safeEnsureDir(dirPath);
|
const success = await FileUtils.safeEnsureDirEnhanced(dirPath);
|
||||||
if (!success) {
|
if (!success) {
|
||||||
throw new Error(`目录创建失败: ${dirPath}`);
|
throw new Error(`目录创建失败: ${dirPath}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,328 @@
|
|||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 错误处理工具类 - 专门处理打包后的权限问题
|
||||||
|
*/
|
||||||
|
class ErrorHandler {
|
||||||
|
/**
|
||||||
|
* 处理文件系统错误
|
||||||
|
*/
|
||||||
|
static handleFileSystemError(error, filePath, operation = 'unknown') {
|
||||||
|
const errorInfo = {
|
||||||
|
originalError: error,
|
||||||
|
filePath: filePath,
|
||||||
|
operation: operation,
|
||||||
|
isPkg: process.pkg !== undefined,
|
||||||
|
platform: process.platform,
|
||||||
|
errorCode: error.code,
|
||||||
|
errorMessage: error.message
|
||||||
|
};
|
||||||
|
|
||||||
|
// 记录错误信息
|
||||||
|
console.error(`文件系统错误 [${operation}]:`, {
|
||||||
|
filePath: filePath,
|
||||||
|
errorCode: error.code,
|
||||||
|
errorMessage: error.message,
|
||||||
|
isPkg: errorInfo.isPkg,
|
||||||
|
platform: errorInfo.platform
|
||||||
|
});
|
||||||
|
|
||||||
|
// 根据错误类型提供解决方案
|
||||||
|
switch (error.code) {
|
||||||
|
case 'EPERM':
|
||||||
|
return this.handlePermissionError(errorInfo);
|
||||||
|
case 'EACCES':
|
||||||
|
return this.handleAccessError(errorInfo);
|
||||||
|
case 'EBUSY':
|
||||||
|
return this.handleBusyError(errorInfo);
|
||||||
|
case 'ENOENT':
|
||||||
|
return this.handleNotFoundError(errorInfo);
|
||||||
|
case 'EISDIR':
|
||||||
|
return this.handleIsDirectoryError(errorInfo);
|
||||||
|
case 'ENOTDIR':
|
||||||
|
return this.handleNotDirectoryError(errorInfo);
|
||||||
|
default:
|
||||||
|
return this.handleGenericError(errorInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理权限错误 (EPERM)
|
||||||
|
*/
|
||||||
|
static handlePermissionError(errorInfo) {
|
||||||
|
const { filePath, isPkg, platform } = errorInfo;
|
||||||
|
|
||||||
|
console.error('权限错误 (EPERM) 解决方案:');
|
||||||
|
|
||||||
|
if (platform === 'win32') {
|
||||||
|
console.error('Windows 权限问题解决方案:');
|
||||||
|
console.error('1. 以管理员身份运行程序');
|
||||||
|
console.error('2. 检查文件/目录权限');
|
||||||
|
console.error('3. 检查防病毒软件是否阻止访问');
|
||||||
|
console.error('4. 检查文件是否被其他程序占用');
|
||||||
|
|
||||||
|
if (isPkg) {
|
||||||
|
console.error('5. 打包环境特殊处理:');
|
||||||
|
console.error(' - 确保程序有写入权限');
|
||||||
|
console.error(' - 尝试使用用户目录而不是程序目录');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error('Unix/Linux 权限问题解决方案:');
|
||||||
|
console.error('1. 检查文件权限: chmod 755 <file>');
|
||||||
|
console.error('2. 检查目录权限: chmod 755 <directory>');
|
||||||
|
console.error('3. 检查用户权限');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'PERMISSION_ERROR',
|
||||||
|
message: `权限不足,无法${errorInfo.operation}文件: ${filePath}`,
|
||||||
|
solutions: this.getPermissionSolutions(errorInfo),
|
||||||
|
retryable: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理访问错误 (EACCES)
|
||||||
|
*/
|
||||||
|
static handleAccessError(errorInfo) {
|
||||||
|
const { filePath, platform } = errorInfo;
|
||||||
|
|
||||||
|
console.error('访问错误 (EACCES) 解决方案:');
|
||||||
|
console.error('1. 检查文件/目录是否存在');
|
||||||
|
console.error('2. 检查用户是否有访问权限');
|
||||||
|
console.error('3. 检查文件系统权限');
|
||||||
|
|
||||||
|
if (platform === 'win32') {
|
||||||
|
console.error('4. 检查 Windows 安全设置');
|
||||||
|
console.error('5. 尝试以管理员身份运行');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'ACCESS_ERROR',
|
||||||
|
message: `访问被拒绝: ${filePath}`,
|
||||||
|
solutions: this.getAccessSolutions(errorInfo),
|
||||||
|
retryable: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理文件占用错误 (EBUSY)
|
||||||
|
*/
|
||||||
|
static handleBusyError(errorInfo) {
|
||||||
|
const { filePath } = errorInfo;
|
||||||
|
|
||||||
|
console.error('文件占用错误 (EBUSY) 解决方案:');
|
||||||
|
console.error('1. 关闭可能占用文件的程序');
|
||||||
|
console.error('2. 等待文件释放后重试');
|
||||||
|
console.error('3. 重启相关程序');
|
||||||
|
console.error('4. 检查是否有其他进程在使用文件');
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'BUSY_ERROR',
|
||||||
|
message: `文件被占用: ${filePath}`,
|
||||||
|
solutions: this.getBusySolutions(errorInfo),
|
||||||
|
retryable: true,
|
||||||
|
retryDelay: 1000 // 1秒后重试
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理文件不存在错误 (ENOENT)
|
||||||
|
*/
|
||||||
|
static handleNotFoundError(errorInfo) {
|
||||||
|
const { filePath } = errorInfo;
|
||||||
|
|
||||||
|
console.error('文件不存在错误 (ENOENT) 解决方案:');
|
||||||
|
console.error('1. 检查文件路径是否正确');
|
||||||
|
console.error('2. 确保目录存在');
|
||||||
|
console.error('3. 检查文件名是否正确');
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'NOT_FOUND_ERROR',
|
||||||
|
message: `文件不存在: ${filePath}`,
|
||||||
|
solutions: this.getNotFoundSolutions(errorInfo),
|
||||||
|
retryable: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理是目录错误 (EISDIR)
|
||||||
|
*/
|
||||||
|
static handleIsDirectoryError(errorInfo) {
|
||||||
|
const { filePath } = errorInfo;
|
||||||
|
|
||||||
|
console.error('是目录错误 (EISDIR) 解决方案:');
|
||||||
|
console.error('1. 检查路径是否指向目录而不是文件');
|
||||||
|
console.error('2. 确保使用正确的文件路径');
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'IS_DIRECTORY_ERROR',
|
||||||
|
message: `路径是目录而不是文件: ${filePath}`,
|
||||||
|
solutions: this.getIsDirectorySolutions(errorInfo),
|
||||||
|
retryable: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理不是目录错误 (ENOTDIR)
|
||||||
|
*/
|
||||||
|
static handleNotDirectoryError(errorInfo) {
|
||||||
|
const { filePath } = errorInfo;
|
||||||
|
|
||||||
|
console.error('不是目录错误 (ENOTDIR) 解决方案:');
|
||||||
|
console.error('1. 检查路径是否指向文件而不是目录');
|
||||||
|
console.error('2. 确保使用正确的目录路径');
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'NOT_DIRECTORY_ERROR',
|
||||||
|
message: `路径不是目录: ${filePath}`,
|
||||||
|
solutions: this.getNotDirectorySolutions(errorInfo),
|
||||||
|
retryable: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理通用错误
|
||||||
|
*/
|
||||||
|
static handleGenericError(errorInfo) {
|
||||||
|
const { filePath, errorCode, errorMessage } = errorInfo;
|
||||||
|
|
||||||
|
console.error(`通用文件系统错误 (${errorCode}): ${errorMessage}`);
|
||||||
|
console.error('1. 检查文件系统状态');
|
||||||
|
console.error('2. 检查磁盘空间');
|
||||||
|
console.error('3. 检查文件系统权限');
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'GENERIC_ERROR',
|
||||||
|
message: `文件系统错误: ${errorMessage}`,
|
||||||
|
errorCode: errorCode,
|
||||||
|
solutions: this.getGenericSolutions(errorInfo),
|
||||||
|
retryable: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取权限错误解决方案
|
||||||
|
*/
|
||||||
|
static getPermissionSolutions(errorInfo) {
|
||||||
|
const solutions = [];
|
||||||
|
|
||||||
|
if (errorInfo.platform === 'win32') {
|
||||||
|
solutions.push('以管理员身份运行程序');
|
||||||
|
solutions.push('检查文件和目录权限');
|
||||||
|
solutions.push('检查防病毒软件设置');
|
||||||
|
solutions.push('检查文件是否被其他程序占用');
|
||||||
|
|
||||||
|
if (errorInfo.isPkg) {
|
||||||
|
solutions.push('尝试使用用户目录而不是程序目录');
|
||||||
|
solutions.push('检查程序安装目录的写入权限');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
solutions.push('使用 chmod 命令修改文件权限');
|
||||||
|
solutions.push('检查用户和组权限');
|
||||||
|
solutions.push('使用 sudo 运行程序');
|
||||||
|
}
|
||||||
|
|
||||||
|
return solutions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取访问错误解决方案
|
||||||
|
*/
|
||||||
|
static getAccessSolutions(errorInfo) {
|
||||||
|
return [
|
||||||
|
'检查文件/目录是否存在',
|
||||||
|
'检查用户访问权限',
|
||||||
|
'检查文件系统权限',
|
||||||
|
errorInfo.platform === 'win32' ? '以管理员身份运行' : '使用 sudo 运行'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取文件占用错误解决方案
|
||||||
|
*/
|
||||||
|
static getBusySolutions(errorInfo) {
|
||||||
|
return [
|
||||||
|
'关闭占用文件的程序',
|
||||||
|
'等待文件释放后重试',
|
||||||
|
'重启相关程序',
|
||||||
|
'检查进程列表'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取文件不存在错误解决方案
|
||||||
|
*/
|
||||||
|
static getNotFoundSolutions(errorInfo) {
|
||||||
|
return [
|
||||||
|
'检查文件路径是否正确',
|
||||||
|
'确保目录存在',
|
||||||
|
'检查文件名是否正确',
|
||||||
|
'检查路径分隔符'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取是目录错误解决方案
|
||||||
|
*/
|
||||||
|
static getIsDirectorySolutions(errorInfo) {
|
||||||
|
return [
|
||||||
|
'检查路径是否指向目录',
|
||||||
|
'使用正确的文件路径',
|
||||||
|
'检查路径分隔符'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取不是目录错误解决方案
|
||||||
|
*/
|
||||||
|
static getNotDirectorySolutions(errorInfo) {
|
||||||
|
return [
|
||||||
|
'检查路径是否指向文件',
|
||||||
|
'使用正确的目录路径',
|
||||||
|
'检查路径分隔符'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取通用错误解决方案
|
||||||
|
*/
|
||||||
|
static getGenericSolutions(errorInfo) {
|
||||||
|
return [
|
||||||
|
'检查文件系统状态',
|
||||||
|
'检查磁盘空间',
|
||||||
|
'检查文件系统权限',
|
||||||
|
'重启程序或系统'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否为可重试的错误
|
||||||
|
*/
|
||||||
|
static isRetryableError(error) {
|
||||||
|
const retryableCodes = ['EPERM', 'EACCES', 'EBUSY', 'EAGAIN', 'ENOSPC'];
|
||||||
|
return retryableCodes.includes(error.code);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取重试延迟时间
|
||||||
|
*/
|
||||||
|
static getRetryDelay(error, attempt = 1) {
|
||||||
|
const baseDelay = 1000; // 1秒
|
||||||
|
const maxDelay = 10000; // 10秒
|
||||||
|
const delay = Math.min(baseDelay * Math.pow(2, attempt - 1), maxDelay);
|
||||||
|
|
||||||
|
// 对于特定错误类型,使用不同的延迟策略
|
||||||
|
switch (error.code) {
|
||||||
|
case 'EBUSY':
|
||||||
|
return Math.min(delay, 2000); // 文件占用错误,较短延迟
|
||||||
|
case 'EPERM':
|
||||||
|
case 'EACCES':
|
||||||
|
return Math.min(delay, 5000); // 权限错误,中等延迟
|
||||||
|
default:
|
||||||
|
return delay;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = ErrorHandler;
|
||||||
@@ -50,6 +50,141 @@ class FileUtils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 增强的目录创建方法(专门处理打包后的权限问题)
|
||||||
|
*/
|
||||||
|
static async safeEnsureDirEnhanced(dirPath) {
|
||||||
|
try {
|
||||||
|
// 规范化路径
|
||||||
|
const normalizedPath = path.resolve(dirPath);
|
||||||
|
|
||||||
|
// 检查是否在打包环境中
|
||||||
|
const isPkg = process.pkg !== undefined;
|
||||||
|
|
||||||
|
if (isPkg) {
|
||||||
|
// 在打包环境中,使用更保守的方法
|
||||||
|
return await this.createDirectoryRecursive(normalizedPath);
|
||||||
|
} else {
|
||||||
|
// 在开发环境中,使用标准方法
|
||||||
|
await fs.ensureDir(normalizedPath);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`增强目录创建失败: ${dirPath}`, error.message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 递归创建目录(处理权限问题)
|
||||||
|
*/
|
||||||
|
static async createDirectoryRecursive(dirPath) {
|
||||||
|
try {
|
||||||
|
const parts = dirPath.split(path.sep);
|
||||||
|
let currentPath = '';
|
||||||
|
|
||||||
|
for (const part of parts) {
|
||||||
|
if (!part) continue;
|
||||||
|
|
||||||
|
currentPath = currentPath ? path.join(currentPath, part) : part;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 检查目录是否已存在
|
||||||
|
const stats = await fs.stat(currentPath);
|
||||||
|
if (!stats.isDirectory()) {
|
||||||
|
throw new Error(`路径存在但不是目录: ${currentPath}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code === 'ENOENT') {
|
||||||
|
// 目录不存在,尝试创建
|
||||||
|
try {
|
||||||
|
await fs.mkdir(currentPath);
|
||||||
|
// 验证创建是否成功
|
||||||
|
await fs.access(currentPath);
|
||||||
|
} catch (mkdirError) {
|
||||||
|
console.error(`创建目录失败: ${currentPath}`, mkdirError.message);
|
||||||
|
throw mkdirError;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`递归创建目录失败: ${dirPath}`, error.message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 安全写入文件(处理权限问题)
|
||||||
|
*/
|
||||||
|
static async safeWriteFile(filePath, data, options = {}) {
|
||||||
|
try {
|
||||||
|
// 确保目录存在
|
||||||
|
const dirPath = path.dirname(filePath);
|
||||||
|
const dirCreated = await this.safeEnsureDirEnhanced(dirPath);
|
||||||
|
|
||||||
|
if (!dirCreated) {
|
||||||
|
throw new Error(`无法创建目录: ${dirPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查文件是否被占用
|
||||||
|
if (await fs.pathExists(filePath)) {
|
||||||
|
try {
|
||||||
|
// 尝试删除现有文件
|
||||||
|
await fs.remove(filePath);
|
||||||
|
} catch (removeError) {
|
||||||
|
console.warn(`删除现有文件失败: ${filePath}`, removeError.message);
|
||||||
|
// 继续尝试写入,可能会覆盖
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 写入文件
|
||||||
|
await fs.writeFile(filePath, data, options);
|
||||||
|
|
||||||
|
// 验证写入是否成功
|
||||||
|
await fs.access(filePath);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`安全写入文件失败: ${filePath}`, error.message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 安全创建写入流(处理权限问题)
|
||||||
|
*/
|
||||||
|
static async safeCreateWriteStream(filePath) {
|
||||||
|
try {
|
||||||
|
// 确保目录存在
|
||||||
|
const dirPath = path.dirname(filePath);
|
||||||
|
const dirCreated = await this.safeEnsureDirEnhanced(dirPath);
|
||||||
|
|
||||||
|
if (!dirCreated) {
|
||||||
|
throw new Error(`无法创建目录: ${dirPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查文件是否被占用
|
||||||
|
if (await fs.pathExists(filePath)) {
|
||||||
|
try {
|
||||||
|
await fs.remove(filePath);
|
||||||
|
} catch (removeError) {
|
||||||
|
console.warn(`删除现有文件失败: ${filePath}`, removeError.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建写入流
|
||||||
|
return fs.createWriteStream(filePath);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`创建写入流失败: ${filePath}`, error.message);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 安全检查文件是否存在(兼容 pkg 打包)
|
* 安全检查文件是否存在(兼容 pkg 打包)
|
||||||
*/
|
*/
|
||||||
@@ -129,6 +264,57 @@ class FileUtils {
|
|||||||
pkgVersion: process.pkg ? process.pkg.version : null,
|
pkgVersion: process.pkg ? process.pkg.version : null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查文件权限
|
||||||
|
*/
|
||||||
|
static async checkFilePermissions(filePath, mode = 'w') {
|
||||||
|
try {
|
||||||
|
const nativeFs = require('fs').promises;
|
||||||
|
await nativeFs.access(filePath, mode === 'w' ? nativeFs.constants.W_OK : nativeFs.constants.R_OK);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查目录权限
|
||||||
|
*/
|
||||||
|
static async checkDirectoryPermissions(dirPath, mode = 'w') {
|
||||||
|
try {
|
||||||
|
const nativeFs = require('fs').promises;
|
||||||
|
await nativeFs.access(dirPath, mode === 'w' ? nativeFs.constants.W_OK : nativeFs.constants.R_OK);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 等待文件释放(处理文件占用问题)
|
||||||
|
*/
|
||||||
|
static async waitForFileRelease(filePath, maxWaitTime = 5000) {
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
while (Date.now() - startTime < maxWaitTime) {
|
||||||
|
try {
|
||||||
|
const nativeFs = require('fs').promises;
|
||||||
|
await nativeFs.access(filePath, nativeFs.constants.W_OK);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code === 'EBUSY' || error.code === 'EACCES') {
|
||||||
|
// 文件被占用或无权限,等待一段时间后重试
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// 其他错误,直接返回
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = FileUtils;
|
module.exports = FileUtils;
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
<div v-if="activeTab === 'config'" class="tab-content">
|
<div v-if="activeTab === 'config'" class="tab-content">
|
||||||
<RepositoryConfigComponent :config="config" :migrating="migrating" :migration-progress="migrationProgress"
|
<RepositoryConfigComponent :config="config" :migrating="migrating" :migration-progress="migrationProgress"
|
||||||
:migration-percent="migrationPercent" :migration-result="migrationResult" @save-config="saveConfig"
|
:migration-percent="migrationPercent" :migration-result="migrationResult" @save-config="saveConfig"
|
||||||
@reset-config="resetConfig" @select-download-dir="selectDownloadDir" @test-download-dir="testDownloadDir"
|
@reset-config="resetConfig" @test-download-dir="testDownloadDir"
|
||||||
@config-saved="handleConfigSaved" />
|
@config-saved="handleConfigSaved" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -5,15 +5,8 @@
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>下载目录</label>
|
<label>下载目录</label>
|
||||||
<div class="path-input-group">
|
<div class="path-input-group">
|
||||||
<input
|
<input v-model="config.downloadDir" type="text" placeholder="设置下载目录路径,例如: ./downloads 或 D:\downloads"
|
||||||
v-model="config.downloadDir"
|
class="form-input" />
|
||||||
type="text"
|
|
||||||
placeholder="设置下载目录路径,例如: ./downloads 或 D:\downloads"
|
|
||||||
class="form-input"
|
|
||||||
/>
|
|
||||||
<button type="button" @click="selectDownloadDir" class="btn btn-secondary">
|
|
||||||
选择目录
|
|
||||||
</button>
|
|
||||||
<button type="button" @click="testDownloadDir" class="btn btn-outline">
|
<button type="button" @click="testDownloadDir" class="btn btn-outline">
|
||||||
测试
|
测试
|
||||||
</button>
|
</button>
|
||||||
@@ -29,11 +22,7 @@
|
|||||||
<!-- 自动迁移选项 -->
|
<!-- 自动迁移选项 -->
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="checkbox-label">
|
<label class="checkbox-label">
|
||||||
<input
|
<input v-model="config.autoMigration" type="checkbox" class="form-checkbox" />
|
||||||
v-model="config.autoMigration"
|
|
||||||
type="checkbox"
|
|
||||||
class="form-checkbox"
|
|
||||||
/>
|
|
||||||
<span>自动迁移旧下载文件</span>
|
<span>自动迁移旧下载文件</span>
|
||||||
</label>
|
</label>
|
||||||
<small class="form-help">
|
<small class="form-help">
|
||||||
@@ -61,12 +50,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="migration-log">
|
<div class="migration-log">
|
||||||
<h5>详细日志</h5>
|
<h5>详细日志</h5>
|
||||||
<div
|
<div v-for="(item, index) in migrationResult.log.slice(0, 10)" :key="index" class="log-item"
|
||||||
v-for="(item, index) in migrationResult.log.slice(0, 10)"
|
:class="(item as any).status">
|
||||||
:key="index"
|
|
||||||
class="log-item"
|
|
||||||
:class="(item as any).status"
|
|
||||||
>
|
|
||||||
<span class="log-status">{{ (item as any).status === 'success' ? '✅' : '⏭️' }}</span>
|
<span class="log-status">{{ (item as any).status === 'success' ? '✅' : '⏭️' }}</span>
|
||||||
<span class="log-text">{{ (item as any).title }} (ID: {{ (item as any).id }})</span>
|
<span class="log-text">{{ (item as any).title }} (ID: {{ (item as any).id }})</span>
|
||||||
<span v-if="(item as any).reason" class="log-reason">{{ (item as any).reason }}</span>
|
<span v-if="(item as any).reason" class="log-reason">{{ (item as any).reason }}</span>
|
||||||
@@ -87,31 +72,18 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>命名模式</label>
|
<label>命名模式</label>
|
||||||
<input
|
<input v-model="config.namingPattern" type="text" placeholder="{artist_name}/{artwork_id}_{title}"
|
||||||
v-model="config.namingPattern"
|
class="form-input" />
|
||||||
type="text"
|
|
||||||
placeholder="{artist_name}/{artwork_id}_{title}"
|
|
||||||
class="form-input"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>最大文件大小 (MB)</label>
|
<label>最大文件大小 (MB)</label>
|
||||||
<input
|
<input v-model.number="config.maxFileSize" type="number" placeholder="0表示无限制" class="form-input" />
|
||||||
v-model.number="config.maxFileSize"
|
|
||||||
type="number"
|
|
||||||
placeholder="0表示无限制"
|
|
||||||
class="form-input"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>允许的文件扩展名</label>
|
<label>允许的文件扩展名</label>
|
||||||
<input
|
<input :value="config.allowedExtensions.join(',')"
|
||||||
:value="config.allowedExtensions.join(',')"
|
|
||||||
@input="(e) => config.allowedExtensions = (e.target as HTMLInputElement).value.split(',').map(ext => ext.trim()).filter(ext => ext)"
|
@input="(e) => config.allowedExtensions = (e.target as HTMLInputElement).value.split(',').map(ext => ext.trim()).filter(ext => ext)"
|
||||||
type="text"
|
type="text" placeholder=".jpg,.png,.gif,.webp" class="form-input" />
|
||||||
placeholder=".jpg,.png,.gif,.webp"
|
|
||||||
class="form-input"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
<button @click="saveConfig" class="btn btn-primary" :disabled="saving">
|
<button @click="saveConfig" class="btn btn-primary" :disabled="saving">
|
||||||
@@ -141,7 +113,6 @@ interface Emits {
|
|||||||
(e: 'update:config', config: RepositoryConfig): void
|
(e: 'update:config', config: RepositoryConfig): void
|
||||||
(e: 'save-config'): void
|
(e: 'save-config'): void
|
||||||
(e: 'reset-config'): void
|
(e: 'reset-config'): void
|
||||||
(e: 'select-download-dir'): void
|
|
||||||
(e: 'test-download-dir'): void
|
(e: 'test-download-dir'): void
|
||||||
(e: 'config-saved'): void
|
(e: 'config-saved'): void
|
||||||
}
|
}
|
||||||
@@ -151,11 +122,6 @@ const emit = defineEmits<Emits>()
|
|||||||
|
|
||||||
const saving = ref(false)
|
const saving = ref(false)
|
||||||
|
|
||||||
// 选择下载目录
|
|
||||||
const selectDownloadDir = () => {
|
|
||||||
emit('select-download-dir')
|
|
||||||
}
|
|
||||||
|
|
||||||
// 测试下载目录
|
// 测试下载目录
|
||||||
const testDownloadDir = () => {
|
const testDownloadDir = () => {
|
||||||
emit('test-download-dir')
|
emit('test-download-dir')
|
||||||
|
|||||||
Reference in New Issue
Block a user