summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/archive.rs36
-rw-r--r--tests/archive.rs50
2 files changed, 81 insertions, 5 deletions
diff --git a/src/archive.rs b/src/archive.rs
index 0076364..70392fa 100644
--- a/src/archive.rs
+++ b/src/archive.rs
@@ -1,4 +1,6 @@
+use std::collections::BTreeMap;
use std::fs::{self, File};
+use std::os::unix::fs::MetadataExt;
use std::path::Path;
use anyhow::{bail, Context, Result};
@@ -26,7 +28,7 @@ impl NativeTarWriter {
let output = output.as_ref();
let file = File::create(output).with_context(|| format!("create archive {}", output.display()))?;
let mut archive = Builder::new(file);
- append_tree(&mut archive, rootfs, Path::new(""))?;
+ append_tree(&mut archive, rootfs, Path::new(""), &mut BTreeMap::new())?;
archive
.finish()
.with_context(|| format!("finish archive {}", output.display()))?;
@@ -34,7 +36,12 @@ impl NativeTarWriter {
}
}
-fn append_tree(archive: &mut Builder<File>, rootfs: &Path, relative: &Path) -> Result<()> {
+fn append_tree(
+ archive: &mut Builder<File>,
+ rootfs: &Path,
+ relative: &Path,
+ hardlink_targets: &mut BTreeMap<(u64, u64), std::path::PathBuf>,
+) -> Result<()> {
let directory = rootfs.join(relative);
let mut entries = fs::read_dir(&directory)
.with_context(|| format!("read rootfs directory {}", directory.display()))?
@@ -54,9 +61,9 @@ fn append_tree(archive: &mut Builder<File>, rootfs: &Path, relative: &Path) -> R
if file_type.is_dir() {
append_directory(archive, &archive_path, &metadata)?;
- append_tree(archive, rootfs, &archive_path)?;
+ append_tree(archive, rootfs, &archive_path, hardlink_targets)?;
} else if file_type.is_file() {
- append_file(archive, &path, &archive_path, &metadata)?;
+ append_file(archive, &path, &archive_path, &metadata, hardlink_targets)?;
} else if file_type.is_symlink() {
append_symlink(archive, &path, &archive_path, &metadata)?;
} else {
@@ -91,12 +98,31 @@ fn append_file(
source: &Path,
path: &Path,
metadata: &fs::Metadata,
+ hardlink_targets: &mut BTreeMap<(u64, u64), std::path::PathBuf>,
) -> Result<()> {
+ let key = (metadata.dev(), metadata.ino());
+ if let Some(target) = hardlink_targets.get(&key) {
+ return append_hardlink(archive, path, target, metadata);
+ }
let mut header = deterministic_header(metadata, EntryType::Regular, metadata.len());
let input = File::open(source).with_context(|| format!("open rootfs file {}", source.display()))?;
archive
.append_data(&mut header, path, input)
- .with_context(|| format!("append file {}", path.display()))
+ .with_context(|| format!("append file {}", path.display()))?;
+ hardlink_targets.insert(key, path.to_path_buf());
+ Ok(())
+}
+
+fn append_hardlink(
+ archive: &mut Builder<File>,
+ path: &Path,
+ target: &Path,
+ metadata: &fs::Metadata,
+) -> Result<()> {
+ let mut header = deterministic_header(metadata, EntryType::Link, 0);
+ archive
+ .append_link(&mut header, path, target)
+ .with_context(|| format!("append hardlink {}", path.display()))
}
fn append_symlink(
diff --git a/tests/archive.rs b/tests/archive.rs
index 3956849..e31b65a 100644
--- a/tests/archive.rs
+++ b/tests/archive.rs
@@ -48,6 +48,56 @@ fn writes_deterministic_tar_with_files_directories_and_symlinks() {
}
#[test]
+fn preserves_hardlinks_in_the_archive() {
+ use std::os::unix::fs::MetadataExt;
+
+ let fixture = tempdir().expect("fixture directory");
+ let rootfs = fixture.path().join("rootfs");
+ fs::create_dir_all(rootfs.join("usr/bin")).expect("create rootfs tree");
+ let original = rootfs.join("usr/bin/controller");
+ let alias = rootfs.join("usr/bin/controller-link");
+ fs::write(&original, "controller\n").expect("write controller");
+ fs::hard_link(&original, &alias).expect("create hardlink");
+ assert_eq!(
+ fs::metadata(&original).expect("stat original").ino(),
+ fs::metadata(&alias).expect("stat alias").ino()
+ );
+
+ let artifact = fixture.path().join("controller.tar");
+ NativeTarWriter::new()
+ .write(&rootfs, &artifact)
+ .expect("write archive");
+
+ let mut archive = Archive::new(fs::File::open(artifact).expect("open archive"));
+ let entries = archive
+ .entries()
+ .expect("read archive entries")
+ .map(|entry| {
+ let entry = entry.expect("read entry");
+ (
+ entry.path().expect("entry path").into_owned(),
+ entry.header().entry_type(),
+ entry.link_name().expect("entry link name").map(|path| path.into_owned()),
+ )
+ })
+ .collect::<Vec<_>>();
+
+ assert_eq!(
+ entries,
+ vec![
+ ("usr".into(), tar::EntryType::Directory, None),
+ ("usr/bin".into(), tar::EntryType::Directory, None),
+ ("usr/bin/controller".into(), tar::EntryType::Regular, None),
+ (
+ "usr/bin/controller-link".into(),
+ tar::EntryType::Link,
+ Some("usr/bin/controller".into())
+ ),
+ ]
+ );
+}
+
+#[test]
fn rejects_a_missing_rootfs() {
let fixture = tempdir().expect("fixture directory");
let error = NativeTarWriter::new()