@@ -65,6 +65,7 @@ use crate::{
6565pub struct OverlayFileSystem < P , S > {
6666 primary : Arc < P > ,
6767 secondaries : S ,
68+ opaque_prefixes : Vec < PathBuf > ,
6869}
6970
7071impl < P , S > OverlayFileSystem < P , S >
7879 OverlayFileSystem {
7980 primary : Arc :: new ( primary) ,
8081 secondaries,
82+ opaque_prefixes : Vec :: new ( ) ,
83+ }
84+ }
85+
86+ /// Create a new [`FileSystem`] with opaque prefixes that hide any secondary
87+ /// entries under those paths.
88+ pub fn new_with_opaque_prefixes (
89+ primary : P ,
90+ secondaries : S ,
91+ opaque_prefixes : Vec < PathBuf > ,
92+ ) -> Self {
93+ OverlayFileSystem {
94+ primary : Arc :: new ( primary) ,
95+ secondaries,
96+ opaque_prefixes,
8197 }
8298 }
8399
@@ -96,8 +112,31 @@ where
96112 & mut self . secondaries
97113 }
98114
115+ fn is_opaque ( & self , path : & Path ) -> bool {
116+ let normalized = if path. is_absolute ( ) {
117+ path. to_path_buf ( )
118+ } else {
119+ Path :: new ( "/" ) . join ( path)
120+ } ;
121+
122+ self . opaque_prefixes
123+ . iter ( )
124+ . any ( |prefix| normalized. starts_with ( prefix) )
125+ }
126+
127+ fn secondaries_iter < ' a > (
128+ & ' a self ,
129+ path : & Path ,
130+ ) -> Box < dyn Iterator < Item = & ' a ( dyn FileSystem + Send ) > + ' a > {
131+ if self . is_opaque ( path) {
132+ Box :: new ( std:: iter:: empty ( ) )
133+ } else {
134+ Box :: new ( self . secondaries . filesystems ( ) . into_iter ( ) )
135+ }
136+ }
137+
99138 fn permission_error_or_not_found ( & self , path : & Path ) -> Result < ( ) , FsError > {
100- for fs in self . secondaries . filesystems ( ) {
139+ for fs in self . secondaries_iter ( path ) {
101140 if ops:: exists ( fs, path) {
102141 return Err ( FsError :: PermissionDenied ) ;
103142 }
@@ -132,7 +171,7 @@ where
132171 }
133172
134173 // Otherwise scan the secondaries
135- for fs in self . secondaries . filesystems ( ) {
174+ for fs in self . secondaries_iter ( path ) {
136175 match fs. readlink ( path) {
137176 Err ( e) if should_continue ( e) => continue ,
138177 other => return other,
@@ -148,7 +187,7 @@ where
148187 let mut white_outs = HashSet :: new ( ) ;
149188
150189 let filesystems = std:: iter:: once ( & self . primary as & ( dyn FileSystem + Send ) )
151- . chain ( self . secondaries ( ) . filesystems ( ) ) ;
190+ . chain ( self . secondaries_iter ( path ) ) ;
152191
153192 for fs in filesystems {
154193 match fs. read_dir ( path) {
@@ -239,7 +278,7 @@ where
239278 // If the directory is contained in a secondary file system then we need to create a
240279 // whiteout file so that it is suppressed and is no longer returned in `readdir` calls.
241280
242- let had_at_least_one_success = self . secondaries . filesystems ( ) . into_iter ( ) . any ( |fs| {
281+ let had_at_least_one_success = self . secondaries_iter ( path ) . any ( |fs| {
243282 fs. read_dir ( path) . is_ok ( ) && ops:: create_white_out ( & self . primary , path) . is_ok ( )
244283 } ) ;
245284
@@ -297,7 +336,8 @@ where
297336 // the secondaries, in which case we need to copy it to the
298337 // primary rather than rename it
299338 if !had_at_least_one_success {
300- for fs in self . secondaries . filesystems ( ) {
339+ let secondaries: Vec < _ > = self . secondaries_iter ( & from) . collect ( ) ;
340+ for fs in secondaries {
301341 if fs. metadata ( & from) . is_ok ( ) {
302342 ops:: copy_reference_ext ( fs, & self . primary , & from, & to) . await ?;
303343 had_at_least_one_success = true ;
@@ -309,7 +349,8 @@ where
309349 // If the rename operation was a success then we need to update any
310350 // whiteout files on the primary before we return success.
311351 if had_at_least_one_success {
312- for fs in self . secondaries . filesystems ( ) {
352+ let secondaries: Vec < _ > = self . secondaries_iter ( & from) . collect ( ) ;
353+ for fs in secondaries {
313354 if fs. metadata ( & from) . is_ok ( ) {
314355 tracing:: trace!(
315356 path=%from. display( ) ,
@@ -347,7 +388,7 @@ where
347388 }
348389
349390 // Otherwise scan the secondaries
350- for fs in self . secondaries . filesystems ( ) {
391+ for fs in self . secondaries_iter ( path ) {
351392 match fs. metadata ( path) {
352393 Err ( e) if should_continue ( e) => continue ,
353394 other => return other,
@@ -376,7 +417,7 @@ where
376417 }
377418
378419 // Otherwise scan the secondaries
379- for fs in self . secondaries . filesystems ( ) {
420+ for fs in self . secondaries_iter ( path ) {
380421 match fs. symlink_metadata ( path) {
381422 Err ( e) if should_continue ( e) => continue ,
382423 other => return other,
@@ -395,7 +436,7 @@ where
395436
396437 // If the file is contained in a secondary then then we need to create a
397438 // whiteout file so that it is suppressed.
398- let had_at_least_one_success = self . secondaries . filesystems ( ) . into_iter ( ) . any ( |fs| {
439+ let had_at_least_one_success = self . secondaries_iter ( path ) . any ( |fs| {
399440 fs. metadata ( path) . is_ok ( ) && ops:: create_white_out ( & self . primary , path) . is_ok ( )
400441 } ) ;
401442
@@ -503,7 +544,7 @@ where
503544
504545 // If the file is on a secondary then we should open it
505546 if !ops:: has_white_out ( & self . primary , path) {
506- for fs in self . secondaries . filesystems ( ) {
547+ for fs in self . secondaries_iter ( path ) {
507548 let mut sub_conf = conf. clone ( ) ;
508549 sub_conf. create = false ;
509550 sub_conf. create_new = false ;
@@ -1169,6 +1210,72 @@ mod tests {
11691210 ) ;
11701211 }
11711212
1213+ #[ test]
1214+ fn overlay_opaque_prefix_hides_secondaries ( ) {
1215+ let primary = MemFS :: default ( ) ;
1216+ let secondary = MemFS :: default ( ) ;
1217+
1218+ ops:: create_dir_all ( & primary, "/app/wp-content" ) . unwrap ( ) ;
1219+ primary
1220+ . new_open_options ( )
1221+ . create ( true )
1222+ . write ( true )
1223+ . open ( "/app/wp-content/host.txt" )
1224+ . unwrap ( ) ;
1225+
1226+ ops:: create_dir_all ( & secondary, "/app/wp-content/themes/twentyten" ) . unwrap ( ) ;
1227+
1228+ let overlay = OverlayFileSystem :: new_with_opaque_prefixes (
1229+ primary,
1230+ [ secondary] ,
1231+ vec ! [ PathBuf :: from( "/app/wp-content" ) ] ,
1232+ ) ;
1233+
1234+ let entries: Vec < _ > = overlay
1235+ . read_dir ( Path :: new ( "/app/wp-content" ) )
1236+ . unwrap ( )
1237+ . map ( |entry| entry. unwrap ( ) . path )
1238+ . collect ( ) ;
1239+
1240+ assert_eq ! ( entries, vec![ PathBuf :: from( "/app/wp-content/host.txt" ) ] ) ;
1241+ assert_eq ! (
1242+ overlay
1243+ . metadata( Path :: new( "/app/wp-content/themes" ) )
1244+ . unwrap_err( ) ,
1245+ FsError :: EntryNotFound
1246+ ) ;
1247+ }
1248+
1249+ #[ test]
1250+ fn overlay_opaque_prefix_prevents_parent_copy_up_on_create ( ) {
1251+ let primary = MemFS :: default ( ) ;
1252+ let secondary = MemFS :: default ( ) ;
1253+
1254+ ops:: create_dir_all ( & secondary, "/app/wp-content/themes" ) . unwrap ( ) ;
1255+
1256+ let overlay = OverlayFileSystem :: new_with_opaque_prefixes (
1257+ primary,
1258+ [ secondary] ,
1259+ vec ! [ PathBuf :: from( "/app/wp-content" ) ] ,
1260+ ) ;
1261+
1262+ let err = overlay
1263+ . new_open_options ( )
1264+ . create ( true )
1265+ . write ( true )
1266+ . open ( "/app/wp-content/themes/foo.txt" )
1267+ . unwrap_err ( ) ;
1268+ assert_eq ! ( err, FsError :: EntryNotFound ) ;
1269+
1270+ assert_eq ! (
1271+ overlay
1272+ . primary( )
1273+ . metadata( Path :: new( "/app/wp-content/themes" ) )
1274+ . unwrap_err( ) ,
1275+ FsError :: EntryNotFound
1276+ ) ;
1277+ }
1278+
11721279 #[ tokio:: test]
11731280 async fn remove_directory ( ) {
11741281 let primary = MemFS :: default ( ) ;
0 commit comments