增加仓库管理功能
This commit is contained in:
+38
-2
@@ -19,11 +19,13 @@ backend/
|
|||||||
│ ├── auth.js # 认证路由
|
│ ├── auth.js # 认证路由
|
||||||
│ ├── artwork.js # 作品路由
|
│ ├── artwork.js # 作品路由
|
||||||
│ ├── artist.js # 作者路由
|
│ ├── artist.js # 作者路由
|
||||||
│ └── download.js # 下载路由
|
│ ├── download.js # 下载路由
|
||||||
|
│ └── repository.js # 仓库管理路由
|
||||||
├── services/ # 服务层
|
├── services/ # 服务层
|
||||||
│ ├── artwork.js # 作品服务
|
│ ├── artwork.js # 作品服务
|
||||||
│ ├── artist.js # 作者服务
|
│ ├── artist.js # 作者服务
|
||||||
│ └── download.js # 下载服务
|
│ ├── download.js # 下载服务
|
||||||
|
│ └── repository.js # 仓库管理服务
|
||||||
└── utils/ # 工具类
|
└── utils/ # 工具类
|
||||||
└── response.js # 响应工具
|
└── response.js # 响应工具
|
||||||
```
|
```
|
||||||
@@ -79,6 +81,29 @@ backend/
|
|||||||
- `GET /api/proxy/image` - 图片代理服务
|
- `GET /api/proxy/image` - 图片代理服务
|
||||||
- 参数: `url` (图片URL)
|
- 参数: `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**: 作品相关路由
|
- **artwork.js**: 作品相关路由
|
||||||
- **artist.js**: 作者相关路由
|
- **artist.js**: 作者相关路由
|
||||||
- **download.js**: 下载相关路由
|
- **download.js**: 下载相关路由
|
||||||
|
- **repository.js**: 仓库管理路由
|
||||||
- **proxy.js**: 代理服务路由
|
- **proxy.js**: 代理服务路由
|
||||||
|
|
||||||
### 服务层
|
### 服务层
|
||||||
@@ -117,6 +143,7 @@ backend/
|
|||||||
- **artwork.js**: 作品服务,处理作品API调用
|
- **artwork.js**: 作品服务,处理作品API调用
|
||||||
- **artist.js**: 作者服务,处理作者API调用
|
- **artist.js**: 作者服务,处理作者API调用
|
||||||
- **download.js**: 下载服务,处理文件下载
|
- **download.js**: 下载服务,处理文件下载
|
||||||
|
- **repository.js**: 仓库管理服务,处理文件管理和配置
|
||||||
|
|
||||||
### 工具类
|
### 工具类
|
||||||
|
|
||||||
@@ -148,6 +175,15 @@ backend/
|
|||||||
- 自动刷新令牌
|
- 自动刷新令牌
|
||||||
- 登录状态管理
|
- 登录状态管理
|
||||||
|
|
||||||
|
### 5. 仓库管理
|
||||||
|
- 文件存储配置管理
|
||||||
|
- 作品文件浏览和搜索
|
||||||
|
- 按作者分类浏览
|
||||||
|
- 文件预览和下载
|
||||||
|
- 自动迁移旧项目
|
||||||
|
- 磁盘使用情况监控
|
||||||
|
- 作品删除管理
|
||||||
|
|
||||||
## 🔒 安全特性
|
## 🔒 安全特性
|
||||||
|
|
||||||
- 统一的错误处理
|
- 统一的错误处理
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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 }
|
||||||
@@ -9,6 +9,7 @@ const artworkRoutes = require('./routes/artwork');
|
|||||||
const artistRoutes = require('./routes/artist');
|
const artistRoutes = require('./routes/artist');
|
||||||
const downloadRoutes = require('./routes/download');
|
const downloadRoutes = require('./routes/download');
|
||||||
const proxyRoutes = require('./routes/proxy');
|
const proxyRoutes = require('./routes/proxy');
|
||||||
|
const repositoryRoutes = require('./routes/repository');
|
||||||
|
|
||||||
// 导入中间件 - 临时注释掉来定位问题
|
// 导入中间件 - 临时注释掉来定位问题
|
||||||
const { errorHandler } = require('./middleware/errorHandler');
|
const { errorHandler } = require('./middleware/errorHandler');
|
||||||
@@ -101,6 +102,7 @@ class PixivServer {
|
|||||||
this.app.use('/api/artwork', authMiddleware, artworkRoutes);
|
this.app.use('/api/artwork', authMiddleware, artworkRoutes);
|
||||||
this.app.use('/api/artist', authMiddleware, artistRoutes);
|
this.app.use('/api/artist', authMiddleware, artistRoutes);
|
||||||
this.app.use('/api/download', authMiddleware, downloadRoutes);
|
this.app.use('/api/download', authMiddleware, downloadRoutes);
|
||||||
|
this.app.use('/api/repository', repositoryRoutes); // 仓库管理,不需要认证
|
||||||
this.app.use('/api/proxy', proxyRoutes); // 图片代理,不需要认证
|
this.app.use('/api/proxy', proxyRoutes); // 图片代理,不需要认证
|
||||||
|
|
||||||
// 404 处理
|
// 404 处理
|
||||||
|
|||||||
@@ -374,8 +374,10 @@ class DownloadService {
|
|||||||
const artistName = (artwork.user.name || 'Unknown Artist').replace(/[<>:"/\\|?*]/g, '_');
|
const artistName = (artwork.user.name || 'Unknown Artist').replace(/[<>:"/\\|?*]/g, '_');
|
||||||
const artworkTitle = (artwork.title || 'Untitled').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);
|
await fs.ensureDir(artworkDir);
|
||||||
|
|
||||||
// 获取图片URL
|
// 获取图片URL
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -32,6 +32,7 @@ onMounted(async () => {
|
|||||||
<RouterLink to="/search" class="nav-link" v-if="isLoggedIn">搜索</RouterLink>
|
<RouterLink to="/search" class="nav-link" v-if="isLoggedIn">搜索</RouterLink>
|
||||||
<RouterLink to="/downloads" class="nav-link" v-if="isLoggedIn">下载管理</RouterLink>
|
<RouterLink to="/downloads" class="nav-link" v-if="isLoggedIn">下载管理</RouterLink>
|
||||||
<RouterLink to="/artists" class="nav-link" v-if="isLoggedIn">作者管理</RouterLink>
|
<RouterLink to="/artists" class="nav-link" v-if="isLoggedIn">作者管理</RouterLink>
|
||||||
|
<RouterLink to="/repository" class="nav-link" v-if="isLoggedIn">仓库管理</RouterLink>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="nav-auth">
|
<div class="nav-auth">
|
||||||
|
|||||||
@@ -44,6 +44,12 @@ const router = createRouter({
|
|||||||
name: 'artists',
|
name: 'artists',
|
||||||
component: () => import('@/views/ArtistsView.vue'),
|
component: () => import('@/views/ArtistsView.vue'),
|
||||||
meta: { requiresAuth: true }
|
meta: { requiresAuth: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/repository',
|
||||||
|
name: 'repository',
|
||||||
|
component: () => import('@/views/RepositoryView.vue'),
|
||||||
|
meta: { requiresAuth: true }
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,187 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
export interface RepositoryConfig {
|
||||||
|
downloadDir: string
|
||||||
|
fileStructure: 'artist/artwork' | 'artwork' | 'flat'
|
||||||
|
namingPattern: string
|
||||||
|
maxFileSize: number
|
||||||
|
allowedExtensions: string[]
|
||||||
|
autoMigration: boolean
|
||||||
|
migrationRules: any[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RepositoryStats {
|
||||||
|
totalArtworks: number
|
||||||
|
totalArtists: number
|
||||||
|
totalSize: number
|
||||||
|
diskUsage: {
|
||||||
|
total: number
|
||||||
|
used: number
|
||||||
|
free: number
|
||||||
|
usagePercent: number
|
||||||
|
}
|
||||||
|
lastScan: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Artist {
|
||||||
|
name: string
|
||||||
|
artworkCount: number
|
||||||
|
totalSize: number
|
||||||
|
lastUpdated: number | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ArtworkFile {
|
||||||
|
name: string
|
||||||
|
path: string
|
||||||
|
size: number
|
||||||
|
extension: string
|
||||||
|
modifiedAt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Artwork {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
artist: string
|
||||||
|
artistPath: string
|
||||||
|
path: string
|
||||||
|
files: ArtworkFile[]
|
||||||
|
size: number
|
||||||
|
createdAt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MigrationResult {
|
||||||
|
success: boolean
|
||||||
|
message: string
|
||||||
|
log: Array<{
|
||||||
|
type: string
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
source?: string
|
||||||
|
target?: string
|
||||||
|
status: 'success' | 'skipped'
|
||||||
|
reason?: string
|
||||||
|
}>
|
||||||
|
totalMigrated: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useRepositoryStore = defineStore('repository', () => {
|
||||||
|
const config = ref<RepositoryConfig | null>(null)
|
||||||
|
const stats = ref<RepositoryStats | null>(null)
|
||||||
|
|
||||||
|
// API基础URL
|
||||||
|
const API_BASE = '/api/repository'
|
||||||
|
|
||||||
|
// 通用API调用函数
|
||||||
|
const apiCall = async (endpoint: string, options: RequestInit = {}) => {
|
||||||
|
const response = await fetch(`${API_BASE}${endpoint}`, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...options.headers,
|
||||||
|
},
|
||||||
|
...options,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json().catch(() => ({}))
|
||||||
|
throw new Error(errorData.message || `HTTP ${response.status}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
return data.data || data
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化仓库
|
||||||
|
const initialize = async () => {
|
||||||
|
return await apiCall('/initialize', { method: 'POST' })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取配置
|
||||||
|
const getConfig = async (): Promise<RepositoryConfig> => {
|
||||||
|
const result = await apiCall('/config')
|
||||||
|
config.value = result
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新配置
|
||||||
|
const updateConfig = async (newConfig: Partial<RepositoryConfig>) => {
|
||||||
|
const result = await apiCall('/config', {
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify(newConfig),
|
||||||
|
})
|
||||||
|
if (config.value) {
|
||||||
|
config.value = { ...config.value, ...newConfig }
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取统计信息
|
||||||
|
const getStats = async (): Promise<RepositoryStats> => {
|
||||||
|
const result = await apiCall('/stats')
|
||||||
|
stats.value = result
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取作者列表
|
||||||
|
const getArtists = async (offset = 0, limit = 50) => {
|
||||||
|
return await apiCall(`/artists?offset=${offset}&limit=${limit}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取作者作品
|
||||||
|
const getArtworksByArtist = async (artistName: string, offset = 0, limit = 20) => {
|
||||||
|
return await apiCall(`/artists/${encodeURIComponent(artistName)}/artworks?offset=${offset}&limit=${limit}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索作品
|
||||||
|
const searchArtworks = async (query: string, offset = 0, limit = 20) => {
|
||||||
|
return await apiCall(`/search?q=${encodeURIComponent(query)}&offset=${offset}&limit=${limit}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取作品详情
|
||||||
|
const getArtwork = async (artworkId: string): Promise<Artwork> => {
|
||||||
|
return await apiCall(`/artworks/${artworkId}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除作品
|
||||||
|
const deleteArtwork = async (artworkId: string) => {
|
||||||
|
return await apiCall(`/artworks/${artworkId}`, { method: 'DELETE' })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 迁移旧项目
|
||||||
|
const migrateOldProjects = async (sourceDir: string): Promise<MigrationResult> => {
|
||||||
|
return await apiCall('/migrate', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ sourceDir }),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取文件信息
|
||||||
|
const getFileInfo = async (filePath: string) => {
|
||||||
|
return await apiCall(`/file-info?path=${encodeURIComponent(filePath)}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取目录结构
|
||||||
|
const getDirectory = async (dirPath = '') => {
|
||||||
|
return await apiCall(`/directory?path=${encodeURIComponent(dirPath)}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
// 状态
|
||||||
|
config,
|
||||||
|
stats,
|
||||||
|
|
||||||
|
// 方法
|
||||||
|
initialize,
|
||||||
|
getConfig,
|
||||||
|
updateConfig,
|
||||||
|
getStats,
|
||||||
|
getArtists,
|
||||||
|
getArtworksByArtist,
|
||||||
|
searchArtworks,
|
||||||
|
getArtwork,
|
||||||
|
deleteArtwork,
|
||||||
|
migrateOldProjects,
|
||||||
|
getFileInfo,
|
||||||
|
getDirectory,
|
||||||
|
}
|
||||||
|
})
|
||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user