@@ -7,6 +7,7 @@ use libcnb::build::BuildContext;
77use libcnb:: data:: layer:: LayerName ;
88use std:: path:: Path ;
99use std:: path:: PathBuf ;
10+ use walkdir:: WalkDir ;
1011
1112use tempfile as _;
1213
@@ -126,9 +127,12 @@ impl AppCache {
126127 /// - If the files cannot be moved/coppied into the cache
127128 /// then then an error will be raised.
128129 pub fn save ( & self ) -> Result < & AppCache , CacheError > {
130+ save ( self ) ?;
129131 match self . keep_path {
130- KeepPath :: Runtime => preserve_path_save ( self ) ?,
131- KeepPath :: BuildOnly => remove_path_save ( self ) ?,
132+ KeepPath :: Runtime => { }
133+ KeepPath :: BuildOnly => {
134+ fs_err:: remove_dir_all ( & self . path ) . map_err ( CacheError :: IoError ) ?;
135+ }
132136 } ;
133137
134138 Ok ( self )
@@ -148,7 +152,7 @@ impl AppCache {
148152 fs_err:: create_dir_all ( & self . path ) . map_err ( CacheError :: IoError ) ?;
149153 fs_err:: create_dir_all ( & self . cache ) . map_err ( CacheError :: IoError ) ?;
150154
151- fs_extra:: dir:: move_dir (
155+ fs_extra:: dir:: copy (
152156 & self . cache ,
153157 & self . path ,
154158 & CopyOptions {
@@ -164,6 +168,9 @@ impl AppCache {
164168 cache : self . cache . clone ( ) ,
165169 error,
166170 } ) ?;
171+ copy_mtime_r ( & self . cache , & self . path ) ?;
172+
173+ fs_err:: remove_dir_all ( & self . cache ) . map_err ( CacheError :: IoError ) ?;
167174
168175 Ok ( self )
169176 }
@@ -275,7 +282,7 @@ pub fn build<B: libcnb::Buildpack>(
275282/// # Errors
276283///
277284/// - If the copy command fails an `IoExtraError` will be raised.
278- fn preserve_path_save ( store : & AppCache ) -> Result < & AppCache , CacheError > {
285+ fn save ( store : & AppCache ) -> Result < & AppCache , CacheError > {
279286 fs_extra:: dir:: copy (
280287 & store. path ,
281288 & store. cache ,
@@ -292,37 +299,40 @@ fn preserve_path_save(store: &AppCache) -> Result<&AppCache, CacheError> {
292299 error,
293300 } ) ?;
294301
302+ copy_mtime_r ( & store. path , & store. cache ) ?;
303+
295304 Ok ( store)
296305}
297306
298- /// Move contents of application path into the cache
299- ///
300- /// This action is destructive, after execution the application path
301- /// will be empty. Files from the application path are considered
302- /// cannonical and will overwrite files with the same name in the
303- /// cache.
304- ///
305- /// # Errors
307+ /// Copies the mtime information from a path to another path
306308///
307- /// - If the move command fails an `IoExtraError` will be raised.
308- fn remove_path_save ( store : & AppCache ) -> Result < & AppCache , CacheError > {
309- fs_extra:: dir:: move_dir (
310- & store. path ,
311- & store. cache ,
312- & CopyOptions {
313- overwrite : true ,
314- copy_inside : true , // Recursive
315- content_only : true , // Don't copy top level directory name
316- ..CopyOptions :: default ( )
317- } ,
318- )
319- . map_err ( |error| CacheError :: DestructiveMoveAppToCacheError {
320- path : store. path . clone ( ) ,
321- cache : store. cache . clone ( ) ,
322- error,
323- } ) ?;
324-
325- Ok ( store)
309+ /// This is information used for the LRU cleaner so that older files are removed first.
310+ fn copy_mtime_r ( from : & Path , to_path : & Path ) -> Result < ( ) , CacheError > {
311+ for entry in WalkDir :: new ( from) . into_iter ( ) . filter_map ( Result :: ok) {
312+ let relative = entry
313+ . path ( )
314+ . strip_prefix ( from)
315+ . expect ( "Walkdir path should return path with prefix of called root" ) ;
316+
317+ let mtime = entry
318+ . metadata ( )
319+ . map_err ( |error| std:: io:: Error :: new ( std:: io:: ErrorKind :: Other , error) )
320+ . map ( |metadata| filetime:: FileTime :: from_last_modification_time ( & metadata) )
321+ . map_err ( |error| CacheError :: Mtime {
322+ from : entry. path ( ) . to_path_buf ( ) ,
323+ to_path : to_path. join ( relative) ,
324+ error,
325+ } ) ?;
326+
327+ filetime:: set_file_mtime ( to_path. join ( relative) , mtime) . map_err ( |error| {
328+ CacheError :: Mtime {
329+ from : entry. path ( ) . to_path_buf ( ) ,
330+ to_path : to_path. join ( relative) ,
331+ error,
332+ }
333+ } ) ?;
334+ }
335+ Ok ( ( ) )
326336}
327337
328338/// Converts a path inside of an app to a valid layer name for libcnb.
@@ -374,11 +384,10 @@ fn is_empty_dir(path: &Path) -> bool {
374384
375385#[ cfg( test) ]
376386mod tests {
377- use std:: str:: FromStr ;
378-
379- use libcnb:: data:: layer_name;
380-
381387 use super :: * ;
388+ use filetime:: FileTime ;
389+ use libcnb:: data:: layer_name;
390+ use std:: str:: FromStr ;
382391
383392 #[ test]
384393 fn test_to_layer_name ( ) {
@@ -387,6 +396,52 @@ mod tests {
387396 assert_eq ! ( layer_name!( "cache_my_input" ) , layer) ;
388397 }
389398
399+ #[ test]
400+ fn test_layer_name_cache_state ( ) {
401+ let layer_name = layer_name ! ( "name" ) ;
402+ let tempdir = tempfile:: tempdir ( ) . unwrap ( ) ;
403+ let path = tempdir. path ( ) ;
404+ assert_eq ! (
405+ CacheState :: NewEmpty ,
406+ layer_name_cache_state( path, & layer_name)
407+ ) ;
408+ fs_err:: create_dir_all ( path. join ( layer_name. as_str ( ) ) ) . unwrap ( ) ;
409+ assert_eq ! (
410+ CacheState :: ExistsEmpty ,
411+ layer_name_cache_state( path, & layer_name)
412+ ) ;
413+ fs_err:: write ( path. join ( layer_name. as_str ( ) ) . join ( "file" ) , "data" ) . unwrap ( ) ;
414+ assert_eq ! (
415+ CacheState :: ExistsWithContents ,
416+ layer_name_cache_state( path, & layer_name)
417+ ) ;
418+ }
419+
420+ #[ test]
421+ fn test_load_does_not_clobber_files ( ) {
422+ let tmpdir = tempfile:: tempdir ( ) . unwrap ( ) ;
423+ let cache_path = tmpdir. path ( ) . join ( "cache" ) ;
424+ let app_path = tmpdir. path ( ) . join ( "app" ) ;
425+ fs_err:: create_dir_all ( & cache_path) . unwrap ( ) ;
426+ fs_err:: create_dir_all ( & app_path) . unwrap ( ) ;
427+
428+ fs_err:: write ( app_path. join ( "a.txt" ) , "app" ) . unwrap ( ) ;
429+ fs_err:: write ( cache_path. join ( "a.txt" ) , "cache" ) . unwrap ( ) ;
430+
431+ let store = AppCache {
432+ path : app_path. clone ( ) ,
433+ cache : cache_path,
434+ limit : Byte :: from_u64 ( 512 ) ,
435+ keep_path : KeepPath :: Runtime ,
436+ cache_state : CacheState :: NewEmpty ,
437+ } ;
438+
439+ store. load ( ) . unwrap ( ) ;
440+
441+ let contents = fs_err:: read_to_string ( app_path. join ( "a.txt" ) ) . unwrap ( ) ;
442+ assert_eq ! ( "app" , contents) ;
443+ }
444+
390445 #[ test]
391446 fn test_copying_back_to_cache ( ) {
392447 let tmpdir = tempfile:: tempdir ( ) . unwrap ( ) ;
@@ -448,4 +503,89 @@ mod tests {
448503 assert ! ( store. cache. join( "lol.txt" ) . exists( ) ) ;
449504 assert ! ( !store. path. join( "lol.txt" ) . exists( ) ) ;
450505 }
506+
507+ #[ test]
508+ fn mtime_preserved_keep_path_build_only ( ) {
509+ let mtime = FileTime :: from_unix_time ( 1000 , 0 ) ;
510+ let tmpdir = tempfile:: tempdir ( ) . unwrap ( ) ;
511+ let filename = "totoro.txt" ;
512+ let app_path = tmpdir. path ( ) . join ( "app" ) ;
513+ let cache_path = tmpdir. path ( ) . join ( "cache" ) ;
514+
515+ fs_err:: create_dir_all ( & cache_path) . unwrap ( ) ;
516+ fs_err:: create_dir_all ( & app_path) . unwrap ( ) ;
517+
518+ let store = AppCache {
519+ path : app_path. clone ( ) ,
520+ cache : cache_path. clone ( ) ,
521+ limit : Byte :: from_u64 ( 512 ) ,
522+ keep_path : KeepPath :: BuildOnly ,
523+ cache_state : CacheState :: NewEmpty ,
524+ } ;
525+
526+ fs_err:: write ( app_path. join ( filename) , "catbus" ) . unwrap ( ) ;
527+ filetime:: set_file_mtime ( app_path. join ( filename) , mtime) . unwrap ( ) ;
528+
529+ store. save ( ) . unwrap ( ) ;
530+
531+ // Cache file mtime should match app file mtime
532+ let actual = fs_err:: metadata ( cache_path. join ( filename) )
533+ . map ( |metadata| FileTime :: from_last_modification_time ( & metadata) )
534+ . unwrap ( ) ;
535+ assert_eq ! ( mtime, actual) ;
536+
537+ // File was removed already after save
538+ assert ! ( !store. path. join( filename) . exists( ) ) ;
539+
540+ store. load ( ) . unwrap ( ) ;
541+
542+ // App path mtime should match cache file mtime
543+ let actual = fs_err:: metadata ( app_path. join ( filename) )
544+ . map ( |metadata| FileTime :: from_last_modification_time ( & metadata) )
545+ . unwrap ( ) ;
546+ assert_eq ! ( mtime, actual) ;
547+ }
548+
549+ #[ test]
550+ fn mtime_preserved_keep_path_runtime ( ) {
551+ let mtime = FileTime :: from_unix_time ( 1000 , 0 ) ;
552+ let tmpdir = tempfile:: tempdir ( ) . unwrap ( ) ;
553+ let filename = "totoro.txt" ;
554+ let app_path = tmpdir. path ( ) . join ( "app" ) ;
555+ let cache_path = tmpdir. path ( ) . join ( "cache" ) ;
556+
557+ fs_err:: create_dir_all ( & cache_path) . unwrap ( ) ;
558+ fs_err:: create_dir_all ( & app_path) . unwrap ( ) ;
559+
560+ let store = AppCache {
561+ path : app_path. clone ( ) ,
562+ cache : cache_path. clone ( ) ,
563+ limit : Byte :: from_u64 ( 512 ) ,
564+ keep_path : KeepPath :: Runtime ,
565+ cache_state : CacheState :: NewEmpty ,
566+ } ;
567+
568+ fs_err:: write ( app_path. join ( filename) , "catbus" ) . unwrap ( ) ;
569+ filetime:: set_file_mtime ( app_path. join ( filename) , mtime) . unwrap ( ) ;
570+
571+ store. save ( ) . unwrap ( ) ;
572+
573+ // Cache file mtime should match app file mtime
574+ let actual = fs_err:: metadata ( cache_path. join ( filename) )
575+ . map ( |metadata| FileTime :: from_last_modification_time ( & metadata) )
576+ . unwrap ( ) ;
577+ assert_eq ! ( mtime, actual) ;
578+
579+ // Remove app path to test loading
580+ fs_err:: remove_dir_all ( & app_path) . unwrap ( ) ;
581+ assert ! ( !store. path. join( filename) . exists( ) ) ;
582+
583+ store. load ( ) . unwrap ( ) ;
584+
585+ // App path mtime should match cache file mtime
586+ let actual = fs_err:: metadata ( app_path. join ( filename) )
587+ . map ( |metadata| FileTime :: from_last_modification_time ( & metadata) )
588+ . unwrap ( ) ;
589+ assert_eq ! ( mtime, actual) ;
590+ }
451591}
0 commit comments