mirror of
https://github.com/RGBCube/uutils-coreutils
synced 2025-07-28 11:37:44 +00:00
cp: Implement --sparse
flag (#3766)
* cp: Refactor `reflink`/`sparse` handling to enable `--sparse` flag `--sparse` and `--reflink` options have a lot of similarities: - They have similar options (`always`, `never`, `auto`) - Both need OS specific handling - They can be mutually exclusive Prior to this change, `sparse` was defined as `CopyMode`, but `reflink` wasn't. Given the similarities, it makes sense to handle them similarly. The idea behind this change is to move all OS specific file copy handling in the `copy_on_write_*` functions. Those function then dispatch to the correct logic depending on the arguments (at the moment, the tuple `(reflink, sparse)`). Also, move the handling of `--reflink=never` from `copy_file` to the `copy_on_write_*` functions, at the cost of a bit of code duplication, to allow `copy_on_write_*` to handle all cases (and later handle `--reflink=never` with `--sparse`). * cp: Implement `--sparse` flag This begins to address #3362 At the moment, only the `--sparse=always` logic matches the requirement form GNU cp info page, i.e. always make holes in destination when possible. Sparse copy is done by copying the source to the destination block by block (blocks being of the destination's fs block size). If the block only holds NUL bytes, we don't write to the destination. About `--sparse=auto`: according to GNU cp info page, the destination file will be made sparse if the source file is sparse as well. The next step are likely to use `lseek` with `SEEK_HOLE` detect if the source file has holes. Currently, this has the same behaviour as `--sparse=never`. This `SEEK_HOLE` logic can also be applied to `--sparse=always` to improve performance when copying sparse files. About `--sparse=never`: from my understanding, it is not guaranteed that Rust's `fs::copy` will always produce a file with no holes, as ["platform-specific behavior may change in the future"](https://doc.rust-lang.org/std/fs/fn.copy.html#platform-specific-behavior) About other platforms: - `macos`: The solution may be to use `fcntl` command `F_PUNCHHOLE`. - `windows`: I only see `FSCTL_SET_SPARSE`. This should pass the following GNU tests: - `tests/cp/sparse.sh` - `tests/cp/sparse-2.sh` - `tests/cp/sparse-extents.sh` - `tests/cp/sparse-extents-2.sh` `sparse-perf.sh` needs `--sparse=auto`, and in particular a way to skip holes in the source file. Co-authored-by: Sylvestre Ledru <sylvestre@debian.org>
This commit is contained in:
parent
90a9829287
commit
e1991525af
2 changed files with 239 additions and 33 deletions
|
@ -1388,6 +1388,113 @@ fn test_closes_file_descriptors() {
|
|||
.succeeds();
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
#[test]
|
||||
fn test_cp_sparse_never_empty() {
|
||||
let (at, mut ucmd) = at_and_ucmd!();
|
||||
|
||||
const BUFFER_SIZE: usize = 4096 * 4;
|
||||
let buf: [u8; BUFFER_SIZE] = [0; BUFFER_SIZE];
|
||||
|
||||
at.make_file("src_file1");
|
||||
at.write_bytes("src_file1", &buf);
|
||||
|
||||
ucmd.args(&["--sparse=never", "src_file1", "dst_file_non_sparse"])
|
||||
.succeeds();
|
||||
assert_eq!(at.read_bytes("dst_file_non_sparse"), buf);
|
||||
assert_eq!(
|
||||
at.metadata("dst_file_non_sparse").blocks() * 512,
|
||||
buf.len() as u64
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
#[test]
|
||||
fn test_cp_sparse_always_empty() {
|
||||
let (at, mut ucmd) = at_and_ucmd!();
|
||||
|
||||
const BUFFER_SIZE: usize = 4096 * 4;
|
||||
let buf: [u8; BUFFER_SIZE] = [0; BUFFER_SIZE];
|
||||
|
||||
at.make_file("src_file1");
|
||||
at.write_bytes("src_file1", &buf);
|
||||
|
||||
ucmd.args(&["--sparse=always", "src_file1", "dst_file_sparse"])
|
||||
.succeeds();
|
||||
|
||||
assert_eq!(at.read_bytes("dst_file_sparse"), buf);
|
||||
assert_eq!(at.metadata("dst_file_sparse").blocks(), 0);
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
#[test]
|
||||
fn test_cp_sparse_always_non_empty() {
|
||||
let (at, mut ucmd) = at_and_ucmd!();
|
||||
|
||||
const BUFFER_SIZE: usize = 4096 * 16 + 3;
|
||||
let mut buf: [u8; BUFFER_SIZE] = [0; BUFFER_SIZE];
|
||||
let blocks_to_touch = [buf.len() / 3, 2 * (buf.len() / 3)];
|
||||
|
||||
for i in blocks_to_touch {
|
||||
buf[i] = b'x';
|
||||
}
|
||||
|
||||
at.make_file("src_file1");
|
||||
at.write_bytes("src_file1", &buf);
|
||||
|
||||
ucmd.args(&["--sparse=always", "src_file1", "dst_file_sparse"])
|
||||
.succeeds();
|
||||
|
||||
let touched_block_count =
|
||||
blocks_to_touch.len() as u64 * at.metadata("dst_file_sparse").blksize() / 512;
|
||||
|
||||
assert_eq!(at.read_bytes("dst_file_sparse"), buf);
|
||||
assert_eq!(at.metadata("dst_file_sparse").blocks(), touched_block_count);
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
#[test]
|
||||
fn test_cp_sparse_invalid_option() {
|
||||
let (at, mut ucmd) = at_and_ucmd!();
|
||||
|
||||
at.make_file("src_file1");
|
||||
|
||||
ucmd.args(&["--sparse=invalid", "src_file1", "dst_file"])
|
||||
.fails();
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
#[test]
|
||||
fn test_cp_sparse_always_reflink_always() {
|
||||
let (at, mut ucmd) = at_and_ucmd!();
|
||||
|
||||
at.make_file("src_file1");
|
||||
|
||||
ucmd.args(&[
|
||||
"--sparse=always",
|
||||
"--reflink=always",
|
||||
"src_file1",
|
||||
"dst_file",
|
||||
])
|
||||
.fails();
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
#[test]
|
||||
fn test_cp_sparse_never_reflink_always() {
|
||||
let (at, mut ucmd) = at_and_ucmd!();
|
||||
|
||||
at.make_file("src_file1");
|
||||
|
||||
ucmd.args(&[
|
||||
"--sparse=never",
|
||||
"--reflink=always",
|
||||
"src_file1",
|
||||
"dst_file",
|
||||
])
|
||||
.fails();
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
#[test]
|
||||
fn test_cp_reflink_always_override() {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue