From 6b0c0867cc3f36029ed672f6deb4229adf72934d Mon Sep 17 00:00:00 2001 From: Eric Coissac Date: Fri, 28 Aug 2026 14:06:37 +0200 Subject: [PATCH] implement hybrid hard-link copy strategy for directory bootstrap Introduces recursive directory replication functions that default to hard-linking for space efficiency, with automatic fallback to full file copying on cross-filesystem failures. Enforces mandatory real copies for presence and counts subdirectories to guarantee independent data matrices before merging, while preserving the existing separation between immutable index files and mutable matrix files. --- src/obikmerge/src/merge.rs | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/obikmerge/src/merge.rs b/src/obikmerge/src/merge.rs index b204759b..62ff8bef 100644 --- a/src/obikmerge/src/merge.rs +++ b/src/obikmerge/src/merge.rs @@ -514,15 +514,35 @@ fn choose_base(sources: &[&KmerIndex], src_genomes: &[Vec], mode: Me .unwrap() } +/// Copies `src` (the base source's whole `partitions/` tree) into `dst` at +/// bootstrap, before any actual merging happens. `merge_partition` (right +/// after) widens the pre-existing layers' `presence`/`counts` matrix files +/// in place — appending the new sources' genome columns — so those must +/// stay real, independent copies; everything else in a layer directory +/// (`mphf.bin`/`unitigs.bin`/`evidence.bin`/`unitigs.bin.idx`/ +/// `fingerprint.bin`/`layer_meta.json`) is never touched again once copied +/// here, exactly the same "kmer identity vs. data matrix" split +/// `obikselect::select_layer::copy_layer_files` makes — so those are safe +/// to hard-link instead, avoiding a second full copy of a layer's largest +/// files on a big index. Falls back to a real copy per file when linking +/// itself fails (e.g. `src`/`dst` on different filesystems). fn copy_dir_all(src: &Path, dst: &Path) -> io::Result<()> { + copy_dir_all_inner(src, dst, false) +} + +fn copy_dir_all_inner(src: &Path, dst: &Path, force_copy: bool) -> io::Result<()> { fs::create_dir_all(dst)?; for entry in fs::read_dir(src)? { let entry = entry?; let src_path = entry.path(); let dst_path = dst.join(entry.file_name()); if src_path.is_dir() { - copy_dir_all(&src_path, &dst_path)?; - } else { + // Once inside a `presence`/`counts` matrix subdirectory, every + // file below it — however deep — must be a real copy too. + let name = entry.file_name(); + let is_matrix_dir = name == "presence" || name == "counts"; + copy_dir_all_inner(&src_path, &dst_path, force_copy || is_matrix_dir)?; + } else if force_copy || fs::hard_link(&src_path, &dst_path).is_err() { fs::copy(&src_path, &dst_path)?; } }