From 2b6dfe414a4c592200ccfcc6426708d4a781a9d4 Mon Sep 17 00:00:00 2001 From: Eric Ernst Date: Tue, 9 Nov 2021 10:56:39 -0800 Subject: [PATCH] watchers: don't dereference symlinks when copying files The current implementation just copies the file, dereferencing any simlinks in the process. This results in symlinks no being preserved, and a change in layout relative to the mount that we are making watchable. What we want is something like "cp -d" This isn't available in a crate, so let's go ahead and introduce a copy function which will create a symlink with same relative path if the source file is a symlink. Regular files are handled with the standard fs::copy. Introduce a unit test to verify symlinks are now handled appropriately. Fixes: #2950 Signed-off-by: Eric Ernst --- src/agent/src/watcher.rs | 109 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 101 insertions(+), 8 deletions(-) diff --git a/src/agent/src/watcher.rs b/src/agent/src/watcher.rs index b111aa166f..187d85ccc8 100644 --- a/src/agent/src/watcher.rs +++ b/src/agent/src/watcher.rs @@ -79,6 +79,16 @@ impl Drop for Storage { } } +async fn copy(from: impl AsRef, to: impl AsRef) -> Result<()> { + // if source is a symlink, just create new symlink with same link source + if fs::symlink_metadata(&from).await?.file_type().is_symlink() { + fs::symlink(fs::read_link(&from).await?, to).await?; + } else { + fs::copy(from, to).await?; + } + Ok(()) +} + impl Storage { async fn new(storage: protos::Storage) -> Result { let entry = Storage { @@ -110,19 +120,13 @@ impl Storage { dest_file_path }; - debug!( - logger, - "Copy from {} to {}", - source_file_path.display(), - dest_file_path.display() - ); - fs::copy(&source_file_path, &dest_file_path) + copy(&source_file_path, &dest_file_path) .await .with_context(|| { format!( "Copy from {} to {} failed", source_file_path.display(), - dest_file_path.display() + dest_file_path.display(), ) })?; @@ -843,6 +847,95 @@ mod tests { } } + #[tokio::test] + async fn test_copy() { + // prepare tmp src/destination + let source_dir = tempfile::tempdir().unwrap(); + let dest_dir = tempfile::tempdir().unwrap(); + + // verify copy of a regular file + let src_file = source_dir.path().join("file.txt"); + let dst_file = dest_dir.path().join("file.txt"); + fs::write(&src_file, "foo").unwrap(); + copy(&src_file, &dst_file).await.unwrap(); + // verify destination: + assert!(!fs::symlink_metadata(dst_file) + .unwrap() + .file_type() + .is_symlink()); + + // verify copy of a symlink + let src_symlink_file = source_dir.path().join("symlink_file.txt"); + let dst_symlink_file = dest_dir.path().join("symlink_file.txt"); + tokio::fs::symlink(&src_file, &src_symlink_file) + .await + .unwrap(); + copy(src_symlink_file, &dst_symlink_file).await.unwrap(); + // verify destination: + assert!(fs::symlink_metadata(&dst_symlink_file) + .unwrap() + .file_type() + .is_symlink()); + assert_eq!(fs::read_link(&dst_symlink_file).unwrap(), src_file); + assert_eq!(fs::read_to_string(&dst_symlink_file).unwrap(), "foo") + } + + #[tokio::test] + async fn watch_directory_with_symlinks() { + // Prepare source directory: + // ./tmp/.data/file.txt + // ./tmp/1.txt -> ./tmp/.data/file.txt + let source_dir = tempfile::tempdir().unwrap(); + fs::create_dir_all(source_dir.path().join(".data")).unwrap(); + fs::write(source_dir.path().join(".data/file.txt"), "two").unwrap(); + tokio::fs::symlink( + source_dir.path().join(".data/file.txt"), + source_dir.path().join("1.txt"), + ) + .await + .unwrap(); + + let dest_dir = tempfile::tempdir().unwrap(); + + let mut entry = Storage::new(protos::Storage { + source: source_dir.path().display().to_string(), + mount_point: dest_dir.path().display().to_string(), + ..Default::default() + }) + .await + .unwrap(); + + let logger = slog::Logger::root(slog::Discard, o!()); + + assert_eq!(entry.scan(&logger).await.unwrap(), 2); + + // Should copy no files since nothing is changed since last check + assert_eq!(entry.scan(&logger).await.unwrap(), 0); + + // Should copy 1 file + thread::sleep(Duration::from_secs(1)); + fs::write(source_dir.path().join(".data/file.txt"), "updated").unwrap(); + assert_eq!(entry.scan(&logger).await.unwrap(), 2); + assert_eq!( + fs::read_to_string(dest_dir.path().join(".data/file.txt")).unwrap(), + "updated" + ); + assert_eq!( + fs::read_to_string(dest_dir.path().join("1.txt")).unwrap(), + "updated" + ); + + // Verify that resulting 1.txt is a symlink: + assert!(tokio::fs::symlink_metadata(dest_dir.path().join("1.txt")) + .await + .unwrap() + .file_type() + .is_symlink()); + + // Should copy no new files after copy happened + assert_eq!(entry.scan(&logger).await.unwrap(), 0); + } + #[tokio::test] async fn watch_directory() { // Prepare source directory: