const fs = require('fs').promises const path = require('path') const { promisify } = require('util') const { exec } = require('child_process') const ConfigManager = require('../config/config-manager') const execAsync = promisify(exec) const { defaultLogger } = require('../utils/logger'); // 创建logger实例 const logger = defaultLogger.child('RepositoryService'); class RepositoryService { constructor() { // 初始化配置管理器 this.configManager = new ConfigManager() this.config = null // 磁盘使用情况缓存 this.diskUsageCache = { data: null, timestamp: 0, cacheDuration: 5 * 60 * 1000 // 5分钟缓存 } // 缓存文件路径 this.cacheFilePath = null } // 获取当前工作目录(基于配置) getCurrentBaseDir() { if (this.config && this.config.downloadDir) { // 如果是相对路径,转换为绝对路径 return path.isAbsolute(this.config.downloadDir) ? this.config.downloadDir : path.resolve(process.cwd(), this.config.downloadDir) } // 默认返回项目根目录下的downloads文件夹 return path.resolve(process.cwd(), 'downloads') } // 初始化仓库 async initialize() { try { // 初始化配置管理器 await this.configManager.initialize() // 加载配置 await this.loadConfig() // 确保下载目录存在 const currentBaseDir = this.getCurrentBaseDir() await fs.mkdir(currentBaseDir, { recursive: true }) // 初始化缓存文件路径 this.cacheFilePath = path.join(path.dirname(this.configManager.getConfigPath()), 'disk-usage-cache.json') // 加载持久化缓存 await this.loadPersistentCache() return { success: true, message: '仓库初始化成功' } } catch (error) { throw new Error(`仓库初始化失败: ${error.message}`) } } // 加载配置 async loadConfig() { try { this.config = await this.configManager.readConfig() } catch (error) { logger.error('加载配置失败:', error) // 如果加载失败,使用默认配置对象 this.config = { downloadDir: "./downloads", fileStructure: "artist/artwork", namingPattern: "{artist_name}/{artwork_id}_{title}", maxFileSize: 0, allowedExtensions: [".jpg", ".png", ".gif", ".webp"], autoMigration: false, migrationRules: [], lastUpdated: new Date().toISOString() } } } // 保存配置 async saveConfig() { try { await this.configManager.saveConfig(this.config) } 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 resetConfig() { try { this.config = await this.configManager.resetToDefault() return { success: true, message: '配置已重置为默认值' } } catch (error) { throw new Error(`重置配置失败: ${error.message}`) } } // 获取仓库统计信息 async getStats(forceRefresh = false) { try { const stats = await this.scanRepository() return { totalArtworks: stats.artworks.length, totalArtists: stats.artists.length, totalSize: stats.totalSize, diskUsage: await this.getDiskUsage(forceRefresh), 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 currentBaseDir = this.getCurrentBaseDir() // 扫描作者目录 const artistEntries = await fs.readdir(currentBaseDir, { 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(currentBaseDir, 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) const currentBaseDir = this.getCurrentBaseDir() files.push({ name: entry.name, path: path.relative(currentBaseDir, 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(forceRefresh = false) { try { const currentBaseDir = this.getCurrentBaseDir() // 检查是否在打包环境中 const isPkg = process.pkg !== undefined // 尝试使用 fs.statfs (Node.js 内置方法) - 最快的方法 if (!isPkg && typeof fs.statfs === 'function') { try { const stats = await fs.statfs(currentBaseDir) 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 (statfsError) { logger.info('fs.statfs 调用失败:', statfsError.message) } } else { logger.info('fs.statfs 在打包环境中不可用,尝试使用系统命令') } // 如果 fs.statfs 不可用,尝试使用系统命令 if (process.platform === 'win32') { // Windows 系统 - 优先使用最快的命令 const methods = [ // 方法1: 使用 PowerShell (通常比 wmic 快) async () => { try { // 设置超时,避免长时间等待 const { stdout } = await Promise.race([ execAsync('powershell "Get-WmiObject -Class Win32_LogicalDisk | Select-Object Size,FreeSpace,Caption | ConvertTo-Json"'), new Promise((_, reject) => setTimeout(() => reject(new Error('PowerShell 超时')), 3000)) ]) const disks = JSON.parse(stdout) const diskArray = Array.isArray(disks) ? disks : [disks] for (const disk of diskArray) { const caption = disk.Caption const freeSpace = parseInt(disk.FreeSpace) const totalSize = parseInt(disk.Size) // 检查当前目录是否在这个磁盘上 if (currentBaseDir.toUpperCase().startsWith(caption.toUpperCase())) { const used = totalSize - freeSpace return { total: totalSize, used, free: freeSpace, usagePercent: Math.round((used / totalSize) * 100) } } } } catch (error) { logger.info('PowerShell 方法失败:', error.message) throw error } }, // 方法2: 使用 wmic (备用方案) async () => { try { const { stdout } = await Promise.race([ execAsync('wmic logicaldisk get size,freespace,caption /format:csv'), new Promise((_, reject) => setTimeout(() => reject(new Error('wmic 超时')), 3000)) ]) const lines = stdout.trim().split('\n').slice(1) // 跳过标题行 for (const line of lines) { const parts = line.split(',') if (parts.length >= 3) { const caption = parts[0].trim() const freeSpace = parseInt(parts[1]) const totalSize = parseInt(parts[2]) // 检查当前目录是否在这个磁盘上 if (currentBaseDir.toUpperCase().startsWith(caption.toUpperCase())) { const used = totalSize - freeSpace return { total: totalSize, used, free: freeSpace, usagePercent: Math.round((used / totalSize) * 100) } } } } } catch (error) { logger.info('wmic 方法失败:', error.message) throw error } } ] // 尝试所有方法,但限制总时间 for (const method of methods) { try { const result = await Promise.race([ method(), new Promise((_, reject) => setTimeout(() => reject(new Error('磁盘信息获取超时')), 5000)) ]) if (result) { return result } } catch (error) { logger.info(`磁盘使用情况获取方法失败:`, error.message) continue } } } else { // Unix/Linux 系统 try { const { stdout } = await Promise.race([ execAsync(`df -B1 "${currentBaseDir}" | tail -1`), new Promise((_, reject) => setTimeout(() => reject(new Error('df 命令超时')), 3000)) ]) const parts = stdout.trim().split(/\s+/) if (parts.length >= 4) { const total = parseInt(parts[1]) const used = parseInt(parts[2]) const free = parseInt(parts[3]) return { total, used, free, usagePercent: Math.round((used / total) * 100) } } } catch (dfError) { logger.info('df 命令失败:', dfError.message) } } // 如果系统命令都失败,返回基于缓存的估算值 return await this.getCachedDiskUsage(currentBaseDir, forceRefresh) } catch (error) { logger.error('获取磁盘使用情况失败:', error) return { total: 0, used: 0, free: 0, usagePercent: 0, note: '获取失败' } } } // 获取作者作品 async getArtworksByArtist(artistName, offset = 0, limit = 50) { try { const stats = await this.scanRepository() const artistArtworks = stats.artworks.filter(artwork => artwork.artist.toLowerCase() === artistName.toLowerCase() ) 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 isArtworkDownloaded(artworkId) { try { // 确保配置已加载 await this.loadConfig() // 使用当前配置的目录 const currentBaseDir = this.getCurrentBaseDir() // 扫描所有作者目录 const artistEntries = await fs.readdir(currentBaseDir, { withFileTypes: true }) for (const artistEntry of artistEntries) { if (!artistEntry.isDirectory()) continue // 跳过配置文件和隐藏文件 if (artistEntry.name.startsWith('.') || artistEntry.name === '.repository-config.json') { continue } const artistPath = path.join(currentBaseDir, artistEntry.name) // 扫描作者下的作品目录 const artworkEntries = await fs.readdir(artistPath, { withFileTypes: true }) for (const artworkEntry of artworkEntries) { if (!artworkEntry.isDirectory()) continue // 检查是否是目标作品目录(包含数字ID) const artworkMatch = artworkEntry.name.match(/^(\d+)_(.+)$/) if (artworkMatch && artworkMatch[1] === artworkId.toString()) { const artworkPath = path.join(artistPath, artworkEntry.name) // 检查作品信息文件 - 这是最可靠的判断标准 const infoPath = path.join(artworkPath, 'artwork_info.json') let artworkInfo; try { const infoContent = await fs.readFile(infoPath, 'utf8') artworkInfo = JSON.parse(infoContent) } catch (error) { // 信息文件不存在或无法读取,认为未下载 return false } // 检查是否有图片文件 const files = await this.scanArtworkFiles(artworkPath) if (files.length === 0) { // 有信息文件但没有图片文件,认为未下载 return false } // 检查图片数量是否与artwork_info.json中记录的一致 const expectedImageCount = artworkInfo.page_count || 1 if (files.length < expectedImageCount) { // 图片文件数量不足,认为下载不完整 logger.info(`作品 ${artworkId} 图片数量不匹配: 期望 ${expectedImageCount} 个,实际 ${files.length} 个`) return false } // 有信息文件、有图片文件且数量匹配,认为已下载 // logger.info(`作品 ${artworkId} 已完整下载: ${files.length}/${expectedImageCount} 个图片文件`) return true } } } return false } catch (error) { logger.error('检查作品下载状态失败:', error) return false } } // 检查目录是否存在 async checkDirectoryExists(dirPath) { try { // 如果是相对路径,转换为绝对路径 const fullPath = path.isAbsolute(dirPath) ? dirPath : path.resolve(process.cwd(), dirPath) const stats = await fs.stat(fullPath) return stats.isDirectory() } catch (error) { return false } } // 搜索作品 async searchArtworks(query, offset = 0, limit = 50) { 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 { // 确保配置已加载 await this.loadConfig() const currentBaseDir = this.getCurrentBaseDir() const result = { success: true, message: '迁移完成', log: [], totalMigrated: 0 } // 确保目标目录存在 await fs.mkdir(currentBaseDir, { recursive: true }) // 扫描源目录 const sourceEntries = await fs.readdir(sourceDir, { withFileTypes: true }) for (const entry of sourceEntries) { if (!entry.isDirectory()) continue const oldDirPath = path.join(sourceDir, entry.name) const newDirPath = path.join(currentBaseDir, entry.name) // 检查是否已存在 try { await fs.access(newDirPath) result.log.push({ id: entry.name, title: entry.name, status: 'skipped', reason: '目录已存在' }) continue } catch (error) { // 目录不存在,可以迁移 } try { // 直接移动整个目录 await fs.rename(oldDirPath, newDirPath) result.log.push({ id: entry.name, title: entry.name, status: 'success' }) result.totalMigrated++ } catch (error) { result.log.push({ id: entry.name, title: entry.name, status: 'error', reason: error.message }) } } return result } catch (error) { throw new Error(`迁移失败: ${error.message}`) } } // 从旧目录迁移到新目录 async migrateFromOldToNew(oldDir, newDir) { try { const result = { success: true, message: '迁移完成', log: [], totalMigrated: 0 } // 检查旧目录是否存在 try { await fs.access(oldDir) } catch (error) { return { ...result, message: '旧目录不存在,无需迁移' } } // 确保新目录存在 await fs.mkdir(newDir, { recursive: true }) // 扫描旧目录 const oldEntries = await fs.readdir(oldDir, { withFileTypes: true }) for (const entry of oldEntries) { if (!entry.isDirectory()) continue const oldEntryPath = path.join(oldDir, entry.name) const newEntryPath = path.join(newDir, entry.name) // 检查是否已存在 try { await fs.access(newEntryPath) result.log.push({ id: entry.name, title: entry.name, status: 'skipped', reason: '目录已存在' }) continue } catch (error) { // 目录不存在,可以迁移 } try { // 直接移动整个目录 await fs.rename(oldEntryPath, newEntryPath) result.log.push({ id: entry.name, title: entry.name, status: 'success' }) result.totalMigrated++ } catch (error) { result.log.push({ id: entry.name, title: entry.name, status: 'error', reason: error.message }) } } return result } 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.findArtworkByIdOptimized(artworkId) if (!artwork) { throw new Error('作品不存在') } await fs.rm(artwork.path, { recursive: true, force: true }) // 优化:直接检查作者目录是否为空,避免重复扫描 const artistDir = artwork.artistPath try { const artistEntries = await fs.readdir(artistDir, { withFileTypes: true }) const hasArtworks = artistEntries.some(entry => entry.isDirectory() && entry.name.match(/^\d+_/) ) if (!hasArtworks) { await fs.rmdir(artistDir) } } catch (error) { // 如果读取目录失败,可能目录已经不存在,忽略错误 logger.warn(`检查作者目录失败: ${error.message}`) } return { success: true, message: '作品删除成功' } } catch (error) { throw new Error(`删除作品失败: ${error.message}`) } } // 优化的作品查找方法:直接通过文件系统查找,避免全仓库扫描 async findArtworkByIdOptimized(artworkId) { try { // 确保配置已加载 await this.loadConfig() // 使用当前配置的目录 const currentBaseDir = this.getCurrentBaseDir() // 扫描所有作者目录 const artistEntries = await fs.readdir(currentBaseDir, { 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(currentBaseDir, artistName) // 扫描作者下的作品目录 const artworkEntries = await fs.readdir(artistPath, { withFileTypes: true }) for (const artworkEntry of artworkEntries) { if (!artworkEntry.isDirectory()) continue // 检查是否是目标作品目录(包含数字ID) const artworkMatch = artworkEntry.name.match(/^(\d+)_(.+)$/) if (artworkMatch && artworkMatch[1] === artworkId.toString()) { const artworkPath = path.join(artistPath, artworkEntry.name) const title = artworkMatch[2] // 找到目标作品,返回基本信息(不需要扫描文件详情) return { id: artworkId, title: title, artist: artistName, artistPath: artistPath, path: artworkPath } } } } return null // 未找到作品 } catch (error) { throw new Error(`查找作品失败: ${error.message}`) } } // 加载持久化缓存 async loadPersistentCache() { try { if (!this.cacheFilePath) { return } const cacheData = await fs.readFile(this.cacheFilePath, 'utf8') const cache = JSON.parse(cacheData) // 检查缓存是否有效(24小时内) const now = Date.now() const cacheAge = now - cache.timestamp const maxCacheAge = 24 * 60 * 60 * 1000 // 24小时 if (cacheAge < maxCacheAge) { this.diskUsageCache.data = cache.data this.diskUsageCache.timestamp = cache.timestamp logger.info('已加载持久化缓存,缓存年龄:', Math.round(cacheAge / 1000 / 60), '分钟') } else { logger.info('持久化缓存已过期,将重新计算') } } catch (error) { logger.info('加载持久化缓存失败,将使用内存缓存:', error.message) } } // 保存持久化缓存 async savePersistentCache() { try { if (!this.cacheFilePath || !this.diskUsageCache.data) { return } const cacheData = { data: this.diskUsageCache.data, timestamp: this.diskUsageCache.timestamp, savedAt: new Date().toISOString() } await fs.writeFile(this.cacheFilePath, JSON.stringify(cacheData, null, 2), 'utf8') logger.info('持久化缓存已保存') } catch (error) { logger.error('保存持久化缓存失败:', error.message) } } // 清除磁盘使用情况缓存 async clearDiskUsageCache() { try { // 清除内存缓存 this.diskUsageCache.data = null this.diskUsageCache.timestamp = 0 // 删除持久化缓存文件 if (this.cacheFilePath) { try { await fs.unlink(this.cacheFilePath) logger.info('持久化缓存文件已删除') } catch (error) { logger.info('删除持久化缓存文件失败:', error.message) } } return { success: true, message: '缓存已清除' } } catch (error) { throw new Error(`清除缓存失败: ${error.message}`) } } // 获取缓存的磁盘使用情况 async getCachedDiskUsage(currentBaseDir, forceRefresh = false) { const now = Date.now() // 检查内存缓存是否有效(除非强制刷新) if (!forceRefresh && this.diskUsageCache.data && (now - this.diskUsageCache.timestamp) < this.diskUsageCache.cacheDuration) { logger.info('使用内存缓存的磁盘使用情况') return this.diskUsageCache.data } // 缓存过期或不存在,计算新的值 try { // 快速估算:只计算前几层目录 const estimatedSize = await this.quickDirectorySizeEstimate(currentBaseDir) // 基于估算值创建磁盘使用情况 const estimatedTotal = Math.max(estimatedSize * 200, 100 * 1024 * 1024 * 1024) // 至少100GB const estimatedUsed = estimatedSize const estimatedFree = estimatedTotal - estimatedUsed const result = { total: estimatedTotal, used: estimatedUsed, free: estimatedFree, usagePercent: Math.round((estimatedUsed / estimatedTotal) * 100), note: '基于目录估算(缓存5分钟)' } // 更新内存缓存 this.diskUsageCache.data = result this.diskUsageCache.timestamp = now // 保存到持久化缓存 await this.savePersistentCache() return result } catch (error) { logger.info('快速估算失败,返回默认值:', error.message) return { total: 0, used: 0, free: 0, usagePercent: 0, note: '无法获取磁盘信息' } } } // 快速目录大小估算(只计算前几层) async quickDirectorySizeEstimate(dirPath, maxDepth = 2, currentDepth = 0) { try { if (currentDepth >= maxDepth) { return 0 } let totalSize = 0 const entries = await fs.readdir(dirPath, { withFileTypes: true }) // 限制处理的文件数量,避免过长时间 const maxFiles = 1000 let processedFiles = 0 for (const entry of entries) { if (processedFiles >= maxFiles) { break } const fullPath = path.join(dirPath, entry.name) if (entry.isDirectory()) { totalSize += await this.quickDirectorySizeEstimate(fullPath, maxDepth, currentDepth + 1) } else { try { const stats = await fs.stat(fullPath) totalSize += stats.size processedFiles++ } catch (error) { // 忽略无法访问的文件 } } } // 如果达到文件数量限制,进行估算 if (processedFiles >= maxFiles) { const remainingEntries = entries.length - processedFiles const avgFileSize = totalSize / processedFiles totalSize += remainingEntries * avgFileSize } return totalSize } catch (error) { logger.error('快速目录大小估算失败:', error) return 0 } } // 计算目录大小(完整版本,用于需要精确值时) async calculateDirectorySize(dirPath) { try { let totalSize = 0 const entries = await fs.readdir(dirPath, { withFileTypes: true }) for (const entry of entries) { const fullPath = path.join(dirPath, entry.name) if (entry.isDirectory()) { totalSize += await this.calculateDirectorySize(fullPath) } else { try { const stats = await fs.stat(fullPath) totalSize += stats.size } catch (error) { // 忽略无法访问的文件 logger.info(`无法访问文件: ${fullPath}`) } } } return totalSize } catch (error) { logger.error('计算目录大小失败:', error) return 0 } } // 获取文件预览URL async getFilePreviewUrl(filePath) { // 这里可以返回一个代理URL,用于前端预览 const relativePath = path.relative(this.baseDir, filePath) return `/api/repository/preview?path=${encodeURIComponent(relativePath)}` } } module.exports = RepositoryService