88mod backend;
99mod metadata;
1010
11+ use bytes:: BytesMut ;
12+ use futures_util:: { StreamExt , TryStreamExt } ;
1113use objectstore_types:: { Metadata , Scope } ;
1214
1315use std:: path:: Path ;
@@ -18,17 +20,21 @@ use crate::backend::{BackendStream, BoxedBackend};
1820pub use backend:: BigTableConfig ;
1921pub use metadata:: * ;
2022
23+ /// The threshold up until which we will go to the small backend.
24+ const SMALL_THRESHOLD : usize = 50 * 1024 ; // 50 KiB
25+
2126/// High-level asynchronous service for storing and retrieving objects.
2227#[ derive( Clone , Debug ) ]
2328pub struct StorageService ( Arc < StorageServiceInner > ) ;
2429
2530#[ derive( Debug ) ]
2631struct StorageServiceInner {
27- backend : BoxedBackend ,
32+ small_backend : BoxedBackend ,
33+ large_backend : BoxedBackend ,
2834}
2935
3036/// Configuration to initialize a [`StorageService`].
31- #[ derive( Debug ) ]
37+ #[ derive( Debug , Clone ) ]
3238pub enum StorageConfig < ' a > {
3339 /// Use a local filesystem as the storage backend.
3440 FileSystem {
@@ -48,22 +54,17 @@ pub enum StorageConfig<'a> {
4854
4955impl StorageService {
5056 /// Creates a new `StorageService` with the specified configuration.
51- pub async fn new ( config : StorageConfig < ' _ > ) -> anyhow:: Result < Self > {
52- let backend = match config {
53- StorageConfig :: FileSystem { path } => Box :: new ( backend:: LocalFs :: new ( path) ) ,
54- StorageConfig :: S3Compatible { endpoint, bucket } => {
55- if let Some ( endpoint) = endpoint {
56- Box :: new ( backend:: S3Compatible :: without_token ( endpoint, bucket) )
57- } else {
58- backend:: gcs ( bucket) . await ?
59- }
60- }
61- StorageConfig :: BigTable ( config) => {
62- Box :: new ( backend:: BigTableBackend :: new ( config) . await ?)
63- }
57+ pub async fn new (
58+ small_config : StorageConfig < ' _ > ,
59+ large_config : StorageConfig < ' _ > ,
60+ ) -> anyhow:: Result < Self > {
61+ let small_backend = create_backend ( small_config) . await ?;
62+ let large_backend = create_backend ( large_config) . await ?;
63+
64+ let inner = StorageServiceInner {
65+ small_backend,
66+ large_backend,
6467 } ;
65-
66- let inner = StorageServiceInner { backend } ;
6768 Ok ( Self ( Arc :: new ( inner) ) )
6869 }
6970
@@ -73,16 +74,33 @@ impl StorageService {
7374 usecase : String ,
7475 scope : Scope ,
7576 metadata : & Metadata ,
76- stream : BackendStream ,
77+ mut stream : BackendStream ,
7778 ) -> anyhow:: Result < ScopedKey > {
78- let key = ObjectKey :: for_backend ( 1 ) ;
79+ let mut first_chunk = BytesMut :: new ( ) ;
80+ let mut backend_id = 1 ; // 1 = small files backend
81+ while let Some ( chunk) = stream. try_next ( ) . await ? {
82+ first_chunk. extend_from_slice ( & chunk) ;
83+
84+ if first_chunk. len ( ) > SMALL_THRESHOLD {
85+ backend_id = 2 ; // 2 = large files backend
86+ break ;
87+ }
88+ }
89+ let stream = futures_util:: stream:: once ( async { Ok ( first_chunk. into ( ) ) } )
90+ . chain ( stream)
91+ . boxed ( ) ;
92+
93+ let key = ObjectKey :: for_backend ( backend_id) ;
7994 let key = ScopedKey {
8095 usecase,
8196 scope,
8297 key,
8398 } ;
8499
85- self . 0 . backend . put_object ( & key, metadata, stream) . await ?;
100+ self . 0
101+ . small_backend
102+ . put_object ( & key, metadata, stream)
103+ . await ?;
86104 Ok ( key)
87105 }
88106
@@ -91,15 +109,37 @@ impl StorageService {
91109 & self ,
92110 key : & ScopedKey ,
93111 ) -> anyhow:: Result < Option < ( Metadata , BackendStream ) > > {
94- self . 0 . backend . get_object ( key) . await
112+ match key. key . backend {
113+ 1 => self . 0 . small_backend . get_object ( key) . await ,
114+ 2 => self . 0 . large_backend . get_object ( key) . await ,
115+ _ => anyhow:: bail!( "invalid backend" ) ,
116+ }
95117 }
96118
97119 /// Deletes an object stored at the given key, if it exists.
98120 pub async fn delete_object ( & self , key : & ScopedKey ) -> anyhow:: Result < ( ) > {
99- self . 0 . backend . delete_object ( key) . await
121+ match key. key . backend {
122+ 1 => self . 0 . small_backend . delete_object ( key) . await ,
123+ 2 => self . 0 . large_backend . delete_object ( key) . await ,
124+ _ => anyhow:: bail!( "invalid backend" ) ,
125+ }
100126 }
101127}
102128
129+ async fn create_backend ( config : StorageConfig < ' _ > ) -> anyhow:: Result < BoxedBackend > {
130+ Ok ( match config {
131+ StorageConfig :: FileSystem { path } => Box :: new ( backend:: LocalFs :: new ( path) ) ,
132+ StorageConfig :: S3Compatible { endpoint, bucket } => {
133+ if let Some ( endpoint) = endpoint {
134+ Box :: new ( backend:: S3Compatible :: without_token ( endpoint, bucket) )
135+ } else {
136+ backend:: gcs ( bucket) . await ?
137+ }
138+ }
139+ StorageConfig :: BigTable ( config) => Box :: new ( backend:: BigTableBackend :: new ( config) . await ?) ,
140+ } )
141+ }
142+
103143#[ cfg( test) ]
104144mod tests {
105145 use bytes:: BytesMut ;
@@ -118,7 +158,7 @@ mod tests {
118158 let config = StorageConfig :: FileSystem {
119159 path : tempdir. path ( ) ,
120160 } ;
121- let service = StorageService :: new ( config) . await . unwrap ( ) ;
161+ let service = StorageService :: new ( config. clone ( ) , config ) . await . unwrap ( ) ;
122162
123163 let key = service
124164 . put_object (
@@ -146,7 +186,7 @@ mod tests {
146186 endpoint : None ,
147187 bucket : "sbx-warp-benchmark-bucket" ,
148188 } ;
149- let service = StorageService :: new ( config) . await . unwrap ( ) ;
189+ let service = StorageService :: new ( config. clone ( ) , config ) . await . unwrap ( ) ;
150190
151191 let key = service
152192 . put_object (
@@ -174,7 +214,7 @@ mod tests {
174214 endpoint : Some ( "http://localhost:8333" ) ,
175215 bucket : "whatever" ,
176216 } ;
177- let service = StorageService :: new ( config) . await . unwrap ( ) ;
217+ let service = StorageService :: new ( config. clone ( ) , config ) . await . unwrap ( ) ;
178218
179219 let key = service
180220 . put_object (
0 commit comments