Skip to content

Commit f21dfcc

Browse files
authored
Close #302 Copy Mtimes in the AppCache (#337)
* Test loading cache does not overwrite files When copying from the cache back into the app, it should not replace files that already exist. This test asserts already passing behavior. * Add test for code coverage * Group import lines * Test mtime save/load behavior Adds tests for ensuring FIFO behavior in the cache works based on mime types. This comment #302 (comment). Currently it works locally but fails on CI, that tells me the mtime-preserving behavior is platform dependent. * Commons changelog * Copy and delete instead of moving files * Manually copy mtime after copying directory contents * Proper error handling * Remove nearly identical function * Add docs * Copy cache and delete instead of moving
1 parent cc50d62 commit f21dfcc

File tree

4 files changed

+189
-35
lines changed

4 files changed

+189
-35
lines changed

commons/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog for commons features
22

3+
## 2024-08-16
4+
5+
### Fixed
6+
7+
- `AppCache` will now preserve mtime of files when copying them to/from the cache (https://github.com/heroku/buildpacks-ruby/pull/336)
8+
39
## 2024-08-15
410

511
### Changed

commons/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ sha2 = "0.10"
3232
tempfile = "3"
3333
thiserror = "1"
3434
walkdir = "2"
35+
filetime = "0.2"
3536

3637
[dev-dependencies]
3738
filetime = "0.2"

commons/src/cache/app_cache.rs

Lines changed: 175 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use libcnb::build::BuildContext;
77
use libcnb::data::layer::LayerName;
88
use std::path::Path;
99
use std::path::PathBuf;
10+
use walkdir::WalkDir;
1011

1112
use 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)]
376386
mod 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
}

commons/src/cache/error.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,13 @@ pub enum CacheError {
3131
error: fs_extra::error::Error,
3232
},
3333

34+
#[error("Cannot copy mtime. From: {from} To: {to_path}\nError: {error}")]
35+
Mtime {
36+
from: PathBuf,
37+
to_path: PathBuf,
38+
error: std::io::Error,
39+
},
40+
3441
#[error("IO error: {0}")]
3542
IoError(std::io::Error),
3643

0 commit comments

Comments
 (0)