-
Couldn't load subscription status.
- Fork 6.7k
feat: integrated to minio #5748
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
b21a47b
9d79f61
6132b3a
0a48ec4
af78411
eaf4dd6
6542569
8808d13
d6b6f9b
6a3abe0
cade8dd
f1e89cf
832b86b
8c5728d
92bb2ad
07027d1
964e3cf
88e479a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| export type S3TtlSchemaType = { | ||
| _id: string; | ||
| bucketName: string; | ||
| minioKey: string; | ||
| expiredTime: Date; | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,132 @@ | ||
| import { Client, type RemoveOptions, type CopyConditions, type LifecycleConfig } from 'minio'; | ||
| import { | ||
| type ExtensionType, | ||
| type CreatePostPresignedUrlOptions, | ||
| type CreatePostPresignedUrlParams, | ||
| type CreatePostPresignedUrlResult, | ||
| type S3BucketName, | ||
| type S3OptionsType | ||
| } from '../type'; | ||
| import { defaultS3Options, Mimes } from '../constants'; | ||
| import path from 'node:path'; | ||
| import { MongoS3TTL } from '../schema'; | ||
| import { UserError } from '@fastgpt/global/common/error/utils'; | ||
| import { getNanoid } from '@fastgpt/global/common/string/tools'; | ||
| import { addHours } from 'date-fns'; | ||
|
|
||
| export class S3BaseBucket { | ||
| private _client: Client; | ||
| private _externalClient: Client | undefined; | ||
|
|
||
| /** | ||
| * | ||
| * @param _bucket the bucket you want to operate | ||
| * @param options the options for the s3 client | ||
| */ | ||
| constructor( | ||
| private readonly _bucket: S3BucketName, | ||
| public options: Partial<S3OptionsType> = defaultS3Options | ||
| ) { | ||
| options = { ...defaultS3Options, ...options }; | ||
| this.options = options; | ||
| this._client = new Client(options as S3OptionsType); | ||
|
|
||
| if (this.options.externalBaseURL) { | ||
| const externalBaseURL = new URL(this.options.externalBaseURL); | ||
| const endpoint = externalBaseURL.hostname; | ||
| const useSSL = externalBaseURL.protocol === 'https'; | ||
|
|
||
| this._externalClient = new Client({ | ||
| useSSL: useSSL, | ||
| endPoint: endpoint, | ||
| port: options.port, | ||
| accessKey: options.accessKey, | ||
| secretKey: options.secretKey, | ||
| transportAgent: options.transportAgent | ||
| }); | ||
| } | ||
|
|
||
| const init = async () => { | ||
| if (!(await this.exist())) { | ||
| await this.client.makeBucket(this._bucket); | ||
| } | ||
| await this.options.afterInit?.(); | ||
| }; | ||
| init(); | ||
| } | ||
c121914yu marked this conversation as resolved.
Show resolved
Hide resolved
c121914yu marked this conversation as resolved.
Show resolved
Hide resolved
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
c121914yu marked this conversation as resolved.
Show resolved
Hide resolved
c121914yu marked this conversation as resolved.
Show resolved
Hide resolved
c121914yu marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| get name(): string { | ||
| return this._bucket; | ||
| } | ||
|
|
||
| protected get client(): Client { | ||
| return this._externalClient ?? this._client; | ||
| } | ||
|
|
||
| move(src: string, dst: string, options?: CopyConditions): Promise<void> { | ||
| const bucket = this.name; | ||
| this.client.copyObject(bucket, dst, `/${bucket}/${src}`, options); | ||
| return this.delete(src); | ||
| } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
|
|
||
| copy(src: string, dst: string, options?: CopyConditions): ReturnType<Client['copyObject']> { | ||
| return this.client.copyObject(this.name, src, dst, options); | ||
| } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: Race Condition and Parameter Swap in Move MethodThe
c121914yu marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| exist(): Promise<boolean> { | ||
| return this.client.bucketExists(this.name); | ||
| } | ||
|
|
||
| delete(objectKey: string, options?: RemoveOptions): Promise<void> { | ||
| return this.client.removeObject(this.name, objectKey, options); | ||
| } | ||
|
|
||
| async createPostPresignedUrl( | ||
| params: CreatePostPresignedUrlParams, | ||
| options: CreatePostPresignedUrlOptions = {} | ||
| ): Promise<CreatePostPresignedUrlResult> { | ||
| try { | ||
| const { expiredHours } = options; | ||
| const filename = params.filename; | ||
| const ext = path.extname(filename).toLowerCase() as ExtensionType; | ||
| const contentType = Mimes[ext] ?? 'application/octet-stream'; | ||
| const maxFileSize = this.options.maxFileSize as number; | ||
|
|
||
| const key = (() => { | ||
| if ('rawKey' in params) return params.rawKey; | ||
|
|
||
| return `${params.source}/${params.teamId}/${getNanoid(6)}-${filename}`; | ||
| })(); | ||
|
|
||
| const policy = this.client.newPostPolicy(); | ||
| policy.setKey(key); | ||
| policy.setBucket(this.name); | ||
| policy.setContentType(contentType); | ||
| policy.setContentLengthRange(1, maxFileSize); | ||
| policy.setExpires(new Date(Date.now() + 10 * 60 * 1000)); | ||
| policy.setUserMetaData({ | ||
| 'content-type': contentType, | ||
| 'content-disposition': `attachment; filename="${encodeURIComponent(filename)}"`, | ||
| 'origin-filename': encodeURIComponent(filename), | ||
| 'upload-time': new Date().toISOString() | ||
| }); | ||
|
|
||
| const { formData, postURL } = await this.client.presignedPostPolicy(policy); | ||
|
|
||
| if (expiredHours) { | ||
| await MongoS3TTL.create({ | ||
| minioKey: key, | ||
| bucketName: this.name, | ||
| expiredTime: addHours(new Date(), expiredHours) | ||
| }); | ||
| } | ||
|
|
||
| return { | ||
| url: postURL, | ||
| fields: formData | ||
| }; | ||
| } catch (error) { | ||
| return Promise.reject(error); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| import { S3BaseBucket } from './base'; | ||
| import { S3Buckets } from '../constants'; | ||
| import { type S3OptionsType } from '../type'; | ||
|
|
||
| export class S3PrivateBucket extends S3BaseBucket { | ||
| constructor(options?: Partial<S3OptionsType>) { | ||
| super(S3Buckets.private, options); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| import { S3BaseBucket } from './base'; | ||
| import { S3Buckets } from '../constants'; | ||
| import { type S3OptionsType } from '../type'; | ||
|
|
||
| export class S3PublicBucket extends S3BaseBucket { | ||
| constructor(options?: Partial<S3OptionsType>) { | ||
| super(S3Buckets.public, { | ||
| ...options, | ||
| afterInit: async () => { | ||
| const bucket = this.name; | ||
| const policy = JSON.stringify({ | ||
| Version: '2012-10-17', | ||
| Statement: [ | ||
| { | ||
| Effect: 'Allow', | ||
| Principal: '*', | ||
| Action: 's3:GetObject', | ||
| Resource: `arn:aws:s3:::${bucket}/*` | ||
| } | ||
| ] | ||
| }); | ||
| try { | ||
| await this.client.setBucketPolicy(bucket, policy); | ||
| } catch (error) { | ||
| // NOTE: maybe it was a cloud S3 that doesn't allow us to set the policy, so that cause the error, | ||
| // maybe we can ignore the error, or we have other plan to handle this. | ||
| console.error('Failed to set bucket policy:', error); | ||
| } | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| createPublicUrl(objectKey: string): string { | ||
xqvvu marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| const protocol = this.options.useSSL ? 'https' : 'http'; | ||
| const hostname = this.options.endPoint; | ||
| const port = this.options.port; | ||
| const bucket = this.name; | ||
|
|
||
| const url = new URL(`${protocol}://${hostname}:${port}/${bucket}/${objectKey}`); | ||
|
|
||
| if (this.options.externalBaseURL) { | ||
| const externalBaseURL = new URL(this.options.externalBaseURL); | ||
|
|
||
| url.port = externalBaseURL.port; | ||
| url.hostname = externalBaseURL.hostname; | ||
| url.protocol = externalBaseURL.protocol; | ||
| } | ||
|
|
||
| return url.toString(); | ||
| } | ||
| } | ||
This file was deleted.
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| import type { S3PrivateBucket } from './buckets/private'; | ||
| import type { S3PublicBucket } from './buckets/public'; | ||
| import { HttpProxyAgent } from 'http-proxy-agent'; | ||
| import { HttpsProxyAgent } from 'https-proxy-agent'; | ||
| import type { ClientOptions } from 'minio'; | ||
|
|
||
| export const Mimes = { | ||
| '.gif': 'image/gif', | ||
| '.png': 'image/png', | ||
| '.jpg': 'image/jpeg', | ||
| '.jpeg': 'image/jpeg', | ||
| '.webp': 'image/webp', | ||
| '.svg': 'image/svg+xml', | ||
|
|
||
| '.csv': 'text/csv', | ||
| '.txt': 'text/plain', | ||
|
|
||
| '.pdf': 'application/pdf', | ||
| '.zip': 'application/zip', | ||
| '.json': 'application/json', | ||
| '.doc': 'application/msword', | ||
| '.js': 'application/javascript', | ||
| '.xls': 'application/vnd.ms-excel', | ||
| '.ppt': 'application/vnd.ms-powerpoint', | ||
| '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', | ||
| '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', | ||
| '.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation' | ||
| } as const; | ||
|
|
||
| export const defaultS3Options: { | ||
| externalBaseURL?: string; | ||
| maxFileSize?: number; | ||
| afterInit?: () => Promise<void> | void; | ||
| } & ClientOptions = { | ||
| maxFileSize: 1024 ** 3, // 1GB | ||
|
|
||
| useSSL: process.env.S3_USE_SSL === 'true', | ||
| endPoint: process.env.S3_ENDPOINT || 'localhost', | ||
| externalBaseURL: process.env.S3_EXTERNAL_BASE_URL, | ||
| accessKey: process.env.S3_ACCESS_KEY || 'minioadmin', | ||
| secretKey: process.env.S3_SECRET_KEY || 'minioadmin', | ||
| port: process.env.S3_PORT ? parseInt(process.env.S3_PORT) : 9000, | ||
| transportAgent: process.env.HTTP_PROXY | ||
| ? new HttpProxyAgent(process.env.HTTP_PROXY) | ||
| : process.env.HTTPS_PROXY | ||
| ? new HttpsProxyAgent(process.env.HTTPS_PROXY) | ||
| : undefined | ||
| }; | ||
|
|
||
| export const S3Buckets = { | ||
| public: process.env.S3_PUBLIC_BUCKET || 'fastgpt-public', | ||
| private: process.env.S3_PRIVATE_BUCKET || 'fastgpt-private' | ||
| } as const; | ||
|
|
||
| export const S3BucketMap = { | ||
| public: null as unknown as S3PublicBucket, | ||
| private: null as unknown as S3PrivateBucket | ||
| }; |
Uh oh!
There was an error while loading. Please reload this page.