@@ -21,8 +21,8 @@ use std::sync::Arc;
2121/// An error returned when constructing a [`FilesystemStoreV2`].
2222#[ derive( Debug ) ]
2323pub enum FilesystemStoreV2Error {
24- /// The data directory contains a file at the top level , indicating it was previously used
25- /// by [`FilesystemStore`] (v1). Contains the path of the offending file.
24+ /// The data directory contains a file where v2 expects a namespace directory , indicating it
25+ /// was previously used by [`FilesystemStore`] (v1). Contains the path of the offending file.
2626 ///
2727 /// [`FilesystemStore`]: crate::fs_store::v1::FilesystemStore
2828 V1DataDetected ( PathBuf ) ,
@@ -35,7 +35,7 @@ impl fmt::Display for FilesystemStoreV2Error {
3535 match self {
3636 Self :: V1DataDetected ( path) => write ! (
3737 f,
38- "Found file `{}` in the top-level data directory. \
38+ "Found file `{}` where FilesystemStoreV2 expects a namespace directory. \
3939 This indicates the directory was previously used by FilesystemStore (v1). \
4040 Please migrate your data or use a different directory.",
4141 path. display( )
@@ -97,18 +97,28 @@ impl FilesystemStoreV2 {
9797 /// Constructs a new [`FilesystemStoreV2`].
9898 ///
9999 /// Returns [`FilesystemStoreV2Error::V1DataDetected`] if the data directory already exists
100- /// and contains files at the top level , which would indicate it was previously used by a
101- /// [`FilesystemStore`] (v1). The v2 store expects only directories (namespaces) at the top
102- /// level.
100+ /// and contains files where v2 expects namespace directories , which would indicate it was
101+ /// previously used by a [`FilesystemStore`] (v1). The v2 store expects only directories at
102+ /// the top level and one level down .
103103 ///
104104 /// [`FilesystemStore`]: crate::fs_store::v1::FilesystemStore
105105 pub fn new ( data_dir : PathBuf ) -> Result < Self , FilesystemStoreV2Error > {
106106 if data_dir. exists ( ) {
107107 for entry in fs:: read_dir ( & data_dir) ? {
108108 let entry = entry?;
109- if entry. file_type ( ) ?. is_file ( ) {
109+ let file_type = entry. file_type ( ) ?;
110+ if file_type. is_file ( ) {
110111 return Err ( FilesystemStoreV2Error :: V1DataDetected ( entry. path ( ) ) ) ;
111112 }
113+
114+ if file_type. is_dir ( ) {
115+ for child_entry in fs:: read_dir ( entry. path ( ) ) ? {
116+ let child_entry = child_entry?;
117+ if child_entry. file_type ( ) ?. is_file ( ) {
118+ return Err ( FilesystemStoreV2Error :: V1DataDetected ( child_entry. path ( ) ) ) ;
119+ }
120+ }
121+ }
112122 }
113123 }
114124
@@ -699,6 +709,7 @@ mod tests {
699709 fs:: create_dir_all ( & temp_path) . unwrap ( ) ;
700710
701711 // Create a file at the top level, as v1 would for an empty primary namespace
712+ // and an empty secondary namespace.
702713 fs:: write ( temp_path. join ( "some_key" ) , b"data" ) . unwrap ( ) ;
703714
704715 // V2 construction should fail
@@ -713,13 +724,59 @@ mod tests {
713724 // Clean up
714725 let _ = fs:: remove_dir_all ( & temp_path) ;
715726
727+ // Create a file one level down, as v1 would for a non-empty primary namespace
728+ // and an empty secondary namespace.
729+ fs:: create_dir_all ( temp_path. join ( "some_namespace" ) ) . unwrap ( ) ;
730+ fs:: write ( temp_path. join ( "some_namespace" ) . join ( "some_key" ) , b"data" ) . unwrap ( ) ;
731+
732+ match FilesystemStoreV2 :: new ( temp_path. clone ( ) ) {
733+ Err ( FilesystemStoreV2Error :: V1DataDetected ( path) ) => {
734+ assert_eq ! ( path, temp_path. join( "some_namespace" ) . join( "some_key" ) ) ;
735+ } ,
736+ Err ( err) => panic ! ( "Expected V1DataDetected, got {:?}" , err) ,
737+ Ok ( _) => panic ! ( "Expected error for directory with files one level down" ) ,
738+ }
739+
740+ let _ = fs:: remove_dir_all ( & temp_path) ;
741+
742+ // A v1 write with an empty primary namespace and non-empty secondary namespace
743+ // is rejected by the KVStore API, but its filesystem layout would be the same
744+ // one-level shape.
745+ fs:: create_dir_all ( temp_path. join ( "some_secondary_namespace" ) ) . unwrap ( ) ;
746+ fs:: write ( temp_path. join ( "some_secondary_namespace" ) . join ( "some_key" ) , b"data" ) . unwrap ( ) ;
747+
748+ match FilesystemStoreV2 :: new ( temp_path. clone ( ) ) {
749+ Err ( FilesystemStoreV2Error :: V1DataDetected ( path) ) => {
750+ assert_eq ! ( path, temp_path. join( "some_secondary_namespace" ) . join( "some_key" ) ) ;
751+ } ,
752+ Err ( err) => panic ! ( "Expected V1DataDetected, got {:?}" , err) ,
753+ Ok ( _) => panic ! ( "Expected error for directory with files one level down" ) ,
754+ }
755+
756+ let _ = fs:: remove_dir_all ( & temp_path) ;
757+
716758 // An empty directory should succeed
717759 fs:: create_dir_all ( & temp_path) . unwrap ( ) ;
718760 let result = FilesystemStoreV2 :: new ( temp_path. clone ( ) ) ;
719761 assert ! ( result. is_ok( ) ) ;
720762
721- // A directory with only subdirectories should succeed
722- fs:: create_dir_all ( temp_path. join ( "some_namespace" ) ) . unwrap ( ) ;
763+ // A directory with only namespace subdirectories should succeed
764+ fs:: create_dir_all ( temp_path. join ( "some_namespace" ) . join ( "some_sub_namespace" ) ) . unwrap ( ) ;
765+ let result = FilesystemStoreV2 :: new ( temp_path. clone ( ) ) ;
766+ assert ! ( result. is_ok( ) ) ;
767+
768+ // V1 data with non-empty primary and secondary namespaces has the same filesystem
769+ // layout as valid v2 data, so construction must not reject this shape.
770+ let fs_store = result. unwrap ( ) ;
771+ KVStoreSync :: write (
772+ & fs_store,
773+ "some_namespace" ,
774+ "some_sub_namespace" ,
775+ "some_key" ,
776+ b"data" . to_vec ( ) ,
777+ )
778+ . unwrap ( ) ;
779+
723780 let result = FilesystemStoreV2 :: new ( temp_path) ;
724781 assert ! ( result. is_ok( ) ) ;
725782 }
0 commit comments