文件迁移配置优化,在浏览界面增加识别已经下载过的作品
This commit is contained in:
@@ -4,13 +4,14 @@ const path = require('path');
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
const ArtworkService = require('./artwork');
|
||||
const ArtistService = require('./artist');
|
||||
const ConfigManager = require('../config/config-manager');
|
||||
|
||||
class DownloadService {
|
||||
constructor(auth) {
|
||||
this.auth = auth;
|
||||
this.artworkService = new ArtworkService(auth);
|
||||
this.artistService = new ArtistService(auth);
|
||||
this.downloadPath = path.join(__dirname, '../../downloads');
|
||||
this.configManager = new ConfigManager();
|
||||
this.dataPath = path.join(__dirname, '../../data');
|
||||
this.tasksFile = path.join(this.dataPath, 'download_tasks.json');
|
||||
this.historyFile = path.join(this.dataPath, 'download_history.json');
|
||||
@@ -20,13 +21,33 @@ class DownloadService {
|
||||
this.initialized = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前下载路径
|
||||
*/
|
||||
async getDownloadPath() {
|
||||
try {
|
||||
const config = await this.configManager.readConfig();
|
||||
const downloadDir = config.downloadDir || './downloads';
|
||||
|
||||
// 如果是相对路径,转换为绝对路径
|
||||
return path.isAbsolute(downloadDir)
|
||||
? downloadDir
|
||||
: path.resolve(process.cwd(), downloadDir);
|
||||
} catch (error) {
|
||||
console.error('获取下载路径失败:', error);
|
||||
// 返回默认路径
|
||||
return path.resolve(process.cwd(), 'downloads');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化服务
|
||||
*/
|
||||
async init() {
|
||||
try {
|
||||
// 确保目录存在
|
||||
await fs.ensureDir(this.downloadPath);
|
||||
const downloadPath = await this.getDownloadPath();
|
||||
await fs.ensureDir(downloadPath);
|
||||
await fs.ensureDir(this.dataPath);
|
||||
|
||||
// 加载历史记录
|
||||
@@ -36,7 +57,7 @@ class DownloadService {
|
||||
await this.loadTasks();
|
||||
|
||||
this.initialized = true;
|
||||
console.log('下载服务初始化完成');
|
||||
console.log('下载服务初始化完成,下载路径:', downloadPath);
|
||||
} catch (error) {
|
||||
console.error('下载服务初始化失败:', error);
|
||||
this.initialized = false;
|
||||
@@ -152,10 +173,11 @@ class DownloadService {
|
||||
async getDownloadedFiles() {
|
||||
try {
|
||||
const files = [];
|
||||
const artists = await fs.readdir(this.downloadPath);
|
||||
const downloadPath = await this.getDownloadPath();
|
||||
const artists = await fs.readdir(downloadPath);
|
||||
|
||||
for (const artist of artists) {
|
||||
const artistPath = path.join(this.downloadPath, artist);
|
||||
const artistPath = path.join(downloadPath, artist);
|
||||
const artistStat = await fs.stat(artistPath);
|
||||
|
||||
if (artistStat.isDirectory()) {
|
||||
@@ -198,20 +220,39 @@ class DownloadService {
|
||||
*/
|
||||
async isArtworkDownloaded(artworkId) {
|
||||
try {
|
||||
// 从历史记录中查找
|
||||
const historyItem = this.history.find(item =>
|
||||
item.artwork_id === artworkId && item.status === 'completed'
|
||||
);
|
||||
const downloadPath = await this.getDownloadPath();
|
||||
|
||||
if (historyItem) {
|
||||
// 检查文件是否还存在
|
||||
const exists = await fs.pathExists(historyItem.download_path);
|
||||
if (exists) {
|
||||
const files = await fs.readdir(historyItem.download_path);
|
||||
const imageFiles = files.filter(file =>
|
||||
/\.(jpg|jpeg|png|gif|webp)$/i.test(file)
|
||||
);
|
||||
return imageFiles.length > 0;
|
||||
// 扫描下载目录查找作品
|
||||
const artists = await fs.readdir(downloadPath);
|
||||
|
||||
for (const artist of artists) {
|
||||
const artistPath = path.join(downloadPath, artist);
|
||||
const artistStat = await fs.stat(artistPath);
|
||||
|
||||
if (artistStat.isDirectory()) {
|
||||
const artworks = await fs.readdir(artistPath);
|
||||
|
||||
for (const artwork of artworks) {
|
||||
// 检查是否是作品目录(包含数字ID)
|
||||
const artworkMatch = artwork.match(/^(\d+)_(.+)$/);
|
||||
if (artworkMatch) {
|
||||
const foundArtworkId = artworkMatch[1];
|
||||
|
||||
if (parseInt(foundArtworkId) === parseInt(artworkId)) {
|
||||
// 找到作品目录,检查是否包含图片文件
|
||||
const artworkPath = path.join(artistPath, artwork);
|
||||
const artworkStat = await fs.stat(artworkPath);
|
||||
|
||||
if (artworkStat.isDirectory()) {
|
||||
const files = await fs.readdir(artworkPath);
|
||||
const imageFiles = files.filter(file =>
|
||||
/\.(jpg|jpeg|png|gif|webp)$/i.test(file)
|
||||
);
|
||||
return imageFiles.length > 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -228,18 +269,37 @@ class DownloadService {
|
||||
async getDownloadedArtworkIds() {
|
||||
try {
|
||||
const downloadedIds = new Set();
|
||||
const downloadPath = await this.getDownloadPath();
|
||||
|
||||
// 从历史记录中获取
|
||||
for (const item of this.history) {
|
||||
if (item.artwork_id && item.status === 'completed') {
|
||||
const exists = await fs.pathExists(item.download_path);
|
||||
if (exists) {
|
||||
const files = await fs.readdir(item.download_path);
|
||||
const imageFiles = files.filter(file =>
|
||||
/\.(jpg|jpeg|png|gif|webp)$/i.test(file)
|
||||
);
|
||||
if (imageFiles.length > 0) {
|
||||
downloadedIds.add(item.artwork_id);
|
||||
// 扫描下载目录获取所有已下载的作品ID
|
||||
const artists = await fs.readdir(downloadPath);
|
||||
|
||||
for (const artist of artists) {
|
||||
const artistPath = path.join(downloadPath, artist);
|
||||
const artistStat = await fs.stat(artistPath);
|
||||
|
||||
if (artistStat.isDirectory()) {
|
||||
const artworks = await fs.readdir(artistPath);
|
||||
|
||||
for (const artwork of artworks) {
|
||||
// 检查是否是作品目录(包含数字ID)
|
||||
const artworkMatch = artwork.match(/^(\d+)_(.+)$/);
|
||||
if (artworkMatch) {
|
||||
const artworkId = artworkMatch[1];
|
||||
|
||||
// 检查作品目录是否包含图片文件
|
||||
const artworkPath = path.join(artistPath, artwork);
|
||||
const artworkStat = await fs.stat(artworkPath);
|
||||
|
||||
if (artworkStat.isDirectory()) {
|
||||
const files = await fs.readdir(artworkPath);
|
||||
const imageFiles = files.filter(file =>
|
||||
/\.(jpg|jpeg|png|gif|webp)$/i.test(file)
|
||||
);
|
||||
if (imageFiles.length > 0) {
|
||||
downloadedIds.add(parseInt(artworkId));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -279,7 +339,8 @@ class DownloadService {
|
||||
*/
|
||||
async deleteDownloadedFiles(artist, artwork) {
|
||||
try {
|
||||
const targetPath = path.join(this.downloadPath, artist, artwork);
|
||||
const downloadPath = await this.getDownloadPath();
|
||||
const targetPath = path.join(downloadPath, artist, artwork);
|
||||
if (await fs.pathExists(targetPath)) {
|
||||
await fs.remove(targetPath);
|
||||
|
||||
@@ -375,7 +436,8 @@ class DownloadService {
|
||||
const artworkTitle = (artwork.title || 'Untitled').replace(/[<>:"/\\|?*]/g, '_');
|
||||
|
||||
// 创建作品目录 - 使用仓库管理格式
|
||||
const artistDir = path.join(this.downloadPath, artistName);
|
||||
const downloadPath = await this.getDownloadPath();
|
||||
const artistDir = path.join(downloadPath, artistName);
|
||||
const artworkDirName = `${artworkId}_${artworkTitle}`;
|
||||
const artworkDir = path.join(artistDir, artworkDirName);
|
||||
await fs.ensureDir(artworkDir);
|
||||
|
||||
+231
-71
@@ -2,19 +2,41 @@ 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)
|
||||
|
||||
class RepositoryService {
|
||||
constructor() {
|
||||
this.baseDir = process.env.DOWNLOAD_DIR || path.join(process.cwd(), 'downloads')
|
||||
this.configFile = path.join(this.baseDir, '.repository-config.json')
|
||||
// 初始化配置管理器
|
||||
this.configManager = new ConfigManager()
|
||||
this.config = 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 fs.mkdir(this.baseDir, { recursive: true })
|
||||
// 初始化配置管理器
|
||||
await this.configManager.initialize()
|
||||
|
||||
// 加载配置
|
||||
await this.loadConfig()
|
||||
|
||||
// 确保下载目录存在
|
||||
const currentBaseDir = this.getCurrentBaseDir()
|
||||
await fs.mkdir(currentBaseDir, { recursive: true })
|
||||
|
||||
return { success: true, message: '仓库初始化成功' }
|
||||
} catch (error) {
|
||||
throw new Error(`仓库初始化失败: ${error.message}`)
|
||||
@@ -24,30 +46,21 @@ class RepositoryService {
|
||||
// 加载配置
|
||||
async loadConfig() {
|
||||
try {
|
||||
const configData = await fs.readFile(this.configFile, 'utf8')
|
||||
this.config = JSON.parse(configData)
|
||||
this.config = await this.configManager.readConfig()
|
||||
} 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()
|
||||
console.error('加载配置失败:', error)
|
||||
// 如果加载失败,使用默认配置
|
||||
this.config = await this.configManager.readConfig()
|
||||
}
|
||||
}
|
||||
|
||||
// 保存配置
|
||||
async saveConfig() {
|
||||
try {
|
||||
await fs.writeFile(this.configFile, JSON.stringify(this.config, null, 2))
|
||||
await this.configManager.saveConfig(this.config)
|
||||
} catch (error) {
|
||||
throw new Error(`保存配置失败: ${error.message}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取仓库配置
|
||||
@@ -63,6 +76,16 @@ class RepositoryService {
|
||||
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() {
|
||||
try {
|
||||
@@ -89,8 +112,11 @@ class RepositoryService {
|
||||
// 确保配置已加载
|
||||
await this.loadConfig()
|
||||
|
||||
// 使用当前配置的目录
|
||||
const currentBaseDir = this.getCurrentBaseDir()
|
||||
|
||||
// 扫描作者目录
|
||||
const artistEntries = await fs.readdir(this.baseDir, { withFileTypes: true })
|
||||
const artistEntries = await fs.readdir(currentBaseDir, { withFileTypes: true })
|
||||
|
||||
for (const artistEntry of artistEntries) {
|
||||
if (!artistEntry.isDirectory()) continue
|
||||
@@ -101,7 +127,7 @@ class RepositoryService {
|
||||
}
|
||||
|
||||
const artistName = artistEntry.name
|
||||
const artistPath = path.join(this.baseDir, artistName)
|
||||
const artistPath = path.join(currentBaseDir, artistName)
|
||||
|
||||
// 扫描作者下的作品目录
|
||||
const artworkEntries = await fs.readdir(artistPath, { withFileTypes: true })
|
||||
@@ -164,9 +190,10 @@ class RepositoryService {
|
||||
|
||||
if (this.config.allowedExtensions.includes(ext)) {
|
||||
const stats = await fs.stat(filePath)
|
||||
const currentBaseDir = this.getCurrentBaseDir()
|
||||
files.push({
|
||||
name: entry.name,
|
||||
path: path.relative(this.baseDir, filePath),
|
||||
path: path.relative(currentBaseDir, filePath),
|
||||
size: stats.size,
|
||||
extension: ext,
|
||||
modifiedAt: stats.mtime
|
||||
@@ -194,7 +221,8 @@ class RepositoryService {
|
||||
// 获取磁盘使用情况
|
||||
async getDiskUsage() {
|
||||
try {
|
||||
const stats = await fs.statfs(this.baseDir)
|
||||
const currentBaseDir = this.getCurrentBaseDir()
|
||||
const stats = await fs.statfs(currentBaseDir)
|
||||
const total = stats.blocks * stats.bsize
|
||||
const free = stats.bavail * stats.bsize
|
||||
const used = total - free
|
||||
@@ -239,6 +267,65 @@ class RepositoryService {
|
||||
}
|
||||
}
|
||||
|
||||
// 检查作品是否已下载
|
||||
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 files = await this.scanArtworkFiles(artworkPath)
|
||||
return files.length > 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
} catch (error) {
|
||||
console.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 = 20) {
|
||||
try {
|
||||
@@ -295,65 +382,138 @@ class RepositoryService {
|
||||
// 自动迁移旧项目
|
||||
async migrateOldProjects(sourceDir) {
|
||||
try {
|
||||
const migrationLog = []
|
||||
// 确保配置已加载
|
||||
await this.loadConfig()
|
||||
|
||||
const currentBaseDir = this.getCurrentBaseDir()
|
||||
const result = {
|
||||
success: true,
|
||||
message: '迁移完成',
|
||||
log: [],
|
||||
totalMigrated: 0
|
||||
}
|
||||
|
||||
// 确保目标目录存在
|
||||
await fs.mkdir(currentBaseDir, { recursive: true })
|
||||
|
||||
// 扫描源目录
|
||||
const scanSource = async (dirPath, relativePath = '') => {
|
||||
const entries = await fs.readdir(dirPath, { withFileTypes: true })
|
||||
const sourceEntries = await fs.readdir(sourceDir, { withFileTypes: true })
|
||||
|
||||
for (const entry of sourceEntries) {
|
||||
if (!entry.isDirectory()) continue
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dirPath, entry.name)
|
||||
const newRelativePath = path.join(relativePath, entry.name)
|
||||
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)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
await scanSource(sourceDir)
|
||||
|
||||
return {
|
||||
return result
|
||||
} catch (error) {
|
||||
throw new Error(`迁移失败: ${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
// 从旧目录迁移到新目录
|
||||
async migrateFromOldToNew(oldDir, newDir) {
|
||||
try {
|
||||
const result = {
|
||||
success: true,
|
||||
message: '迁移完成',
|
||||
log: migrationLog,
|
||||
totalMigrated: migrationLog.filter(item => item.status === 'success').length
|
||||
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}`)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user