Compare commits

..

No commits in common. "baf92f470ade749ed2a390655bbf429cb41f9999" and "d0eb167245ff4f59a06434a37f1e9936b8d293c1" have entirely different histories.

6 changed files with 76 additions and 154 deletions

View File

@ -46,15 +46,15 @@
"@swc/core": "^1.10.7", "@swc/core": "^1.10.7",
"@types/express": "^5.0.0", "@types/express": "^5.0.0",
"@types/jest": "^29.5.14", "@types/jest": "^29.5.14",
"@types/multer": "^2.0.0",
"@types/node": "^22.16.4", "@types/node": "^22.16.4",
"@types/stream-throttle": "^0.1.4", "@types/stream-throttle": "^0.1.4",
"@types/supertest": "^6.0.2", "@types/supertest": "^6.0.2",
"@types/ws": "^8.18.1", "@types/ws": "^8.18.1",
"eslint": "^9.18.0", "eslint": "^9.18.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-prettier": "^5.2.2",
"globals": "^16.0.0", "globals": "^16.0.0",
"jest": "^29.7.0", "jest": "^29.7.0",
"multer": "^2.0.2",
"prettier": "^3.4.2", "prettier": "^3.4.2",
"source-map-support": "^0.5.21", "source-map-support": "^0.5.21",
"supertest": "^7.0.0", "supertest": "^7.0.0",

80
pnpm-lock.yaml generated
View File

@ -96,9 +96,6 @@ importers:
'@types/jest': '@types/jest':
specifier: ^29.5.14 specifier: ^29.5.14
version: 29.5.14 version: 29.5.14
'@types/multer':
specifier: ^2.0.0
version: 2.0.0
'@types/node': '@types/node':
specifier: ^22.16.4 specifier: ^22.16.4
version: 22.16.5 version: 22.16.5
@ -114,15 +111,18 @@ importers:
eslint: eslint:
specifier: ^9.18.0 specifier: ^9.18.0
version: 9.31.0 version: 9.31.0
eslint-config-prettier:
specifier: ^10.0.1
version: 10.1.8(eslint@9.31.0)
eslint-plugin-prettier:
specifier: ^5.2.2
version: 5.5.3(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.31.0))(eslint@9.31.0)(prettier@3.6.2)
globals: globals:
specifier: ^16.0.0 specifier: ^16.0.0
version: 16.3.0 version: 16.3.0
jest: jest:
specifier: ^29.7.0 specifier: ^29.7.0
version: 29.7.0(@types/node@22.16.5)(ts-node@10.9.2(@swc/core@1.13.1)(@types/node@22.16.5)(typescript@5.8.3)) version: 29.7.0(@types/node@22.16.5)(ts-node@10.9.2(@swc/core@1.13.1)(@types/node@22.16.5)(typescript@5.8.3))
multer:
specifier: ^2.0.2
version: 2.0.2
prettier: prettier:
specifier: ^3.4.2 specifier: ^3.4.2
version: 3.6.2 version: 3.6.2
@ -931,6 +931,10 @@ packages:
'@paralleldrive/cuid2@2.2.2': '@paralleldrive/cuid2@2.2.2':
resolution: {integrity: sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==} resolution: {integrity: sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==}
'@pkgr/core@0.2.9':
resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==}
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
'@scarf/scarf@1.4.0': '@scarf/scarf@1.4.0':
resolution: {integrity: sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==} resolution: {integrity: sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==}
@ -1131,9 +1135,6 @@ packages:
'@types/mime@1.3.5': '@types/mime@1.3.5':
resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==}
'@types/multer@2.0.0':
resolution: {integrity: sha512-C3Z9v9Evij2yST3RSBktxP9STm6OdMc5uR1xF1SGr98uv8dUlAL2hqwrZ3GVB3uyMyiegnscEK6PGtYvNrjTjw==}
'@types/node@22.16.5': '@types/node@22.16.5':
resolution: {integrity: sha512-bJFoMATwIGaxxx8VJPeM8TonI8t579oRvgAuT8zFugJsJZgzqv0Fu8Mhp68iecjzG7cnN3mO2dJQ5uUM2EFrgQ==} resolution: {integrity: sha512-bJFoMATwIGaxxx8VJPeM8TonI8t579oRvgAuT8zFugJsJZgzqv0Fu8Mhp68iecjzG7cnN3mO2dJQ5uUM2EFrgQ==}
@ -1907,6 +1908,26 @@ packages:
resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
engines: {node: '>=10'} engines: {node: '>=10'}
eslint-config-prettier@10.1.8:
resolution: {integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==}
hasBin: true
peerDependencies:
eslint: '>=7.0.0'
eslint-plugin-prettier@5.5.3:
resolution: {integrity: sha512-NAdMYww51ehKfDyDhv59/eIItUVzU0Io9H2E8nHNGKEeeqlnci+1gCvrHib6EmZdf6GxF+LCV5K7UC65Ezvw7w==}
engines: {node: ^14.18.0 || >=16.0.0}
peerDependencies:
'@types/eslint': '>=8.0.0'
eslint: '>=8.0.0'
eslint-config-prettier: '>= 7.0.0 <10.0.0 || >=10.1.0'
prettier: '>=3.0.0'
peerDependenciesMeta:
'@types/eslint':
optional: true
eslint-config-prettier:
optional: true
eslint-scope@5.1.1: eslint-scope@5.1.1:
resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==}
engines: {node: '>=8.0.0'} engines: {node: '>=8.0.0'}
@ -2005,6 +2026,9 @@ packages:
fast-deep-equal@3.1.3: fast-deep-equal@3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
fast-diff@1.3.0:
resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==}
fast-fifo@1.3.2: fast-fifo@1.3.2:
resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==}
@ -2975,6 +2999,10 @@ packages:
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
engines: {node: '>= 0.8.0'} engines: {node: '>= 0.8.0'}
prettier-linter-helpers@1.0.0:
resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==}
engines: {node: '>=6.0.0'}
prettier@3.6.2: prettier@3.6.2:
resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==} resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==}
engines: {node: '>=14'} engines: {node: '>=14'}
@ -3349,6 +3377,10 @@ packages:
resolution: {integrity: sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==} resolution: {integrity: sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==}
engines: {node: '>=0.10'} engines: {node: '>=0.10'}
synckit@0.11.11:
resolution: {integrity: sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==}
engines: {node: ^14.18.0 || >=16.0.0}
tapable@2.2.2: tapable@2.2.2:
resolution: {integrity: sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==} resolution: {integrity: sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==}
engines: {node: '>=6'} engines: {node: '>=6'}
@ -4599,6 +4631,8 @@ snapshots:
dependencies: dependencies:
'@noble/hashes': 1.8.0 '@noble/hashes': 1.8.0
'@pkgr/core@0.2.9': {}
'@scarf/scarf@1.4.0': {} '@scarf/scarf@1.4.0': {}
'@sec-ant/readable-stream@0.4.1': {} '@sec-ant/readable-stream@0.4.1': {}
@ -4796,10 +4830,6 @@ snapshots:
'@types/mime@1.3.5': {} '@types/mime@1.3.5': {}
'@types/multer@2.0.0':
dependencies:
'@types/express': 5.0.3
'@types/node@22.16.5': '@types/node@22.16.5':
dependencies: dependencies:
undici-types: 6.21.0 undici-types: 6.21.0
@ -5649,6 +5679,20 @@ snapshots:
escape-string-regexp@4.0.0: {} escape-string-regexp@4.0.0: {}
eslint-config-prettier@10.1.8(eslint@9.31.0):
dependencies:
eslint: 9.31.0
eslint-plugin-prettier@5.5.3(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.31.0))(eslint@9.31.0)(prettier@3.6.2):
dependencies:
eslint: 9.31.0
prettier: 3.6.2
prettier-linter-helpers: 1.0.0
synckit: 0.11.11
optionalDependencies:
'@types/eslint': 9.6.1
eslint-config-prettier: 10.1.8(eslint@9.31.0)
eslint-scope@5.1.1: eslint-scope@5.1.1:
dependencies: dependencies:
esrecurse: 4.3.0 esrecurse: 4.3.0
@ -5812,6 +5856,8 @@ snapshots:
fast-deep-equal@3.1.3: {} fast-deep-equal@3.1.3: {}
fast-diff@1.3.0: {}
fast-fifo@1.3.2: {} fast-fifo@1.3.2: {}
fast-glob@3.3.3: fast-glob@3.3.3:
@ -6901,6 +6947,10 @@ snapshots:
prelude-ls@1.2.1: {} prelude-ls@1.2.1: {}
prettier-linter-helpers@1.0.0:
dependencies:
fast-diff: 1.3.0
prettier@3.6.2: {} prettier@3.6.2: {}
pretty-format@29.7.0: pretty-format@29.7.0:
@ -7314,6 +7364,10 @@ snapshots:
symbol-observable@4.0.0: {} symbol-observable@4.0.0: {}
synckit@0.11.11:
dependencies:
'@pkgr/core': 0.2.9
tapable@2.2.2: {} tapable@2.2.2: {}
tar-stream@3.1.7: tar-stream@3.1.7:

View File

@ -1,6 +1,6 @@
import { Inject, Injectable, Logger } from '@nestjs/common'; import { Inject, Injectable, Logger } from '@nestjs/common';
import { AppConfigService } from '../../config/config.service'; import { AppConfigService } from '../../config/config.service';
import { FileInfo, FileUpload, FsList } from './openlist.types'; import { FileInfo, FsList } from './openlist.types';
import { OpenListUtils } from './openlist.utils'; import { OpenListUtils } from './openlist.utils';
import * as moment from 'moment'; import * as moment from 'moment';
@ -148,18 +148,13 @@ export class OpenListService {
filePath: string, filePath: string,
file: any, file: any,
filePathOnserver: string, filePathOnserver: string,
): Promise<FileUpload> { ): Promise<void> {
try { try {
const token = await this.fetchToken( const token = await this.fetchToken(
<string>this.configService.get('OPENLIST_API_BASE_USERNAME'), <string>this.configService.get('OPENLIST_API_BASE_USERNAME'),
<string>this.configService.get('OPENLIST_API_BASE_PASSWORD'), <string>this.configService.get('OPENLIST_API_BASE_PASSWORD'),
); );
return await OpenListUtils.uploadFile( await OpenListUtils.uploadFile(token, filePath, filePathOnserver, file);
token,
filePath,
filePathOnserver,
file,
);
} catch (error) { } catch (error) {
this.logger.error('上传文件失败:', error); this.logger.error('上传文件失败:', error);
throw new Error('上传文件失败'); throw new Error('上传文件失败');

View File

@ -162,13 +162,10 @@ export class OpenListUtils {
file: fs.ReadStream, file: fs.ReadStream,
): Promise<FileUpload> { ): Promise<FileUpload> {
const url = `${this.apiBaseUrl}/api/fs/put`; const url = `${this.apiBaseUrl}/api/fs/put`;
const stats = fs.statSync(filePath);
const fileSize = stats.size;
const headers = { const headers = {
Authorization: `${token}`, Authorization: `${token}`,
'Content-Type': 'application/octet-stream', 'Content-Type': 'application/octet-stream',
'Content-Length': fileSize, 'Content-Length': file.bytesRead,
'File-Path': encodeURIComponent(filePathOnServer), 'File-Path': encodeURIComponent(filePathOnServer),
}; };
@ -177,17 +174,14 @@ export class OpenListUtils {
headers, headers,
params: { params: {
path: filePath, path: filePath,
'As-Task': 'true', 'As-Task': 'true', //作为任务
}, },
maxBodyLength: Infinity,
maxContentLength: Infinity,
}); });
this.logger.log(`文件上传成功: ${filePathOnServer}`); this.logger.log(`文件上传成功: ${filePathOnServer}`);
return response.data; return response.data;
} catch (error) { } catch (error) {
this.logger.error('上传文件失败..', error); this.logger.error('上传文件失败..', error);
throw new Error(`上传文件失败: ${error.message || error}`); throw new Error('上传文件失败..');
} }
} }
} }

View File

@ -10,18 +10,8 @@ import {
Logger, Logger,
Inject, Inject,
Ip, Ip,
UseInterceptors,
UploadedFile,
UseGuards,
} from '@nestjs/common'; } from '@nestjs/common';
import { import { ApiTags, ApiOperation, ApiBody, ApiQuery } from '@nestjs/swagger';
ApiTags,
ApiOperation,
ApiBody,
ApiQuery,
ApiConsumes,
ApiHeader,
} from '@nestjs/swagger';
import { MemeService } from './meme.service'; import { MemeService } from './meme.service';
import { Response } from 'express'; import { Response } from 'express';
import * as fs from 'fs'; import * as fs from 'fs';
@ -29,12 +19,6 @@ 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'; import imageType from 'image-type';
import { FileInterceptor } from '@nestjs/platform-express';
import * as path from 'path';
import { OpenListService } from '../../core/openlist/openlist.service';
import { PathService } from '../../core/path/path.service';
import { TokenAuthGuard } from '../../core/tools/token-auth.guard';
import { AppConfigService } from '../../config/config.service';
class MemeRequestDto { class MemeRequestDto {
character?: string; character?: string;
@ -54,12 +38,6 @@ export class MemeController {
private readonly toolsService: ToolsService, private readonly toolsService: ToolsService,
@Inject(RedisService) @Inject(RedisService)
private readonly redisService: RedisService, private readonly redisService: RedisService,
@Inject(OpenListService)
private readonly openListService: OpenListService,
@Inject(PathService)
private readonly pathService: PathService,
@Inject(AppConfigService)
private readonly configService: AppConfigService,
) {} ) {}
@Post('get') @Post('get')
@ -180,102 +158,4 @@ export class MemeController {
throw new HttpException('服务器错误', HttpStatus.INTERNAL_SERVER_ERROR); throw new HttpException('服务器错误', HttpStatus.INTERNAL_SERVER_ERROR);
} }
} }
/**
*
* @param file
* @param character
* @param status
* @param res
*/
@Post('upload')
@ApiOperation({ summary: '上传表情包并同步' })
@ApiConsumes('multipart/form-data')
@UseInterceptors(FileInterceptor('file'))
@ApiHeader({ name: 'x-token', description: '身份验证token', required: true })
@UseGuards(TokenAuthGuard)
@ApiBody({
description: '上传表情包文件',
schema: {
type: 'object',
properties: {
file: { type: 'string', format: 'binary' },
character: { type: 'string', description: '角色名称' },
status: { type: 'string', description: '状态' },
},
},
})
public async uploadMeme(
@UploadedFile() file: Express.Multer.File,
@Body('character') character: string,
@Body('status') status: string,
) {
if (!file) {
throw new HttpException('未检测到上传文件', HttpStatus.BAD_REQUEST);
}
try {
const fsp = fs.promises;
const safeCharacter = character?.trim() || 'unknown';
const safeStatus = status?.trim() || 'default';
const memeBasePath = this.pathService.get('meme');
const localDir = path.join(memeBasePath, safeCharacter, safeStatus);
await fsp.mkdir(localDir, { recursive: true });
const buffer = file.buffer || (await fsp.readFile(file.path));
const imgType = await imageType(buffer);
if (!imgType || !['jpg', 'png', 'gif', 'webp'].includes(imgType.ext)) {
throw new HttpException(
'不支持的图片格式',
HttpStatus.UNSUPPORTED_MEDIA_TYPE,
);
}
const remoteMemePath = this.configService.get('OPENLIST_API_MEME_PATH');
const remoteDir = `${remoteMemePath}/${safeCharacter}/${safeStatus}/`;
let fileList: string[] = [];
try {
const listResult = await this.openListService.listFiles(remoteDir);
if (
listResult?.code === 200 &&
Array.isArray(listResult.data?.content)
) {
fileList = listResult.data.content.map((f) => f.name);
} else {
this.logger.warn(`目录为空或返回结构异常:${remoteDir}`);
}
} catch (err) {
this.logger.warn(`获取远程目录失败(${remoteDir}),将自动创建`);
}
const usedNumbers = fileList
.map((name) => {
const match = name.match(/^(\d+)\./);
return match ? parseInt(match[1], 10) : null;
})
.filter((n) => n !== null) as number[];
const nextNumber =
usedNumbers.length > 0 ? Math.max(...usedNumbers) + 1 : 1;
const remoteFilename = `${nextNumber}.${imgType.ext}`;
const remoteFilePath = `${remoteDir}${remoteFilename}`;
const localFilePath = path.join(localDir, remoteFilename);
await fsp.writeFile(localFilePath, buffer);
const fileStream = fs.createReadStream(localFilePath);
await this.openListService.uploadFile(
localFilePath,
fileStream,
remoteFilePath,
);
this.logger.log(`表情包上传成功: ${remoteFilePath}`);
return '表情上传成功..';
} catch (error) {
this.logger.error('表情包上传失败:', error);
throw new HttpException(
`上传失败: ${error.message || error}`,
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
} }

View File

@ -16,7 +16,6 @@ import { FilesModule } from '../../core/files/files.module';
RedisModule, RedisModule,
AppConfigModule, AppConfigModule,
FilesModule, FilesModule,
PathModule,
], ],
providers: [MemeService], providers: [MemeService],
controllers: [MemeController], controllers: [MemeController],