mirror of
https://github.com/crystelf/crystelf-core.git
synced 2025-12-05 18:41:56 +00:00
178 lines
6.2 KiB
TypeScript
178 lines
6.2 KiB
TypeScript
import { Inject, Injectable, Logger } from '@nestjs/common';
|
|
import * as path from 'path';
|
|
import { promises as fs } from 'fs';
|
|
import { PathService } from '../path/path.service';
|
|
import { OpenListService } from '../openlist/openlist.service';
|
|
import { AppConfigService } from '../../config/config.service';
|
|
@Injectable()
|
|
export class FilesService {
|
|
private readonly logger = new Logger(FilesService.name);
|
|
|
|
constructor(
|
|
@Inject(PathService)
|
|
private readonly paths: PathService,
|
|
@Inject(AppConfigService)
|
|
private readonly configService: AppConfigService,
|
|
@Inject(OpenListService)
|
|
private readonly openListService: OpenListService,
|
|
) {}
|
|
|
|
/**
|
|
* 创建目录
|
|
* @param targetPath 目标路径
|
|
* @param includeFile 是否包含文件路径
|
|
*/
|
|
public async createDir(targetPath = '', includeFile = false): Promise<void> {
|
|
const root = this.paths.get('root');
|
|
try {
|
|
const dirToCreate = path.isAbsolute(targetPath)
|
|
? includeFile
|
|
? path.dirname(targetPath)
|
|
: targetPath
|
|
: path.join(root, includeFile ? path.dirname(targetPath) : targetPath);
|
|
|
|
await fs.mkdir(dirToCreate, { recursive: true });
|
|
} catch (err) {
|
|
this.logger.error(`创建目录失败: ${err}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 比较本地文件和远程文件 下载缺失的文件
|
|
* @param localPath 本地路径
|
|
* @param localFiles 本地文件列表
|
|
* @param remoteFiles 远程文件列表
|
|
* @param remoteApiPath 远程基准路径
|
|
* @param replacPath 替换的目录,例: `\\crystelf\\meme`
|
|
* @private
|
|
*/
|
|
public async compareAndDownloadFiles(
|
|
localPath: string,
|
|
localFiles: string[],
|
|
remoteFiles: any[],
|
|
remoteApiPath: string,
|
|
replacPath: string,
|
|
) {
|
|
const remoteBasePath = this.configService.get(`OPENLIST_API_BASE_PATH`) as
|
|
| string
|
|
| undefined;
|
|
|
|
const normalizedLocalFiles = localFiles.map((f) =>
|
|
path.normalize(f).replace(/\\/g, '/'),
|
|
);
|
|
|
|
const remoteApiNorm = (remoteApiPath || '')
|
|
.replace(/\\/g, '/')
|
|
.replace(/\/+$/, '');
|
|
const remoteBaseNorm = (remoteBasePath || '')
|
|
.replace(/\\/g, '/')
|
|
.replace(/\/+$/, '');
|
|
let replacPathNorm = (replacPath || '').replace(/\\/g, '/');
|
|
if (replacPathNorm && !replacPathNorm.startsWith('/'))
|
|
replacPathNorm = '/' + replacPathNorm;
|
|
replacPathNorm = replacPathNorm.replace(/\/+$/, '');
|
|
|
|
for (const remoteFile of remoteFiles) {
|
|
const rawRemotePath = String(remoteFile.path || '').replace(/\\/g, '/');
|
|
|
|
let remoteRelativePath = '';
|
|
|
|
if (remoteBaseNorm && rawRemotePath.startsWith(remoteBaseNorm)) {
|
|
remoteRelativePath = rawRemotePath.slice(remoteBaseNorm.length);
|
|
} else if (remoteApiNorm && rawRemotePath.includes(remoteApiNorm)) {
|
|
remoteRelativePath = rawRemotePath.slice(
|
|
rawRemotePath.indexOf(remoteApiNorm),
|
|
);
|
|
} else {
|
|
const rel = path.posix.relative(remoteApiNorm || '/', rawRemotePath);
|
|
remoteRelativePath = rel ? '/' + rel.replace(/\/+/g, '/') : '/';
|
|
}
|
|
|
|
remoteRelativePath = remoteRelativePath.replace(/\/+/g, '/');
|
|
if (!remoteRelativePath.startsWith('/'))
|
|
remoteRelativePath = '/' + remoteRelativePath;
|
|
|
|
let localRelative = remoteRelativePath;
|
|
if (replacPathNorm && localRelative.startsWith(replacPathNorm)) {
|
|
localRelative = localRelative.slice(replacPathNorm.length);
|
|
} else if (replacPathNorm && localRelative.includes(replacPathNorm)) {
|
|
localRelative = localRelative.replace(replacPathNorm, '');
|
|
}
|
|
localRelative = localRelative.replace(/\/+/g, '/').replace(/^\/+/, '');
|
|
const localFilePathRaw = path.join(localPath, localRelative);
|
|
const localFilePath = path.normalize(localFilePathRaw);
|
|
const localFilePathForCompare = localFilePath.replace(/\\/g, '/');
|
|
|
|
//this.logger.debug(`replacPath: ${replacPath}`);
|
|
//this.logger.debug(`remoteBaseNorm: ${remoteBaseNorm}`);
|
|
//this.logger.debug(`rawRemotePath: ${rawRemotePath}`);
|
|
//this.logger.debug(`remoteRelativePath: ${remoteRelativePath}`);
|
|
//this.logger.debug(`localRelative: ${localRelative}`);
|
|
//this.logger.debug(`localFilePath: ${localFilePathForCompare}`);
|
|
|
|
if (remoteFile.is_dir) {
|
|
try {
|
|
const subRemote =
|
|
await this.openListService.listFiles(remoteRelativePath);
|
|
if (subRemote.code === 200 && subRemote.data?.content) {
|
|
await this.compareAndDownloadFiles(
|
|
localPath,
|
|
normalizedLocalFiles,
|
|
subRemote.data.content,
|
|
remoteApiPath,
|
|
replacPath,
|
|
);
|
|
}
|
|
} catch (error) {
|
|
this.logger.error(`递归处理文件夹失败: ${localFilePath}`, error);
|
|
}
|
|
} else {
|
|
if (!normalizedLocalFiles.includes(localFilePathForCompare)) {
|
|
this.logger.log(`文件缺失: ${localFilePath}, 开始下载..`);
|
|
try {
|
|
await fs.mkdir(path.dirname(localFilePath), { recursive: true });
|
|
await this.openListService.downloadFile(
|
|
remoteRelativePath,
|
|
localFilePath,
|
|
);
|
|
this.logger.log(`文件下载成功: ${localFilePath}`);
|
|
normalizedLocalFiles.push(localFilePathForCompare);
|
|
} catch (error) {
|
|
this.logger.error(`下载文件失败: ${localFilePath}`, error);
|
|
}
|
|
} else {
|
|
this.logger.log('本地文件已是最新..');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 获取本地目录的文件列表
|
|
* @param dir 本地路径
|
|
* @private
|
|
*/
|
|
public async getLocalFileList(dir: string): Promise<string[]> {
|
|
const files: string[] = [];
|
|
const dirs: string[] = [];
|
|
try {
|
|
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
for (const entry of entries) {
|
|
const fullPath = path.join(dir, entry.name);
|
|
if (entry.isDirectory()) {
|
|
dirs.push(fullPath);
|
|
} else {
|
|
files.push(fullPath);
|
|
}
|
|
}
|
|
for (const subDir of dirs) {
|
|
const subFiles = await this.getLocalFileList(subDir);
|
|
files.push(...subFiles);
|
|
}
|
|
} catch (error) {
|
|
this.logger.error(`读取本地目录失败: ${dir}`, error);
|
|
}
|
|
return files;
|
|
}
|
|
}
|