@@ -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' ;
1114import { Readable } from 'stream' ;
1215import 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