// 网盘管理系统 - 服务端逻辑 const fs = require('fs'); const path = require('path'); const { promisify } = require('util'); const { spawn } = require('child_process'); const formidable = require('formidable'); const mkdir = promisify(fs.mkdir); const readdir = promisify(fs.readdir); const stat = promisify(fs.stat); const access = promisify(fs.access); const copyFile = promisify(fs.copyFile); const rmdir = promisify(fs.rmdir); const unlink = promisify(fs.unlink); class DiskManager { constructor() { // 数据存储根目录(在 Server 目录下) this.rootDir = path.join(__dirname, 'disk_data'); this.tempDir = path.join(__dirname, 'temp'); this.pythonDir = path.join(__dirname, 'python'); this.ensureRootDir(); } // 确保根目录存在 async ensureRootDir() { try { await access(this.rootDir); } catch (error) { await mkdir(this.rootDir, { recursive: true }); console.log('创建disk_data目录:', this.rootDir); } } // 获取Python命令(尝试python或python3) async getPythonCommand() { return new Promise((resolve) => { const python = spawn('python', ['--version']); python.on('close', (code) => { if (code === 0) { resolve('python'); } else { const python3 = spawn('python3', ['--version']); python3.on('close', (code) => { resolve(code === 0 ? 'python3' : null); }); } }); python.on('error', () => { const python3 = spawn('python3', ['--version']); python3.on('close', (code) => { resolve(code === 0 ? 'python3' : null); }); python3.on('error', () => resolve(null)); }); }); } // 调用Python抠图脚本 async runImageMatting(inputFolder, outputFolder, onProgress) { const pythonCmd = await this.getPythonCommand(); if (!pythonCmd) { throw new Error('未找到Python环境,请安装Python 3.7+'); } const pythonExe = path.join(this.pythonDir, 'venv', 'Scripts', 'python.exe'); const scriptPath = path.join(this.pythonDir, 'image-matting.py'); // 检查是否使用虚拟环境 const usePython = await new Promise((resolve) => { fs.access(pythonExe, fs.constants.F_OK, (err) => { resolve(err ? pythonCmd : pythonExe); }); }); return new Promise((resolve, reject) => { console.log('\n' + '='.repeat(70)); console.log('【Python】🚀 启动AI抠图进程'); console.log('='.repeat(70)); console.log('【Python】命令:', usePython); console.log('【Python】脚本:', scriptPath); console.log('【Python】输入文件夹:', inputFolder); console.log('【Python】输出文件夹:', outputFolder); console.log('='.repeat(70) + '\n'); // 使用 -u 参数让Python输出无缓冲 const python = spawn(usePython, ['-u', scriptPath, inputFolder, outputFolder]); console.log('【Python】进程已启动,PID:', python.pid); let stdout = ''; let stderr = ''; let processed = 0; python.stdout.on('data', (data) => { const output = data.toString(); stdout += output; // 分行打印,保持格式 const lines = output.split('\n'); lines.forEach(line => { if (line.trim()) { console.log(`【Python】抠图: ${line}`); // 捕获进度信息: PROGRESS: 1/22 const progressMatch = line.match(/PROGRESS:\s*(\d+)\/(\d+)/); if (progressMatch) { const current = parseInt(progressMatch[1], 10); const total = parseInt(progressMatch[2], 10); console.log(`【Python】进度: ${current}/${total} (${Math.round(current/total*100)}%)`); // 调用进度回调 if (onProgress) { onProgress(current, total); } } } }); const successMatch = output.match(/成功:?\s*(\d+)|成功\s*(\d+)|Success:\s*(\d+)/); if (successMatch) { processed = parseInt(successMatch[1] || successMatch[2] || successMatch[3], 10); } }); python.stderr.on('data', (data) => { const error = data.toString(); stderr += error; // 实时打印stderr const lines = error.split('\n'); lines.forEach(line => { if (line.trim()) { console.error(`【Python】抠图错误: ${line}`); } }); }); python.on('close', (code) => { console.log('\n' + '='.repeat(70)); console.log(`【Python】抠图进程结束,退出码: ${code}`); if (code === 0) { console.log(`【Python】✅ 抠图成功!处理了 ${processed} 张图片`); console.log('='.repeat(70) + '\n'); resolve({ success: true, processed, message: '抠图完成' }); } else { console.error(`【Python】❌ 抠图失败,退出码: ${code}`); if (stderr) console.error(`【Python】错误详情:\n${stderr}`); console.log('='.repeat(70) + '\n'); reject(new Error(`抠图失败,退出码: ${code}\n${stderr}`)); } }); python.on('error', (error) => { console.error(`【Python】❌ 启动进程失败:`, error); reject(new Error(`启动Python进程失败: ${error.message}`)); }); }); } // 调用Python裁剪脚本 async runCutMiniSize(inputFolder, outputFolder, onProgress) { const pythonCmd = await this.getPythonCommand(); if (!pythonCmd) { throw new Error('未找到Python环境,请安装Python 3.7+'); } const pythonExe = path.join(this.pythonDir, 'venv', 'Scripts', 'python.exe'); const scriptPath = path.join(this.pythonDir, 'cut-mini-size.py'); const usePython = await new Promise((resolve) => { fs.access(pythonExe, fs.constants.F_OK, (err) => { resolve(err ? pythonCmd : pythonExe); }); }); return new Promise((resolve, reject) => { console.log('\n' + '='.repeat(70)); console.log('【Python】✂️ 启动智能裁剪进程'); console.log('='.repeat(70)); console.log('【Python】命令:', usePython); console.log('【Python】脚本:', scriptPath); console.log('【Python】输入文件夹:', inputFolder); console.log('【Python】输出文件夹:', outputFolder); console.log('='.repeat(70) + '\n'); // 使用 -u 参数让Python输出无缓冲 const python = spawn(usePython, ['-u', scriptPath, inputFolder, outputFolder]); console.log('【Python】进程已启动,PID:', python.pid); let stdout = ''; let stderr = ''; let processed = 0; let width = 0; let height = 0; python.stdout.on('data', (data) => { const output = data.toString(); stdout += output; // 分行打印,保持格式 const lines = output.split('\n'); lines.forEach(line => { if (line.trim()) { console.log(`【Python】裁剪: ${line}`); // 捕获进度信息: PROGRESS: 1/22 const progressMatch = line.match(/PROGRESS:\s*(\d+)\/(\d+)/); if (progressMatch) { const current = parseInt(progressMatch[1], 10); const total = parseInt(progressMatch[2], 10); console.log(`【Python】进度: ${current}/${total} (${Math.round(current/total*100)}%)`); // 调用进度回调 if (onProgress) { onProgress(current, total); } } } }); const successMatch = output.match(/成功:?\s*(\d+)|成功\s*(\d+)|Success:\s*(\d+)/); if (successMatch) { processed = parseInt(successMatch[1] || successMatch[2] || successMatch[3], 10); } const sizeMatch = output.match(/width=(\d+).*height=(\d+)/); if (sizeMatch) { width = parseInt(sizeMatch[1], 10); height = parseInt(sizeMatch[2], 10); } }); python.stderr.on('data', (data) => { const error = data.toString(); stderr += error; // 实时打印stderr const lines = error.split('\n'); lines.forEach(line => { if (line.trim()) { console.error(`【Python】裁剪错误: ${line}`); } }); }); python.on('close', (code) => { console.log('\n' + '='.repeat(70)); console.log(`【Python】裁剪进程结束,退出码: ${code}`); if (code === 0) { console.log(`【Python】✅ 裁剪成功!处理了 ${processed} 张图片`); console.log(`【Python】📐 最终尺寸: ${width}x${height}`); console.log('='.repeat(70) + '\n'); resolve({ success: true, processed, width, height, message: `裁剪完成,尺寸: ${width}x${height}` }); } else { console.error(`【Python】❌ 裁剪失败,退出码: ${code}`); if (stderr) console.error(`【Python】错误详情:\n${stderr}`); console.log('='.repeat(70) + '\n'); reject(new Error(`裁剪失败,退出码: ${code}\n${stderr}`)); } }); python.on('error', (error) => { console.error(`【Python】❌ 启动进程失败:`, error); reject(new Error(`启动Python进程失败: ${error.message}`)); }); }); } // 获取安全的文件路径 getSafePath(relativePath) { // 移除开头的斜杠 relativePath = relativePath.replace(/^\/+/, ''); // 解析路径,防止目录遍历攻击 const fullPath = path.join(this.rootDir, relativePath); // 确保路径在根目录内 if (!fullPath.startsWith(this.rootDir)) { throw new Error('非法路径'); } return fullPath; } // 检查文件夹是否包含预览图 async checkFolderPreview(folderPath) { const commonNames = ['01.png', '00.png', '001.png', '0001.png', '1.png', '0.png']; try { const items = await readdir(folderPath); // 首先检查常见的帧文件名 for (const name of commonNames) { if (items.includes(name)) { return { hasPreview: true, previewFile: name }; } } // 检查是否有任何PNG文件 const pngFiles = items.filter(item => item.toLowerCase().endsWith('.png')); if (pngFiles.length > 0) { // 按文件名排序,取第一个 pngFiles.sort(); return { hasPreview: true, previewFile: pngFiles[0] }; } return { hasPreview: false, previewFile: null }; } catch (error) { return { hasPreview: false, previewFile: null }; } } // 检查文件夹是否需要抠图(检测PNG图片是否有非透明背景) async checkNeedMatting(folderPath) { try { const sharp = require('sharp'); const items = await readdir(folderPath); // 获取所有PNG文件(只计算一次,不区分大小写) const pngFiles = items.filter(item => item.toLowerCase().endsWith('.png')); console.log(`[DiskManager] checkNeedMatting: ${folderPath}, 找到 ${pngFiles.length} 个PNG文件`); if (pngFiles.length === 0) { return { needsMatting: false, pngCount: 0 }; } // 检查前3张PNG图片是否有非透明背景(采样检测,提高性能) const samplesToCheck = Math.min(3, pngFiles.length); let hasOpaqueBackground = false; for (let i = 0; i < samplesToCheck; i++) { const filePath = path.join(folderPath, pngFiles[i]); const hasOpaque = await this.checkImageHasOpaqueBackground(filePath); if (hasOpaque) { hasOpaqueBackground = true; break; // 只要有一张有非透明背景,就需要抠图 } } return { needsMatting: hasOpaqueBackground, pngCount: pngFiles.length }; } catch (error) { console.error('[DiskManager] 检查抠图需求失败:', error); return { needsMatting: true, // 出错时默认显示抠图选项(保守策略) pngCount: 0 }; } } // 检查单个图片是否有非透明背景 async checkImageHasOpaqueBackground(imagePath) { try { const sharp = require('sharp'); const image = sharp(imagePath); const metadata = await image.metadata(); // 如果图片没有alpha通道,肯定有不透明背景 if (!metadata.hasAlpha) { return true; } // 获取图片数据,快速检查边缘像素 const { data, info } = await image .ensureAlpha() .raw() .toBuffer({ resolveWithObject: true }); const { width, height, channels } = info; // 采样检测:检查四个角和边缘的像素 const samplePoints = [ { x: 0, y: 0 }, // 左上 { x: width - 1, y: 0 }, // 右上 { x: 0, y: height - 1 }, // 左下 { x: width - 1, y: height - 1 }, // 右下 { x: Math.floor(width / 2), y: 0 }, // 顶部中间 { x: Math.floor(width / 2), y: height - 1 }, // 底部中间 ]; let opaqueCount = 0; for (const point of samplePoints) { const idx = (point.y * width + point.x) * channels; const alpha = data[idx + 3]; // 如果alpha接近255(不透明),说明可能有背景 if (alpha > 250) { const r = data[idx]; const g = data[idx + 1]; const b = data[idx + 2]; // 检查是否是白色或浅色背景(可能需要抠图) if (r > 230 && g > 230 && b > 230) { opaqueCount++; } } } // 如果超过一半的采样点都是不透明的浅色,判断为需要抠图 return opaqueCount >= 3; } catch (error) { console.error('[DiskManager] 检查图片背景失败:', error); return true; // 出错时默认需要抠图 } } // 处理文件列表请求 async handleListRequest(req, res) { try { const url = new URL(req.url, `http://${req.headers.host}`); const relativePath = url.searchParams.get('path') || ''; const recursive = url.searchParams.get('recursive') === 'true'; // 是否递归获取所有子文件夹 const fullPath = this.getSafePath(relativePath); // 检查目录是否存在 try { await access(fullPath); } catch (error) { // 目录不存在,创建它 await mkdir(fullPath, { recursive: true }); } if (recursive) { // 递归获取所有文件夹结构 const allFiles = await this.getFilesRecursive(fullPath, relativePath); res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify({ success: true, files: allFiles, recursive: true })); } else { // 只获取当前目录 const items = await readdir(fullPath); const files = []; for (const item of items) { const itemPath = path.join(fullPath, item); const stats = await stat(itemPath); const itemRelativePath = path.join(relativePath, item).replace(/\\/g, '/'); const fileInfo = { name: item, path: itemRelativePath, type: stats.isDirectory() ? 'directory' : 'file', size: stats.size, modifiedTime: stats.mtime }; // 如果是文件夹,检查是否包含PNG预览图,并提供完整的预览URL if (stats.isDirectory()) { const previewInfo = await this.checkFolderPreview(itemPath); fileInfo.hasPreview = previewInfo.hasPreview; if (previewInfo.hasPreview && previewInfo.previewFile) { // 返回完整的预览URL路径 fileInfo.previewUrl = `/api/disk/preview?path=${encodeURIComponent(itemRelativePath + '/' + previewInfo.previewFile)}`; } // 检查是否需要抠图(是否有非透明背景的PNG) const mattingInfo = await this.checkNeedMatting(itemPath); fileInfo.needsMatting = mattingInfo.needsMatting; fileInfo.pngCount = mattingInfo.pngCount; } files.push(fileInfo); } // 排序:文件夹在前,然后按名称排序 files.sort((a, b) => { if (a.type === 'directory' && b.type !== 'directory') return -1; if (a.type !== 'directory' && b.type === 'directory') return 1; return a.name.localeCompare(b.name, 'zh-CN'); }); res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify({ success: true, files: files, recursive: false })); } } catch (error) { console.error('获取文件列表失败:', error); res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify({ success: false, message: error.message })); } } // 递归获取所有文件夹结构(用于前端缓存) async getFilesRecursive(fullPath, relativePath) { const allFiles = []; const processDirectory = async (dirFullPath, dirRelativePath) => { try { const items = await readdir(dirFullPath); for (const item of items) { const itemPath = path.join(dirFullPath, item); const stats = await stat(itemPath); const itemRelativePath = dirRelativePath ? path.join(dirRelativePath, item).replace(/\\/g, '/') : item; const fileInfo = { name: item, path: itemRelativePath, type: stats.isDirectory() ? 'directory' : 'file', size: stats.size, modifiedTime: stats.mtime }; // 如果是文件夹,检查是否包含PNG if (stats.isDirectory()) { const previewInfo = await this.checkFolderPreview(itemPath); fileInfo.hasPreview = previewInfo.hasPreview; if (previewInfo.hasPreview && previewInfo.previewFile) { fileInfo.previewUrl = `/api/disk/preview?path=${encodeURIComponent(itemRelativePath + '/' + previewInfo.previewFile)}`; } const mattingInfo = await this.checkNeedMatting(itemPath); fileInfo.needsMatting = mattingInfo.needsMatting; fileInfo.pngCount = mattingInfo.pngCount; // 递归处理子文件夹 await processDirectory(itemPath, itemRelativePath); } allFiles.push(fileInfo); } } catch (error) { console.error(`递归读取目录失败: ${dirRelativePath}`, error); } }; await processDirectory(fullPath, relativePath); // 排序:文件夹在前,然后按路径排序 allFiles.sort((a, b) => { if (a.type === 'directory' && b.type !== 'directory') return -1; if (a.type !== 'directory' && b.type === 'directory') return 1; return a.path.localeCompare(b.path, 'zh-CN'); }); console.log(`[DiskManager] 递归获取文件结构完成,共 ${allFiles.length} 个项目`); return allFiles; } // 处理文件上传请求 async handleUploadRequest(req, res) { try { const form = formidable.formidable({ multiples: true, maxFileSize: 500 * 1024 * 1024, // 500MB keepExtensions: true }); form.parse(req, async (err, fields, files) => { if (err) { console.error('解析上传文件失败:', err); res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify({ success: false, message: '上传失败' })); return; } try { // 新版 formidable 将字段解析为数组,需要取第一个元素 const relativePath = Array.isArray(fields.path) ? fields.path[0] : (fields.path || ''); const fileRelativePath = Array.isArray(fields.relativePath) ? fields.relativePath[0] : (fields.relativePath || ''); // 获取上传文件的完整路径 const uploadPath = path.join(relativePath, fileRelativePath); const targetPath = this.getSafePath(uploadPath); // 确保目标目录存在 const targetDir = path.dirname(targetPath); await mkdir(targetDir, { recursive: true }); // 获取上传的文件 const uploadedFile = Array.isArray(files.file) ? files.file[0] : files.file; // 移动文件 await this.moveFile(uploadedFile.filepath, targetPath); res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify({ success: true, message: '上传成功' })); } catch (error) { console.error('保存文件失败:', error); res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify({ success: false, message: error.message })); } }); } catch (error) { console.error('处理上传请求失败:', error); res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify({ success: false, message: error.message })); } } // 移动文件 async moveFile(source, target) { return new Promise((resolve, reject) => { const readStream = fs.createReadStream(source); const writeStream = fs.createWriteStream(target); readStream.on('error', reject); writeStream.on('error', reject); writeStream.on('finish', () => { // 删除临时文件 fs.unlink(source, (err) => { if (err) console.error('删除临时文件失败:', err); resolve(); }); }); readStream.pipe(writeStream); }); } // 处理创建文件夹请求 async handleCreateFolderRequest(req, res) { try { let body = ''; req.on('data', chunk => { body += chunk.toString(); }); req.on('end', async () => { try { const data = JSON.parse(body); const relativePath = data.path || ''; const folderName = data.name; if (!folderName) { throw new Error('文件夹名称不能为空'); } // 验证文件夹名称 if (/[\\/:*?"<>|]/.test(folderName)) { throw new Error('文件夹名称包含非法字符'); } const folderPath = path.join(relativePath, folderName); const fullPath = this.getSafePath(folderPath); // 检查文件夹是否已存在 try { await access(fullPath); throw new Error('文件夹已存在'); } catch (error) { if (error.message === '文件夹已存在') { throw error; } // 文件夹不存在,创建它 await mkdir(fullPath, { recursive: true }); } res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify({ success: true, message: '创建成功' })); } catch (error) { console.error('创建文件夹失败:', error); res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify({ success: false, message: error.message })); } }); } catch (error) { console.error('处理创建文件夹请求失败:', error); res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify({ success: false, message: error.message })); } } // 处理重命名请求 async handleRenameRequest(req, res) { try { let body = ''; req.on('data', chunk => { body += chunk.toString(); }); req.on('end', async () => { try { const data = JSON.parse(body); const oldPath = data.oldPath; const newName = data.newName; if (!oldPath || !newName) { throw new Error('路径和新名称不能为空'); } // 验证新名称 if (/[\\/:*?"<>|]/.test(newName)) { throw new Error('名称包含非法字符'); } const oldFullPath = this.getSafePath(oldPath); const parentDir = path.dirname(oldFullPath); const newFullPath = path.join(parentDir, newName); // 确保新路径也在根目录内 if (!newFullPath.startsWith(this.rootDir)) { throw new Error('非法路径'); } // 检查旧文件是否存在 await access(oldFullPath); // 检查新名称是否已存在 try { await access(newFullPath); throw new Error('该名称已存在'); } catch (error) { if (error.message === '该名称已存在') { throw error; } // 文件不存在,可以重命名 } // 执行重命名 await promisify(fs.rename)(oldFullPath, newFullPath); res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify({ success: true, message: '重命名成功' })); } catch (error) { console.error('重命名失败:', error); res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify({ success: false, message: error.message })); } }); } catch (error) { console.error('处理重命名请求失败:', error); res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify({ success: false, message: error.message })); } } // 处理文件下载请求 async handleDownloadRequest(req, res) { try { const url = new URL(req.url, `http://${req.headers.host}`); const relativePath = url.searchParams.get('path') || ''; const fullPath = this.getSafePath(relativePath); // 检查文件是否存在 await access(fullPath); const stats = await stat(fullPath); if (stats.isDirectory()) { throw new Error('无法下载文件夹'); } // 设置响应头 const fileName = path.basename(fullPath); res.writeHead(200, { 'Content-Type': 'application/octet-stream', 'Content-Disposition': `attachment; filename="${encodeURIComponent(fileName)}"`, 'Content-Length': stats.size }); // 创建读取流并发送文件 const readStream = fs.createReadStream(fullPath); readStream.pipe(res); } catch (error) { console.error('下载文件失败:', error); res.writeHead(404, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify({ success: false, message: '文件不存在' })); } } // 处理图片预览请求 async handlePreviewRequest(req, res) { try { const url = new URL(req.url, `http://${req.headers.host}`); const relativePath = url.searchParams.get('path') || ''; const fullPath = this.getSafePath(relativePath); // 检查文件是否存在 await access(fullPath); const stats = await stat(fullPath); if (stats.isDirectory()) { throw new Error('无法预览文件夹'); } // 获取文件扩展名 const ext = path.extname(fullPath).toLowerCase(); const mimeTypes = { '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.png': 'image/png', '.gif': 'image/gif', '.bmp': 'image/bmp', '.webp': 'image/webp', '.svg': 'image/svg+xml' }; const contentType = mimeTypes[ext]; if (!contentType) { throw new Error('不支持的图片格式'); } // 设置响应头,添加缓存 res.writeHead(200, { 'Content-Type': contentType, 'Content-Length': stats.size, 'Cache-Control': 'public, max-age=86400' }); // 创建读取流并发送文件 const readStream = fs.createReadStream(fullPath); readStream.pipe(res); } catch (error) { console.error('预览图片失败:', error); res.writeHead(404, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify({ success: false, message: '图片不存在' })); } } // 处理移动文件/文件夹请求 async handleMoveRequest(req, res) { try { let body = ''; req.on('data', chunk => { body += chunk.toString(); }); req.on('end', async () => { try { const data = JSON.parse(body); const sourcePath = data.sourcePath; const targetFolder = data.targetFolder; if (!sourcePath) { throw new Error('源路径不能为空'); } const sourceFullPath = this.getSafePath(sourcePath); const fileName = path.basename(sourceFullPath); // 目标路径 const targetPath = targetFolder ? path.join(targetFolder, fileName) : fileName; const targetFullPath = this.getSafePath(targetPath); // 确保源文件存在 await access(sourceFullPath); // 检查是否移动到自身或子目录 if (targetFullPath.startsWith(sourceFullPath + path.sep) || targetFullPath === sourceFullPath) { throw new Error('不能移动到自身或子目录'); } // 检查目标是否已存在 try { await access(targetFullPath); throw new Error('目标位置已存在同名文件或文件夹'); } catch (error) { if (error.message === '目标位置已存在同名文件或文件夹') { throw error; } // 文件不存在,可以移动 } // 确保目标目录存在 const targetDir = path.dirname(targetFullPath); await mkdir(targetDir, { recursive: true }); // 执行移动 await promisify(fs.rename)(sourceFullPath, targetFullPath); res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify({ success: true, message: '移动成功' })); } catch (error) { console.error('移动失败:', error); res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify({ success: false, message: error.message })); } }); } catch (error) { console.error('处理移动请求失败:', error); res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify({ success: false, message: error.message })); } } // 处理复制文件/文件夹请求 async handleCopyRequest(req, res) { try { let body = ''; req.on('data', chunk => { body += chunk.toString(); }); req.on('end', async () => { try { const data = JSON.parse(body); const sourcePath = data.sourcePath; const targetFolder = data.targetFolder; if (!sourcePath) { throw new Error('源路径不能为空'); } const sourceFullPath = this.getSafePath(sourcePath); const fileName = path.basename(sourceFullPath); const targetPath = targetFolder ? path.join(targetFolder, fileName) : fileName; const targetFullPath = this.getSafePath(targetPath); await access(sourceFullPath); if (targetFullPath === sourceFullPath || targetFullPath.startsWith(sourceFullPath + path.sep)) { throw new Error('不能复制到自身或子目录'); } try { await access(targetFullPath); throw new Error('目标位置已存在同名文件或文件夹'); } catch (error) { if (error.message === '目标位置已存在同名文件或文件夹') { throw error; } } await this.copyItemRecursive(sourceFullPath, targetFullPath); res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify({ success: true, message: '复制成功' })); } catch (error) { console.error('复制失败:', error); res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify({ success: false, message: error.message })); } }); } catch (error) { console.error('处理复制请求失败:', error); res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify({ success: false, message: error.message })); } } async copyItemRecursive(source, target) { const stats = await stat(source); if (stats.isDirectory()) { await mkdir(target, { recursive: true }); const entries = await readdir(source); for (const entry of entries) { const childSource = path.join(source, entry); const childTarget = path.join(target, entry); await this.copyItemRecursive(childSource, childTarget); } } else { const targetDir = path.dirname(target); await mkdir(targetDir, { recursive: true }); await copyFile(source, target); } } // 处理删除文件/文件夹请求 async handleDeleteRequest(req, res) { try { let body = ''; req.on('data', chunk => { body += chunk.toString(); }); req.on('end', async () => { try { const data = JSON.parse(body); const paths = data.paths; if (!paths || !Array.isArray(paths) || paths.length === 0) { throw new Error('请选择要删除的文件'); } const errors = []; for (const filePath of paths) { try { const fullPath = this.getSafePath(filePath); await access(fullPath); const stats = await stat(fullPath); if (stats.isDirectory()) { // 递归删除文件夹 await this.deleteDirectory(fullPath); } else { // 删除文件 await promisify(fs.unlink)(fullPath); } } catch (error) { errors.push(`${filePath}: ${error.message}`); } } if (errors.length > 0) { res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify({ success: true, message: `部分删除成功,${errors.length} 个失败`, errors: errors })); } else { res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify({ success: true, message: '删除成功' })); } } catch (error) { console.error('删除失败:', error); res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify({ success: false, message: error.message })); } }); } catch (error) { console.error('处理删除请求失败:', error); res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify({ success: false, message: error.message })); } } // 递归删除目录 async deleteDirectory(dirPath) { const items = await readdir(dirPath); for (const item of items) { const itemPath = path.join(dirPath, item); const stats = await stat(itemPath); if (stats.isDirectory()) { await this.deleteDirectory(itemPath); } else { await promisify(fs.unlink)(itemPath); } } await promisify(fs.rmdir)(dirPath); } // 处理一键抠图请求(使用SSE实时推送进度) async handleRemoveBackgroundRequest(req, res) { console.log('\n' + '▓'.repeat(70)); console.log('[API] 收到一键抠图请求'); console.log('▓'.repeat(70)); // 设置SSE响应头 res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', 'Access-Control-Allow-Origin': '*' }); // 发送进度的辅助函数 const sendProgress = (data) => { console.log('[API] → 发送SSE事件:', data); const message = `data: ${JSON.stringify(data)}\n\n`; res.write(message); console.log('[API] ✓ SSE事件已发送'); }; let body = ''; req.on('data', chunk => { body += chunk.toString(); console.log('[API] → 接收请求数据...'); }); req.on('end', async () => { try { console.log('[API] ✓ 请求数据接收完成'); console.log('[API] → 解析JSON数据...'); const { paths } = JSON.parse(body); console.log('[API] ✓ JSON解析成功'); console.log('[API] 请求路径:', paths); if (!paths || !Array.isArray(paths) || paths.length === 0) { console.warn('[API] ⚠ 路径为空或无效'); res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify({ success: false, message: '请选择要处理的文件或文件夹' })); return; } console.log(`[API] → 开始批量处理,共 ${paths.length} 个项目`); let totalProcessed = 0; let totalFolders = 0; for (let i = 0; i < paths.length; i++) { const relativePath = paths[i]; console.log(`\n[API] 处理项目 ${i + 1}/${paths.length}: ${relativePath}`); // 发送开始处理的进度 sendProgress({ type: 'folder-start', current: i + 1, total: paths.length, folderName: relativePath }); console.log('[API] → 获取安全路径...'); const fullPath = this.getSafePath(relativePath); console.log('[API] ✓ 完整路径:', fullPath); console.log('[API] → 读取文件状态...'); const stats = await stat(fullPath); console.log('[API] ✓ 文件状态:', stats.isDirectory() ? '文件夹' : '文件'); if (stats.isDirectory()) { console.log('[API] → 开始处理文件夹...'); const result = await this.processFolder(fullPath, sendProgress, relativePath); console.log('[API] ✓ 文件夹处理完成'); console.log('[API] 结果:', result); if (result.success) { totalProcessed += result.processed; totalFolders++; // 发送文件夹完成的进度 sendProgress({ type: 'folder-complete', current: i + 1, total: paths.length, folderName: relativePath, processed: result.processed }); } else { console.error('[API] ✗ 文件夹处理失败:', result.message); sendProgress({ type: 'folder-error', current: i + 1, total: paths.length, folderName: relativePath, error: result.message }); } } else { console.log('[API] ⚠ 跳过单个文件(暂不支持)'); } } console.log('\n' + '▓'.repeat(70)); console.log('[API] ✓✓✓ 所有项目处理完成!'); console.log(`[API] 处理文件夹: ${totalFolders} 个`); console.log(`[API] 处理图片: ${totalProcessed} 张`); console.log('▓'.repeat(70)); console.log('[API] → 发送完成消息...'); sendProgress({ type: 'complete', success: true, processed: totalProcessed, folders: totalFolders, message: `处理完成!处理了 ${totalFolders} 个文件夹,共 ${totalProcessed} 张图片` }); res.end(); console.log('[API] ✓ SSE连接已关闭\n'); } catch (error) { console.error('\n' + '▓'.repeat(70)); console.error('[API] ✗✗✗ 请求处理失败'); console.error('[API] 错误类型:', error.name); console.error('[API] 错误信息:', error.message); console.error('[API] 错误堆栈:', error.stack); console.error('▓'.repeat(70) + '\n'); sendProgress({ type: 'error', success: false, message: error.message }); res.end(); } }); } // 处理单个文件夹:只抠图(不裁剪) async processFolder(folderPath, sendProgress, relativePath) { const timestamp = Date.now(); const folderName = path.basename(folderPath); // 创建临时目录 const tempBase = path.join(this.tempDir, `matting_${timestamp}_${folderName}`); const tempMatting = path.join(tempBase, 'output'); try { console.log(`\n${'='.repeat(70)}`); console.log(`[DiskManager] 开始抠背景: ${folderName}`); console.log(`${'='.repeat(70)}`); console.log(`[DiskManager] 原始路径: ${folderPath}`); // 创建临时目录 console.log(`[DiskManager] 创建临时目录...`); await mkdir(tempBase, { recursive: true }); await mkdir(tempMatting, { recursive: true }); console.log(`[DiskManager] ✓ 临时目录创建完成`); console.log(`[DiskManager] - 抠图输出: ${tempMatting}`); // 使用Python脚本进行抠图 console.log(`\n[DiskManager] >>> 开始AI抠背景 <<<`); const mattingResult = await this.runImageMatting(folderPath, tempMatting, (current, total) => { if (sendProgress) { sendProgress({ type: 'image-progress', current: current, total: total, folderName: relativePath || folderName }); } }); if (!mattingResult.success) { throw new Error(`抠背景失败: ${mattingResult.message}`); } console.log(`[DiskManager] ✓ 抠背景完成,处理了 ${mattingResult.processed} 张图片`); // 将处理后的文件复制回原文件夹 console.log(`\n[DiskManager] >>> 保存结果到原文件夹 <<<`); const saveResult = await this.saveProcessedFiles(tempMatting, folderPath); console.log(`[DiskManager] ✓ 保存完成,已替换 ${saveResult.saved} 张图片`); console.log(`\n${'='.repeat(70)}`); console.log(`[DiskManager] ✓✓✓ 处理完成!文件夹: ${folderName} ✓✓✓`); console.log(`${'='.repeat(70)}\n`); return { success: true, processed: saveResult.saved }; } catch (error) { console.error(`\n${'='.repeat(70)}`); console.error(`[DiskManager] ✗✗✗ 处理失败: ${folderName} ✗✗✗`); console.error(`[DiskManager] 错误信息: ${error.message}`); console.error(`${'='.repeat(70)}\n`); return { success: false, processed: 0, message: error.message }; } finally { // 清理临时文件 try { console.log('[DiskManager] 清理临时文件...'); await this.deleteDirectory(tempBase); console.log('[DiskManager] ✓ 临时文件已清理'); } catch (error) { console.warn('[DiskManager] ⚠ 清理临时文件失败:', error.message); } } } // 处理单个文件夹:只裁剪 async processFolderCropOnly(folderPath, sendProgress, relativePath) { const timestamp = Date.now(); const folderName = path.basename(folderPath); // 创建临时目录 const tempBase = path.join(this.tempDir, `crop_${timestamp}_${folderName}`); const tempCrop = path.join(tempBase, 'output'); try { console.log(`\n${'='.repeat(70)}`); console.log(`[DiskManager] 开始剪裁: ${folderName}`); console.log(`${'='.repeat(70)}`); console.log(`[DiskManager] 原始路径: ${folderPath}`); // 创建临时目录 console.log(`[DiskManager] 创建临时目录...`); await mkdir(tempBase, { recursive: true }); await mkdir(tempCrop, { recursive: true }); console.log(`[DiskManager] ✓ 临时目录创建完成`); console.log(`[DiskManager] - 裁剪输出: ${tempCrop}`); // 使用Python脚本裁剪多余透明区域 console.log(`\n[DiskManager] >>> 开始智能裁剪 <<<`); const folderBaseName = path.basename(folderPath); const cropResult = await this.runCutMiniSize(folderPath, tempCrop, (current, total) => { if (sendProgress) { sendProgress({ type: 'image-progress', current: current, total: total, folderName: relativePath || folderBaseName }); } }); if (!cropResult.success) { throw new Error(`裁剪失败: ${cropResult.message}`); } console.log(`[DiskManager] ✓ 裁剪完成,最终尺寸: ${cropResult.width}x${cropResult.height}`); // 将处理后的文件复制回原文件夹 console.log(`\n[DiskManager] >>> 保存结果到原文件夹 <<<`); const saveResult = await this.saveProcessedFiles(tempCrop, folderPath); console.log(`[DiskManager] ✓ 保存完成,已替换 ${saveResult.saved} 张图片`); console.log(`\n${'='.repeat(70)}`); console.log(`[DiskManager] ✓✓✓ 裁剪完成!文件夹: ${folderName} ✓✓✓`); console.log(`${'='.repeat(70)}\n`); return { success: true, processed: saveResult.saved, width: cropResult.width, height: cropResult.height }; } catch (error) { console.error(`\n${'='.repeat(70)}`); console.error(`[DiskManager] ✗✗✗ 裁剪失败: ${folderName} ✗✗✗`); console.error(`[DiskManager] 错误信息: ${error.message}`); console.error(`${'='.repeat(70)}\n`); return { success: false, processed: 0, message: error.message }; } finally { // 清理临时文件 try { console.log('[DiskManager] 清理临时文件...'); await this.deleteDirectory(tempBase); console.log('[DiskManager] ✓ 临时文件已清理'); } catch (error) { console.warn('[DiskManager] ⚠ 清理临时文件失败:', error.message); } } } // 保存处理后的文件到原文件夹(直接替换) async saveProcessedFiles(sourceFolder, targetFolder) { console.log('[DiskManager] → 开始保存文件...'); console.log('[DiskManager] 源文件夹:', sourceFolder); console.log('[DiskManager] 目标文件夹:', targetFolder); let saved = 0; try { console.log('[DiskManager] → 读取源文件夹...'); const files = await readdir(sourceFolder); console.log(`[DiskManager] ✓ 找到 ${files.length} 个文件`); for (let i = 0; i < files.length; i++) { const file = files[i]; const sourcePath = path.join(sourceFolder, file); const targetPath = path.join(targetFolder, file); console.log(`[DiskManager] [${i + 1}/${files.length}] 替换: ${file}`); // 不备份,直接替换原文件 await copyFile(sourcePath, targetPath); saved++; console.log(`[DiskManager] ✓ 已替换`); } console.log(`[DiskManager] ✓ 所有文件保存完成,共 ${saved} 个`); return { saved }; } catch (error) { console.error('[DiskManager] ✗ 保存文件失败:', error); throw error; } } // 处理剪裁最小区域请求(使用SSE实时推送进度) async handleCropMiniRequest(req, res) { console.log('\n' + '▓'.repeat(70)); console.log('[API] 收到剪裁最小区域请求'); console.log('▓'.repeat(70)); // 设置SSE响应头 res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', 'Access-Control-Allow-Origin': '*' }); // 发送进度的辅助函数 const sendProgress = (data) => { console.log('[API] → 发送SSE事件:', data); const message = `data: ${JSON.stringify(data)}\n\n`; res.write(message); console.log('[API] ✓ SSE事件已发送'); }; let body = ''; req.on('data', chunk => { body += chunk.toString(); }); req.on('end', async () => { try { const { paths } = JSON.parse(body); if (!paths || !Array.isArray(paths) || paths.length === 0) { res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify({ success: false, message: '请选择要处理的文件或文件夹' })); return; } console.log(`[API] → 开始批量裁剪,共 ${paths.length} 个项目`); let totalProcessed = 0; let totalFolders = 0; for (let i = 0; i < paths.length; i++) { const relativePath = paths[i]; console.log(`\n[API] 裁剪项目 ${i + 1}/${paths.length}: ${relativePath}`); const fullPath = this.getSafePath(relativePath); const stats = await stat(fullPath); if (stats.isDirectory()) { const result = await this.processFolderCropOnly(fullPath, sendProgress, relativePath); if (result.success) { totalProcessed += result.processed; totalFolders++; } } } console.log('\n' + '▓'.repeat(70)); console.log('[API] ✓✓✓ 所有项目裁剪完成!'); console.log(`[API] 处理文件夹: ${totalFolders} 个`); console.log(`[API] 处理图片: ${totalProcessed} 张`); console.log('▓'.repeat(70)); sendProgress({ type: 'complete', success: true, processed: totalProcessed, folders: totalFolders, message: `裁剪完成!处理了 ${totalFolders} 个文件夹,共 ${totalProcessed} 张图片` }); res.end(); } catch (error) { console.error('[API] 裁剪请求处理失败:', error); sendProgress({ type: 'error', success: false, message: error.message }); res.end(); } }); } } module.exports = DiskManager;