Skip to content

Commit b04d4e6

Browse files
committed
feat: migrate apps' and dataset's avatar to minio
1 parent 8b8563f commit b04d4e6

File tree

16 files changed

+218
-183
lines changed

16 files changed

+218
-183
lines changed

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

Lines changed: 34 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
1-
import { Client } from 'minio';
1+
import { Client, type RemoveOptions, type CopyConditions, type LifecycleConfig } from 'minio';
22
import {
33
defaultS3Options,
4+
type CreatePostPresignedUrlOptions,
45
type CreatePostPresignedUrlParams,
56
type CreatePostPresignedUrlResult,
67
type S3BucketName,
78
type S3Options
89
} from '../types';
910
import type { IBucketBasicOperations } from '../interface';
10-
import { createObjectKey, createPresignedUrlExpires, inferContentType } from '../helpers';
11+
import {
12+
createObjectKey,
13+
createPresignedUrlExpires,
14+
createTempObjectKey,
15+
inferContentType
16+
} from '../helpers';
1117

1218
export class S3BaseBucket implements IBucketBasicOperations {
1319
public client: Client;
@@ -16,11 +22,11 @@ export class S3BaseBucket implements IBucketBasicOperations {
1622
*
1723
* @param _bucket the bucket you want to operate
1824
* @param options the options for the s3 client
19-
* @param afterInit the function to be called after instantiating the s3 service
25+
* @param afterInits the function to be called after instantiating the s3 service
2026
*/
2127
constructor(
2228
private readonly _bucket: S3BucketName,
23-
private readonly afterInit?: () => Promise<void> | void,
29+
private readonly afterInits?: (() => Promise<void> | void)[],
2430
public options: Partial<S3Options> = defaultS3Options
2531
) {
2632
options = { ...defaultS3Options, ...options };
@@ -31,45 +37,54 @@ export class S3BaseBucket implements IBucketBasicOperations {
3137
if (!(await this.exist())) {
3238
await this.client.makeBucket(this._bucket);
3339
}
34-
await this.afterInit?.();
40+
await Promise.all(this.afterInits?.map((afterInit) => afterInit()) ?? []);
3541
};
3642
init();
3743
}
3844

39-
async exist(): Promise<boolean> {
40-
return await this.client.bucketExists(this._bucket);
41-
}
42-
4345
get name(): string {
4446
return this._bucket;
4547
}
4648

47-
upload(): Promise<void> {
48-
throw new Error('Method not implemented.');
49+
async move(src: string, dst: string, options?: CopyConditions): Promise<void> {
50+
const bucket = this.name;
51+
await this.client.copyObject(bucket, dst, `/${bucket}/${src}`, options);
52+
return this.client.removeObject(bucket, src);
4953
}
5054

51-
download(): Promise<void> {
52-
throw new Error('Method not implemented.');
55+
copy(src: string, dst: string, options?: CopyConditions): ReturnType<Client['copyObject']> {
56+
return this.client.copyObject(this.name, src, dst, options);
5357
}
5458

55-
delete(objectKey: string): Promise<void> {
56-
return this.client.removeObject(this._bucket, objectKey);
59+
exist(): Promise<boolean> {
60+
return this.client.bucketExists(this.name);
61+
}
62+
63+
delete(objectKey: string, options?: RemoveOptions): Promise<void> {
64+
return this.client.removeObject(this.name, objectKey, options);
5765
}
5866

5967
get(): Promise<void> {
6068
throw new Error('Method not implemented.');
6169
}
6270

71+
lifecycle(): Promise<LifecycleConfig | null> {
72+
return this.client.getBucketLifecycle(this.name);
73+
}
74+
6375
async createPostPresignedUrl(
64-
params: CreatePostPresignedUrlParams
76+
params: CreatePostPresignedUrlParams,
77+
options: CreatePostPresignedUrlOptions = {}
6578
): Promise<CreatePostPresignedUrlResult> {
66-
const maxFileSize = this.options.maxFileSize as number;
79+
const { temporay } = options;
6780
const contentType = inferContentType(params.filename);
81+
const maxFileSize = this.options.maxFileSize as number;
82+
const key = temporay ? createTempObjectKey(params) : createObjectKey(params);
6883

6984
const policy = this.client.newPostPolicy();
70-
policy.setBucket(this._bucket);
85+
policy.setKey(key);
86+
policy.setBucket(this.name);
7187
policy.setContentType(contentType);
72-
policy.setKey(createObjectKey(params));
7388
policy.setContentLengthRange(1, maxFileSize);
7489
policy.setExpires(createPresignedUrlExpires(10));
7590
policy.setUserMetaData({

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

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,36 @@ import { S3BaseBucket } from './base';
22
import { createBucketPolicy } from '../helpers';
33
import {
44
S3Buckets,
5+
type CreatePostPresignedUrlOptions,
56
type CreatePostPresignedUrlParams,
67
type CreatePostPresignedUrlResult,
78
type S3Options
89
} from '../types';
910
import type { IPublicBucketOperations } from '../interface';
11+
import { lifecycleOfTemporaryAvatars } from '../lifecycle';
1012

1113
export class S3PublicBucket extends S3BaseBucket implements IPublicBucketOperations {
1214
constructor(options?: Partial<S3Options>) {
1315
super(
1416
S3Buckets.public,
15-
async () => {
16-
const bucket = this.name;
17-
const policy = createBucketPolicy(bucket);
18-
try {
19-
await this.client.setBucketPolicy(bucket, policy);
20-
} catch (error) {
21-
// TODO: maybe it was a cloud S3 that doesn't allow us to set the policy, so that cause the error,
22-
// maybe we can ignore the error, or we have other plan to handle this.
17+
[
18+
// set bucket policy
19+
async () => {
20+
const bucket = this.name;
21+
const policy = createBucketPolicy(bucket);
22+
try {
23+
await this.client.setBucketPolicy(bucket, policy);
24+
} catch (error) {
25+
// TODO: 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+
}
28+
},
29+
// set bucket lifecycle
30+
async () => {
31+
const bucket = this.name;
32+
await this.client.setBucketLifecycle(bucket, lifecycleOfTemporaryAvatars);
2333
}
24-
},
34+
],
2535
options
2636
);
2737
}
@@ -36,8 +46,9 @@ export class S3PublicBucket extends S3BaseBucket implements IPublicBucketOperati
3646
}
3747

3848
override createPostPresignedUrl(
39-
params: Omit<CreatePostPresignedUrlParams, 'visibility'>
49+
params: Omit<CreatePostPresignedUrlParams, 'visibility'>,
50+
options: CreatePostPresignedUrlOptions = {}
4051
): Promise<CreatePostPresignedUrlResult> {
41-
return super.createPostPresignedUrl({ ...params, visibility: 'public' });
52+
return super.createPostPresignedUrl({ ...params, visibility: 'public' }, options);
4253
}
4354
}

packages/service/common/s3/helpers.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,3 +69,11 @@ export function createObjectKey({ source, teamId, filename }: CreateObjectKeyPar
6969
const id = crypto.randomBytes(16).toString('hex');
7070
return `${source}/${teamId}/${date}/${id}_${filename}`;
7171
}
72+
73+
/**
74+
* create temporary s3 object key by source, team ID and filename
75+
*/
76+
export function createTempObjectKey(params: CreateObjectKeyParams): string {
77+
const origin = createObjectKey(params);
78+
return `temp/${origin}`;
79+
}

packages/service/common/s3/interface.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
1-
import type { CreateObjectKeyParams } from './types';
1+
import { type LifecycleConfig, type Client, type CopyConditions, type RemoveOptions } from 'minio';
2+
import type { CreateObjectKeyParams, CreatePostPresignedUrlOptions } from './types';
23

34
export interface IBucketBasicOperations {
45
get name(): string;
5-
exist(): Promise<boolean>;
6-
upload(): Promise<void>;
7-
download(): Promise<void>;
8-
delete(objectKey: string): Promise<void>;
96
get(): Promise<void>;
7+
exist(): Promise<boolean>;
8+
delete(objectKey: string, options?: RemoveOptions): Promise<void>;
9+
move(src: string, dst: string, options?: CopyConditions): Promise<void>;
10+
copy(src: string, dst: string, options?: CopyConditions): ReturnType<Client['copyObject']>;
11+
lifecycle(): Promise<LifecycleConfig | null>;
1012
createPostPresignedUrl(
11-
params: CreateObjectKeyParams
13+
params: CreateObjectKeyParams,
14+
options?: CreatePostPresignedUrlOptions
1215
): Promise<{ url: string; fields: Record<string, string> }>;
1316
}
1417

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { type LifecycleRule, type LifecycleConfig } from 'minio';
2+
3+
export function createLifeCycleConfig(rule: LifecycleRule): LifecycleConfig {
4+
return {
5+
Rule: [rule]
6+
};
7+
}
8+
9+
export function assembleLifeCycleConfigs(...configs: LifecycleConfig[]): LifecycleConfig {
10+
if (configs.length === 0) return { Rule: [] };
11+
return {
12+
Rule: configs.flatMap((config) => config.Rule)
13+
};
14+
}
15+
16+
export const lifecycleOfTemporaryAvatars = createLifeCycleConfig({
17+
ID: 'Temporary Avatars Rule',
18+
Prefix: 'temp/avatar/',
19+
Status: 'Enabled',
20+
Expiration: {
21+
Days: 1
22+
}
23+
});

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

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { S3BaseSource } from './base';
22
import {
33
S3Sources,
4+
S3APIPrefix,
5+
type CreatePostPresignedUrlOptions,
46
type CreatePostPresignedUrlParams,
57
type CreatePostPresignedUrlResult,
68
type S3Options
@@ -17,20 +19,36 @@ class S3AvatarSource extends S3BaseSource<S3PublicBucket> {
1719
}
1820

1921
override createPostPresignedUrl(
20-
params: Omit<CreatePostPresignedUrlParams, 'source' | 'visibility'>
22+
params: Omit<CreatePostPresignedUrlParams, 'source' | 'visibility'>,
23+
options: CreatePostPresignedUrlOptions = {}
2124
): Promise<CreatePostPresignedUrlResult> {
22-
return this.bucket.createPostPresignedUrl({
23-
...params,
24-
source: S3Sources.avatar
25-
});
25+
return this.bucket.createPostPresignedUrl(
26+
{
27+
...params,
28+
source: S3Sources.avatar
29+
},
30+
options
31+
);
2632
}
2733

2834
createPublicUrl(objectKey: string): string {
2935
return this.bucket.createPublicUrl(objectKey);
3036
}
3137

32-
removeAvatar(objectKey: string): Promise<void> {
33-
return this.bucket.delete(objectKey);
38+
createAvatarObjectKey(avatarWithPrefix: string): string {
39+
return avatarWithPrefix.replace(S3APIPrefix.avatar, '');
40+
}
41+
42+
removeAvatar(avatarWithPrefix: string): Promise<void> {
43+
const avatarObjectKey = this.createAvatarObjectKey(avatarWithPrefix);
44+
return this.bucket.delete(avatarObjectKey);
45+
}
46+
47+
async moveAvatarFromTemp(tempAvatarWithPrefix: string): Promise<string> {
48+
const tempAvatarObjectKey = this.createAvatarObjectKey(tempAvatarWithPrefix);
49+
const avatarObjectKey = tempAvatarObjectKey.replace(`${S3Sources.temp}/`, '');
50+
await this.bucket.move(tempAvatarObjectKey, avatarObjectKey);
51+
return S3APIPrefix.avatar + avatarObjectKey;
3452
}
3553
}
3654

packages/service/common/s3/types.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,8 @@ export const S3SourcesSchema = z.enum([
6969
'dataset',
7070
'dataset-image',
7171
'invoice',
72-
'rawtext'
72+
'rawtext',
73+
'temp'
7374
]);
7475
export const S3Sources = S3SourcesSchema.enum;
7576
export type S3SourceType = z.infer<typeof S3SourcesSchema>;
@@ -87,8 +88,18 @@ export const CreatePostPresignedUrlParamsSchema = z.object({
8788
});
8889
export type CreatePostPresignedUrlParams = z.infer<typeof CreatePostPresignedUrlParamsSchema>;
8990

91+
export const CreatePostPresignedUrlOptionsSchema = z.object({
92+
temporay: z.boolean().optional()
93+
});
94+
export type CreatePostPresignedUrlOptions = z.infer<typeof CreatePostPresignedUrlOptionsSchema>;
95+
9096
export const CreatePostPresignedUrlResultSchema = z.object({
9197
url: z.string().min(1),
9298
fields: z.record(z.string(), z.string())
9399
});
94100
export type CreatePostPresignedUrlResult = z.infer<typeof CreatePostPresignedUrlResultSchema>;
101+
102+
export const S3APIPrefix = {
103+
avatar: '/api/system/img/'
104+
} as const;
105+
export type S3APIPrefixType = (typeof S3APIPrefix)[keyof typeof S3APIPrefix];

packages/service/common/s3/usage-example.ts

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

0 commit comments

Comments
 (0)