|
|
@@ -16,21 +16,45 @@ const unlink = promisify(fs.unlink);
|
|
|
|
|
|
class DiskManager {
|
|
|
constructor() {
|
|
|
- // 数据存储根目录(在 Server 目录下)
|
|
|
- this.rootDir = path.join(__dirname, 'disk_data');
|
|
|
+ // 用户数据根目录
|
|
|
+ this.usersDir = path.join(__dirname, 'users');
|
|
|
this.tempDir = path.join(__dirname, 'temp');
|
|
|
this.pythonDir = path.join(__dirname, 'python');
|
|
|
- this.ensureRootDir();
|
|
|
}
|
|
|
|
|
|
- // 确保根目录存在
|
|
|
- async ensureRootDir() {
|
|
|
+ // 根据用户名获取用户的数据目录
|
|
|
+ getUserRootDir(username) {
|
|
|
+ if (!username) {
|
|
|
+ throw new Error('用户名不能为空');
|
|
|
+ }
|
|
|
+ // 用户名转换为小写,确保一致性
|
|
|
+ const normalizedUsername = username.toLowerCase();
|
|
|
+ return path.join(this.usersDir, normalizedUsername, 'disk_data');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 确保用户目录存在
|
|
|
+ async ensureUserDir(username) {
|
|
|
+ const userRootDir = this.getUserRootDir(username);
|
|
|
try {
|
|
|
- await access(this.rootDir);
|
|
|
+ await access(userRootDir);
|
|
|
} catch (error) {
|
|
|
- await mkdir(this.rootDir, { recursive: true });
|
|
|
- console.log('创建disk_data目录:', this.rootDir);
|
|
|
+ await mkdir(userRootDir, { recursive: true });
|
|
|
+ console.log('创建用户disk_data目录:', userRootDir);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 从请求中获取用户名(支持查询参数和请求体)
|
|
|
+ getUsernameFromRequest(req, fields = null) {
|
|
|
+ // 先尝试从查询参数获取
|
|
|
+ const url = new URL(req.url, `http://${req.headers.host}`);
|
|
|
+ let username = url.searchParams.get('username');
|
|
|
+
|
|
|
+ // 如果查询参数没有,尝试从请求体(fields)获取
|
|
|
+ if (!username && fields) {
|
|
|
+ username = Array.isArray(fields.username) ? fields.username[0] : (fields.username || null);
|
|
|
}
|
|
|
+
|
|
|
+ return username;
|
|
|
}
|
|
|
|
|
|
// 获取Python命令(尝试python或python3)
|
|
|
@@ -65,7 +89,7 @@ class DiskManager {
|
|
|
}
|
|
|
|
|
|
const pythonExe = path.join(this.pythonDir, 'venv', 'Scripts', 'python.exe');
|
|
|
- const scriptPath = path.join(this.pythonDir, 'image-matting.py');
|
|
|
+ const scriptPath = path.join(this.pythonDir, 'rembg-matting.py');
|
|
|
|
|
|
// 检查是否使用虚拟环境
|
|
|
const usePython = await new Promise((resolve) => {
|
|
|
@@ -271,14 +295,18 @@ class DiskManager {
|
|
|
});
|
|
|
}
|
|
|
|
|
|
- // 获取安全的文件路径
|
|
|
- getSafePath(relativePath) {
|
|
|
+ // 获取安全的文件路径(基于用户名)
|
|
|
+ getSafePath(relativePath, username) {
|
|
|
+ if (!username) {
|
|
|
+ throw new Error('用户名不能为空');
|
|
|
+ }
|
|
|
+ const userRootDir = this.getUserRootDir(username);
|
|
|
// 移除开头的斜杠
|
|
|
relativePath = relativePath.replace(/^\/+/, '');
|
|
|
// 解析路径,防止目录遍历攻击
|
|
|
- const fullPath = path.join(this.rootDir, relativePath);
|
|
|
- // 确保路径在根目录内
|
|
|
- if (!fullPath.startsWith(this.rootDir)) {
|
|
|
+ const fullPath = path.join(userRootDir, relativePath);
|
|
|
+ // 确保路径在用户根目录内
|
|
|
+ if (!fullPath.startsWith(userRootDir)) {
|
|
|
throw new Error('非法路径');
|
|
|
}
|
|
|
return fullPath;
|
|
|
@@ -427,10 +455,21 @@ class DiskManager {
|
|
|
async handleListRequest(req, res) {
|
|
|
try {
|
|
|
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
|
+ const username = url.searchParams.get('username');
|
|
|
+ if (!username) {
|
|
|
+ res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
|
+ res.end(JSON.stringify({ success: false, error: '缺少用户名参数' }));
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
const relativePath = url.searchParams.get('path') || '';
|
|
|
const recursive = url.searchParams.get('recursive') === 'true'; // 是否递归获取所有子文件夹
|
|
|
|
|
|
- const fullPath = this.getSafePath(relativePath);
|
|
|
+ // 确保用户目录存在
|
|
|
+ await this.ensureUserDir(username);
|
|
|
+
|
|
|
+ const fullPath = this.getSafePath(relativePath, username);
|
|
|
+
|
|
|
|
|
|
// 检查目录是否存在
|
|
|
try {
|
|
|
@@ -472,8 +511,8 @@ class DiskManager {
|
|
|
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)}`;
|
|
|
+ // 返回完整的预览URL路径(包含用户名)
|
|
|
+ fileInfo.previewUrl = `/api/disk/preview?username=${encodeURIComponent(username)}&path=${encodeURIComponent(itemRelativePath + '/' + previewInfo.previewFile)}`;
|
|
|
}
|
|
|
|
|
|
// 检查是否需要抠图(是否有非透明背景的PNG)
|
|
|
@@ -588,13 +627,24 @@ class DiskManager {
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
+ // 获取用户名
|
|
|
+ const username = this.getUsernameFromRequest(req, fields);
|
|
|
+ if (!username) {
|
|
|
+ res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
|
+ res.end(JSON.stringify({ success: false, message: '缺少用户名参数' }));
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 确保用户目录存在
|
|
|
+ await this.ensureUserDir(username);
|
|
|
+
|
|
|
// 新版 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 targetPath = this.getSafePath(uploadPath, username);
|
|
|
|
|
|
// 确保目标目录存在
|
|
|
const targetDir = path.dirname(targetPath);
|
|
|
@@ -661,6 +711,16 @@ class DiskManager {
|
|
|
req.on('end', async () => {
|
|
|
try {
|
|
|
const data = JSON.parse(body);
|
|
|
+ const username = data.username;
|
|
|
+ if (!username) {
|
|
|
+ res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
|
+ res.end(JSON.stringify({ success: false, message: '缺少用户名参数' }));
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 确保用户目录存在
|
|
|
+ await this.ensureUserDir(username);
|
|
|
+
|
|
|
const relativePath = data.path || '';
|
|
|
const folderName = data.name;
|
|
|
|
|
|
@@ -674,7 +734,7 @@ class DiskManager {
|
|
|
}
|
|
|
|
|
|
const folderPath = path.join(relativePath, folderName);
|
|
|
- const fullPath = this.getSafePath(folderPath);
|
|
|
+ const fullPath = this.getSafePath(folderPath, username);
|
|
|
|
|
|
// 检查文件夹是否已存在
|
|
|
try {
|
|
|
@@ -723,6 +783,13 @@ class DiskManager {
|
|
|
req.on('end', async () => {
|
|
|
try {
|
|
|
const data = JSON.parse(body);
|
|
|
+ const username = data.username;
|
|
|
+ if (!username) {
|
|
|
+ res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
|
+ res.end(JSON.stringify({ success: false, message: '缺少用户名参数' }));
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
const oldPath = data.oldPath;
|
|
|
const newName = data.newName;
|
|
|
|
|
|
@@ -735,12 +802,13 @@ class DiskManager {
|
|
|
throw new Error('名称包含非法字符');
|
|
|
}
|
|
|
|
|
|
- const oldFullPath = this.getSafePath(oldPath);
|
|
|
+ const userRootDir = this.getUserRootDir(username);
|
|
|
+ const oldFullPath = this.getSafePath(oldPath, username);
|
|
|
const parentDir = path.dirname(oldFullPath);
|
|
|
const newFullPath = path.join(parentDir, newName);
|
|
|
|
|
|
- // 确保新路径也在根目录内
|
|
|
- if (!newFullPath.startsWith(this.rootDir)) {
|
|
|
+ // 确保新路径也在用户根目录内
|
|
|
+ if (!newFullPath.startsWith(userRootDir)) {
|
|
|
throw new Error('非法路径');
|
|
|
}
|
|
|
|
|
|
@@ -789,9 +857,16 @@ class DiskManager {
|
|
|
async handleDownloadRequest(req, res) {
|
|
|
try {
|
|
|
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
|
+ const username = url.searchParams.get('username');
|
|
|
+ if (!username) {
|
|
|
+ res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
|
+ res.end(JSON.stringify({ success: false, message: '缺少用户名参数' }));
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
const relativePath = url.searchParams.get('path') || '';
|
|
|
|
|
|
- const fullPath = this.getSafePath(relativePath);
|
|
|
+ const fullPath = this.getSafePath(relativePath, username);
|
|
|
|
|
|
// 检查文件是否存在
|
|
|
await access(fullPath);
|
|
|
@@ -822,13 +897,109 @@ class DiskManager {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ // 处理获取用户网盘中的帧文件列表
|
|
|
+ async handleGetUserFrames(username, folderName, res) {
|
|
|
+ try {
|
|
|
+ console.log(`[DiskManager] 获取用户帧列表: username=${username}, folderName=${folderName}`);
|
|
|
+
|
|
|
+ const userRootDir = this.getUserRootDir(username);
|
|
|
+ // 处理URL编码和空格
|
|
|
+ const decodedFolderName = decodeURIComponent(folderName).replace(/%20/g, ' ');
|
|
|
+ const folderPath = path.join(userRootDir, decodedFolderName);
|
|
|
+
|
|
|
+ console.log(`[DiskManager] 用户网盘目录: ${userRootDir}`);
|
|
|
+ console.log(`[DiskManager] 解码后的文件夹名: ${decodedFolderName}`);
|
|
|
+ console.log(`[DiskManager] 完整路径: ${folderPath}`);
|
|
|
+
|
|
|
+ // 检查目录是否存在
|
|
|
+ try {
|
|
|
+ await access(folderPath);
|
|
|
+ } catch (accessErr) {
|
|
|
+ console.error(`[DiskManager] 文件夹不存在: ${folderPath}`);
|
|
|
+ res.writeHead(404, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
|
+ res.end(JSON.stringify({
|
|
|
+ error: 'Folder not found',
|
|
|
+ details: `文件夹不存在: ${folderPath}`,
|
|
|
+ folderPath: folderPath
|
|
|
+ }));
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 读取文件夹内容
|
|
|
+ const files = await readdir(folderPath);
|
|
|
+ console.log(`[DiskManager] 找到 ${files.length} 个文件`);
|
|
|
+
|
|
|
+ // 过滤出 PNG 文件,并按数字排序
|
|
|
+ const pngFiles = files
|
|
|
+ .filter(file => file.toLowerCase().endsWith('.png'))
|
|
|
+ .map(file => {
|
|
|
+ let frameNum = null;
|
|
|
+ // 先尝试匹配纯数字格式:00.png, 01.png
|
|
|
+ let match = /^(\d+)\.png$/i.exec(file);
|
|
|
+ if (match) {
|
|
|
+ frameNum = parseInt(match[1], 10);
|
|
|
+ } else {
|
|
|
+ // 再尝试匹配文件名末尾的数字格式
|
|
|
+ match = /(\d+)\.png$/i.exec(file);
|
|
|
+ if (match) {
|
|
|
+ frameNum = parseInt(match[1], 10);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (frameNum !== null) {
|
|
|
+ return {
|
|
|
+ frameNum: frameNum,
|
|
|
+ fileName: file
|
|
|
+ };
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ })
|
|
|
+ .filter(item => item !== null)
|
|
|
+ .sort((a, b) => a.frameNum - b.frameNum);
|
|
|
+
|
|
|
+ if (pngFiles.length === 0) {
|
|
|
+ console.log(`[DiskManager] 文件夹中没有PNG文件`);
|
|
|
+ res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
|
+ res.end(JSON.stringify({
|
|
|
+ error: '该文件夹中没有图片',
|
|
|
+ frames: []
|
|
|
+ }));
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const result = {
|
|
|
+ frames: pngFiles.map(item => item.frameNum),
|
|
|
+ fileNames: pngFiles.map(item => item.fileName),
|
|
|
+ maxFrame: pngFiles.length > 0 ? Math.max(...pngFiles.map(item => item.frameNum)) : 0
|
|
|
+ };
|
|
|
+
|
|
|
+ console.log(`[DiskManager] ✓ 成功获取 ${result.frames.length} 帧`);
|
|
|
+ res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
|
+ res.end(JSON.stringify(result));
|
|
|
+ } catch (error) {
|
|
|
+ console.error('[DiskManager] 获取用户帧列表失败:', error);
|
|
|
+ res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
|
+ res.end(JSON.stringify({
|
|
|
+ error: 'Failed to get frames',
|
|
|
+ details: error.message
|
|
|
+ }));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
// 处理图片预览请求
|
|
|
async handlePreviewRequest(req, res) {
|
|
|
try {
|
|
|
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
|
+ const username = url.searchParams.get('username');
|
|
|
+ if (!username) {
|
|
|
+ res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
|
+ res.end(JSON.stringify({ success: false, message: '缺少用户名参数' }));
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
const relativePath = url.searchParams.get('path') || '';
|
|
|
|
|
|
- const fullPath = this.getSafePath(relativePath);
|
|
|
+ const fullPath = this.getSafePath(relativePath, username);
|
|
|
|
|
|
// 检查文件是否存在
|
|
|
await access(fullPath);
|
|
|
@@ -886,6 +1057,13 @@ class DiskManager {
|
|
|
req.on('end', async () => {
|
|
|
try {
|
|
|
const data = JSON.parse(body);
|
|
|
+ const username = data.username;
|
|
|
+ if (!username) {
|
|
|
+ res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
|
+ res.end(JSON.stringify({ success: false, message: '缺少用户名参数' }));
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
const sourcePath = data.sourcePath;
|
|
|
const targetFolder = data.targetFolder;
|
|
|
|
|
|
@@ -893,12 +1071,13 @@ class DiskManager {
|
|
|
throw new Error('源路径不能为空');
|
|
|
}
|
|
|
|
|
|
- const sourceFullPath = this.getSafePath(sourcePath);
|
|
|
+ const userRootDir = this.getUserRootDir(username);
|
|
|
+ const sourceFullPath = this.getSafePath(sourcePath, username);
|
|
|
const fileName = path.basename(sourceFullPath);
|
|
|
|
|
|
// 目标路径
|
|
|
const targetPath = targetFolder ? path.join(targetFolder, fileName) : fileName;
|
|
|
- const targetFullPath = this.getSafePath(targetPath);
|
|
|
+ const targetFullPath = this.getSafePath(targetPath, username);
|
|
|
|
|
|
// 确保源文件存在
|
|
|
await access(sourceFullPath);
|
|
|
@@ -907,6 +1086,11 @@ class DiskManager {
|
|
|
if (targetFullPath.startsWith(sourceFullPath + path.sep) || targetFullPath === sourceFullPath) {
|
|
|
throw new Error('不能移动到自身或子目录');
|
|
|
}
|
|
|
+
|
|
|
+ // 确保目标路径也在用户根目录内
|
|
|
+ if (!targetFullPath.startsWith(userRootDir)) {
|
|
|
+ throw new Error('非法路径');
|
|
|
+ }
|
|
|
|
|
|
// 检查目标是否已存在
|
|
|
try {
|
|
|
@@ -961,6 +1145,13 @@ class DiskManager {
|
|
|
req.on('end', async () => {
|
|
|
try {
|
|
|
const data = JSON.parse(body);
|
|
|
+ const username = data.username;
|
|
|
+ if (!username) {
|
|
|
+ res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
|
+ res.end(JSON.stringify({ success: false, message: '缺少用户名参数' }));
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
const sourcePath = data.sourcePath;
|
|
|
const targetFolder = data.targetFolder;
|
|
|
|
|
|
@@ -968,16 +1159,22 @@ class DiskManager {
|
|
|
throw new Error('源路径不能为空');
|
|
|
}
|
|
|
|
|
|
- const sourceFullPath = this.getSafePath(sourcePath);
|
|
|
+ const userRootDir = this.getUserRootDir(username);
|
|
|
+ const sourceFullPath = this.getSafePath(sourcePath, username);
|
|
|
const fileName = path.basename(sourceFullPath);
|
|
|
const targetPath = targetFolder ? path.join(targetFolder, fileName) : fileName;
|
|
|
- const targetFullPath = this.getSafePath(targetPath);
|
|
|
+ const targetFullPath = this.getSafePath(targetPath, username);
|
|
|
|
|
|
await access(sourceFullPath);
|
|
|
|
|
|
if (targetFullPath === sourceFullPath || targetFullPath.startsWith(sourceFullPath + path.sep)) {
|
|
|
throw new Error('不能复制到自身或子目录');
|
|
|
}
|
|
|
+
|
|
|
+ // 确保目标路径也在用户根目录内
|
|
|
+ if (!targetFullPath.startsWith(userRootDir)) {
|
|
|
+ throw new Error('非法路径');
|
|
|
+ }
|
|
|
|
|
|
try {
|
|
|
await access(targetFullPath);
|
|
|
@@ -1042,6 +1239,13 @@ class DiskManager {
|
|
|
req.on('end', async () => {
|
|
|
try {
|
|
|
const data = JSON.parse(body);
|
|
|
+ const username = data.username;
|
|
|
+ if (!username) {
|
|
|
+ res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
|
+ res.end(JSON.stringify({ success: false, message: '缺少用户名参数' }));
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
const paths = data.paths;
|
|
|
|
|
|
if (!paths || !Array.isArray(paths) || paths.length === 0) {
|
|
|
@@ -1052,7 +1256,7 @@ class DiskManager {
|
|
|
|
|
|
for (const filePath of paths) {
|
|
|
try {
|
|
|
- const fullPath = this.getSafePath(filePath);
|
|
|
+ const fullPath = this.getSafePath(filePath, username);
|
|
|
await access(fullPath);
|
|
|
const stats = await stat(fullPath);
|
|
|
|
|
|
@@ -1151,9 +1355,20 @@ class DiskManager {
|
|
|
try {
|
|
|
console.log('[API] ✓ 请求数据接收完成');
|
|
|
console.log('[API] → 解析JSON数据...');
|
|
|
- const { paths } = JSON.parse(body);
|
|
|
+ const data = JSON.parse(body);
|
|
|
+ const { paths, username } = data;
|
|
|
console.log('[API] ✓ JSON解析成功');
|
|
|
console.log('[API] 请求路径:', paths);
|
|
|
+ console.log('[API] 用户名:', username);
|
|
|
+
|
|
|
+ if (!username) {
|
|
|
+ res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
|
+ res.end(JSON.stringify({
|
|
|
+ success: false,
|
|
|
+ message: '缺少用户名参数'
|
|
|
+ }));
|
|
|
+ return;
|
|
|
+ }
|
|
|
|
|
|
if (!paths || !Array.isArray(paths) || paths.length === 0) {
|
|
|
console.warn('[API] ⚠ 路径为空或无效');
|
|
|
@@ -1183,7 +1398,7 @@ class DiskManager {
|
|
|
});
|
|
|
|
|
|
console.log('[API] → 获取安全路径...');
|
|
|
- const fullPath = this.getSafePath(relativePath);
|
|
|
+ const fullPath = this.getSafePath(relativePath, username);
|
|
|
console.log('[API] ✓ 完整路径:', fullPath);
|
|
|
|
|
|
console.log('[API] → 读取文件状态...');
|
|
|
@@ -1477,7 +1692,17 @@ class DiskManager {
|
|
|
|
|
|
req.on('end', async () => {
|
|
|
try {
|
|
|
- const { paths } = JSON.parse(body);
|
|
|
+ const data = JSON.parse(body);
|
|
|
+ const { paths, username } = data;
|
|
|
+
|
|
|
+ if (!username) {
|
|
|
+ res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
|
+ res.end(JSON.stringify({
|
|
|
+ success: false,
|
|
|
+ message: '缺少用户名参数'
|
|
|
+ }));
|
|
|
+ return;
|
|
|
+ }
|
|
|
|
|
|
if (!paths || !Array.isArray(paths) || paths.length === 0) {
|
|
|
res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
|
@@ -1497,7 +1722,7 @@ class DiskManager {
|
|
|
const relativePath = paths[i];
|
|
|
console.log(`\n[API] 裁剪项目 ${i + 1}/${paths.length}: ${relativePath}`);
|
|
|
|
|
|
- const fullPath = this.getSafePath(relativePath);
|
|
|
+ const fullPath = this.getSafePath(relativePath, username);
|
|
|
const stats = await stat(fullPath);
|
|
|
|
|
|
if (stats.isDirectory()) {
|