Skip to content

Commit e4a18ab

Browse files
committed
refactor(attachments): use putObject
1 parent 840482d commit e4a18ab

File tree

2 files changed

+85
-30
lines changed

2 files changed

+85
-30
lines changed

src/attachments/attachments.service.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -88,18 +88,23 @@ export class AttachmentsService {
8888
ResourcePermission.CAN_EDIT,
8989
);
9090

91-
const id = await this.s3Service.put(filename, buffer, mimetype, {
92-
folder: 'attachments',
93-
});
91+
const { key, objectName } = await this.s3Service.generateObjectKey(
92+
'attachments',
93+
filename,
94+
);
95+
const metadata = {
96+
filename: encodeFileName(filename),
97+
};
98+
await this.s3Service.putObject(key, buffer, mimetype, metadata);
9499

95100
// Create the resource-attachment relation
96101
await this.resourceAttachmentsService.addAttachmentToResource(
97102
namespaceId,
98103
resourceId,
99-
id,
104+
objectName,
100105
);
101106

102-
return id;
107+
return objectName;
103108
}
104109

105110
async uploadAttachments(

src/s3/s3.service.ts

Lines changed: 75 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,12 @@ import {
44
S3Client,
55
PutObjectCommand,
66
GetObjectCommand,
7+
GetObjectCommandOutput,
78
DeleteObjectCommand,
89
HeadBucketCommand,
910
CreateBucketCommand,
11+
HeadObjectCommand,
12+
HeadObjectCommandOutput,
1013
} from '@aws-sdk/client-s3';
1114
import { Readable } from 'stream';
1215
import generateId from 'omniboxd/utils/generate-id';
@@ -21,11 +24,24 @@ export interface PutOptions {
2124
folder?: string;
2225
}
2326

24-
export interface ObjectMeta {
25-
contentType?: string;
26-
contentLength?: number;
27-
metadata?: Record<string, string>;
28-
lastModified?: Date;
27+
export class ObjectMeta {
28+
constructor(
29+
public contentType?: string,
30+
public contentLength?: number,
31+
public metadata?: Record<string, string>,
32+
public lastModified?: Date,
33+
) {}
34+
35+
static fromResponse(
36+
response: GetObjectCommandOutput | HeadObjectCommandOutput,
37+
): ObjectMeta {
38+
return new ObjectMeta(
39+
response.ContentType,
40+
response.ContentLength,
41+
response.Metadata,
42+
response.LastModified,
43+
);
44+
}
2945
}
3046

3147
@Injectable()
@@ -73,7 +89,24 @@ export class S3Service implements OnModuleInit {
7389
await this.ensureBucket();
7490
}
7591

76-
private generateId(filename: string, length: number = 32): string {
92+
private async ensureBucket(): Promise<void> {
93+
try {
94+
await this.s3Client.send(new HeadBucketCommand({ Bucket: this.bucket }));
95+
} catch (error: any) {
96+
if (
97+
error.name === 'NotFound' ||
98+
error.$metadata?.httpStatusCode === 404
99+
) {
100+
await this.s3Client.send(
101+
new CreateBucketCommand({ Bucket: this.bucket }),
102+
);
103+
} else {
104+
throw error;
105+
}
106+
}
107+
}
108+
109+
private generateId(filename?: string, length: number = 32): string {
77110
const uuid = generateId(length);
78111
// Get the original filename to extract the proper extension
79112
const originalFilename = getOriginalFileName(filename);
@@ -88,21 +121,23 @@ export class S3Service implements OnModuleInit {
88121
return `${uuid}${ext}`;
89122
}
90123

91-
private async ensureBucket(): Promise<void> {
92-
try {
93-
await this.s3Client.send(new HeadBucketCommand({ Bucket: this.bucket }));
94-
} catch (error: any) {
95-
if (
96-
error.name === 'NotFound' ||
97-
error.$metadata?.httpStatusCode === 404
98-
) {
99-
await this.s3Client.send(
100-
new CreateBucketCommand({ Bucket: this.bucket }),
101-
);
102-
} else {
103-
throw error;
124+
// There might still be race conditions but the probability should be low enough
125+
async generateObjectKey(
126+
prefix: string,
127+
filename?: string,
128+
length: number = 32,
129+
): Promise<{ key: string; objectName: string }> {
130+
if (!prefix.endsWith('/')) {
131+
prefix += '/';
132+
}
133+
for (let i = 0; i < 5; i++) {
134+
const objectName = this.generateId(filename, length);
135+
const key = `${prefix}${objectName}`;
136+
if ((await this.headObject(key)) === null) {
137+
return { key, objectName };
104138
}
105139
}
140+
throw new Error('Unable to generate unique S3 key');
106141
}
107142

108143
async putObject(
@@ -131,15 +166,30 @@ export class S3Service implements OnModuleInit {
131166
const response = await this.s3Client.send(command);
132167
return {
133168
stream: response.Body as Readable,
134-
meta: {
135-
contentType: response.ContentType,
136-
contentLength: response.ContentLength,
137-
metadata: response.Metadata,
138-
lastModified: response.LastModified,
139-
},
169+
meta: ObjectMeta.fromResponse(response),
140170
};
141171
}
142172

173+
async headObject(key: string): Promise<ObjectMeta | null> {
174+
const command = new HeadObjectCommand({
175+
Bucket: this.bucket,
176+
Key: key,
177+
});
178+
179+
try {
180+
const response = await this.s3Client.send(command);
181+
return ObjectMeta.fromResponse(response);
182+
} catch (error: any) {
183+
if (
184+
error?.name === 'NotFound' ||
185+
error?.$metadata?.httpStatusCode === 404
186+
) {
187+
return null;
188+
}
189+
throw error;
190+
}
191+
}
192+
143193
async deleteObject(key: string) {
144194
const command = new DeleteObjectCommand({
145195
Bucket: this.bucket,

0 commit comments

Comments
 (0)