Skip to content

Commit e36b0e5

Browse files
committed
refactor(files): use s3 service
1 parent 4974373 commit e36b0e5

File tree

3 files changed

+87
-87
lines changed

3 files changed

+87
-87
lines changed

src/files/files.module.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@ import { FilesService } from './files.service';
33
import { ConfigModule } from '@nestjs/config';
44
import { TypeOrmModule } from '@nestjs/typeorm';
55
import { File } from './entities/file.entity';
6+
import { S3Module } from 'omniboxd/s3/s3.module';
67

78
@Module({
8-
imports: [ConfigModule, TypeOrmModule.forFeature([File])],
9+
imports: [ConfigModule, TypeOrmModule.forFeature([File]), S3Module],
910
providers: [FilesService],
1011
exports: [FilesService],
1112
})

src/files/files.service.ts

Lines changed: 21 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,14 @@ import { File } from './entities/file.entity';
55
import { InjectRepository } from '@nestjs/typeorm';
66
import { AppException } from 'omniboxd/common/exceptions/app.exception';
77
import { I18nService } from 'nestjs-i18n';
8-
import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';
9-
import { createPresignedPost, PresignedPost } from '@aws-sdk/s3-presigned-post';
10-
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
8+
import { PresignedPost } from '@aws-sdk/s3-presigned-post';
119
import { formatFileSize } from '../utils/format-file-size';
10+
import { S3Service } from 'omniboxd/s3/s3.service';
11+
12+
const s3Prefix = 'uploaded-files';
1213

1314
@Injectable()
1415
export class FilesService {
15-
private readonly s3Client: S3Client;
16-
private readonly s3InternalClient: S3Client;
17-
private readonly s3Bucket: string;
18-
private readonly s3Prefix: string;
1916
private readonly s3MaxFileSize: number;
2017

2118
constructor(
@@ -24,65 +21,12 @@ export class FilesService {
2421
@InjectRepository(File)
2522
private readonly fileRepo: Repository<File>,
2623
private readonly i18n: I18nService,
24+
private readonly s3Service: S3Service,
2725
) {
28-
const accessKeyId = configService.get<string>('OBB_S3_ACCESS_KEY_ID');
29-
const secretAccessKey = configService.get<string>(
30-
'OBB_S3_SECRET_ACCESS_KEY',
31-
);
32-
if (!accessKeyId || !secretAccessKey) {
33-
throw new Error('S3 credentials not set');
34-
}
35-
36-
const s3Endpoint = configService.get<string>('OBB_S3_ENDPOINT');
37-
if (!s3Endpoint) {
38-
throw new Error('S3 endpoint not set');
39-
}
40-
41-
const s3InternalEndpoint = configService.get<string>(
42-
'OBB_S3_INTERNAL_ENDPOINT',
43-
);
44-
45-
const s3Bucket = configService.get<string>('OBB_S3_BUCKET');
46-
if (!s3Bucket) {
47-
throw new Error('S3 bucket not set');
48-
}
49-
50-
const s3Prefix = configService.get<string>('OBB_S3_PREFIX');
51-
if (!s3Prefix) {
52-
throw new Error('S3 prefix not set');
53-
}
54-
55-
const s3Region = configService.get<string>('OBB_S3_REGION', 'us-east-1');
56-
5726
this.s3MaxFileSize = configService.get<number>(
5827
'OBB_S3_MAX_FILE_SIZE',
5928
20 * 1024 * 1024,
6029
);
61-
62-
this.s3Client = new S3Client({
63-
region: s3Region,
64-
credentials: {
65-
accessKeyId,
66-
secretAccessKey,
67-
},
68-
endpoint: s3Endpoint,
69-
});
70-
71-
if (s3InternalEndpoint && s3InternalEndpoint != s3Endpoint) {
72-
this.s3InternalClient = new S3Client({
73-
region: s3Region,
74-
credentials: {
75-
accessKeyId,
76-
secretAccessKey,
77-
},
78-
endpoint: s3InternalEndpoint,
79-
});
80-
} else {
81-
this.s3InternalClient = this.s3Client;
82-
}
83-
84-
this.s3Bucket = s3Bucket;
85-
this.s3Prefix = s3Prefix;
8630
}
8731

8832
async createFile(
@@ -121,53 +65,45 @@ export class FilesService {
12165
throw new AppException(message, 'FILE_TOO_LARGE', HttpStatus.BAD_REQUEST);
12266
}
12367
const disposition = `attachment; filename*=UTF-8''${encodeURIComponent(filename)}`;
124-
return await createPresignedPost(this.s3Client, {
125-
Bucket: this.s3Bucket,
126-
Key: `${this.s3Prefix}/${fileId}`,
127-
Conditions: [
128-
['content-length-range', 0, this.s3MaxFileSize],
129-
{ 'content-type': mimetype },
130-
{ 'content-disposition': disposition },
131-
],
132-
Fields: {
133-
'content-type': mimetype,
134-
'content-disposition': disposition,
135-
},
136-
Expires: 900, // 900 seconds
137-
});
68+
return await this.s3Service.generateUploadForm(
69+
`${s3Prefix}/${fileId}`,
70+
true,
71+
mimetype,
72+
disposition,
73+
this.s3MaxFileSize,
74+
);
13875
}
13976

14077
private async generateDownloadUrl(
14178
namespaceId: string,
14279
fileId: string,
143-
s3Client: S3Client,
80+
isPublic: boolean,
14481
): Promise<string> {
14582
const file = await this.getFile(namespaceId, fileId);
14683
if (!file) {
14784
const message = this.i18n.t('resource.errors.fileNotFound');
14885
throw new AppException(message, 'FILE_NOT_FOUND', HttpStatus.NOT_FOUND);
14986
}
15087

151-
const command = new GetObjectCommand({
152-
Bucket: this.s3Bucket,
153-
Key: `${this.s3Prefix}/${fileId}`,
154-
ResponseContentDisposition: `attachment; filename*=UTF-8''${encodeURIComponent(file.name)}`,
155-
});
156-
157-
return await getSignedUrl(s3Client, command, { expiresIn: 900 });
88+
const disposition = `attachment; filename*=UTF-8''${encodeURIComponent(file.name)}`;
89+
return await this.s3Service.generateDownloadUrl(
90+
`${s3Prefix}/${fileId}`,
91+
isPublic,
92+
disposition,
93+
);
15894
}
15995

16096
async generatePublicDownloadUrl(
16197
namespaceId: string,
16298
fileId: string,
16399
): Promise<string> {
164-
return this.generateDownloadUrl(namespaceId, fileId, this.s3Client);
100+
return this.generateDownloadUrl(namespaceId, fileId, true);
165101
}
166102

167103
async generateInternalDownloadUrl(
168104
namespaceId: string,
169105
fileId: string,
170106
): Promise<string> {
171-
return this.generateDownloadUrl(namespaceId, fileId, this.s3InternalClient);
107+
return this.generateDownloadUrl(namespaceId, fileId, false);
172108
}
173109
}

src/s3/s3.service.ts

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ import {
1414
import { Readable } from 'stream';
1515
import generateId from 'omniboxd/utils/generate-id';
1616
import { getOriginalFileName } from 'omniboxd/utils/encode-filename';
17+
import { createPresignedPost, PresignedPost } from '@aws-sdk/s3-presigned-post';
18+
import { Conditions } from '@aws-sdk/s3-presigned-post/dist-types/types';
19+
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
1720

1821
export class ObjectMeta {
1922
constructor(
@@ -38,6 +41,7 @@ export class ObjectMeta {
3841
@Injectable()
3942
export class S3Service implements OnModuleInit {
4043
private readonly s3Client: S3Client;
44+
private readonly s3PublicClient: S3Client;
4145
private readonly bucket: string;
4246

4347
constructor(configService: ConfigService) {
@@ -50,6 +54,9 @@ export class S3Service implements OnModuleInit {
5054
}
5155

5256
const s3Endpoint = configService.get<string>('OBB_S3_ENDPOINT');
57+
const s3PublicEndpoint = configService.get<string>(
58+
'OBB_S3_PUBLIC_ENDPOINT',
59+
);
5360
if (!s3Endpoint) {
5461
throw new Error('S3 endpoint not set');
5562
}
@@ -72,7 +79,19 @@ export class S3Service implements OnModuleInit {
7279
endpoint: s3Endpoint,
7380
forcePathStyle,
7481
});
75-
82+
if (s3PublicEndpoint && s3PublicEndpoint != s3Endpoint) {
83+
this.s3PublicClient = new S3Client({
84+
region: s3Region,
85+
credentials: {
86+
accessKeyId,
87+
secretAccessKey,
88+
},
89+
endpoint: s3PublicEndpoint,
90+
forcePathStyle,
91+
});
92+
} else {
93+
this.s3PublicClient = this.s3Client;
94+
}
7695
this.bucket = s3Bucket;
7796
}
7897

@@ -188,4 +207,48 @@ export class S3Service implements OnModuleInit {
188207
});
189208
return await this.s3Client.send(command);
190209
}
210+
211+
async generateUploadForm(
212+
key: string,
213+
isPublic: boolean,
214+
contentType?: string,
215+
contentDisposition?: string,
216+
maxSize?: number,
217+
): Promise<PresignedPost> {
218+
const s3Client = isPublic ? this.s3PublicClient : this.s3Client;
219+
const conditions: Conditions[] = [];
220+
const fields: Record<string, string> = {};
221+
if (contentType) {
222+
conditions.push({ 'content-type': contentType });
223+
fields['content-type'] = contentType;
224+
}
225+
if (contentDisposition) {
226+
conditions.push({ 'content-disposition': contentDisposition });
227+
fields['content-disposition'] = contentDisposition;
228+
}
229+
if (maxSize) {
230+
conditions.push(['content-length-range', 0, maxSize]);
231+
}
232+
return await createPresignedPost(s3Client, {
233+
Bucket: this.bucket,
234+
Key: key,
235+
Conditions: conditions,
236+
Fields: fields,
237+
Expires: 900, // 900 seconds
238+
});
239+
}
240+
241+
async generateDownloadUrl(
242+
key: string,
243+
isPublic: boolean,
244+
contentDisposition?: string,
245+
): Promise<string> {
246+
const s3Client = isPublic ? this.s3PublicClient : this.s3Client;
247+
const command = new GetObjectCommand({
248+
Bucket: this.bucket,
249+
Key: key,
250+
ResponseContentDisposition: contentDisposition,
251+
});
252+
return await getSignedUrl(s3Client, command, { expiresIn: 900 });
253+
}
191254
}

0 commit comments

Comments
 (0)