Skip to content

Commit a180442

Browse files
committed
perf: s3 controller
1 parent a23ed0a commit a180442

File tree

15 files changed

+227
-229
lines changed

15 files changed

+227
-229
lines changed

packages/service/common/s3/buckets/base.ts

Lines changed: 57 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { addLog } from '../../system/log';
1515
import { addS3DelJob } from '../mq';
1616
import { type Readable } from 'node:stream';
1717
import { type UploadFileByBufferParams, UploadFileByBufferSchema } from '../type';
18+
import { parseFileExtensionFromUrl } from '@fastgpt/global/common/string/tools';
1819

1920
export class S3BaseBucket {
2021
private _client: Client;
@@ -26,7 +27,7 @@ export class S3BaseBucket {
2627
* @param options the options for the s3 client
2728
*/
2829
constructor(
29-
private readonly bucketName: string,
30+
public readonly bucketName: string,
3031
public options: Partial<S3OptionsType> = defaultS3Options
3132
) {
3233
options = { ...defaultS3Options, ...options };
@@ -56,20 +57,18 @@ export class S3BaseBucket {
5657
}
5758

5859
const init = async () => {
59-
if (!(await this.exist())) {
60+
// Not exists bucket, create it
61+
if (!(await this.client.bucketExists(this.bucketName))) {
6062
await this.client.makeBucket(this.bucketName);
6163
}
6264
await this.options.afterInit?.();
63-
console.log(`S3 init success: ${this.name}`);
65+
console.log(`S3 init success: ${this.bucketName}`);
6466
};
6567
if (this.options.init) {
6668
init();
6769
}
6870
}
6971

70-
get name(): string {
71-
return this.bucketName;
72-
}
7372
get client(): Client {
7473
return this._client;
7574
}
@@ -110,21 +109,17 @@ export class S3BaseBucket {
110109
copyConditions?: CopyConditions;
111110
};
112111
}): ReturnType<Client['copyObject']> {
113-
const bucket = this.name;
112+
const bucket = this.bucketName;
114113
if (options?.temporary) {
115114
await MongoS3TTL.create({
116115
minioKey: to,
117-
bucketName: this.name,
116+
bucketName: this.bucketName,
118117
expiredTime: addHours(new Date(), 24)
119118
});
120119
}
121120
return this.client.copyObject(bucket, to, `${bucket}/${from}`, options?.copyConditions);
122121
}
123122

124-
exist(): Promise<boolean> {
125-
return this.client.bucketExists(this.name);
126-
}
127-
128123
async delete(objectKey: string, options?: RemoveOptions): Promise<void> {
129124
try {
130125
if (!objectKey) return Promise.resolve();
@@ -133,39 +128,55 @@ export class S3BaseBucket {
133128
const fileParsedPrefix = `${path.dirname(objectKey)}/${path.basename(objectKey, path.extname(objectKey))}-parsed`;
134129
await this.addDeleteJob({ prefix: fileParsedPrefix });
135130

136-
return await this.client.removeObject(this.name, objectKey, options);
131+
return await this.client.removeObject(this.bucketName, objectKey, options);
137132
} catch (error) {
138133
if (error instanceof S3Error) {
139134
if (error.code === 'InvalidObjectName') {
140-
addLog.warn(`${this.name} delete object not found: ${objectKey}`, error);
135+
addLog.warn(`${this.bucketName} delete object not found: ${objectKey}`, error);
141136
return Promise.resolve();
142137
}
143138
}
144139
return Promise.reject(error);
145140
}
146141
}
147142

143+
// 列出文件
148144
listObjectsV2(
149145
...params: Parameters<Client['listObjectsV2']> extends [string, ...infer R] ? R : never
150146
) {
151-
return this.client.listObjectsV2(this.name, ...params);
147+
return this.client.listObjectsV2(this.bucketName, ...params);
152148
}
153149

150+
// 上传文件
154151
putObject(...params: Parameters<Client['putObject']> extends [string, ...infer R] ? R : never) {
155-
return this.client.putObject(this.name, ...params);
152+
return this.client.putObject(this.bucketName, ...params);
156153
}
157154

158-
getObject(...params: Parameters<Client['getObject']> extends [string, ...infer R] ? R : never) {
159-
return this.client.getObject(this.name, ...params);
155+
// 获取文件流
156+
getFileStream(
157+
...params: Parameters<Client['getObject']> extends [string, ...infer R] ? R : never
158+
) {
159+
return this.client.getObject(this.bucketName, ...params);
160160
}
161161

162-
statObject(...params: Parameters<Client['statObject']> extends [string, ...infer R] ? R : never) {
163-
return this.client.statObject(this.name, ...params);
162+
// 获取文件状态
163+
async statObject(
164+
...params: Parameters<Client['statObject']> extends [string, ...infer R] ? R : never
165+
) {
166+
try {
167+
return await this.client.statObject(this.bucketName, ...params);
168+
} catch (error) {
169+
if (error instanceof S3Error && error.message === 'Not Found') {
170+
return null;
171+
}
172+
return Promise.reject(error);
173+
}
164174
}
165175

176+
// 判断文件是否存在
166177
async isObjectExists(key: string): Promise<boolean> {
167178
try {
168-
await this.client.statObject(this.name, key);
179+
await this.client.statObject(this.bucketName, key);
169180
return true;
170181
} catch (err) {
171182
if (err instanceof S3Error && err.message === 'Not Found') {
@@ -175,6 +186,7 @@ export class S3BaseBucket {
175186
}
176187
}
177188

189+
// 将文件流转换为Buffer
178190
async fileStreamToBuffer(stream: Readable): Promise<Buffer> {
179191
const chunks: Buffer[] = [];
180192
for await (const chunk of stream) {
@@ -184,7 +196,7 @@ export class S3BaseBucket {
184196
}
185197

186198
addDeleteJob(params: Omit<Parameters<typeof addS3DelJob>[0], 'bucketName'>) {
187-
return addS3DelJob({ ...params, bucketName: this.name });
199+
return addS3DelJob({ ...params, bucketName: this.bucketName });
188200
}
189201

190202
async createPostPresignedUrl(
@@ -202,7 +214,7 @@ export class S3BaseBucket {
202214

203215
const policy = this.externalClient.newPostPolicy();
204216
policy.setKey(key);
205-
policy.setBucket(this.name);
217+
policy.setBucket(this.bucketName);
206218
policy.setContentType(contentType);
207219
if (formatMaxFileSize) {
208220
policy.setContentLengthRange(1, formatMaxFileSize);
@@ -220,7 +232,7 @@ export class S3BaseBucket {
220232
if (expiredHours) {
221233
await MongoS3TTL.create({
222234
minioKey: key,
223-
bucketName: this.name,
235+
bucketName: this.bucketName,
224236
expiredTime: addHours(new Date(), expiredHours)
225237
});
226238
}
@@ -242,7 +254,7 @@ export class S3BaseBucket {
242254
const { key, expiredHours } = parsed;
243255
const expires = expiredHours ? expiredHours * 60 * 60 : 30 * 60; // expires 的单位是秒 默认 30 分钟
244256

245-
return await this.externalClient.presignedGetObject(this.name, key, expires);
257+
return await this.externalClient.presignedGetObject(this.bucketName, key, expires);
246258
}
247259

248260
async createPreviewUrl(params: createPreviewUrlParams) {
@@ -251,15 +263,15 @@ export class S3BaseBucket {
251263
const { key, expiredHours } = parsed;
252264
const expires = expiredHours ? expiredHours * 60 * 60 : 30 * 60; // expires 的单位是秒 默认 30 分钟
253265

254-
return await this.client.presignedGetObject(this.name, key, expires);
266+
return await this.client.presignedGetObject(this.bucketName, key, expires);
255267
}
256268

257269
async uploadFileByBuffer(params: UploadFileByBufferParams) {
258270
const { key, buffer, contentType } = UploadFileByBufferSchema.parse(params);
259271

260272
await MongoS3TTL.create({
261273
minioKey: key,
262-
bucketName: this.name,
274+
bucketName: this.bucketName,
263275
expiredTime: addHours(new Date(), 1)
264276
});
265277
await this.putObject(key, buffer, undefined, {
@@ -274,4 +286,22 @@ export class S3BaseBucket {
274286
})
275287
};
276288
}
289+
290+
// 对外包装的方法
291+
// 获取文件元数据
292+
async getFileMetadata(key: string) {
293+
const stat = await this.statObject(key);
294+
if (!stat) return;
295+
296+
const contentLength = stat.size;
297+
const filename: string = decodeURIComponent(stat.metaData['origin-filename']);
298+
const extension = parseFileExtensionFromUrl(filename);
299+
const contentType: string = stat.metaData['content-type'];
300+
return {
301+
filename,
302+
extension,
303+
contentType,
304+
contentLength
305+
};
306+
}
277307
}

packages/service/common/s3/buckets/public.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export class S3PublicBucket extends S3BaseBucket {
77
super(S3Buckets.public, {
88
...options,
99
afterInit: async () => {
10-
const bucket = this.name;
10+
const bucket = this.bucketName;
1111
const policy = JSON.stringify({
1212
Version: '2012-10-17',
1313
Statement: [
@@ -34,7 +34,7 @@ export class S3PublicBucket extends S3BaseBucket {
3434
const protocol = this.options.useSSL ? 'https' : 'http';
3535
const hostname = this.options.endPoint;
3636
const port = this.options.port;
37-
const bucket = this.name;
37+
const bucket = this.bucketName;
3838

3939
const url = new URL(`${protocol}://${hostname}:${port}/${bucket}/${objectKey}`);
4040

packages/service/common/s3/sources/avatar.ts

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,9 @@ import { imageBaseUrl } from '@fastgpt/global/common/file/image/constants';
55
import type { ClientSession } from 'mongoose';
66
import { getFileS3Key } from '../utils';
77

8-
class S3AvatarSource {
9-
private bucket: S3PublicBucket;
10-
8+
class S3AvatarSource extends S3PublicBucket {
119
constructor() {
12-
this.bucket = new S3PublicBucket();
10+
super();
1311
}
1412

1513
get prefix(): string {
@@ -27,7 +25,7 @@ class S3AvatarSource {
2725
}) {
2826
const { fileKey } = getFileS3Key.avatar({ teamId, filename });
2927

30-
return this.bucket.createPostPresignedUrl(
28+
return this.createPostPresignedUrl(
3129
{ filename, rawKey: fileKey },
3230
{
3331
expiredHours: autoExpired ? 1 : undefined, // 1 Hours
@@ -36,19 +34,15 @@ class S3AvatarSource {
3634
);
3735
}
3836

39-
createPublicUrl(objectKey: string): string {
40-
return this.bucket.createPublicUrl(objectKey);
41-
}
42-
4337
async removeAvatarTTL(avatar: string, session?: ClientSession): Promise<void> {
4438
const key = avatar.slice(this.prefix.length);
45-
await MongoS3TTL.deleteOne({ minioKey: key, bucketName: this.bucket.name }, session);
39+
await MongoS3TTL.deleteOne({ minioKey: key, bucketName: this.bucketName }, session);
4640
}
4741

4842
async deleteAvatar(avatar: string, session?: ClientSession): Promise<void> {
4943
const key = avatar.slice(this.prefix.length);
50-
await MongoS3TTL.deleteOne({ minioKey: key, bucketName: this.bucket.name }, session);
51-
await this.bucket.delete(key);
44+
await MongoS3TTL.deleteOne({ minioKey: key, bucketName: this.bucketName }, session);
45+
await this.delete(key);
5246
}
5347

5448
async refreshAvatar(newAvatar?: string, oldAvatar?: string, session?: ClientSession) {
@@ -78,7 +72,7 @@ class S3AvatarSource {
7872
}) {
7973
const from = key.slice(this.prefix.length);
8074
const to = `${S3Sources.avatar}/${teamId}/${filename}`;
81-
await this.bucket.copy({ from, to, options: { temporary } });
75+
await this.copy({ from, to, options: { temporary } });
8276
return this.prefix.concat(to);
8377
}
8478
}

packages/service/common/s3/sources/chat/index.ts

Lines changed: 8 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { parseFileExtensionFromUrl } from '@fastgpt/global/common/string/tools';
21
import { S3PrivateBucket } from '../../buckets/private';
32
import { S3Sources } from '../../type';
43
import {
@@ -14,11 +13,9 @@ import { S3Buckets } from '../../constants';
1413
import path from 'path';
1514
import { getFileS3Key } from '../../utils';
1615

17-
export class S3ChatSource {
18-
private bucket: S3PrivateBucket;
19-
16+
export class S3ChatSource extends S3PrivateBucket {
2017
constructor() {
21-
this.bucket = new S3PrivateBucket();
18+
super();
2219
}
2320

2421
static parseChatUrl(url: string | URL) {
@@ -51,46 +48,19 @@ export class S3ChatSource {
5148
}
5249
}
5350

54-
// 获取文件流
55-
getChatFileStream(key: string) {
56-
return this.bucket.getObject(key);
57-
}
58-
59-
// 获取文件状态
60-
getChatFileStat(key: string) {
61-
return this.bucket.statObject(key);
62-
}
63-
64-
// 获取文件元数据
65-
async getFileMetadata(key: string) {
66-
const stat = await this.getChatFileStat(key);
67-
if (!stat) return { filename: '', extension: '', contentLength: 0, contentType: '' };
68-
69-
const contentLength = stat.size;
70-
const filename: string = decodeURIComponent(stat.metaData['origin-filename']);
71-
const extension = parseFileExtensionFromUrl(filename);
72-
const contentType: string = stat.metaData['content-type'];
73-
return {
74-
filename,
75-
extension,
76-
contentType,
77-
contentLength
78-
};
79-
}
80-
8151
async createGetChatFileURL(params: { key: string; expiredHours?: number; external: boolean }) {
8252
const { key, expiredHours = 1, external = false } = params; // 默认一个小时
8353

8454
if (external) {
85-
return await this.bucket.createExternalUrl({ key, expiredHours });
55+
return await this.createExternalUrl({ key, expiredHours });
8656
}
87-
return await this.bucket.createPreviewUrl({ key, expiredHours });
57+
return await this.createPreviewUrl({ key, expiredHours });
8858
}
8959

9060
async createUploadChatFileURL(params: CheckChatFileKeys) {
9161
const { appId, chatId, uId, filename, expiredTime } = ChatFileUploadSchema.parse(params);
9262
const { fileKey } = getFileS3Key.chat({ appId, chatId, uId, filename });
93-
return await this.bucket.createPostPresignedUrl(
63+
return await this.createPostPresignedUrl(
9464
{ rawKey: fileKey, filename },
9565
{ expiredHours: expiredTime ? differenceInHours(expiredTime, new Date()) : 24 }
9666
);
@@ -100,11 +70,11 @@ export class S3ChatSource {
10070
const { appId, chatId, uId } = DelChatFileByPrefixSchema.parse(params);
10171

10272
const prefix = [S3Sources.chat, appId, uId, chatId].filter(Boolean).join('/');
103-
return this.bucket.addDeleteJob({ prefix });
73+
return this.addDeleteJob({ prefix });
10474
}
10575

10676
deleteChatFileByKey(key: string) {
107-
return this.bucket.addDeleteJob({ key });
77+
return this.addDeleteJob({ key });
10878
}
10979

11080
async uploadChatFileByBuffer(params: UploadFileParams) {
@@ -117,7 +87,7 @@ export class S3ChatSource {
11787
filename
11888
});
11989

120-
return this.bucket.uploadFileByBuffer({
90+
return this.uploadFileByBuffer({
12191
key: fileKey,
12292
buffer,
12393
contentType

0 commit comments

Comments
 (0)