Skip to content

Commit b8f10a8

Browse files
xqvvuc121914yu
andauthored
feat: integrated to minio (#5748)
* feat: migrate to minio * feat: migrate apps' and dataset's avatar to minio * feat: migrate more avatars to minio * fix: lock file * feat: migrate copyright settings' logo to minio * feat: integrate minio * chore: improve code * chore: rename variables * refactor: s3 class * fix: s3 and mongo operations * chore: add session for avatar source * fix: init s3 buckets * fix: bugbot issues * expired time code * perf: avatar code * union type * export favouriteContract * empty bucket check --------- Co-authored-by: archer <545436317@qq.com>
1 parent fd2e0bd commit b8f10a8

File tree

45 files changed

+855
-522
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+855
-522
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export type S3TtlSchemaType = {
2+
_id: string;
3+
bucketName: string;
4+
minioKey: string;
5+
expiredTime: Date;
6+
};

packages/global/common/tsRest/fastgpt/contracts/core/chat/setting.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { ObjectIdSchema } from '../../../../../type';
88
import { initContract } from '@ts-rest/core';
99

1010
const c = initContract();
11-
const favouriteContract = c.router({
11+
export const favouriteContract = c.router({
1212
list: {
1313
path: '/proApi/core/chat/setting/favourite/list',
1414
method: 'GET',

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

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -109,23 +109,6 @@ const getIdFromPath = (path?: string) => {
109109

110110
return id;
111111
};
112-
// 删除旧的头像,新的头像去除过期时间
113-
export const refreshSourceAvatar = async (
114-
path?: string,
115-
oldPath?: string,
116-
session?: ClientSession
117-
) => {
118-
const newId = getIdFromPath(path);
119-
const oldId = getIdFromPath(oldPath);
120-
121-
if (!newId || newId === oldId) return;
122-
123-
await MongoImage.updateOne({ _id: newId }, { $unset: { expiredTime: 1 } }, { session });
124-
125-
if (oldId) {
126-
await MongoImage.deleteOne({ _id: oldId }, { session });
127-
}
128-
};
129112
export const removeImageByPath = (path?: string, session?: ClientSession) => {
130113
if (!path) return;
131114

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import { Client, type RemoveOptions, type CopyConditions, type LifecycleConfig } from 'minio';
2+
import {
3+
type ExtensionType,
4+
type CreatePostPresignedUrlOptions,
5+
type CreatePostPresignedUrlParams,
6+
type CreatePostPresignedUrlResult,
7+
type S3BucketName,
8+
type S3OptionsType
9+
} from '../type';
10+
import { defaultS3Options, Mimes } from '../constants';
11+
import path from 'node:path';
12+
import { MongoS3TTL } from '../schema';
13+
import { UserError } from '@fastgpt/global/common/error/utils';
14+
import { getNanoid } from '@fastgpt/global/common/string/tools';
15+
import { addHours } from 'date-fns';
16+
17+
export class S3BaseBucket {
18+
private _client: Client;
19+
private _externalClient: Client | undefined;
20+
21+
/**
22+
*
23+
* @param _bucket the bucket you want to operate
24+
* @param options the options for the s3 client
25+
*/
26+
constructor(
27+
private readonly _bucket: S3BucketName,
28+
public options: Partial<S3OptionsType> = defaultS3Options
29+
) {
30+
options = { ...defaultS3Options, ...options };
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+
}
48+
49+
const init = async () => {
50+
if (!(await this.exist())) {
51+
await this.client.makeBucket(this._bucket);
52+
}
53+
await this.options.afterInit?.();
54+
};
55+
init();
56+
}
57+
58+
get name(): string {
59+
return this._bucket;
60+
}
61+
62+
protected get client(): Client {
63+
return this._externalClient ?? this._client;
64+
}
65+
66+
move(src: string, dst: string, options?: CopyConditions): Promise<void> {
67+
const bucket = this.name;
68+
this.client.copyObject(bucket, dst, `/${bucket}/${src}`, options);
69+
return this.delete(src);
70+
}
71+
72+
copy(src: string, dst: string, options?: CopyConditions): ReturnType<Client['copyObject']> {
73+
return this.client.copyObject(this.name, src, dst, options);
74+
}
75+
76+
exist(): Promise<boolean> {
77+
return this.client.bucketExists(this.name);
78+
}
79+
80+
delete(objectKey: string, options?: RemoveOptions): Promise<void> {
81+
return this.client.removeObject(this.name, objectKey, options);
82+
}
83+
84+
async createPostPresignedUrl(
85+
params: CreatePostPresignedUrlParams,
86+
options: CreatePostPresignedUrlOptions = {}
87+
): Promise<CreatePostPresignedUrlResult> {
88+
try {
89+
const { expiredHours } = options;
90+
const filename = params.filename;
91+
const ext = path.extname(filename).toLowerCase() as ExtensionType;
92+
const contentType = Mimes[ext] ?? 'application/octet-stream';
93+
const maxFileSize = this.options.maxFileSize as number;
94+
95+
const key = (() => {
96+
if ('rawKey' in params) return params.rawKey;
97+
98+
return `${params.source}/${params.teamId}/${getNanoid(6)}-${filename}`;
99+
})();
100+
101+
const policy = this.client.newPostPolicy();
102+
policy.setKey(key);
103+
policy.setBucket(this.name);
104+
policy.setContentType(contentType);
105+
policy.setContentLengthRange(1, maxFileSize);
106+
policy.setExpires(new Date(Date.now() + 10 * 60 * 1000));
107+
policy.setUserMetaData({
108+
'content-type': contentType,
109+
'content-disposition': `attachment; filename="${encodeURIComponent(filename)}"`,
110+
'origin-filename': encodeURIComponent(filename),
111+
'upload-time': new Date().toISOString()
112+
});
113+
114+
const { formData, postURL } = await this.client.presignedPostPolicy(policy);
115+
116+
if (expiredHours) {
117+
await MongoS3TTL.create({
118+
minioKey: key,
119+
bucketName: this.name,
120+
expiredTime: addHours(new Date(), expiredHours)
121+
});
122+
}
123+
124+
return {
125+
url: postURL,
126+
fields: formData
127+
};
128+
} catch (error) {
129+
return Promise.reject(error);
130+
}
131+
}
132+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { S3BaseBucket } from './base';
2+
import { S3Buckets } from '../constants';
3+
import { type S3OptionsType } from '../type';
4+
5+
export class S3PrivateBucket extends S3BaseBucket {
6+
constructor(options?: Partial<S3OptionsType>) {
7+
super(S3Buckets.private, options);
8+
}
9+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { S3BaseBucket } from './base';
2+
import { S3Buckets } from '../constants';
3+
import { type S3OptionsType } from '../type';
4+
5+
export class S3PublicBucket extends S3BaseBucket {
6+
constructor(options?: Partial<S3OptionsType>) {
7+
super(S3Buckets.public, {
8+
...options,
9+
afterInit: async () => {
10+
const bucket = this.name;
11+
const policy = JSON.stringify({
12+
Version: '2012-10-17',
13+
Statement: [
14+
{
15+
Effect: 'Allow',
16+
Principal: '*',
17+
Action: 's3:GetObject',
18+
Resource: `arn:aws:s3:::${bucket}/*`
19+
}
20+
]
21+
});
22+
try {
23+
await this.client.setBucketPolicy(bucket, policy);
24+
} catch (error) {
25+
// NOTE: maybe it was a cloud S3 that doesn't allow us to set the policy, so that cause the error,
26+
// maybe we can ignore the error, or we have other plan to handle this.
27+
console.error('Failed to set bucket policy:', error);
28+
}
29+
}
30+
});
31+
}
32+
33+
createPublicUrl(objectKey: string): string {
34+
const protocol = this.options.useSSL ? 'https' : 'http';
35+
const hostname = this.options.endPoint;
36+
const port = this.options.port;
37+
const bucket = this.name;
38+
39+
const url = new URL(`${protocol}://${hostname}:${port}/${bucket}/${objectKey}`);
40+
41+
if (this.options.externalBaseURL) {
42+
const externalBaseURL = new URL(this.options.externalBaseURL);
43+
44+
url.port = externalBaseURL.port;
45+
url.hostname = externalBaseURL.hostname;
46+
url.protocol = externalBaseURL.protocol;
47+
}
48+
49+
return url.toString();
50+
}
51+
}

packages/service/common/s3/config.ts

Lines changed: 0 additions & 10 deletions
This file was deleted.

packages/service/common/s3/const.ts

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

0 commit comments

Comments
 (0)