Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions crates/turborepo-cache/src/cache_archive/restore.rs
Original file line number Diff line number Diff line change
Expand Up @@ -934,4 +934,48 @@ mod tests {

Ok(())
}

#[test]
fn test_restore_with_existing_symlink_does_not_overwrite_target() {
let input_dir = tempdir().unwrap();
let input_files = &[
TarFile::Directory {
path: AnchoredSystemPathBuf::from_raw("dist").unwrap(),
},
TarFile::File {
path: AnchoredSystemPathBuf::from_raw("dist/index.js").unwrap(),
body: b"console.log('hello')".to_vec(),
},
];
let archive_path = generate_tar(&input_dir, input_files).unwrap();
// create a symlink to something in the output
let output_dir = tempdir().unwrap();
let output_path = AbsoluteSystemPath::from_std_path(output_dir.path()).unwrap();

// Create dist -> src symlink
let output_dist = output_path.join_component("dist");
let output_src = output_path.join_component("src");
output_src.create_dir_all().unwrap();
output_dist.symlink_to_dir("src").unwrap();

let output_dir_path = output_dir.path().to_string_lossy();
let anchor = AbsoluteSystemPath::new(&output_dir_path).unwrap();

assert!(
!output_src.join_component("index.js").try_exists().unwrap(),
"src is empty before restore"
);

let mut cache_reader = CacheReader::open(&archive_path).unwrap();

let actual = cache_reader.restore(anchor).unwrap();
assert_eq!(
actual,
into_anchored_system_path_vec(vec!["dist", "dist/index.js",])
);
assert!(
!output_src.join_component("index.js").try_exists().unwrap(),
"src should remain empty after restore"
);
}
}
69 changes: 68 additions & 1 deletion crates/turborepo-cache/src/cache_archive/restore_directory.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::{backtrace::Backtrace, ffi::OsString, io};
use std::{backtrace::Backtrace, ffi::OsString, fs, io};

use camino::Utf8Component;
use tar::Entry;
Expand Down Expand Up @@ -92,6 +92,73 @@ impl CachedDirTree {
//
// This could _still_ error, but we don't care.
let resolved_name = anchor.resolve(processed_name);

// Before attempting to create the directory, check if there's a symlink
// at this location that would cause cache restoration to the wrong location.
if let Ok(metadata) = resolved_name.symlink_metadata() {
if metadata.is_symlink() {
debug!(
"Found symlink at directory location {:?}, checking if it should be removed",
resolved_name
);

// Check if the symlink points to a sibling directory (like dist -> src)
if let Ok(link_target) = resolved_name.read_link() {
debug!(
"Symlink target: {:?}, is_relative: {}, components: {:?}",
link_target,
link_target.is_relative(),
link_target.components().collect::<Vec<_>>()
);

let is_relative = link_target.is_relative();
let is_sibling = is_relative
&& link_target.components().count() == 1
&& !link_target.as_str().starts_with('.');

debug!(
"Symlink analysis: is_relative={}, is_sibling={}, target='{}'",
is_relative,
is_sibling,
link_target.as_str()
);

if is_sibling {
debug!(
"Found sibling symlink at directory location {:?}, removing to ensure \
correct cache restoration",
resolved_name
);

// On Windows, directory symlinks need to be removed with remove_dir()
// On other platforms, use remove_file() for symlinks
#[cfg(windows)]
let removal_result = fs::remove_dir(resolved_name.as_path());
#[cfg(not(windows))]
let removal_result = fs::remove_file(resolved_name.as_path());

match removal_result {
Ok(()) => {
debug!("Successfully removed symlink at {:?}", resolved_name);
}
Err(e) => {
debug!("Failed to remove symlink at {:?}: {}", resolved_name, e);
// Continue anyway - the directory creation
// might still work
}
}
} else {
debug!(
"Symlink at {:?} is not a sibling symlink, leaving it intact",
resolved_name
);
}
} else {
debug!("Could not read symlink target at {:?}", resolved_name);
}
}
}

let directory_exists = resolved_name.try_exists();
if matches!(directory_exists, Ok(false)) {
resolved_name.create_dir_all()?;
Expand Down
Loading