@@ -68,6 +68,9 @@ use super::FileIOBuilder;
6868/// The storage carries all supported storage services in paimon
6969#[ derive( Debug ) ]
7070pub enum Storage {
71+ /// A caller-provided opendal operator, explicitly scoped to filesystem semantics
72+ /// (see `FileIOBuilder::with_fs_operator`).
73+ CustomFs { op : Operator } ,
7174 #[ cfg( feature = "storage-memory" ) ]
7275 Memory { op : Operator } ,
7376 #[ cfg( feature = "storage-fs" ) ]
@@ -111,7 +114,10 @@ pub enum Storage {
111114
112115impl Storage {
113116 pub ( crate ) fn build ( file_io_builder : FileIOBuilder ) -> crate :: Result < Self > {
114- let ( scheme_str, props) = file_io_builder. into_parts ( ) ;
117+ let ( scheme_str, props, operator) = file_io_builder. into_parts ( ) ;
118+ if let Some ( op) = operator {
119+ return Ok ( Self :: CustomFs { op } ) ;
120+ }
115121 let scheme = scheme_str. to_ascii_lowercase ( ) ;
116122 match scheme. as_str ( ) {
117123 #[ cfg( feature = "storage-memory" ) ]
@@ -186,6 +192,22 @@ impl Storage {
186192
187193 pub ( crate ) fn create < ' a > ( & self , path : & ' a str ) -> crate :: Result < ( Operator , Cow < ' a , str > ) > {
188194 match self {
195+ Storage :: CustomFs { op } => {
196+ // The custom operator is a *filesystem* operator: paths resolve with the
197+ // local-filesystem rules below. A scheme'd path would be silently mangled by
198+ // those rules (`s3://bucket/a` → `3://bucket/a`), so reject it instead —
199+ // object-store operators need the bucket/scheme resolution the built-in
200+ // backends perform, which this hook deliberately does not reimplement.
201+ if !path. starts_with ( "file:/" ) && path. contains ( "://" ) {
202+ return Err ( error:: Error :: ConfigInvalid {
203+ message : format ! (
204+ "the custom operator is a filesystem operator and cannot resolve \
205+ the scheme'd path: {path}"
206+ ) ,
207+ } ) ;
208+ }
209+ Ok ( ( op. clone ( ) , Self :: fs_relative_path ( path) ?) )
210+ }
189211 #[ cfg( feature = "storage-memory" ) ]
190212 Storage :: Memory { op } => {
191213 Ok ( ( op. clone ( ) , Cow :: Borrowed ( Self :: memory_relative_path ( path) ?) ) )
@@ -289,7 +311,6 @@ impl Storage {
289311 /// does `PathBuf::from("/").join("C:/dir")`, and because the argument
290312 /// carries a drive prefix `Path::join` replaces the base, yielding the real
291313 /// `C:\dir` on Windows.
292- #[ cfg( feature = "storage-fs" ) ]
293314 fn fs_relative_path ( path : & str ) -> crate :: Result < Cow < ' _ , str > > {
294315 // A `file://` / `file:/` URL is already in scheme-relative form.
295316 if let Some ( stripped) = path. strip_prefix ( "file:/" ) {
@@ -557,3 +578,40 @@ mod fs_relative_path_tests {
557578 assert_eq ! ( rel( r"C:\Users\wh/db.db/t" ) , "C:/Users/wh/db.db/t" ) ;
558579 }
559580}
581+
582+ #[ cfg( all( test, feature = "storage-memory" ) ) ]
583+ mod tests {
584+ use super :: * ;
585+
586+ fn memory_operator ( ) -> Operator {
587+ Operator :: from_config ( opendal:: services:: MemoryConfig :: default ( ) ) . unwrap ( )
588+ }
589+
590+ /// The filesystem operator resolves absolute paths and `file:` URLs with the
591+ /// local-filesystem rules, against the operator's own root.
592+ #[ test]
593+ fn custom_fs_operator_resolves_filesystem_paths ( ) {
594+ let storage = Storage :: CustomFs {
595+ op : memory_operator ( ) ,
596+ } ;
597+ let ( _, relative) = storage. create ( "/warehouse/db/table" ) . unwrap ( ) ;
598+ assert_eq ! ( relative, "warehouse/db/table" ) ;
599+ let ( _, relative) = storage. create ( "file:/warehouse/db/table" ) . unwrap ( ) ;
600+ assert_eq ! ( relative, "warehouse/db/table" ) ;
601+ }
602+
603+ /// A scheme'd path would be silently mangled by the filesystem rules
604+ /// (`s3://bucket/a` → `3://bucket/a`), so it is rejected instead.
605+ #[ test]
606+ fn custom_fs_operator_rejects_schemed_paths ( ) {
607+ let storage = Storage :: CustomFs {
608+ op : memory_operator ( ) ,
609+ } ;
610+ for path in [ "s3://bucket/a" , "oss://bucket/a" , "hdfs://nn/a" ] {
611+ assert ! (
612+ storage. create( path) . is_err( ) ,
613+ "expected {path} to be rejected by the filesystem operator"
614+ ) ;
615+ }
616+ }
617+ }
0 commit comments