Skip to content

Commit 38b423f

Browse files
committed
refactor: s3 class
1 parent 60f886e commit 38b423f

File tree

38 files changed

+234
-633
lines changed

38 files changed

+234
-633
lines changed

packages/service/common/file/image/controller.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { addHours } from 'date-fns';
88
import { imageFileType } from '@fastgpt/global/common/file/constants';
99
import { retryFn } from '@fastgpt/global/common/system/utils';
1010
import { UserError } from '@fastgpt/global/common/error/utils';
11+
import { getS3AvatarSource } from '../../s3/sources/avatar';
1112

1213
export const maxImgSize = 1024 * 1024 * 12;
1314
const base64MimeRegex = /data:image\/([^\)]+);base64/;
@@ -109,6 +110,15 @@ const getIdFromPath = (path?: string) => {
109110

110111
return id;
111112
};
113+
export const refreshSourceAvatarS3 = async (
114+
path?: string,
115+
oldPath?: string,
116+
session?: ClientSession
117+
) => {
118+
const s3AvatarSource = getS3AvatarSource();
119+
await s3AvatarSource.deleteAvatar(oldPath || '', session);
120+
await s3AvatarSource.removeAvatarTTL(path || '', session);
121+
};
112122
// 删除旧的头像,新的头像去除过期时间
113123
export const refreshSourceAvatar = async (
114124
path?: string,

packages/service/common/file/s3Ttl/controller.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { MongoS3TTL } from './schema';
2-
import { S3BucketManager } from '../../s3/buckets/manager';
32
import { addLog } from '../../system/log';
43
import { setCron } from '../../system/cron';
54
import { checkTimerLock } from '../../system/timerLock/utils';
65
import { TimerIdEnum } from '../../system/timerLock/constants';
6+
import { S3PublicBucket } from '../../s3/buckets/public';
7+
import { S3PrivateBucket } from '../../s3/buckets/private';
78

89
export async function clearExpiredMinioFiles() {
910
try {
@@ -17,18 +18,17 @@ export async function clearExpiredMinioFiles() {
1718

1819
addLog.info(`Found ${expiredFiles.length} expired minio files to clean`);
1920

20-
const s3Manager = S3BucketManager.getInstance();
2121
let success = 0;
2222
let fail = 0;
2323

2424
for (const file of expiredFiles) {
2525
try {
2626
const bucket = (() => {
2727
if (file.bucketName === process.env.S3_PUBLIC_BUCKET) {
28-
return s3Manager.getPublicBucket();
28+
return new S3PublicBucket();
2929
}
3030
if (file.bucketName === process.env.S3_PRIVATE_BUCKET) {
31-
return s3Manager.getPrivateBucket();
31+
return new S3PrivateBucket();
3232
}
3333
throw new Error(`Unknown bucket name: ${file.bucketName}`);
3434
})();

packages/service/common/file/s3Ttl/schema.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { Schema, getMongoModel } from '../../mongo';
2-
import { type S3TtlSchemaType } from '@fastgpt/global/common/file/minioTtl/type';
2+
import { type S3TtlSchemaType } from '@fastgpt/global/common/file/s3Ttl/type';
33

44
const collectionName = 's3_ttl_files';
55

6-
const S3TtlSchema = new Schema({
6+
const S3TTLSchema = new Schema({
77
bucketName: {
88
type: String,
99
required: true
@@ -18,7 +18,7 @@ const S3TtlSchema = new Schema({
1818
}
1919
});
2020

21-
S3TtlSchema.index({ expiredTime: 1 });
22-
S3TtlSchema.index({ bucketName: 1, minioKey: 1 });
21+
S3TTLSchema.index({ expiredTime: 1 });
22+
S3TTLSchema.index({ bucketName: 1, minioKey: 1 });
2323

24-
export const MongoS3TTL = getMongoModel<S3TtlSchemaType>(collectionName, S3TtlSchema);
24+
export const MongoS3TTL = getMongoModel<S3TtlSchemaType>(collectionName, S3TTLSchema);

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

Lines changed: 43 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,20 @@
11
import { Client, type RemoveOptions, type CopyConditions, type LifecycleConfig } from 'minio';
22
import {
3-
defaultS3Options,
43
type ExtensionType,
5-
Mimes,
64
type CreatePostPresignedUrlOptions,
75
type CreatePostPresignedUrlParams,
86
type CreatePostPresignedUrlResult,
97
type S3BucketName,
10-
type S3Options
8+
type S3OptionsType
119
} from '../type';
12-
import type { BucketBasicOperationsType } from '../interface';
13-
import { createObjectKey, createTempObjectKey } from '../helpers';
10+
import { defaultS3Options, Mimes } from '../constants';
11+
import { createObjectKey } from '../helpers';
1412
import path from 'node:path';
15-
import { MongoS3TTL } from 'common/file/s3TTL/schema';
13+
import { MongoS3TTL } from '../../file/s3Ttl/schema';
1614

17-
export class S3BaseBucket implements BucketBasicOperationsType {
18-
public client: Client;
15+
export class S3BaseBucket {
16+
private _client: Client;
17+
private _externalClient: Client | undefined;
1918

2019
/**
2120
*
@@ -26,17 +25,32 @@ export class S3BaseBucket implements BucketBasicOperationsType {
2625
constructor(
2726
private readonly _bucket: S3BucketName,
2827
private readonly afterInits?: (() => Promise<void> | void)[],
29-
public options: Partial<S3Options> = defaultS3Options
28+
public options: Partial<S3OptionsType> = defaultS3Options
3029
) {
3130
options = { ...defaultS3Options, ...options };
32-
this.options = options as S3Options;
33-
this.client = new Client(options as S3Options);
31+
this.options = options;
32+
this._client = new Client(options as S3OptionsType);
33+
34+
if (this.options.externalBaseURL) {
35+
const externalBaseURL = new URL(this.options.externalBaseURL);
36+
const endpoint = externalBaseURL.hostname;
37+
const useSSL = externalBaseURL.protocol === 'https';
38+
39+
this._externalClient = new Client({
40+
useSSL: useSSL,
41+
endPoint: endpoint,
42+
port: options.port,
43+
accessKey: options.accessKey,
44+
secretKey: options.secretKey,
45+
transportAgent: options.transportAgent
46+
});
47+
}
3448

3549
const init = async () => {
3650
if (!(await this.exist())) {
3751
await this.client.makeBucket(this._bucket);
3852
}
39-
await Promise.all(this.afterInits?.map((afterInit) => afterInit()) ?? []);
53+
await Promise.all(afterInits?.map((afterInit) => afterInit()) ?? []);
4054
};
4155
init();
4256
}
@@ -45,10 +59,14 @@ export class S3BaseBucket implements BucketBasicOperationsType {
4559
return this._bucket;
4660
}
4761

48-
async move(src: string, dst: string, options?: CopyConditions): Promise<void> {
62+
protected get client(): Client {
63+
return this._externalClient ?? this._client;
64+
}
65+
66+
move(src: string, dst: string, options?: CopyConditions): Promise<void> {
4967
const bucket = this.name;
50-
await this.client.copyObject(bucket, dst, `/${bucket}/${src}`, options);
51-
await this.delete(src);
68+
this.client.copyObject(bucket, dst, `/${bucket}/${src}`, options);
69+
return this.delete(src);
5270
}
5371

5472
copy(src: string, dst: string, options?: CopyConditions): ReturnType<Client['copyObject']> {
@@ -59,27 +77,19 @@ export class S3BaseBucket implements BucketBasicOperationsType {
5977
return this.client.bucketExists(this.name);
6078
}
6179

62-
async delete(objectKey: string, options?: RemoveOptions): Promise<void> {
63-
await this.client.removeObject(this.name, objectKey, options);
64-
}
65-
66-
get(): Promise<void> {
67-
throw new Error('Method not implemented.');
68-
}
69-
70-
getLifecycle(): Promise<LifecycleConfig | null> {
71-
return this.client.getBucketLifecycle(this.name);
80+
delete(objectKey: string, options?: RemoveOptions): Promise<void> {
81+
return this.client.removeObject(this.name, objectKey, options);
7282
}
7383

7484
async createPostPresignedUrl(
7585
params: CreatePostPresignedUrlParams,
7686
options: CreatePostPresignedUrlOptions = {}
7787
): Promise<CreatePostPresignedUrlResult> {
78-
const { temporary, ttl = 7 * 24 } = options;
88+
const { expiredHours } = options;
7989
const ext = path.extname(params.filename).toLowerCase() as ExtensionType;
8090
const contentType = Mimes[ext] ?? 'application/octet-stream';
8191
const maxFileSize = this.options.maxFileSize as number;
82-
const key = temporary ? createTempObjectKey(params) : createObjectKey(params);
92+
const key = createObjectKey(params);
8393

8494
const policy = this.client.newPostPolicy();
8595
policy.setKey(key);
@@ -88,17 +98,19 @@ export class S3BaseBucket implements BucketBasicOperationsType {
8898
policy.setContentLengthRange(1, maxFileSize);
8999
policy.setExpires(new Date(Date.now() + 10 * 60 * 1000));
90100
policy.setUserMetaData({
91-
filename: encodeURIComponent(params.filename),
92-
visibility: params.visibility
101+
'content-type': contentType,
102+
'content-disposition': `attachment; filename="${encodeURIComponent(params.filename)}"`,
103+
'origin-filename': encodeURIComponent(params.filename),
104+
'upload-time': new Date().toISOString()
93105
});
94106

95107
const { formData, postURL } = await this.client.presignedPostPolicy(policy);
96108

97-
if (temporary) {
109+
if (expiredHours) {
98110
await MongoS3TTL.create({
99111
minioKey: key,
100112
bucketName: this.name,
101-
expiredTime: new Date(Date.now() + ttl * 3.6e6)
113+
expiredTime: new Date(Date.now() + expiredHours * 3.6e6)
102114
});
103115
}
104116

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

Lines changed: 0 additions & 23 deletions
This file was deleted.
Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,9 @@
11
import { S3BaseBucket } from './base';
2-
import {
3-
S3Buckets,
4-
type CreatePostPresignedUrlParams,
5-
type CreatePostPresignedUrlResult,
6-
type S3Options
7-
} from '../type';
2+
import { S3Buckets } from '../constants';
3+
import { type S3OptionsType } from '../type';
84

95
export class S3PrivateBucket extends S3BaseBucket {
10-
constructor(options?: Partial<S3Options>) {
6+
constructor(options?: Partial<S3OptionsType>) {
117
super(S3Buckets.private, undefined, options);
128
}
13-
14-
override createPostPresignedUrl(
15-
params: Omit<CreatePostPresignedUrlParams, 'visibility'>
16-
): Promise<CreatePostPresignedUrlResult> {
17-
return super.createPostPresignedUrl({ ...params, visibility: 'private' });
18-
}
199
}
Lines changed: 8 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,24 @@
11
import { S3BaseBucket } from './base';
2-
import { createBucketPolicy } from '../helpers';
3-
import {
4-
S3Buckets,
5-
type CreatePostPresignedUrlOptions,
6-
type CreatePostPresignedUrlParams,
7-
type CreatePostPresignedUrlResult,
8-
type S3Options
9-
} from '../type';
10-
import type { IPublicBucketOperations } from '../interface';
11-
import { lifecycleOfTemporaryAvatars } from '../lifecycle';
2+
import { createS3PublicBucketPolicy } from '../helpers';
3+
import { S3Buckets } from '../constants';
4+
import { type S3OptionsType } from '../type';
125

13-
export class S3PublicBucket extends S3BaseBucket implements IPublicBucketOperations {
14-
constructor(options?: Partial<S3Options>) {
6+
export class S3PublicBucket extends S3BaseBucket {
7+
constructor(options?: Partial<S3OptionsType>) {
158
super(
169
S3Buckets.public,
1710
[
1811
// set bucket policy
1912
async () => {
2013
const bucket = this.name;
21-
const policy = createBucketPolicy(bucket);
14+
const policy = createS3PublicBucketPolicy(bucket);
2215
try {
2316
await this.client.setBucketPolicy(bucket, policy);
2417
} catch (error) {
25-
// TODO: maybe it was a cloud S3 that doesn't allow us to set the policy, so that cause the error,
18+
// NOTE: maybe it was a cloud S3 that doesn't allow us to set the policy, so that cause the error,
2619
// maybe we can ignore the error, or we have other plan to handle this.
20+
console.error('Failed to set bucket policy:', error);
2721
}
28-
},
29-
// set bucket lifecycle
30-
async () => {
31-
const bucket = this.name;
32-
await this.client.setBucketLifecycle(bucket, lifecycleOfTemporaryAvatars);
3322
}
3423
],
3524
options
@@ -54,11 +43,4 @@ export class S3PublicBucket extends S3BaseBucket implements IPublicBucketOperati
5443

5544
return url.toString();
5645
}
57-
58-
override createPostPresignedUrl(
59-
params: Omit<CreatePostPresignedUrlParams, 'visibility'>,
60-
options: CreatePostPresignedUrlOptions = {}
61-
): Promise<CreatePostPresignedUrlResult> {
62-
return super.createPostPresignedUrl({ ...params, visibility: 'public' }, options);
63-
}
6446
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { HttpProxyAgent } from 'http-proxy-agent';
2+
import { HttpsProxyAgent } from 'https-proxy-agent';
3+
import type { ClientOptions } from 'minio';
4+
5+
export const Mimes = {
6+
'.gif': 'image/gif',
7+
'.png': 'image/png',
8+
'.jpg': 'image/jpeg',
9+
'.jpeg': 'image/jpeg',
10+
'.webp': 'image/webp',
11+
'.svg': 'image/svg+xml',
12+
13+
'.csv': 'text/csv',
14+
'.txt': 'text/plain',
15+
16+
'.pdf': 'application/pdf',
17+
'.zip': 'application/zip',
18+
'.json': 'application/json',
19+
'.doc': 'application/msword',
20+
'.js': 'application/javascript',
21+
'.xls': 'application/vnd.ms-excel',
22+
'.ppt': 'application/vnd.ms-powerpoint',
23+
'.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
24+
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
25+
'.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation'
26+
} as const;
27+
28+
export const defaultS3Options: { externalBaseURL?: string; maxFileSize?: number } & ClientOptions =
29+
{
30+
maxFileSize: 1024 ** 3, // 1GB
31+
32+
useSSL: process.env.S3_USE_SSL === 'true',
33+
endPoint: process.env.S3_ENDPOINT || 'localhost',
34+
externalBaseURL: process.env.S3_EXTERNAL_BASE_URL,
35+
accessKey: process.env.S3_ACCESS_KEY || 'minioadmin',
36+
secretKey: process.env.S3_SECRET_KEY || 'minioadmin',
37+
port: process.env.S3_PORT ? parseInt(process.env.S3_PORT) : 9000,
38+
transportAgent: process.env.HTTP_PROXY
39+
? new HttpProxyAgent(process.env.HTTP_PROXY)
40+
: process.env.HTTPS_PROXY
41+
? new HttpsProxyAgent(process.env.HTTPS_PROXY)
42+
: undefined
43+
};
44+
45+
export const S3Buckets = {
46+
plugin: process.env.S3_PLUGIN_BUCKET || 'fastgpt-plugin',
47+
public: process.env.S3_PUBLIC_BUCKET || 'fastgpt-public',
48+
private: process.env.S3_PRIVATE_BUCKET || 'fastgpt-private'
49+
} as const;

0 commit comments

Comments
 (0)