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版本信息