From 031ab5a412cfbe2eaaa241da324d5b6281474438 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E6=9C=B1?= <10714957+xiao-zhu245@user.noreply.gitee.com> Date: Sat, 12 Jul 2025 10:04:56 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E6=96=87=E4=BB=B6=E6=93=8D?= =?UTF-8?q?=E4=BD=9C=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../data/plugins/example-plugin/gsm3-api.js | 135 ++++ server/data/plugins/example-plugin/index.html | 294 +++++++++ server/src/routes/files.ts | 595 +++++++++++++++++- server/src/routes/pluginApi.ts | 6 + 4 files changed, 1006 insertions(+), 24 deletions(-) diff --git a/server/data/plugins/example-plugin/gsm3-api.js b/server/data/plugins/example-plugin/gsm3-api.js index acedef3..78910f3 100644 --- a/server/data/plugins/example-plugin/gsm3-api.js +++ b/server/data/plugins/example-plugin/gsm3-api.js @@ -158,6 +158,141 @@ class GSM3API { return await this.request('/games') } + // ==================== 文件操作API ==================== + + /** + * 读取文件内容 + * @param {string} filePath 文件路径(相对于服务器data目录) + * @param {string} encoding 文件编码,默认为'utf-8',二进制文件使用'binary' + */ + async readFile(filePath, encoding = 'utf-8') { + return await this.request('/files/read', { + method: 'POST', + body: { filePath, encoding } + }) + } + + /** + * 写入文件内容 + * @param {string} filePath 文件路径(相对于服务器data目录) + * @param {string} content 文件内容 + * @param {string} encoding 文件编码,默认为'utf-8' + */ + async writeFile(filePath, content, encoding = 'utf-8') { + return await this.request('/files/write', { + method: 'POST', + body: { filePath, content, encoding } + }) + } + + /** + * 删除文件 + * @param {string} filePath 文件路径(相对于服务器data目录) + */ + async deleteFile(filePath) { + return await this.request('/files/delete', { + method: 'DELETE', + body: { filePath } + }) + } + + /** + * 创建目录 + * @param {string} dirPath 目录路径(相对于服务器data目录) + * @param {boolean} recursive 是否递归创建父目录,默认为true + */ + async createDirectory(dirPath, recursive = true) { + return await this.request('/files/mkdir', { + method: 'POST', + body: { dirPath, recursive } + }) + } + + /** + * 删除目录 + * @param {string} dirPath 目录路径(相对于服务器data目录) + * @param {boolean} recursive 是否递归删除,默认为false + */ + async deleteDirectory(dirPath, recursive = false) { + return await this.request('/files/rmdir', { + method: 'DELETE', + body: { dirPath, recursive } + }) + } + + /** + * 列出目录内容 + * @param {string} dirPath 目录路径(相对于服务器data目录),默认为根目录 + * @param {boolean} includeHidden 是否包含隐藏文件,默认为false + */ + async listDirectory(dirPath = '', includeHidden = false) { + return await this.request('/files/list', { + method: 'POST', + body: { dirPath, includeHidden } + }) + } + + /** + * 获取文件或目录信息 + * @param {string} path 文件或目录路径(相对于服务器data目录) + */ + async getFileInfo(path) { + return await this.request('/files/info', { + method: 'POST', + body: { path } + }) + } + + /** + * 检查文件或目录是否存在 + * @param {string} path 文件或目录路径(相对于服务器data目录) + */ + async exists(path) { + return await this.request('/files/exists', { + method: 'POST', + body: { path } + }) + } + + /** + * 复制文件或目录 + * @param {string} sourcePath 源路径(相对于服务器data目录) + * @param {string} destPath 目标路径(相对于服务器data目录) + * @param {boolean} overwrite 是否覆盖已存在的文件,默认为false + */ + async copy(sourcePath, destPath, overwrite = false) { + return await this.request('/files/copy', { + method: 'POST', + body: { sourcePath, destPath, overwrite } + }) + } + + /** + * 移动/重命名文件或目录 + * @param {string} sourcePath 源路径(相对于服务器data目录) + * @param {string} destPath 目标路径(相对于服务器data目录) + * @param {boolean} overwrite 是否覆盖已存在的文件,默认为false + */ + async move(sourcePath, destPath, overwrite = false) { + return await this.request('/files/move', { + method: 'POST', + body: { sourcePath, destPath, overwrite } + }) + } + + /** + * 搜索文件 + * @param {string} pattern 搜索模式(支持通配符) + * @param {string} searchPath 搜索路径(相对于服务器data目录),默认为根目录 + * @param {boolean} recursive 是否递归搜索子目录,默认为true + */ + async searchFiles(pattern, searchPath = '', recursive = true) { + return await this.request('/files/search', { + method: 'POST', + body: { pattern, searchPath, recursive } + }) + } + // ==================== 通用API ==================== /** diff --git a/server/data/plugins/example-plugin/index.html b/server/data/plugins/example-plugin/index.html index ee9b27e..21ac184 100644 --- a/server/data/plugins/example-plugin/index.html +++ b/server/data/plugins/example-plugin/index.html @@ -308,6 +308,31 @@ + + +
+

📁 文件操作演示

+

体验插件文件操作API的功能:

+ +
+ + + + +
+ +
+ + + + +
+ +
+ + + +
API调用结果: 点击上方按钮查看API响应 @@ -517,6 +542,275 @@ apiResult.style.borderLeft = '4px solid #ff6b6b' } + // ==================== 文件操作函数 ==================== + + // 列出根目录 + async function listRootDirectory() { + try { + if (!window.gsm3) { + throw new Error('GSM3 API对象未找到') + } + + showApiLoading('正在初始化API...') + await window.gsm3.initialize() + + showApiLoading('正在列出根目录...') + const result = await window.gsm3.listDirectory() + showApiResult('根目录内容', result) + if (window.gsm3.showNotification) { + window.gsm3.showNotification('success', '目录列表获取成功') + } + } catch (error) { + showApiError('列出目录失败', error) + if (window.gsm3 && window.gsm3.showNotification) { + window.gsm3.showNotification('error', '列出目录失败: ' + error.message) + } + } + } + + // 创建测试文件 + async function createTestFile() { + try { + if (!window.gsm3) { + throw new Error('GSM3 API对象未找到') + } + + showApiLoading('正在初始化API...') + await window.gsm3.initialize() + + showApiLoading('正在创建测试文件...') + const content = `这是一个测试文件\n创建时间: ${new Date().toLocaleString()}\n随机数: ${Math.random()}` + const result = await window.gsm3.writeFile('test.txt', content) + showApiResult('创建测试文件', result) + if (window.gsm3.showNotification) { + window.gsm3.showNotification('success', '测试文件创建成功') + } + } catch (error) { + showApiError('创建文件失败', error) + if (window.gsm3 && window.gsm3.showNotification) { + window.gsm3.showNotification('error', '创建文件失败: ' + error.message) + } + } + } + + // 读取测试文件 + async function readTestFile() { + try { + if (!window.gsm3) { + throw new Error('GSM3 API对象未找到') + } + + showApiLoading('正在初始化API...') + await window.gsm3.initialize() + + showApiLoading('正在读取测试文件...') + const result = await window.gsm3.readFile('test.txt') + showApiResult('读取测试文件', result) + if (window.gsm3.showNotification) { + window.gsm3.showNotification('success', '文件读取成功') + } + } catch (error) { + showApiError('读取文件失败', error) + if (window.gsm3 && window.gsm3.showNotification) { + window.gsm3.showNotification('error', '读取文件失败: ' + error.message) + } + } + } + + // 删除测试文件 + async function deleteTestFile() { + try { + if (!window.gsm3) { + throw new Error('GSM3 API对象未找到') + } + + showApiLoading('正在初始化API...') + await window.gsm3.initialize() + + showApiLoading('正在删除测试文件...') + const result = await window.gsm3.deleteFile('test.txt') + showApiResult('删除测试文件', result) + if (window.gsm3.showNotification) { + window.gsm3.showNotification('success', '文件删除成功') + } + } catch (error) { + showApiError('删除文件失败', error) + if (window.gsm3 && window.gsm3.showNotification) { + window.gsm3.showNotification('error', '删除文件失败: ' + error.message) + } + } + } + + // 创建测试目录 + async function createTestDirectory() { + try { + if (!window.gsm3) { + throw new Error('GSM3 API对象未找到') + } + + showApiLoading('正在初始化API...') + await window.gsm3.initialize() + + showApiLoading('正在创建测试目录...') + const result = await window.gsm3.createDirectory('test-dir') + showApiResult('创建测试目录', result) + if (window.gsm3.showNotification) { + window.gsm3.showNotification('success', '目录创建成功') + } + } catch (error) { + showApiError('创建目录失败', error) + if (window.gsm3 && window.gsm3.showNotification) { + window.gsm3.showNotification('error', '创建目录失败: ' + error.message) + } + } + } + + // 复制文件 + async function copyTestFile() { + try { + if (!window.gsm3) { + throw new Error('GSM3 API对象未找到') + } + + showApiLoading('正在初始化API...') + await window.gsm3.initialize() + + showApiLoading('正在检查源文件...') + const exists = await window.gsm3.exists('test.txt') + if (!exists.exists) { + showApiError('复制文件失败', new Error('源文件 test.txt 不存在,请先创建测试文件')) + return + } + + showApiLoading('正在复制文件...') + const result = await window.gsm3.copy('test.txt', 'test-copy.txt') + showApiResult('复制文件', result) + if (window.gsm3.showNotification) { + window.gsm3.showNotification('success', '文件复制成功') + } + } catch (error) { + showApiError('复制文件失败', error) + if (window.gsm3 && window.gsm3.showNotification) { + window.gsm3.showNotification('error', '复制文件失败: ' + error.message) + } + } + } + + // 移动文件 + async function moveTestFile() { + try { + if (!window.gsm3) { + throw new Error('GSM3 API对象未找到') + } + + showApiLoading('正在初始化API...') + await window.gsm3.initialize() + + showApiLoading('正在检查源文件...') + const exists = await window.gsm3.exists('test-copy.txt') + if (!exists.exists) { + showApiError('移动文件失败', new Error('源文件 test-copy.txt 不存在,请先复制文件')) + return + } + + showApiLoading('正在移动文件...') + const result = await window.gsm3.move('test-copy.txt', 'test-moved.txt') + showApiResult('移动文件', result) + if (window.gsm3.showNotification) { + window.gsm3.showNotification('success', '文件移动成功') + } + } catch (error) { + showApiError('移动文件失败', error) + if (window.gsm3 && window.gsm3.showNotification) { + window.gsm3.showNotification('error', '移动文件失败: ' + error.message) + } + } + } + + // 搜索文件 + async function searchFiles() { + try { + if (!window.gsm3) { + throw new Error('GSM3 API对象未找到') + } + + showApiLoading('正在初始化API...') + await window.gsm3.initialize() + + showApiLoading('正在搜索文件...') + const result = await window.gsm3.searchFiles('*.txt') + showApiResult('搜索文件 (*.txt)', result) + if (window.gsm3.showNotification) { + window.gsm3.showNotification('success', '文件搜索完成') + } + } catch (error) { + showApiError('搜索文件失败', error) + if (window.gsm3 && window.gsm3.showNotification) { + window.gsm3.showNotification('error', '搜索文件失败: ' + error.message) + } + } + } + + // 检查文件是否存在 + async function checkFileExists() { + try { + if (!window.gsm3) { + throw new Error('GSM3 API对象未找到') + } + + const filePath = document.getElementById('filePathInput').value + if (!filePath) { + showApiError('检查文件失败', new Error('请输入文件路径')) + return + } + + showApiLoading('正在初始化API...') + await window.gsm3.initialize() + + showApiLoading('正在检查文件是否存在...') + const result = await window.gsm3.exists(filePath) + showApiResult(`检查文件是否存在: ${filePath}`, result) + if (window.gsm3.showNotification) { + window.gsm3.showNotification('info', '文件检查完成') + } + } catch (error) { + showApiError('检查文件失败', error) + if (window.gsm3 && window.gsm3.showNotification) { + window.gsm3.showNotification('error', '检查文件失败: ' + error.message) + } + } + } + + // 获取文件信息 + async function getFileInfo() { + try { + if (!window.gsm3) { + throw new Error('GSM3 API对象未找到') + } + + const filePath = document.getElementById('filePathInput').value + if (!filePath) { + showApiError('获取文件信息失败', new Error('请输入文件路径')) + return + } + + showApiLoading('正在初始化API...') + await window.gsm3.initialize() + + showApiLoading('正在获取文件信息...') + const result = await window.gsm3.getFileInfo(filePath) + showApiResult(`获取文件信息: ${filePath}`, result) + if (window.gsm3.showNotification) { + window.gsm3.showNotification('success', '文件信息获取成功') + } + } catch (error) { + showApiError('获取文件信息失败', error) + if (window.gsm3 && window.gsm3.showNotification) { + window.gsm3.showNotification('error', '获取文件信息失败: ' + error.message) + } + } + } + // 控制台欢迎信息 console.log('%c🧩 GSM3 插件系统', 'color: #667eea; font-size: 20px; font-weight: bold;') console.log('%c欢迎使用示例插件!', 'color: #764ba2; font-size: 14px;') diff --git a/server/src/routes/files.ts b/server/src/routes/files.ts index ab7a6b0..550a93a 100644 --- a/server/src/routes/files.ts +++ b/server/src/routes/files.ts @@ -416,30 +416,7 @@ router.post('/save', authenticateToken, async (req: Request, res: Response) => { }) // 创建目录 -router.post('/mkdir', authenticateToken, async (req: Request, res: Response) => { - try { - const { path: dirPath } = req.body - - if (!isValidPath(dirPath)) { - return res.status(400).json({ - status: 'error', - message: '无效的路径' - }) - } - - await fs.mkdir(dirPath, { recursive: true }) - - res.json({ - status: 'success', - message: '目录创建成功' - }) - } catch (error: any) { - res.status(500).json({ - status: 'error', - message: error.message - }) - } -}) +// 原有的mkdir路由已移除,使用插件API专用的mkdir路由 // 删除文件或目录 router.delete('/delete', authenticateToken, async (req: Request, res: Response) => { @@ -983,6 +960,576 @@ async function extractArchive(archivePath: string, targetPath: string) { }) } +// ==================== 插件文件操作API ==================== + +// 读取文件内容(POST方法,用于插件API) +router.post('/read', authenticateToken, async (req: Request, res: Response) => { + try { + const { filePath, encoding = 'utf-8' } = req.body + + if (!filePath) { + return res.status(400).json({ + success: false, + message: '缺少文件路径参数' + }) + } + + // 将相对路径转换为绝对路径(相对于data目录) + const dataDir = path.join(process.cwd(), 'data') + const fullPath = path.resolve(dataDir, filePath) + + // 安全检查:确保文件在data目录内 + if (!fullPath.startsWith(dataDir)) { + return res.status(403).json({ + success: false, + message: '访问被拒绝:文件路径超出允许范围' + }) + } + + const content = await fs.readFile(fullPath, encoding) + res.json({ + success: true, + data: { content, encoding, filePath } + }) + } catch (error: any) { + if (error.code === 'ENOENT') { + return res.status(404).json({ + success: false, + message: '文件不存在' + }) + } + res.status(500).json({ + success: false, + message: error.message + }) + } +}) + +// 写入文件内容 +router.post('/write', authenticateToken, async (req: Request, res: Response) => { + try { + const { filePath, content, encoding = 'utf-8' } = req.body + + if (!filePath || content === undefined) { + return res.status(400).json({ + success: false, + message: '缺少必要参数' + }) + } + + const dataDir = path.join(process.cwd(), 'data') + const fullPath = path.resolve(dataDir, filePath) + + if (!fullPath.startsWith(dataDir)) { + return res.status(403).json({ + success: false, + message: '访问被拒绝:文件路径超出允许范围' + }) + } + + // 确保目录存在 + await fs.mkdir(path.dirname(fullPath), { recursive: true }) + + await fs.writeFile(fullPath, content, encoding) + res.json({ + success: true, + message: '文件写入成功', + data: { filePath, size: Buffer.byteLength(content, encoding) } + }) + } catch (error: any) { + res.status(500).json({ + success: false, + message: error.message + }) + } +}) + +// 删除文件 +router.delete('/delete', authenticateToken, async (req: Request, res: Response) => { + try { + const { filePath } = req.body + + if (!filePath) { + return res.status(400).json({ + success: false, + message: '缺少文件路径参数' + }) + } + + const dataDir = path.join(process.cwd(), 'data') + const fullPath = path.resolve(dataDir, filePath) + + if (!fullPath.startsWith(dataDir)) { + return res.status(403).json({ + success: false, + message: '访问被拒绝:文件路径超出允许范围' + }) + } + + await fs.unlink(fullPath) + res.json({ + success: true, + message: '文件删除成功', + data: { filePath } + }) + } catch (error: any) { + if (error.code === 'ENOENT') { + return res.status(404).json({ + success: false, + message: '文件不存在' + }) + } + res.status(500).json({ + success: false, + message: error.message + }) + } +}) + +// 创建目录 +router.post('/mkdir', authenticateToken, async (req: Request, res: Response) => { + try { + const { dirPath, recursive = true } = req.body + + if (!dirPath) { + return res.status(400).json({ + success: false, + message: '缺少目录路径参数' + }) + } + + const dataDir = path.join(process.cwd(), 'data') + const fullPath = path.resolve(dataDir, dirPath) + + if (!fullPath.startsWith(dataDir)) { + return res.status(403).json({ + success: false, + message: '访问被拒绝:目录路径超出允许范围' + }) + } + + await fs.mkdir(fullPath, { recursive }) + res.json({ + success: true, + message: '目录创建成功', + data: { dirPath } + }) + } catch (error: any) { + res.status(500).json({ + success: false, + message: error.message + }) + } +}) + +// 删除目录 +router.delete('/rmdir', authenticateToken, async (req: Request, res: Response) => { + try { + const { dirPath, recursive = false } = req.body + + if (!dirPath) { + return res.status(400).json({ + success: false, + message: '缺少目录路径参数' + }) + } + + const dataDir = path.join(process.cwd(), 'data') + const fullPath = path.resolve(dataDir, dirPath) + + if (!fullPath.startsWith(dataDir)) { + return res.status(403).json({ + success: false, + message: '访问被拒绝:目录路径超出允许范围' + }) + } + + if (recursive) { + await fs.rm(fullPath, { recursive: true, force: true }) + } else { + await fs.rmdir(fullPath) + } + + res.json({ + success: true, + message: '目录删除成功', + data: { dirPath } + }) + } catch (error: any) { + if (error.code === 'ENOENT') { + return res.status(404).json({ + success: false, + message: '目录不存在' + }) + } + res.status(500).json({ + success: false, + message: error.message + }) + } +}) + +// 列出目录内容(POST方法,用于插件API) +router.post('/list', authenticateToken, async (req: Request, res: Response) => { + try { + const { dirPath = '', includeHidden = false } = req.body + + const dataDir = path.join(process.cwd(), 'data') + const fullPath = dirPath ? path.resolve(dataDir, dirPath) : dataDir + + if (!fullPath.startsWith(dataDir)) { + return res.status(403).json({ + success: false, + message: '访问被拒绝:目录路径超出允许范围' + }) + } + + const stats = await fs.stat(fullPath) + if (!stats.isDirectory()) { + return res.status(400).json({ + success: false, + message: '指定路径不是目录' + }) + } + + const items = await fs.readdir(fullPath) + const files = [] + + for (const item of items) { + // 跳过隐藏文件(除非明确要求包含) + if (!includeHidden && item.startsWith('.')) { + continue + } + + const itemPath = path.join(fullPath, item) + try { + const itemStats = await fs.stat(itemPath) + files.push({ + name: item, + path: path.relative(dataDir, itemPath), + type: itemStats.isDirectory() ? 'directory' : 'file', + size: itemStats.size, + modified: itemStats.mtime.toISOString(), + created: itemStats.birthtime.toISOString() + }) + } catch (error) { + // 跳过无法访问的文件 + continue + } + } + + res.json({ + success: true, + data: files + }) + } catch (error: any) { + if (error.code === 'ENOENT') { + return res.status(404).json({ + success: false, + message: '目录不存在' + }) + } + res.status(500).json({ + success: false, + message: error.message + }) + } +}) + +// 获取文件或目录信息 +router.post('/info', authenticateToken, async (req: Request, res: Response) => { + try { + const { path: itemPath } = req.body + + if (!itemPath) { + return res.status(400).json({ + success: false, + message: '缺少路径参数' + }) + } + + const dataDir = path.join(process.cwd(), 'data') + const fullPath = path.resolve(dataDir, itemPath) + + if (!fullPath.startsWith(dataDir)) { + return res.status(403).json({ + success: false, + message: '访问被拒绝:路径超出允许范围' + }) + } + + const stats = await fs.stat(fullPath) + const info = { + name: path.basename(fullPath), + path: path.relative(dataDir, fullPath), + type: stats.isDirectory() ? 'directory' : 'file', + size: stats.size, + modified: stats.mtime.toISOString(), + created: stats.birthtime.toISOString(), + accessed: stats.atime.toISOString(), + permissions: stats.mode.toString(8), + isReadable: true, + isWritable: true + } + + res.json({ + success: true, + data: info + }) + } catch (error: any) { + if (error.code === 'ENOENT') { + return res.status(404).json({ + success: false, + message: '文件或目录不存在' + }) + } + res.status(500).json({ + success: false, + message: error.message + }) + } +}) + +// 检查文件或目录是否存在 +router.post('/exists', authenticateToken, async (req: Request, res: Response) => { + try { + const { path: itemPath } = req.body + + if (!itemPath) { + return res.status(400).json({ + success: false, + message: '缺少路径参数' + }) + } + + const dataDir = path.join(process.cwd(), 'data') + const fullPath = path.resolve(dataDir, itemPath) + + if (!fullPath.startsWith(dataDir)) { + return res.status(403).json({ + success: false, + message: '访问被拒绝:路径超出允许范围' + }) + } + + try { + const stats = await fs.stat(fullPath) + res.json({ + success: true, + data: { + exists: true, + type: stats.isDirectory() ? 'directory' : 'file' + } + }) + } catch (error: any) { + if (error.code === 'ENOENT') { + res.json({ + success: true, + data: { + exists: false + } + }) + } else { + throw error + } + } + } catch (error: any) { + res.status(500).json({ + success: false, + message: error.message + }) + } +}) + +// 复制文件或目录 +router.post('/copy', authenticateToken, async (req: Request, res: Response) => { + try { + const { sourcePath, destPath, overwrite = false } = req.body + + if (!sourcePath || !destPath) { + return res.status(400).json({ + success: false, + message: '缺少源路径或目标路径参数' + }) + } + + const dataDir = path.join(process.cwd(), 'data') + const fullSourcePath = path.resolve(dataDir, sourcePath) + const fullDestPath = path.resolve(dataDir, destPath) + + if (!fullSourcePath.startsWith(dataDir) || !fullDestPath.startsWith(dataDir)) { + return res.status(403).json({ + success: false, + message: '访问被拒绝:路径超出允许范围' + }) + } + + // 检查目标是否已存在 + try { + await fs.access(fullDestPath) + if (!overwrite) { + return res.status(409).json({ + success: false, + message: '目标文件已存在' + }) + } + } catch (error) { + // 目标不存在,可以继续 + } + + // 确保目标目录存在 + await fs.mkdir(path.dirname(fullDestPath), { recursive: true }) + + await fs.copyFile(fullSourcePath, fullDestPath) + + res.json({ + success: true, + message: '文件复制成功', + data: { sourcePath, destPath } + }) + } catch (error: any) { + if (error.code === 'ENOENT') { + return res.status(404).json({ + success: false, + message: '源文件不存在' + }) + } + res.status(500).json({ + success: false, + message: error.message + }) + } +}) + +// 移动/重命名文件或目录 +router.post('/move', authenticateToken, async (req: Request, res: Response) => { + try { + const { sourcePath, destPath, overwrite = false } = req.body + + if (!sourcePath || !destPath) { + return res.status(400).json({ + success: false, + message: '缺少源路径或目标路径参数' + }) + } + + const dataDir = path.join(process.cwd(), 'data') + const fullSourcePath = path.resolve(dataDir, sourcePath) + const fullDestPath = path.resolve(dataDir, destPath) + + if (!fullSourcePath.startsWith(dataDir) || !fullDestPath.startsWith(dataDir)) { + return res.status(403).json({ + success: false, + message: '访问被拒绝:路径超出允许范围' + }) + } + + // 检查目标是否已存在 + try { + await fs.access(fullDestPath) + if (!overwrite) { + return res.status(409).json({ + success: false, + message: '目标文件已存在' + }) + } + } catch (error) { + // 目标不存在,可以继续 + } + + // 确保目标目录存在 + await fs.mkdir(path.dirname(fullDestPath), { recursive: true }) + + await fs.rename(fullSourcePath, fullDestPath) + + res.json({ + success: true, + message: '文件移动成功', + data: { sourcePath, destPath } + }) + } catch (error: any) { + if (error.code === 'ENOENT') { + return res.status(404).json({ + success: false, + message: '源文件不存在' + }) + } + res.status(500).json({ + success: false, + message: error.message + }) + } +}) + +// 搜索文件 +router.post('/search', authenticateToken, async (req: Request, res: Response) => { + try { + const { pattern, searchPath = '', recursive = true } = req.body + + if (!pattern) { + return res.status(400).json({ + success: false, + message: '缺少搜索模式参数' + }) + } + + const dataDir = path.join(process.cwd(), 'data') + const fullSearchPath = searchPath ? path.resolve(dataDir, searchPath) : dataDir + + if (!fullSearchPath.startsWith(dataDir)) { + return res.status(403).json({ + success: false, + message: '访问被拒绝:搜索路径超出允许范围' + }) + } + + const results: any[] = [] + + async function searchRecursive(currentPath: string) { + try { + const items = await fs.readdir(currentPath) + + for (const item of items) { + const itemPath = path.join(currentPath, item) + const stats = await fs.stat(itemPath) + + // 简单的通配符匹配 + const regex = new RegExp(pattern.replace(/\*/g, '.*').replace(/\?/g, '.'), 'i') + if (regex.test(item)) { + results.push({ + name: item, + path: path.relative(dataDir, itemPath), + type: stats.isDirectory() ? 'directory' : 'file', + size: stats.size, + modified: stats.mtime.toISOString() + }) + } + + if (recursive && stats.isDirectory()) { + await searchRecursive(itemPath) + } + } + } catch (error) { + // 跳过无法访问的目录 + } + } + + await searchRecursive(fullSearchPath) + + res.json({ + success: true, + data: results + }) + } catch (error: any) { + res.status(500).json({ + success: false, + message: error.message + }) + } +}) + +// ==================== 任务管理API ==================== + // 获取任务状态 router.get('/tasks', authenticateToken, async (req: Request, res: Response) => { try { diff --git a/server/src/routes/pluginApi.ts b/server/src/routes/pluginApi.ts index 8f64bc4..9e75089 100644 --- a/server/src/routes/pluginApi.ts +++ b/server/src/routes/pluginApi.ts @@ -4,6 +4,7 @@ import type { InstanceManager } from '../modules/instance/InstanceManager.js' import type { SystemManager } from '../modules/system/SystemManager.js' import type { TerminalManager } from '../modules/terminal/TerminalManager.js' import type { GameManager } from '../modules/game/GameManager.js' +import filesRouter from './files.js' import logger from '../utils/logger.js' const router = Router() @@ -253,6 +254,11 @@ router.get('/games', async (req: Request, res: Response) => { } }) +// ==================== 文件操作API ==================== + +// 转发文件操作请求到files路由 +router.use('/files', filesRouter) + // ==================== 通用API ==================== // 获取API版本信息