Compare commits

..

No commits in common. "1b380d8b8e3c98fb2c9001bf139379fd7549d5aa" and "8d62d0a9799eb970d916e5d2bcaa976dbd2301f1" have entirely different histories.

13 changed files with 76 additions and 140 deletions

0
LICENSE Executable file → Normal file
View File

0
README.md Executable file → Normal file
View File

0
nest-cli.json Executable file → Normal file
View File

1
package.json Executable file → Normal file
View File

@ -25,7 +25,6 @@
"@nestjs/swagger": "^11.2.0", "@nestjs/swagger": "^11.2.0",
"@nestjs/websockets": "^11.1.6", "@nestjs/websockets": "^11.1.6",
"axios": "1.11.0", "axios": "1.11.0",
"image-type": "^6.0.0",
"ioredis": "^5.6.1", "ioredis": "^5.6.1",
"moment": "^2.30.1", "moment": "^2.30.1",
"reflect-metadata": "^0.2.2", "reflect-metadata": "^0.2.2",

26
pnpm-lock.yaml generated Executable file → Normal file
View File

@ -38,9 +38,6 @@ importers:
axios: axios:
specifier: 1.11.0 specifier: 1.11.0
version: 1.11.0 version: 1.11.0
image-type:
specifier: ^6.0.0
version: 6.0.0
ioredis: ioredis:
specifier: ^5.6.1 specifier: ^5.6.1
version: 5.6.1 version: 5.6.1
@ -2065,10 +2062,6 @@ packages:
resolution: {integrity: sha512-VZR5I7k5wkD0HgFnMsq5hOsSc710MJMu5Nc5QYsbe38NN5iPV/XTObYLc/cpttRTf6lX538+5uO1ZQRhYibiZQ==} resolution: {integrity: sha512-VZR5I7k5wkD0HgFnMsq5hOsSc710MJMu5Nc5QYsbe38NN5iPV/XTObYLc/cpttRTf6lX538+5uO1ZQRhYibiZQ==}
engines: {node: '>=18'} engines: {node: '>=18'}
file-type@20.5.0:
resolution: {integrity: sha512-BfHZtG/l9iMm4Ecianu7P8HRD2tBHLtjXinm4X62XBOYzi7CYA7jyqfJzOvXHqzVrVPYqBo2/GvbARMaaJkKVg==}
engines: {node: '>=18'}
file-type@21.0.0: file-type@21.0.0:
resolution: {integrity: sha512-ek5xNX2YBYlXhiUXui3D/BXa3LdqPmoLJ7rqEx2bKJ7EAUEfmXgW0Das7Dc6Nr9MvqaOnIqiPV0mZk/r/UpNAg==} resolution: {integrity: sha512-ek5xNX2YBYlXhiUXui3D/BXa3LdqPmoLJ7rqEx2bKJ7EAUEfmXgW0Das7Dc6Nr9MvqaOnIqiPV0mZk/r/UpNAg==}
engines: {node: '>=20'} engines: {node: '>=20'}
@ -2308,10 +2301,6 @@ packages:
resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==}
engines: {node: '>= 4'} engines: {node: '>= 4'}
image-type@6.0.0:
resolution: {integrity: sha512-efpcYd/E9A7a+oanft11ceIbO9Aw0iszfJ7Qfh4QLWl2Ulsth9nnllV/L1TmzKwlQ2O5FuT08vy5zxLnGxZe8w==}
engines: {node: '>=20'}
import-fresh@3.3.1: import-fresh@3.3.1:
resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
engines: {node: '>=6'} engines: {node: '>=6'}
@ -5897,15 +5886,6 @@ snapshots:
token-types: 6.0.3 token-types: 6.0.3
uint8array-extras: 1.4.0 uint8array-extras: 1.4.0
file-type@20.5.0:
dependencies:
'@tokenizer/inflate': 0.2.7
strtok3: 10.3.2
token-types: 6.0.3
uint8array-extras: 1.4.0
transitivePeerDependencies:
- supports-color
file-type@21.0.0: file-type@21.0.0:
dependencies: dependencies:
'@tokenizer/inflate': 0.2.7 '@tokenizer/inflate': 0.2.7
@ -6157,12 +6137,6 @@ snapshots:
ignore@7.0.5: {} ignore@7.0.5: {}
image-type@6.0.0:
dependencies:
file-type: 20.5.0
transitivePeerDependencies:
- supports-color
import-fresh@3.3.1: import-fresh@3.3.1:
dependencies: dependencies:
parent-module: 1.0.1 parent-module: 1.0.1

View File

@ -53,72 +53,42 @@ export class FilesService {
remoteApiPath: string, remoteApiPath: string,
replacPath: string, replacPath: string,
) { ) {
const remoteBasePath = this.configService.get(`OPENLIST_API_BASE_PATH`) as const remoteBasePath = this.configService.get(`OPENLIST_API_BASE_PATH`);
| string
| undefined;
const normalizedLocalFiles = localFiles.map((f) => const normalizedLocalFiles = localFiles.map((f) =>
path.normalize(f).replace(/\\/g, '/'), 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) { for (const remoteFile of remoteFiles) {
const rawRemotePath = String(remoteFile.path || '').replace(/\\/g, '/'); let relativePath = path.relative(remoteApiPath, remoteFile.path);
//this.logger.debug(`relativePath: ${relativePath}`);
let remoteRelativePath = ''; //this.logger.debug(remoteBasePath);
if (remoteBasePath) {
if (remoteBaseNorm && rawRemotePath.startsWith(remoteBaseNorm)) { let remoteRelativePath = relativePath.replace(remoteBasePath, ''); //服务器下载用目录
remoteRelativePath = rawRemotePath.slice(remoteBaseNorm.length); //this.logger.debug(`remoteRelativePath: ${remoteRelativePath}`); //√\
} else if (remoteApiNorm && rawRemotePath.includes(remoteApiNorm)) { remoteRelativePath = path
remoteRelativePath = rawRemotePath.slice( .normalize(remoteRelativePath)
rawRemotePath.indexOf(remoteApiNorm), .replace(/\\/g, '/');
); replacPath = path.normalize(replacPath).replace(/\\/g, '/');
} else { relativePath = remoteRelativePath.replace(replacPath, ''); //本地储存用
const rel = path.posix.relative(remoteApiNorm || '/', rawRemotePath); this.logger.debug(`replacPath: ${relativePath}`);
remoteRelativePath = rel ? '/' + rel.replace(/\/+/g, '/') : '/'; relativePath = path.normalize(relativePath).replace(/\\/g, '/');
} this.logger.debug(`relativePathEd: ${relativePath}`);
const localFilePath = path
remoteRelativePath = remoteRelativePath.replace(/\/+/g, '/'); .normalize(path.join(localPath, relativePath))
if (!remoteRelativePath.startsWith('/')) .replace(/\\/g, '/');
remoteRelativePath = '/' + remoteRelativePath; //this.logger.debug(`localFilePath: ${localFilePath}`);
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) { if (remoteFile.is_dir) {
try { try {
const subRemote = //const localDirPath = path.dirname(localFilePath);
//await fs.mkdir(localDirPath, { recursive: true });
//this.logger.log(`文件夹已创建: ${localDirPath}`);
//相关逻辑已在oplist工具中处理
const subRemoteFiles =
await this.openListService.listFiles(remoteRelativePath); await this.openListService.listFiles(remoteRelativePath);
if (subRemote.code === 200 && subRemote.data?.content) { if (subRemoteFiles.code === 200 && subRemoteFiles.data.content) {
await this.compareAndDownloadFiles( await this.compareAndDownloadFiles(
localPath, localPath,
normalizedLocalFiles, normalizedLocalFiles,
subRemote.data.content, subRemoteFiles.data.content,
remoteApiPath, remoteApiPath,
replacPath, replacPath,
); );
@ -127,16 +97,25 @@ export class FilesService {
this.logger.error(`递归处理文件夹失败: ${localFilePath}`, error); this.logger.error(`递归处理文件夹失败: ${localFilePath}`, error);
} }
} else { } else {
if (!normalizedLocalFiles.includes(localFilePathForCompare)) { const normalizedLocalFiles = localFiles.map((f) =>
path.normalize(f).replace(/\\/g, '/'),
);
//this.logger.debug(
//`normalizedLocalFiles: ${JSON.stringify(normalizedLocalFiles)}`,
//);
if (!normalizedLocalFiles.includes(localFilePath)) {
this.logger.log(`文件缺失: ${localFilePath}, 开始下载..`); this.logger.log(`文件缺失: ${localFilePath}, 开始下载..`);
try { try {
await fs.mkdir(path.dirname(localFilePath), { recursive: true });
await this.openListService.downloadFile( await this.openListService.downloadFile(
remoteRelativePath, remoteRelativePath,
localFilePath, localFilePath,
); );
this.logger.log(`文件下载成功: ${localFilePath}`); this.logger.log(`文件下载成功: ${localFilePath}`);
normalizedLocalFiles.push(localFilePathForCompare); normalizedLocalFiles.push(localFilePath);
this.logger.debug(
`localFilePath: ${JSON.stringify(normalizedLocalFiles)}`,
);
} catch (error) { } catch (error) {
this.logger.error(`下载文件失败: ${localFilePath}`, error); this.logger.error(`下载文件失败: ${localFilePath}`, error);
} }
@ -144,6 +123,9 @@ export class FilesService {
this.logger.log('本地文件已是最新..'); this.logger.log('本地文件已是最新..');
} }
} }
} else {
this.logger.error(`未配置远程根路径..`);
}
} }
} }

View File

@ -56,7 +56,7 @@ export class OpenListUtils {
let data = JSON.stringify({ let data = JSON.stringify({
path: path, path: path,
}); });
//this.logger.debug(path); this.logger.debug(path);
let config = { let config = {
method: 'post', method: 'post',
url: `${url}`, url: `${url}`,
@ -68,7 +68,7 @@ export class OpenListUtils {
}; };
let response = await axios(config); let response = await axios(config);
//this.logger.debug(response); //this.logger.debug(response);
//this.logger.log(`列出目录${path}成功..`); this.logger.log(`列出目录${path}成功..`);
return response.data; return response.data;
} catch (error) { } catch (error) {
this.logger.error(`列出目录${path}失败..`, error); this.logger.error(`列出目录${path}失败..`, error);
@ -99,7 +99,7 @@ export class OpenListUtils {
data: data, data: data,
}; };
const response = await axios(config); const response = await axios(config);
//this.logger.log(`获取文件信息成功: ${filePath}`); this.logger.log(`获取文件信息成功: ${filePath}`);
return response.data; return response.data;
} catch (error) { } catch (error) {
this.logger.error(`获取文件信息失败: ${filePath}`, error); this.logger.error(`获取文件信息失败: ${filePath}`, error);

View File

@ -24,9 +24,7 @@ async function bootstrap() {
} }
} }
const app = await NestFactory.create(AppModule, { cors: true }); const app = await NestFactory.create(AppModule);
const expressApp = app.getHttpAdapter().getInstance();
expressApp.set('trust proxy', true);
app.setGlobalPrefix('api', { app.setGlobalPrefix('api', {
exclude: [ exclude: [
'cdn', 'cdn',
@ -35,6 +33,7 @@ async function bootstrap() {
{ path: 'public/(.*)', method: RequestMethod.ALL }, { path: 'public/(.*)', method: RequestMethod.ALL },
], ],
}); });
app.useGlobalInterceptors(new ResponseInterceptor()); app.useGlobalInterceptors(new ResponseInterceptor());
app.useGlobalFilters(new AllExceptionsFilter()); app.useGlobalFilters(new AllExceptionsFilter());
const systemService = app.get(SystemService); const systemService = app.get(SystemService);

View File

@ -10,7 +10,7 @@ import { FilesService } from '../../core/files/files.service';
export class CdnService { export class CdnService {
private readonly logger = new Logger(CdnService.name); private readonly logger = new Logger(CdnService.name);
private filePath: string; private filePath: string;
private readonly updateMs = 10 * 60 * 1000; // 10min private readonly updateMs = 15 * 60 * 1000; // 15min
@Inject(PathService) @Inject(PathService)
private readonly paths: PathService; private readonly paths: PathService;
constructor( constructor(
@ -24,7 +24,7 @@ export class CdnService {
private readonly filesService: FilesService, private readonly filesService: FilesService,
) { ) {
this.startAutoUpdate(); this.startAutoUpdate();
this.logger.log(`晶灵云数据中心初始化.. 数据存储在: ${this.filePath}`); this.logger.log(`晶灵云数据中心初始化.. 数据存储在: ${this.filePath}`);
} }
private startAutoUpdate() { private startAutoUpdate() {
@ -40,7 +40,7 @@ export class CdnService {
let remoteFileList = remoteFiles.data.content; let remoteFileList = remoteFiles.data.content;
const localFiles = const localFiles =
await this.filesService.getLocalFileList(cdnPath); await this.filesService.getLocalFileList(cdnPath);
//this.logger.debug(`localFlies: ${JSON.stringify(localFiles)}`); this.logger.debug(`localFlies: ${JSON.stringify(localFiles)}`);
await this.filesService.compareAndDownloadFiles( await this.filesService.compareAndDownloadFiles(
cdnPath, cdnPath,
localFiles, localFiles,
@ -55,7 +55,7 @@ export class CdnService {
this.logger.error(`晶灵cdn检查更新失败: ${error}`); this.logger.error(`晶灵cdn检查更新失败: ${error}`);
} }
} else { } else {
this.logger.warn('未配置远程cdn地址..'); this.logger.warn('未配置远程表情包地址..');
} }
}, this.updateMs); }, this.updateMs);
} }

View File

@ -18,7 +18,6 @@ import * as fs from 'fs';
import { Throttle } from 'stream-throttle'; import { Throttle } from 'stream-throttle';
import { ToolsService } from '../../core/tools/tools.service'; import { ToolsService } from '../../core/tools/tools.service';
import { RedisService } from '../../core/redis/redis.service'; import { RedisService } from '../../core/redis/redis.service';
import imageType from 'image-type';
class MemeRequestDto { class MemeRequestDto {
character?: string; character?: string;
@ -114,20 +113,6 @@ export class MemeController {
); );
}); });
const fd = await fs.promises.open(memePath, 'r');
const { buffer } = await fd.read(Buffer.alloc(4100), 0, 4100, 0);
await fd.close();
const type = await imageType(buffer);
const isAnimatedImage =
type?.mime === 'image/gif' ||
type?.mime === 'image/webp' ||
type?.mime === 'image/apng';
//this.logger.debug(type?.mime);
const singleRate = 200 * 1024; // 100 KB/s * 3
const maxThreads = 2;
const maxRate = singleRate * maxThreads;
if (hasValidToken) { if (hasValidToken) {
this.logger.log(`[${method}] 有token的入不限速 => ${memePath}`); this.logger.log(`[${method}] 有token的入不限速 => ${memePath}`);
stream.pipe(res); stream.pipe(res);
@ -139,18 +124,15 @@ export class MemeController {
bytes, bytes,
1, 1,
); );
if (total > maxRate && !isAnimatedImage) { if (total > 100 * 1024) {
this.logger.warn(`[${method}] ${ip} 超过速率限制,断开连接..`); this.logger.warn(`[${method}]${ip} 超过速率限制,断开连接..`);
stream.destroy(); stream.destroy();
res.end(); res.end();
} }
}); });
const throttle = new Throttle({ rate: singleRate }); const throttle = new Throttle({ rate: 100 * 1024 });
this.logger.log( this.logger.log(`[${method}] 白嫖入限速! (${ip}) => ${memePath}`);
`[${method}] 白嫖入限速! (${ip}) => ${memePath}
`,
);
stream.pipe(throttle).pipe(res); stream.pipe(throttle).pipe(res);
} }
} catch (e) { } catch (e) {

0
start.sh Executable file → Normal file
View File

0
tsconfig.build.json Executable file → Normal file
View File

0
tsconfig.json Executable file → Normal file
View File