@@ -3,7 +3,7 @@ import { GetObjectCommand, PutObjectCommand, S3Client } from "@aws-sdk/client-s3
33import { getSignedUrl } from "@aws-sdk/s3-request-presigner" ;
44
55interface IObjectStoreClient {
6- putObject ( key : string , body : ReadableStream | string , contentType : string ) : Promise < void > ;
6+ putObject ( key : string , body : ReadableStream | string , contentType : string ) : Promise < string > ;
77 getObject ( key : string ) : Promise < string > ;
88 presign ( key : string , method : "PUT" | "GET" , expiresIn : number ) : Promise < string > ;
99}
@@ -36,15 +36,17 @@ class Aws4FetchClient implements IObjectStoreClient {
3636 return url . toString ( ) ;
3737 }
3838
39- async putObject ( key : string , body : ReadableStream | string , contentType : string ) : Promise < void > {
40- const response = await this . awsClient . fetch ( this . buildUrl ( key ) , {
39+ async putObject ( key : string , body : ReadableStream | string , contentType : string ) : Promise < string > {
40+ const objectUrl = this . buildUrl ( key ) ;
41+ const response = await this . awsClient . fetch ( objectUrl , {
4142 method : "PUT" ,
4243 headers : { "Content-Type" : contentType } ,
4344 body,
4445 } ) ;
4546 if ( ! response . ok ) {
4647 throw new Error ( `Failed to upload to object store: ${ response . statusText } ` ) ;
4748 }
49+ return objectUrl ;
4850 }
4951
5052 async getObject ( key : string ) : Promise < string > {
@@ -69,7 +71,7 @@ class Aws4FetchClient implements IObjectStoreClient {
6971
7072type AwsSdkConfig = {
7173 bucket : string ;
72- baseUrl ? : string ;
74+ baseUrl : string ;
7375 region ?: string ;
7476} ;
7577
@@ -78,25 +80,47 @@ class AwsSdkClient implements IObjectStoreClient {
7880
7981 constructor ( private readonly config : AwsSdkConfig ) {
8082 this . s3Client = new S3Client ( {
81- ...( config . baseUrl ? { endpoint : config . baseUrl , forcePathStyle : true } : { } ) ,
83+ endpoint : config . baseUrl ,
84+ forcePathStyle : true ,
8285 ...( config . region ? { region : config . region } : { } ) ,
8386 } ) ;
8487 }
8588
86- async putObject ( key : string , body : ReadableStream | string , contentType : string ) : Promise < void > {
89+ /**
90+ * Callers use a single logical key (same as aws4fetch path: `bucket/object/...`).
91+ * S3 APIs take Bucket + Key where Key must not repeat the bucket name.
92+ */
93+ private toS3ObjectKey ( logicalKey : string ) : string {
94+ const prefix = `${ this . config . bucket } /` ;
95+ if ( logicalKey . startsWith ( prefix ) ) {
96+ return logicalKey . slice ( prefix . length ) ;
97+ }
98+ return logicalKey ;
99+ }
100+
101+ private logicalObjectUrl ( logicalKey : string ) : string {
102+ const url = new URL ( this . config . baseUrl ) ;
103+ url . pathname = `/${ logicalKey } ` ;
104+ return url . href ;
105+ }
106+
107+ async putObject ( key : string , body : ReadableStream | string , contentType : string ) : Promise < string > {
108+ const s3Key = this . toS3ObjectKey ( key ) ;
87109 await this . s3Client . send (
88110 new PutObjectCommand ( {
89111 Bucket : this . config . bucket ,
90- Key : key ,
91- Body : body as string ,
112+ Key : s3Key ,
113+ Body : body ,
92114 ContentType : contentType ,
93115 } )
94116 ) ;
117+ return this . logicalObjectUrl ( key ) ;
95118 }
96119
97120 async getObject ( key : string ) : Promise < string > {
121+ const s3Key = this . toS3ObjectKey ( key ) ;
98122 const response = await this . s3Client . send (
99- new GetObjectCommand ( { Bucket : this . config . bucket , Key : key } )
123+ new GetObjectCommand ( { Bucket : this . config . bucket , Key : s3Key } )
100124 ) ;
101125 if ( ! response . Body ) {
102126 throw new Error ( `Empty response body from object store for key: ${ key } ` ) ;
@@ -105,10 +129,11 @@ class AwsSdkClient implements IObjectStoreClient {
105129 }
106130
107131 async presign ( key : string , method : "PUT" | "GET" , expiresIn : number ) : Promise < string > {
132+ const s3Key = this . toS3ObjectKey ( key ) ;
108133 const command =
109134 method === "PUT"
110- ? new PutObjectCommand ( { Bucket : this . config . bucket , Key : key } )
111- : new GetObjectCommand ( { Bucket : this . config . bucket , Key : key } ) ;
135+ ? new PutObjectCommand ( { Bucket : this . config . bucket , Key : s3Key } )
136+ : new GetObjectCommand ( { Bucket : this . config . bucket , Key : s3Key } ) ;
112137
113138 return getSignedUrl ( this . s3Client , command , { expiresIn } ) ;
114139 }
@@ -123,8 +148,12 @@ export type ObjectStoreClientConfig = {
123148 service ?: string ;
124149} ;
125150
126- export class ObjectStoreClient {
127- private constructor ( private readonly impl : IObjectStoreClient ) { }
151+ export class ObjectStoreClient implements IObjectStoreClient {
152+ private constructor (
153+ private readonly impl : IObjectStoreClient ,
154+ /** When set, logical keys may start with `${bucket}/…`; AwsSdkClient strips that prefix for S3 APIs. */
155+ readonly bucket : string | undefined
156+ ) { }
128157
129158 static create ( config : ObjectStoreClientConfig ) : ObjectStoreClient {
130159 if ( config . accessKeyId && config . secretAccessKey ) {
@@ -135,7 +164,8 @@ export class ObjectStoreClient {
135164 secretAccessKey : config . secretAccessKey ,
136165 region : config . region ,
137166 service : config . service ,
138- } )
167+ } ) ,
168+ config . bucket
139169 ) ;
140170 }
141171
@@ -151,11 +181,12 @@ export class ObjectStoreClient {
151181 bucket : config . bucket ,
152182 baseUrl : config . baseUrl ,
153183 region : config . region ,
154- } )
184+ } ) ,
185+ config . bucket
155186 ) ;
156187 }
157188
158- putObject ( key : string , body : ReadableStream | string , contentType : string ) : Promise < void > {
189+ putObject ( key : string , body : ReadableStream | string , contentType : string ) : Promise < string > {
159190 return this . impl . putObject ( key , body , contentType ) ;
160191 }
161192
0 commit comments