Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 102 additions & 0 deletions packages/service/common/s3/buckets/base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { Client, type RemoveOptions, type CopyConditions, type LifecycleConfig } from 'minio';
import {
defaultS3Options,
type CreatePostPresignedUrlOptions,
type CreatePostPresignedUrlParams,
type CreatePostPresignedUrlResult,
type S3BucketName,
type S3Options
} from '../types';
import type { IBucketBasicOperations } from '../interface';
import {
createObjectKey,
createPresignedUrlExpires,
createTempObjectKey,
inferContentType
} from '../helpers';

export class S3BaseBucket implements IBucketBasicOperations {
public client: Client;

/**
*
* @param _bucket the bucket you want to operate
* @param options the options for the s3 client
* @param afterInits the function to be called after instantiating the s3 service
*/
constructor(
private readonly _bucket: S3BucketName,
private readonly afterInits?: (() => Promise<void> | void)[],
public options: Partial<S3Options> = defaultS3Options
) {
options = { ...defaultS3Options, ...options };
this.options = options as S3Options;
this.client = new Client(options as S3Options);

const init = async () => {
if (!(await this.exist())) {
await this.client.makeBucket(this._bucket);
}
await Promise.all(this.afterInits?.map((afterInit) => afterInit()) ?? []);
};
init();
}

get name(): string {
return this._bucket;
}

async move(src: string, dst: string, options?: CopyConditions): Promise<void> {
const bucket = this.name;
await this.client.copyObject(bucket, dst, `/${bucket}/${src}`, options);
return this.client.removeObject(bucket, src);
}

copy(src: string, dst: string, options?: CopyConditions): ReturnType<Client['copyObject']> {
return this.client.copyObject(this.name, src, dst, options);
}

exist(): Promise<boolean> {
return this.client.bucketExists(this.name);
}

delete(objectKey: string, options?: RemoveOptions): Promise<void> {
return this.client.removeObject(this.name, objectKey, options);
}

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

lifecycle(): Promise<LifecycleConfig | null> {
return this.client.getBucketLifecycle(this.name);
}

async createPostPresignedUrl(
params: CreatePostPresignedUrlParams,
options: CreatePostPresignedUrlOptions = {}
): Promise<CreatePostPresignedUrlResult> {
const { temporay } = options;
const contentType = inferContentType(params.filename);
const maxFileSize = this.options.maxFileSize as number;
const key = temporay ? createTempObjectKey(params) : createObjectKey(params);

const policy = this.client.newPostPolicy();
policy.setKey(key);
policy.setBucket(this.name);
policy.setContentType(contentType);
policy.setContentLengthRange(1, maxFileSize);
policy.setExpires(createPresignedUrlExpires(10));
policy.setUserMetaData({
filename: encodeURIComponent(params.filename),
visibility: params.visibility
});

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

return {
url: postURL,
fields: formData
};
}
}
24 changes: 24 additions & 0 deletions packages/service/common/s3/buckets/manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { type S3BaseBucket } from './base';
import { type S3Options } from '../types';
import { S3PublicBucket } from './public';
import { S3PrivateBucket } from './private';

export class S3BucketManager {
private static instance: S3BucketManager;
private publicBucket: S3PublicBucket | null = null;
private privateBucket: S3PrivateBucket | null = null;

private constructor() {}

static getInstance(): S3BucketManager {
return (this.instance ??= new S3BucketManager());
}

getPublicBucket(options?: Partial<S3Options>): S3PublicBucket {
return (this.publicBucket ??= new S3PublicBucket(options));
}

getPrivateBucket(options?: Partial<S3Options>): S3PrivateBucket {
return (this.privateBucket ??= new S3PrivateBucket(options));
}
}
19 changes: 19 additions & 0 deletions packages/service/common/s3/buckets/private.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { S3BaseBucket } from './base';
import {
S3Buckets,
type CreatePostPresignedUrlParams,
type CreatePostPresignedUrlResult,
type S3Options
} from '../types';

export class S3PrivateBucket extends S3BaseBucket {
constructor(options?: Partial<S3Options>) {
super(S3Buckets.private, undefined, options);
}

override createPostPresignedUrl(
params: Omit<CreatePostPresignedUrlParams, 'visibility'>
): Promise<CreatePostPresignedUrlResult> {
return super.createPostPresignedUrl({ ...params, visibility: 'private' });
}
}
54 changes: 54 additions & 0 deletions packages/service/common/s3/buckets/public.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { S3BaseBucket } from './base';
import { createBucketPolicy } from '../helpers';
import {
S3Buckets,
type CreatePostPresignedUrlOptions,
type CreatePostPresignedUrlParams,
type CreatePostPresignedUrlResult,
type S3Options
} from '../types';
import type { IPublicBucketOperations } from '../interface';
import { lifecycleOfTemporaryAvatars } from '../lifecycle';

export class S3PublicBucket extends S3BaseBucket implements IPublicBucketOperations {
constructor(options?: Partial<S3Options>) {
super(
S3Buckets.public,
[
// set bucket policy
async () => {
const bucket = this.name;
const policy = createBucketPolicy(bucket);
try {
await this.client.setBucketPolicy(bucket, policy);
} catch (error) {
// TODO: 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.
}
},
// set bucket lifecycle
async () => {
const bucket = this.name;
await this.client.setBucketLifecycle(bucket, lifecycleOfTemporaryAvatars);
}
],
options
);
}

createPublicUrl(objectKey: string): string {
const protocol = this.options.useSSL ? 'https' : 'http';
const hostname = this.options.endPoint;
const port = this.options.port;
const bucket = this.name;

return `${protocol}://${hostname}:${port}/${bucket}/${objectKey}`;
}

override createPostPresignedUrl(
params: Omit<CreatePostPresignedUrlParams, 'visibility'>,
options: CreatePostPresignedUrlOptions = {}
): Promise<CreatePostPresignedUrlResult> {
return super.createPostPresignedUrl({ ...params, visibility: 'public' }, options);
}
}
10 changes: 0 additions & 10 deletions packages/service/common/s3/config.ts

This file was deleted.

20 changes: 0 additions & 20 deletions packages/service/common/s3/const.ts

This file was deleted.

Loading
Loading