From 84b712246d5b9a564f55a66abe9dcfd5fbd3ca22 Mon Sep 17 00:00:00 2001 From: kjqwer <2990346238@qq.com> Date: Thu, 21 Aug 2025 14:59:52 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E4=BB=93=E5=BA=93=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/README.md | 40 +- backend/routes/repository.js | 280 +++++++ backend/scripts/migrate-downloads.js | 88 +++ backend/server.js | 2 + backend/services/download.js | 6 +- backend/services/repository.js | 414 ++++++++++ ui/src/App.vue | 1 + ui/src/router/index.ts | 6 + ui/src/stores/repository.ts | 187 +++++ ui/src/views/RepositoryView.vue | 1040 ++++++++++++++++++++++++++ 10 files changed, 2060 insertions(+), 4 deletions(-) create mode 100644 backend/routes/repository.js create mode 100644 backend/scripts/migrate-downloads.js create mode 100644 backend/services/repository.js create mode 100644 ui/src/stores/repository.ts create mode 100644 ui/src/views/RepositoryView.vue diff --git a/backend/README.md b/backend/README.md index e1372e7..67e95f8 100644 --- a/backend/README.md +++ b/backend/README.md @@ -19,11 +19,13 @@ backend/ │ ├── auth.js # 认证路由 │ ├── artwork.js # 作品路由 │ ├── artist.js # 作者路由 -│ └── download.js # 下载路由 +│ ├── download.js # 下载路由 +│ └── repository.js # 仓库管理路由 ├── services/ # 服务层 │ ├── artwork.js # 作品服务 │ ├── artist.js # 作者服务 -│ └── download.js # 下载服务 +│ ├── download.js # 下载服务 +│ └── repository.js # 仓库管理服务 └── utils/ # 工具类 └── response.js # 响应工具 ``` @@ -79,6 +81,29 @@ backend/ - `GET /api/proxy/image` - 图片代理服务 - 参数: `url` (图片URL) +### 仓库管理相关 + +- `POST /api/repository/initialize` - 初始化仓库 +- `GET /api/repository/config` - 获取仓库配置 +- `PUT /api/repository/config` - 更新仓库配置 +- `GET /api/repository/stats` - 获取仓库统计信息 +- `GET /api/repository/artists` - 获取作者列表 + - 参数: `offset`, `limit` +- `GET /api/repository/artists/:artistName/artworks` - 获取作者作品列表 + - 参数: `offset`, `limit` +- `GET /api/repository/search` - 搜索作品 + - 参数: `q`, `offset`, `limit` +- `GET /api/repository/artworks/:artworkId` - 获取作品详情 +- `DELETE /api/repository/artworks/:artworkId` - 删除作品 +- `POST /api/repository/migrate` - 自动迁移旧项目 + - 参数: `sourceDir` (源目录路径) +- `GET /api/repository/preview` - 文件预览代理 + - 参数: `path` (文件路径) +- `GET /api/repository/file-info` - 获取文件信息 + - 参数: `path` (文件路径) +- `GET /api/repository/directory` - 获取目录结构 + - 参数: `path` (目录路径) + ## 🔧 配置说明 ### 代理配置 @@ -110,6 +135,7 @@ backend/ - **artwork.js**: 作品相关路由 - **artist.js**: 作者相关路由 - **download.js**: 下载相关路由 +- **repository.js**: 仓库管理路由 - **proxy.js**: 代理服务路由 ### 服务层 @@ -117,6 +143,7 @@ backend/ - **artwork.js**: 作品服务,处理作品API调用 - **artist.js**: 作者服务,处理作者API调用 - **download.js**: 下载服务,处理文件下载 +- **repository.js**: 仓库管理服务,处理文件管理和配置 ### 工具类 @@ -148,6 +175,15 @@ backend/ - 自动刷新令牌 - 登录状态管理 +### 5. 仓库管理 +- 文件存储配置管理 +- 作品文件浏览和搜索 +- 按作者分类浏览 +- 文件预览和下载 +- 自动迁移旧项目 +- 磁盘使用情况监控 +- 作品删除管理 + ## 🔒 安全特性 - 统一的错误处理 diff --git a/backend/routes/repository.js b/backend/routes/repository.js new file mode 100644 index 0000000..f6b982d --- /dev/null +++ b/backend/routes/repository.js @@ -0,0 +1,280 @@ +const express = require('express') +const path = require('path') +const fs = require('fs').promises +const RepositoryService = require('../services/repository') +const ResponseUtil = require('../utils/response') + +const router = express.Router() +const repositoryService = new RepositoryService() + +// 初始化仓库 +router.post('/initialize', async (req, res) => { + try { + const result = await repositoryService.initialize() + res.json(ResponseUtil.success(result)) + } catch (error) { + res.status(500).json(ResponseUtil.error(error.message)) + } +}) + +// 获取仓库配置 +router.get('/config', async (req, res) => { + try { + const config = await repositoryService.getConfig() + res.json(ResponseUtil.success(config)) + } catch (error) { + res.status(500).json(ResponseUtil.error(error.message)) + } +}) + +// 更新仓库配置 +router.put('/config', async (req, res) => { + try { + const result = await repositoryService.updateConfig(req.body) + res.json(ResponseUtil.success(result)) + } catch (error) { + res.status(500).json(ResponseUtil.error(error.message)) + } +}) + +// 获取仓库统计信息 +router.get('/stats', async (req, res) => { + try { + const stats = await repositoryService.getStats() + res.json(ResponseUtil.success(stats)) + } catch (error) { + res.status(500).json(ResponseUtil.error(error.message)) + } +}) + +// 获取所有作者列表 +router.get('/artists', async (req, res) => { + try { + const { offset = 0, limit = 50 } = req.query + const artists = await repositoryService.getArtists(parseInt(offset), parseInt(limit)) + res.json(ResponseUtil.success(artists)) + } catch (error) { + res.status(500).json(ResponseUtil.error(error.message)) + } +}) + +// 获取作者作品列表 +router.get('/artists/:artistName/artworks', async (req, res) => { + try { + const { artistName } = req.params + const { offset = 0, limit = 20 } = req.query + const artworks = await repositoryService.getArtworksByArtist( + artistName, + parseInt(offset), + parseInt(limit) + ) + res.json(ResponseUtil.success(artworks)) + } catch (error) { + res.status(500).json(ResponseUtil.error(error.message)) + } +}) + +// 搜索作品 +router.get('/search', async (req, res) => { + try { + const { q, offset = 0, limit = 20 } = req.query + if (!q) { + return res.status(400).json(ResponseUtil.error('搜索关键词不能为空')) + } + + const results = await repositoryService.searchArtworks(q, parseInt(offset), parseInt(limit)) + res.json(ResponseUtil.success(results)) + } catch (error) { + res.status(500).json(ResponseUtil.error(error.message)) + } +}) + +// 获取作品详情 +router.get('/artworks/:artworkId', async (req, res) => { + try { + const { artworkId } = req.params + const artwork = await repositoryService.findArtworkById(artworkId) + + if (!artwork) { + return res.status(404).json(ResponseUtil.error('作品不存在')) + } + + res.json(ResponseUtil.success(artwork)) + } catch (error) { + res.status(500).json(ResponseUtil.error(error.message)) + } +}) + +// 删除作品 +router.delete('/artworks/:artworkId', async (req, res) => { + try { + const { artworkId } = req.params + const result = await repositoryService.deleteArtwork(artworkId) + res.json(ResponseUtil.success(result)) + } catch (error) { + res.status(500).json(ResponseUtil.error(error.message)) + } +}) + +// 自动迁移旧项目 +router.post('/migrate', async (req, res) => { + try { + const { sourceDir } = req.body + if (!sourceDir) { + return res.status(400).json(ResponseUtil.error('源目录不能为空')) + } + + const result = await repositoryService.migrateOldProjects(sourceDir) + res.json(ResponseUtil.success(result)) + } catch (error) { + res.status(500).json(ResponseUtil.error(error.message)) + } +}) + +// 文件预览代理 +router.get('/preview', async (req, res) => { + try { + const { path: filePath } = req.query + if (!filePath) { + return res.status(400).json(ResponseUtil.error('文件路径不能为空')) + } + + const fullPath = path.join(repositoryService.baseDir, filePath) + + // 安全检查:确保文件在仓库目录内 + const relativePath = path.relative(repositoryService.baseDir, fullPath) + + if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) { + return res.status(403).json(ResponseUtil.error('访问被拒绝')) + } + + // 检查文件是否存在 + try { + await fs.access(fullPath) + } catch (error) { + return res.status(404).json(ResponseUtil.error('文件不存在')) + } + + // 获取文件信息 + const stats = await fs.stat(fullPath) + const ext = path.extname(fullPath).toLowerCase() + + // 设置响应头 + res.setHeader('Content-Type', getContentType(ext)) + res.setHeader('Content-Length', stats.size) + res.setHeader('Cache-Control', 'public, max-age=3600') + + // 流式传输文件 + const fileStream = require('fs').createReadStream(fullPath) + fileStream.pipe(res) + } catch (error) { + res.status(500).json(ResponseUtil.error(error.message)) + } +}) + +// 获取文件信息 +router.get('/file-info', async (req, res) => { + try { + const { path: filePath } = req.query + if (!filePath) { + return res.status(400).json(ResponseUtil.error('文件路径不能为空')) + } + + const fullPath = path.join(repositoryService.baseDir, filePath) + + // 安全检查 + const relativePath = path.relative(repositoryService.baseDir, fullPath) + if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) { + return res.status(403).json(ResponseUtil.error('访问被拒绝')) + } + + const stats = await fs.stat(fullPath) + const ext = path.extname(fullPath).toLowerCase() + + res.json(ResponseUtil.success({ + name: path.basename(fullPath), + path: filePath, + size: stats.size, + extension: ext, + modifiedAt: stats.mtime, + createdAt: stats.birthtime, + contentType: getContentType(ext), + previewUrl: `/api/repository/preview?path=${encodeURIComponent(filePath)}` + })) + } catch (error) { + res.status(500).json(ResponseUtil.error(error.message)) + } +}) + +// 获取目录结构 +router.get('/directory', async (req, res) => { + try { + const { path: dirPath = '' } = req.query + const fullPath = path.join(repositoryService.baseDir, dirPath) + + // 安全检查 + const relativePath = path.relative(repositoryService.baseDir, fullPath) + if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) { + return res.status(403).json(ResponseUtil.error('访问被拒绝')) + } + + const entries = await fs.readdir(fullPath, { withFileTypes: true }) + const items = [] + + for (const entry of entries) { + const itemPath = path.join(dirPath, entry.name) + const fullItemPath = path.join(fullPath, entry.name) + + if (entry.isDirectory()) { + items.push({ + type: 'directory', + name: entry.name, + path: itemPath + }) + } else { + const ext = path.extname(entry.name).toLowerCase() + if (repositoryService.config.allowedExtensions.includes(ext)) { + const stats = await fs.stat(fullItemPath) + items.push({ + type: 'file', + name: entry.name, + path: itemPath, + size: stats.size, + extension: ext, + modifiedAt: stats.mtime + }) + } + } + } + + res.json(ResponseUtil.success({ + path: dirPath, + items: items.sort((a, b) => { + // 目录在前,文件在后 + if (a.type !== b.type) { + return a.type === 'directory' ? -1 : 1 + } + return a.name.localeCompare(b.name) + }) + })) + } catch (error) { + res.status(500).json(ResponseUtil.error(error.message)) + } +}) + +// 获取内容类型 +function getContentType(extension) { + const contentTypes = { + '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg', + '.png': 'image/png', + '.gif': 'image/gif', + '.webp': 'image/webp', + '.bmp': 'image/bmp', + '.svg': 'image/svg+xml' + } + + return contentTypes[extension.toLowerCase()] || 'application/octet-stream' +} + +module.exports = router \ No newline at end of file diff --git a/backend/scripts/migrate-downloads.js b/backend/scripts/migrate-downloads.js new file mode 100644 index 0000000..42cdfb0 --- /dev/null +++ b/backend/scripts/migrate-downloads.js @@ -0,0 +1,88 @@ +const fs = require('fs').promises +const path = require('path') +const fsExtra = require('fs-extra') + +/** + * 转换现有的下载格式为仓库管理格式 + * 从: {artistName}_{artworkId}/{artworkTitle}/ + * 到: {artistName}/{artworkId}_{artworkTitle}/ + */ +async function migrateDownloads() { + const downloadsPath = path.join(__dirname, '../../downloads') + + try { + console.log('开始转换下载格式...') + + // 读取downloads目录 + const entries = await fs.readdir(downloadsPath, { withFileTypes: true }) + + for (const entry of entries) { + if (!entry.isDirectory()) continue + + const oldDirName = entry.name + console.log(`处理目录: ${oldDirName}`) + + // 解析目录名: {artistName}_{artworkId} + const match = oldDirName.match(/^(.+)_(\d+)$/) + if (!match) { + console.log(`跳过不符合格式的目录: ${oldDirName}`) + continue + } + + const [, artistName, artworkId] = match + const oldDirPath = path.join(downloadsPath, oldDirName) + + // 读取作品目录 + const artworkEntries = await fs.readdir(oldDirPath, { withFileTypes: true }) + + for (const artworkEntry of artworkEntries) { + if (!artworkEntry.isDirectory()) continue + + const artworkTitle = artworkEntry.name + const oldArtworkPath = path.join(oldDirPath, artworkTitle) + + // 新的目录结构 + const newArtistDir = path.join(downloadsPath, artistName) + const newArtworkDirName = `${artworkId}_${artworkTitle}` + const newArtworkPath = path.join(newArtistDir, newArtworkDirName) + + console.log(`转换: ${oldArtworkPath} -> ${newArtworkPath}`) + + try { + // 创建新的作者目录 + await fsExtra.ensureDir(newArtistDir) + + // 移动作品目录 + await fsExtra.move(oldArtworkPath, newArtworkPath) + + console.log(`✓ 成功转换: ${artworkTitle}`) + } catch (error) { + console.error(`✗ 转换失败: ${artworkTitle}`, error.message) + } + } + + // 检查原目录是否为空,如果为空则删除 + try { + const remainingEntries = await fs.readdir(oldDirPath) + if (remainingEntries.length === 0) { + await fsExtra.remove(oldDirPath) + console.log(`删除空目录: ${oldDirPath}`) + } + } catch (error) { + console.error(`删除目录失败: ${oldDirPath}`, error.message) + } + } + + console.log('转换完成!') + + } catch (error) { + console.error('转换过程中发生错误:', error) + } +} + +// 如果直接运行此脚本 +if (require.main === module) { + migrateDownloads() +} + +module.exports = { migrateDownloads } \ No newline at end of file diff --git a/backend/server.js b/backend/server.js index 8669f04..161b140 100644 --- a/backend/server.js +++ b/backend/server.js @@ -9,6 +9,7 @@ const artworkRoutes = require('./routes/artwork'); const artistRoutes = require('./routes/artist'); const downloadRoutes = require('./routes/download'); const proxyRoutes = require('./routes/proxy'); +const repositoryRoutes = require('./routes/repository'); // 导入中间件 - 临时注释掉来定位问题 const { errorHandler } = require('./middleware/errorHandler'); @@ -101,6 +102,7 @@ class PixivServer { this.app.use('/api/artwork', authMiddleware, artworkRoutes); this.app.use('/api/artist', authMiddleware, artistRoutes); this.app.use('/api/download', authMiddleware, downloadRoutes); + this.app.use('/api/repository', repositoryRoutes); // 仓库管理,不需要认证 this.app.use('/api/proxy', proxyRoutes); // 图片代理,不需要认证 // 404 处理 diff --git a/backend/services/download.js b/backend/services/download.js index 17168cf..afee057 100644 --- a/backend/services/download.js +++ b/backend/services/download.js @@ -374,8 +374,10 @@ class DownloadService { const artistName = (artwork.user.name || 'Unknown Artist').replace(/[<>:"/\\|?*]/g, '_'); const artworkTitle = (artwork.title || 'Untitled').replace(/[<>:"/\\|?*]/g, '_'); - // 创建作品目录 - const artworkDir = path.join(this.downloadPath, `${artistName}_${artworkId}`, artworkTitle); + // 创建作品目录 - 使用仓库管理格式 + const artistDir = path.join(this.downloadPath, artistName); + const artworkDirName = `${artworkId}_${artworkTitle}`; + const artworkDir = path.join(artistDir, artworkDirName); await fs.ensureDir(artworkDir); // 获取图片URL diff --git a/backend/services/repository.js b/backend/services/repository.js new file mode 100644 index 0000000..f43d7d8 --- /dev/null +++ b/backend/services/repository.js @@ -0,0 +1,414 @@ +const fs = require('fs').promises +const path = require('path') +const { promisify } = require('util') +const { exec } = require('child_process') +const execAsync = promisify(exec) + +class RepositoryService { + constructor() { + this.baseDir = process.env.DOWNLOAD_DIR || path.join(process.cwd(), 'downloads') + this.configFile = path.join(this.baseDir, '.repository-config.json') + } + + // 初始化仓库 + async initialize() { + try { + await fs.mkdir(this.baseDir, { recursive: true }) + await this.loadConfig() + return { success: true, message: '仓库初始化成功' } + } catch (error) { + throw new Error(`仓库初始化失败: ${error.message}`) + } + } + + // 加载配置 + async loadConfig() { + try { + const configData = await fs.readFile(this.configFile, 'utf8') + this.config = JSON.parse(configData) + } catch (error) { + // 如果配置文件不存在,创建默认配置 + this.config = { + downloadDir: this.baseDir, + autoMigration: false, + migrationRules: [], + fileStructure: 'artist/artwork', // artist/artwork, artwork, flat + namingPattern: '{artist_name}/{artwork_id}_{title}', + maxFileSize: 0, // 0表示无限制 + allowedExtensions: ['.jpg', '.png', '.gif', '.webp'] + } + await this.saveConfig() + } + } + + // 保存配置 + async saveConfig() { + try { + await fs.writeFile(this.configFile, JSON.stringify(this.config, null, 2)) + } catch (error) { + throw new Error(`保存配置失败: ${error.message}`) + } + } + + // 获取仓库配置 + async getConfig() { + await this.loadConfig() + return this.config + } + + // 更新仓库配置 + async updateConfig(newConfig) { + this.config = { ...this.config, ...newConfig } + await this.saveConfig() + return { success: true, message: '配置更新成功' } + } + + // 获取仓库统计信息 + async getStats() { + try { + const stats = await this.scanRepository() + return { + totalArtworks: stats.artworks.length, + totalArtists: stats.artists.length, + totalSize: stats.totalSize, + diskUsage: await this.getDiskUsage(), + lastScan: new Date().toISOString() + } + } catch (error) { + throw new Error(`获取统计信息失败: ${error.message}`) + } + } + + // 扫描仓库 + async scanRepository() { + const artworks = [] + const artists = new Set() + let totalSize = 0 + + try { + // 确保配置已加载 + await this.loadConfig() + + // 扫描作者目录 + const artistEntries = await fs.readdir(this.baseDir, { withFileTypes: true }) + + for (const artistEntry of artistEntries) { + if (!artistEntry.isDirectory()) continue + + // 跳过配置文件和隐藏文件 + if (artistEntry.name.startsWith('.') || artistEntry.name === '.repository-config.json') { + continue + } + + const artistName = artistEntry.name + const artistPath = path.join(this.baseDir, artistName) + + // 扫描作者下的作品目录 + const artworkEntries = await fs.readdir(artistPath, { withFileTypes: true }) + + for (const artworkEntry of artworkEntries) { + if (!artworkEntry.isDirectory()) continue + + const fullPath = path.join(artistPath, artworkEntry.name) + + // 检查是否是作品目录(包含数字ID) + const artworkMatch = artworkEntry.name.match(/^(\d+)_(.+)$/) + if (artworkMatch) { + const artworkId = artworkMatch[1] + const title = artworkMatch[2] + + // 扫描作品文件 + const files = await this.scanArtworkFiles(fullPath) + + if (files.length > 0) { + artworks.push({ + id: artworkId, + title: title, + artist: artistName, + artistPath: artistPath, + path: fullPath, + files: files, + size: files.reduce((sum, file) => sum + file.size, 0), + createdAt: await this.getFileCreationTime(fullPath) + }) + artists.add(artistName) + totalSize += files.reduce((sum, file) => sum + file.size, 0) + } + } + } + } + + return { + artworks, + artists: Array.from(artists), + totalSize + } + } catch (error) { + throw new Error(`扫描仓库失败: ${error.message}`) + } + } + + // 扫描作品文件 + async scanArtworkFiles(artworkPath) { + try { + // 确保配置已加载 + await this.loadConfig() + + const files = [] + const entries = await fs.readdir(artworkPath, { withFileTypes: true }) + + for (const entry of entries) { + if (entry.isFile()) { + const filePath = path.join(artworkPath, entry.name) + const ext = path.extname(entry.name).toLowerCase() + + if (this.config.allowedExtensions.includes(ext)) { + const stats = await fs.stat(filePath) + files.push({ + name: entry.name, + path: path.relative(this.baseDir, filePath), + size: stats.size, + extension: ext, + modifiedAt: stats.mtime + }) + } + } + } + + return files + } catch (error) { + return [] + } + } + + // 获取文件创建时间 + async getFileCreationTime(filePath) { + try { + const stats = await fs.stat(filePath) + return stats.birthtime + } catch (error) { + return new Date() + } + } + + // 获取磁盘使用情况 + async getDiskUsage() { + try { + const stats = await fs.statfs(this.baseDir) + const total = stats.blocks * stats.bsize + const free = stats.bavail * stats.bsize + const used = total - free + + return { + total, + used, + free, + usagePercent: Math.round((used / total) * 100) + } + } catch (error) { + return { total: 0, used: 0, free: 0, usagePercent: 0 } + } + } + + // 按作者浏览作品 + async getArtworksByArtist(artistName, offset = 0, limit = 20) { + try { + const stats = await this.scanRepository() + const artistArtworks = stats.artworks.filter(artwork => + artwork.artist === artistName + ) + + return { + artworks: artistArtworks.slice(offset, offset + limit), + total: artistArtworks.length, + offset, + limit + } + } catch (error) { + throw new Error(`获取作者作品失败: ${error.message}`) + } + } + + // 按作品ID查找 + async findArtworkById(artworkId) { + try { + const stats = await this.scanRepository() + return stats.artworks.find(artwork => artwork.id === artworkId) + } catch (error) { + throw new Error(`查找作品失败: ${error.message}`) + } + } + + // 搜索作品 + async searchArtworks(query, offset = 0, limit = 20) { + try { + const stats = await this.scanRepository() + const filtered = stats.artworks.filter(artwork => + artwork.title.toLowerCase().includes(query.toLowerCase()) || + artwork.artist.toLowerCase().includes(query.toLowerCase()) || + artwork.id.includes(query) + ) + + return { + artworks: filtered.slice(offset, offset + limit), + total: filtered.length, + offset, + limit + } + } catch (error) { + throw new Error(`搜索作品失败: ${error.message}`) + } + } + + // 获取所有作者列表 + async getArtists(offset = 0, limit = 50) { + try { + const stats = await this.scanRepository() + const artists = stats.artists.slice(offset, offset + limit) + + // 获取每个作者的统计信息 + const artistsWithStats = artists.map(artistName => { + const artistArtworks = stats.artworks.filter(artwork => artwork.artist === artistName) + const totalSize = artistArtworks.reduce((sum, artwork) => sum + artwork.size, 0) + + return { + name: artistName, + artworkCount: artistArtworks.length, + totalSize, + lastUpdated: artistArtworks.length > 0 + ? Math.max(...artistArtworks.map(a => new Date(a.createdAt).getTime())) + : null + } + }) + + return { + artists: artistsWithStats, + total: stats.artists.length, + offset, + limit + } + } catch (error) { + throw new Error(`获取作者列表失败: ${error.message}`) + } + } + + // 自动迁移旧项目 + async migrateOldProjects(sourceDir) { + try { + const migrationLog = [] + + // 扫描源目录 + const scanSource = async (dirPath, relativePath = '') => { + const entries = await fs.readdir(dirPath, { withFileTypes: true }) + + for (const entry of entries) { + const fullPath = path.join(dirPath, entry.name) + const newRelativePath = path.join(relativePath, entry.name) + + if (entry.isDirectory()) { + // 检查是否是作品目录 + const artworkMatch = entry.name.match(/^(\d+)_(.+)$/) + if (artworkMatch) { + const artworkId = artworkMatch[1] + const title = artworkMatch[2] + + // 检查是否已存在 + const existingArtwork = await this.findArtworkById(artworkId) + if (!existingArtwork) { + // 迁移作品 + const targetPath = path.join(this.baseDir, newRelativePath) + await fs.mkdir(path.dirname(targetPath), { recursive: true }) + await this.copyDirectory(fullPath, targetPath) + + migrationLog.push({ + type: 'artwork', + id: artworkId, + title: title, + source: fullPath, + target: targetPath, + status: 'success' + }) + } else { + migrationLog.push({ + type: 'artwork', + id: artworkId, + title: title, + source: fullPath, + status: 'skipped', + reason: '已存在' + }) + } + } else { + // 递归扫描子目录 + await scanSource(fullPath, newRelativePath) + } + } + } + } + + await scanSource(sourceDir) + + return { + success: true, + message: '迁移完成', + log: migrationLog, + totalMigrated: migrationLog.filter(item => item.status === 'success').length + } + } catch (error) { + throw new Error(`迁移失败: ${error.message}`) + } + } + + // 复制目录 + async copyDirectory(source, target) { + try { + await fs.mkdir(target, { recursive: true }) + const entries = await fs.readdir(source, { withFileTypes: true }) + + for (const entry of entries) { + const sourcePath = path.join(source, entry.name) + const targetPath = path.join(target, entry.name) + + if (entry.isDirectory()) { + await this.copyDirectory(sourcePath, targetPath) + } else { + await fs.copyFile(sourcePath, targetPath) + } + } + } catch (error) { + throw new Error(`复制目录失败: ${error.message}`) + } + } + + // 删除作品 + async deleteArtwork(artworkId) { + try { + const artwork = await this.findArtworkById(artworkId) + if (!artwork) { + throw new Error('作品不存在') + } + + await fs.rm(artwork.path, { recursive: true, force: true }) + + // 检查作者目录是否为空,如果为空则删除 + const artistDir = artwork.artistPath + const artistArtworks = await this.getArtworksByArtist(artwork.artist) + if (artistArtworks.artworks.length === 0) { + await fs.rmdir(artistDir) + } + + return { success: true, message: '作品删除成功' } + } catch (error) { + throw new Error(`删除作品失败: ${error.message}`) + } + } + + // 获取文件预览URL + async getFilePreviewUrl(filePath) { + // 这里可以返回一个代理URL,用于前端预览 + const relativePath = path.relative(this.baseDir, filePath) + return `/api/repository/preview?path=${encodeURIComponent(relativePath)}` + } +} + +module.exports = RepositoryService \ No newline at end of file diff --git a/ui/src/App.vue b/ui/src/App.vue index 2af4342..0294082 100644 --- a/ui/src/App.vue +++ b/ui/src/App.vue @@ -32,6 +32,7 @@ onMounted(async () => { 搜索 下载管理 作者管理 + 仓库管理