增加仓库管理功能
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user