@@ -4,10 +4,14 @@ use std::{
44} ;
55
66use common:: {
7- config:: { ObjectStorageBackend , ObjectStorageConfig } ,
7+ config:: { ObjectStorageBackend , ObjectStorageConfig , S3Config } ,
88 errors:: MegaError ,
99} ;
10- use object_store:: { aws:: AmazonS3Builder , gcp:: GoogleCloudStorageBuilder , local:: LocalFileSystem } ;
10+ use object_store:: {
11+ aws:: { AmazonS3 , AmazonS3Builder } ,
12+ gcp:: GoogleCloudStorageBuilder ,
13+ local:: LocalFileSystem ,
14+ } ;
1115
1216use crate :: {
1317 adapter:: { BackendStore , ObjectStoreAdapter , UploadStrategy } ,
@@ -42,6 +46,7 @@ impl MegaObjectStorageWrapper {
4246 let store = BackendStore :: Local ( Arc :: new ( fs) ) ;
4347 let adapter = Arc :: new ( ObjectStoreAdapter {
4448 store,
49+ presign_store : None ,
4550 upload_strategy : UploadStrategy :: SinglePut ,
4651 } ) ;
4752 MegaObjectStorageWrapper :: new ( adapter)
@@ -62,26 +67,58 @@ impl ObjectStorageFactory {
6267 }
6368}
6469
65- /// Shared S3 / S3-compatible construction (differs only by endpoint and upload strategy).
66- async fn build_s3_like (
67- cfg : & ObjectStorageConfig ,
70+ fn build_amazon_s3 (
71+ s3_cfg : & S3Config ,
72+ endpoint : & str ,
6873 compatible : bool ,
69- ) -> Result < MegaObjectStorageWrapper , MegaError > {
70- let s3_cfg = cfg. s3 . clone ( ) ;
74+ ) -> Result < AmazonS3 , MegaError > {
7175 let mut builder = AmazonS3Builder :: new ( )
7276 . with_region ( & s3_cfg. region )
7377 . with_bucket_name ( & s3_cfg. bucket )
7478 . with_access_key_id ( & s3_cfg. access_key_id )
7579 . with_secret_access_key ( & s3_cfg. secret_access_key ) ;
76- if compatible {
77- builder = builder
78- . with_endpoint ( & s3_cfg. endpoint_url )
79- . with_allow_http ( true )
80- . with_virtual_hosted_style_request ( false ) ;
80+
81+ if compatible || !endpoint. is_empty ( ) {
82+ if !endpoint. is_empty ( ) {
83+ builder = builder. with_endpoint ( endpoint) ;
84+ }
85+ if compatible {
86+ // S3-compatible (RustFS/MinIO): path-style; allow plain HTTP for
87+ // in-cluster endpoints. HTTPS public endpoints still work with
88+ // allow_http(true).
89+ builder = builder
90+ . with_allow_http ( true )
91+ . with_virtual_hosted_style_request ( false ) ;
92+ } else if endpoint. starts_with ( "http://" ) {
93+ builder = builder. with_allow_http ( true ) ;
94+ }
8195 }
82- let s3 = builder
83- . build ( )
84- . map_err ( |e| MegaError :: Other ( e. to_string ( ) ) ) ?;
96+
97+ builder. build ( ) . map_err ( |e| MegaError :: Other ( e. to_string ( ) ) )
98+ }
99+
100+ /// Shared S3 / S3-compatible construction (differs only by endpoint and upload strategy).
101+ async fn build_s3_like (
102+ cfg : & ObjectStorageConfig ,
103+ compatible : bool ,
104+ ) -> Result < MegaObjectStorageWrapper , MegaError > {
105+ let s3_cfg = & cfg. s3 ;
106+ let endpoint = if compatible {
107+ s3_cfg. endpoint_url . as_str ( )
108+ } else {
109+ // Real AWS: empty endpoint uses the default regional endpoint.
110+ ""
111+ } ;
112+ let s3 = build_amazon_s3 ( s3_cfg, endpoint, compatible) ?;
113+
114+ let presign_store = {
115+ let presign_ep = s3_cfg. presign_endpoint_url . trim ( ) ;
116+ if presign_ep. is_empty ( ) {
117+ None
118+ } else {
119+ Some ( Arc :: new ( build_amazon_s3 ( s3_cfg, presign_ep, compatible) ?) )
120+ }
121+ } ;
85122
86123 let store = BackendStore :: S3 ( Arc :: new ( s3) ) ;
87124 let upload_strategy = if compatible {
@@ -91,6 +128,7 @@ async fn build_s3_like(
91128 } ;
92129 let adapter = Arc :: new ( ObjectStoreAdapter {
93130 store,
131+ presign_store,
94132 upload_strategy,
95133 } ) ;
96134
@@ -106,6 +144,7 @@ async fn build_gcs(cfg: &ObjectStorageConfig) -> Result<MegaObjectStorageWrapper
106144 let store = BackendStore :: Gcs ( Arc :: new ( gcs) ) ;
107145 let adapter = Arc :: new ( ObjectStoreAdapter {
108146 store,
147+ presign_store : None ,
109148 upload_strategy : UploadStrategy :: SinglePut ,
110149 } ) ;
111150
@@ -122,8 +161,103 @@ async fn build_local(cfg: &ObjectStorageConfig) -> Result<MegaObjectStorageWrapp
122161 let store = BackendStore :: Local ( Arc :: new ( fs) ) ;
123162 let adapter = Arc :: new ( ObjectStoreAdapter {
124163 store,
164+ presign_store : None ,
125165 upload_strategy : UploadStrategy :: SinglePut ,
126166 } ) ;
127167
128168 Ok ( MegaObjectStorageWrapper :: new ( adapter) )
129169}
170+
171+ #[ cfg( test) ]
172+ mod tests {
173+ use std:: time:: Duration ;
174+
175+ use common:: config:: {
176+ GcsConfig , LocalConfig , ObjectStorageBackend , ObjectStorageConfig , S3Config ,
177+ } ;
178+ use object_store:: { path:: Path , signer:: Signer } ;
179+ use reqwest:: Method ;
180+
181+ use super :: build_amazon_s3;
182+ use crate :: object_storage:: { ObjectKey , ObjectNamespace } ;
183+
184+ fn sample_s3_config ( endpoint : & str , presign : & str ) -> S3Config {
185+ S3Config {
186+ region : "us-east-1" . into ( ) ,
187+ bucket : "buck2hub-assets" . into ( ) ,
188+ access_key_id : "testkey" . into ( ) ,
189+ secret_access_key : "testsecret" . into ( ) ,
190+ endpoint_url : endpoint. into ( ) ,
191+ presign_endpoint_url : presign. into ( ) ,
192+ }
193+ }
194+
195+ #[ tokio:: test]
196+ async fn signed_url_uses_endpoint_url_when_presign_unset ( ) {
197+ let s3_cfg = sample_s3_config ( "http://rustfs.internal:9000" , "" ) ;
198+ let s3 = build_amazon_s3 ( & s3_cfg, & s3_cfg. endpoint_url , true ) . unwrap ( ) ;
199+ let path = Path :: from ( "orion-images/abc/debian-13-buck2.qcow2" ) ;
200+ let url = s3
201+ . signed_url ( Method :: GET , & path, Duration :: from_secs ( 60 ) )
202+ . await
203+ . unwrap ( )
204+ . to_string ( ) ;
205+ assert ! (
206+ url. starts_with( "http://rustfs.internal:9000/" ) ,
207+ "expected internal host, got {url}"
208+ ) ;
209+ }
210+
211+ #[ tokio:: test]
212+ async fn signed_url_uses_presign_endpoint_when_set ( ) {
213+ let s3_cfg = sample_s3_config (
214+ "http://rustfs.mega-dev.svc.cluster.local:9000" ,
215+ "https://rustfs.xuanwu.openatom.cn" ,
216+ ) ;
217+ let presign = build_amazon_s3 ( & s3_cfg, & s3_cfg. presign_endpoint_url , true ) . unwrap ( ) ;
218+ let path = Path :: from ( "orion-images/abc/debian-13-buck2.qcow2" ) ;
219+ let url = presign
220+ . signed_url ( Method :: GET , & path, Duration :: from_secs ( 60 ) )
221+ . await
222+ . unwrap ( )
223+ . to_string ( ) ;
224+ assert ! (
225+ url. starts_with( "https://rustfs.xuanwu.openatom.cn/" ) ,
226+ "expected public host, got {url}"
227+ ) ;
228+ assert ! (
229+ !url. contains( "svc.cluster.local" ) ,
230+ "presign URL must not use in-cluster host: {url}"
231+ ) ;
232+ }
233+
234+ #[ tokio:: test]
235+ async fn factory_wires_presign_store_for_compatible ( ) {
236+ use crate :: factory:: ObjectStorageFactory ;
237+
238+ let cfg = ObjectStorageConfig {
239+ storage_type : ObjectStorageBackend :: S3Compatible ,
240+ s3 : sample_s3_config (
241+ "http://rustfs.internal:9000" ,
242+ "https://rustfs.example.public" ,
243+ ) ,
244+ gcs : GcsConfig :: default ( ) ,
245+ local : LocalConfig :: default ( ) ,
246+ } ;
247+ let wrapper = ObjectStorageFactory :: build ( & cfg) . await . unwrap ( ) ;
248+ let key = ObjectKey {
249+ namespace : ObjectNamespace :: OrionImage ,
250+ key : "deadbeef/debian-13-buck2.qcow2" . into ( ) ,
251+ } ;
252+ let url = wrapper
253+ . inner
254+ . signed_url ( & key, Method :: GET , Duration :: from_secs ( 120 ) )
255+ . await
256+ . unwrap ( )
257+ . expect ( "s3compatible must support presign" ) ;
258+ assert ! (
259+ url. starts_with( "https://rustfs.example.public/" ) ,
260+ "factory should sign with presign_endpoint_url, got {url}"
261+ ) ;
262+ }
263+ }
0 commit comments