reflink-copy-0.1.23/.cargo_vcs_info.json0000644000000001360000000000100135210ustar { "git": { "sha1": "26b73fe679063b1cf0724e1a89f7766a7a11dff4" }, "path_in_vcs": "" }reflink-copy-0.1.23/.github/FUNDING.yml000064400000000000000000000000231046102023000154610ustar 00000000000000github: [NobodyXu] reflink-copy-0.1.23/.github/dependabot.yml000064400000000000000000000005201046102023000164760ustar 00000000000000# Dependabot dependency version checks / updates version: 2 updates: - package-ecosystem: "github-actions" # Workflow files stored in the # default location of `.github/workflows` directory: "/" schedule: interval: "daily" - package-ecosystem: "cargo" directory: "/" schedule: interval: "daily" reflink-copy-0.1.23/.github/workflows/build.yml000064400000000000000000000112721046102023000175330ustar 00000000000000name: Build on: workflow_dispatch: pull_request: push: branches: - main env: CARGO_TERM_COLOR: always jobs: build: strategy: fail-fast: false matrix: include: - target: x86_64-unknown-linux-gnu os: ubuntu-latest - target: x86_64-apple-darwin os: macos-latest - target: x86_64-pc-windows-msvc os: windows-latest - target: x86_64-unknown-linux-musl os: ubuntu-latest - target: armv7-unknown-linux-musleabihf os: ubuntu-20.04 use-cross: true - target: armv7-unknown-linux-gnueabihf os: ubuntu-20.04 use-cross: true - target: aarch64-unknown-linux-musl os: ubuntu-latest use-cross: true - target: aarch64-unknown-linux-gnu os: ubuntu-latest use-cross: true runs-on: ${{ matrix.os }} name: ${{ matrix.target }} env: CARGO_BUILD_TARGET: ${{ matrix.target }} steps: - uses: actions/checkout@v4 - name: Install cross if: matrix.use-cross uses: taiki-e/install-action@v2 with: tool: cross - name: Install host target if: "!matrix.use-cross" run: rustup target add ${{ matrix.target }} - name: Install musl-tools if: ${{ matrix.target == 'x86_64-unknown-linux-musl' }} run: sudo apt-get update && sudo apt-get install -y musl-tools - uses: Swatinem/rust-cache@v2 - name: Setup Dev-Drive ReFS1 if: ${{ matrix.os == 'windows-latest' }} uses: samypr100/setup-dev-drive@v3 with: drive-size: 1GB drive-format: ReFS drive-type: Dynamic drive-path: "${{ runner.temp }}/dev-drives/refs1.vhdx" mount-path: "${{ runner.temp }}/dev-drives/refs1" - name: Setup Dev-Drive ReFS2 if: ${{ matrix.os == 'windows-latest' }} uses: samypr100/setup-dev-drive@v3 with: drive-size: 16GB drive-format: ReFS drive-type: Dynamic drive-path: "${{ runner.temp }}/dev-drives/refs2.vhdx" mount-path: "${{ runner.temp }}/dev-drives/refs2" - name: Setup Dev-Drive NTFS if: ${{ matrix.os == 'windows-latest' }} uses: samypr100/setup-dev-drive@v3 with: drive-size: 1GB drive-format: NTFS drive-type: Dynamic drive-path: "${{ runner.temp }}/dev-drives/ntfs.vhdx" mount-path: "${{ runner.temp }}/dev-drives/ntfs" - name: Test if: "! matrix.use-cross" run: cargo test --target ${{ matrix.target }} -- --include-ignored --show-output env: RUST_BACKTRACE: 1 - name: Test using cross if: "matrix.use-cross" run: cross test --target ${{ matrix.target }} -- --include-ignored --show-output env: RUST_BACKTRACE: 1 cross-check: strategy: fail-fast: false matrix: target: [aarch64-linux-android, armv7-linux-androideabi, aarch64-apple-darwin] env: CARGO_BUILD_TARGET: ${{ matrix.target }} runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Install rustup target run: rustup target add ${{ matrix.target }} - uses: Swatinem/rust-cache@v2 - name: Cross check run: cargo check --target ${{ matrix.target }} check: strategy: fail-fast: false matrix: os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - uses: Swatinem/rust-cache@v2 - name: Clippy run: cargo clippy --no-deps - name: Format run: cargo fmt --all --check - name: Check feature tracing run: cargo check --all-features minimal-versions: strategy: fail-fast: false matrix: os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - name: Install nightly run: | rustup toolchain install nightly --no-self-update --profile minimal - name: Create Cargo.lock with minimal versions run: cargo +nightly -Zminimal-versions update - uses: Swatinem/rust-cache@v2 - run: cargo check --all-features cross-check-unsupported-platform: strategy: fail-fast: false matrix: target: [x86_64-unknown-illumos] env: CARGO_BUILD_TARGET: ${{ matrix.target }} runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Install rustup target run: | rustup toolchain install nightly --profile minimal --component rust-src - uses: Swatinem/rust-cache@v2 - name: Cross check run: cargo +nightly check --target ${{ matrix.target }} -Zbuild-std reflink-copy-0.1.23/.github/workflows/publish.yml000064400000000000000000000011221046102023000200730ustar 00000000000000name: Release-plz permissions: pull-requests: write contents: write on: push: branches: - main jobs: release-plz: name: Release-plz runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v4 with: fetch-depth: 0 - name: Install Rust toolchain uses: dtolnay/rust-toolchain@stable - name: Run release-plz uses: MarcoIeni/release-plz-action@v0.5 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} reflink-copy-0.1.23/.gitignore000064400000000000000000000000471046102023000143020ustar 00000000000000/target **/*.rs.bk # JetBrains /.idea reflink-copy-0.1.23/.travis.yml000064400000000000000000000005041046102023000144210ustar 00000000000000os: - linux - osx - windows dist: xenial language: rust rust: - stable - beta - nightly cache: cargo script: - rustup target list | grep '(default)' | awk '{print $1}' - cargo build --verbose --all - cargo test --verbose --all --no-fail-fast -- --nocapture - RUST_BACKTRACE=1 cargo run --example compare reflink-copy-0.1.23/CHANGELOG.md000064400000000000000000000030721046102023000141240ustar 00000000000000# Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] ## [0.1.23](https://github.com/cargo-bins/reflink-copy/compare/v0.1.22...v0.1.23) - 2025-01-18 ### Other - Implement reflink_block for lunix ([#93](https://github.com/cargo-bins/reflink-copy/pull/93)) ## [0.1.22](https://github.com/cargo-bins/reflink-copy/compare/v0.1.21...v0.1.22) - 2025-01-11 ### Other - Add reflink_block function ([#85](https://github.com/cargo-bins/reflink-copy/pull/85)) ## [0.1.21](https://github.com/cargo-bins/reflink-copy/compare/v0.1.20...v0.1.21) - 2025-01-08 ### Other - Bump windows from 0.58.0 to 0.59.0 ([#90](https://github.com/cargo-bins/reflink-copy/pull/90)) ## [0.1.20](https://github.com/cargo-bins/reflink-copy/compare/v0.1.19...v0.1.20) - 2024-11-10 ### Other - Implement check_reflink_support for windows ([#83](https://github.com/cargo-bins/reflink-copy/pull/83)) - Fail reflink_or_copy if target file already exist ([#81](https://github.com/cargo-bins/reflink-copy/pull/81)) - Bump tempfile from 3.8.0 to 3.12.0 ([#77](https://github.com/cargo-bins/reflink-copy/pull/77)) ## [0.1.19](https://github.com/cargo-bins/reflink-copy/compare/v0.1.18...v0.1.19) - 2024-07-04 ### Other - Bump windows from 0.57.0 to 0.58.0 ([#74](https://github.com/cargo-bins/reflink-copy/pull/74)) - Use release-plz in publish.yml ([#72](https://github.com/cargo-bins/reflink-copy/pull/72)) reflink-copy-0.1.23/Cargo.lock0000644000000276500000000000100115060ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 4 [[package]] name = "aho-corasick" version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] [[package]] name = "bitflags" version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "errno" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" dependencies = [ "libc", "windows-sys 0.52.0", ] [[package]] name = "fastrand" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" [[package]] name = "libc" version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" [[package]] name = "linux-raw-sys" version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] name = "memchr" version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "once_cell" version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "pin-project-lite" version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" [[package]] name = "proc-macro2" version = "1.0.67" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" dependencies = [ "proc-macro2", ] [[package]] name = "reflink-copy" version = "0.1.23" dependencies = [ "cfg-if", "libc", "regex", "rustix", "tempfile", "tracing", "tracing-attributes", "walkdir", "windows", ] [[package]] name = "regex" version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", "regex-automata", "regex-syntax", ] [[package]] name = "regex-automata" version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" dependencies = [ "aho-corasick", "memchr", "regex-syntax", ] [[package]] name = "regex-syntax" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "rustix" version = "0.38.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys", "windows-sys 0.52.0", ] [[package]] name = "same-file" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" dependencies = [ "winapi-util", ] [[package]] name = "syn" version = "2.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7303ef2c05cd654186cb250d29049a24840ca25d2747c25c0381c8d9e2f582e8" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "tempfile" version = "3.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" dependencies = [ "cfg-if", "fastrand", "once_cell", "rustix", "windows-sys 0.59.0", ] [[package]] name = "tracing" version = "0.1.37" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" dependencies = [ "cfg-if", "pin-project-lite", "tracing-core", ] [[package]] name = "tracing-attributes" version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "tracing-core" version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "walkdir" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" dependencies = [ "same-file", "winapi-util", ] [[package]] name = "winapi-util" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ "windows-sys 0.59.0", ] [[package]] name = "windows" version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f919aee0a93304be7f62e8e5027811bbba96bcb1de84d6618be56e43f8a32a1" dependencies = [ "windows-core", "windows-targets 0.53.0", ] [[package]] name = "windows-core" version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "810ce18ed2112484b0d4e15d022e5f598113e220c53e373fb31e67e21670c1ce" dependencies = [ "windows-implement", "windows-interface", "windows-result", "windows-strings", "windows-targets 0.53.0", ] [[package]] name = "windows-implement" version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83577b051e2f49a058c308f17f273b570a6a758386fc291b5f6a934dd84e48c1" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "windows-interface" version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb26fd936d991781ea39e87c3a27285081e3c0da5ca0fcbc02d368cc6f52ff01" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "windows-result" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d08106ce80268c4067c0571ca55a9b4e9516518eaa1a1fe9b37ca403ae1d1a34" dependencies = [ "windows-targets 0.53.0", ] [[package]] name = "windows-strings" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b888f919960b42ea4e11c2f408fadb55f78a9f236d5eef084103c8ce52893491" dependencies = [ "windows-targets 0.53.0", ] [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ "windows-targets 0.52.6", ] [[package]] name = "windows-sys" version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ "windows-targets 0.52.6", ] [[package]] name = "windows-targets" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] [[package]] name = "windows-targets" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b" dependencies = [ "windows_aarch64_gnullvm 0.53.0", "windows_aarch64_msvc 0.53.0", "windows_i686_gnu 0.53.0", "windows_i686_gnullvm 0.53.0", "windows_i686_msvc 0.53.0", "windows_x86_64_gnu 0.53.0", "windows_x86_64_gnullvm 0.53.0", "windows_x86_64_msvc 0.53.0", ] [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_gnullvm" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_aarch64_msvc" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnu" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_gnullvm" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_i686_msvc" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnu" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_gnullvm" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "windows_x86_64_msvc" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" reflink-copy-0.1.23/Cargo.toml0000644000000047510000000000100115260ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2018" name = "reflink-copy" version = "0.1.23" authors = ["Jiahao XU "] build = false autolib = false autobins = false autoexamples = false autotests = false autobenches = false description = "copy-on-write mechanism on supported file systems" homepage = "https://github.com/cargo-bins/reflink-copy" documentation = "https://docs.rs/reflink-copy" readme = "README.md" keywords = [ "reflink", "COW", "copy", "btrfs", "xfs", ] categories = [ "filesystem", "os", ] license = "MIT/Apache-2.0" repository = "https://github.com/cargo-bins/reflink-copy" [lib] name = "reflink_copy" path = "src/lib.rs" [[example]] name = "check_reflink_support" path = "examples/check_reflink_support.rs" [[example]] name = "clone_or_copy" path = "examples/clone_or_copy.rs" [[example]] name = "clonefile" path = "examples/clonefile.rs" [[example]] name = "compare" path = "examples/compare.rs" [[example]] name = "copy_folder" path = "examples/copy_folder.rs" [[example]] name = "reflink_block" path = "examples/reflink_block.rs" [[test]] name = "reflink" path = "tests/reflink.rs" [[test]] name = "reflink_win" path = "tests/reflink_win.rs" [dependencies.cfg-if] version = "1.0.0" [dependencies.tracing] version = "0.1.37" optional = true default-features = false [dependencies.tracing-attributes] version = "0.1.26" optional = true [dev-dependencies.regex] version = "1.11.1" [dev-dependencies.tempfile] version = "3.12.0" [dev-dependencies.walkdir] version = "2.5.0" [features] tracing = [ "dep:tracing", "dep:tracing-attributes", ] [target.'cfg(any(target_os = "linux", target_os = "android"))'.dependencies.rustix] version = "0.38.20" features = [ "fs", "std", ] default-features = false [target.'cfg(target_os = "linux")'.dependencies.libc] version = "0.2.169" [target."cfg(windows)".dependencies.windows] version = "0.59.0" features = [ "Win32_Storage_FileSystem", "Win32_Foundation", "Win32_System_Ioctl", "Win32_System_IO", "Win32_System_SystemServices", ] reflink-copy-0.1.23/Cargo.toml.orig000064400000000000000000000023061046102023000152010ustar 00000000000000[package] name = "reflink-copy" version = "0.1.23" authors = ["Jiahao XU "] edition = "2018" description = "copy-on-write mechanism on supported file systems" documentation = "https://docs.rs/reflink-copy" homepage = "https://github.com/cargo-bins/reflink-copy" repository = "https://github.com/cargo-bins/reflink-copy" readme = "README.md" keywords = ["reflink", "COW", "copy", "btrfs", "xfs"] categories = ["filesystem", "os"] license = "MIT/Apache-2.0" [dependencies] cfg-if = "1.0.0" tracing = { version = "0.1.37", default-features = false, optional = true } tracing-attributes = { version = "0.1.26", optional = true } [target.'cfg(any(target_os = "linux", target_os = "android"))'.dependencies.rustix] version = "0.38.20" default-features = false features = ["fs", "std"] [target.'cfg(target_os = "linux")'.dependencies.libc] version = "0.2.169" [target.'cfg(windows)'.dependencies] windows = { version = "0.59.0", features = ["Win32_Storage_FileSystem", "Win32_Foundation", "Win32_System_Ioctl", "Win32_System_IO", "Win32_System_SystemServices"] } [features] tracing = ["dep:tracing", "dep:tracing-attributes"] [dev-dependencies] tempfile = "3.12.0" regex = "1.11.1" walkdir = "2.5.0" reflink-copy-0.1.23/LICENSE-APACHE000064400000000000000000000227731046102023000142500ustar 00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS reflink-copy-0.1.23/LICENSE-MIT000064400000000000000000000017771046102023000137610ustar 00000000000000Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. reflink-copy-0.1.23/README.md000064400000000000000000000013251046102023000135710ustar 00000000000000# reflink-copy [![Build](https://github.com/cargo-bins/reflink/actions/workflows/build.yml/badge.svg)](https://github.com/cargo-bins/reflink/actions/workflows/build.yml) Cross-platform(!) COW reflink copy of files Some file systems implement COW (copy on write) functionality in order to speed up file copies. On a high level, the new file does not actually get copied, but shares the same on-disk data with the source file. As soon as one of the files is modified, the actual copying is done by the underlying OS. This library supports Linux, Android, OSX, ios and Windows. As soon as other OS support the functionality, support will be added. For implementation details, visit the [docs](https://docs.rs/reflink-copy). reflink-copy-0.1.23/examples/check_reflink_support.rs000064400000000000000000000007031046102023000210600ustar 00000000000000// cargo run --example check_reflink_support V:\folder1 X:\folder2 fn main() -> std::io::Result<()> { let args: Vec<_> = std::env::args().collect(); if args.len() < 3 { eprintln!("Usage: {} ", args[0]); return Ok(()); } let src_path = &args[1]; let tgt_path = &args[2]; let result = reflink_copy::check_reflink_support(src_path, tgt_path)?; println!("{result:?}"); Ok(()) } reflink-copy-0.1.23/examples/clone_or_copy.rs000064400000000000000000000010411046102023000173230ustar 00000000000000// cargo run --example clone_or_copy V:\file.bin V:\file-clone.bin fn main() -> std::io::Result<()> { let args: Vec<_> = std::env::args().collect(); if args.len() < 3 { eprintln!("Usage: {} ", args[0]); return Ok(()); } let src_file = &args[1]; let tgt_file = &args[2]; let result = reflink_copy::reflink_or_copy(src_file, tgt_file)?; match result { Some(_) => println!("File has been copied"), None => println!("File has been cloned"), } Ok(()) } reflink-copy-0.1.23/examples/clonefile.rs000064400000000000000000000005731046102023000164420ustar 00000000000000// cargo run --example clonefile V:\file.bin V:\file-clone.bin fn main() -> std::io::Result<()> { let args: Vec<_> = std::env::args().collect(); if args.len() < 3 { eprintln!("Usage: {} ", args[0]); return Ok(()); } let src_file = &args[1]; let tgt_file = &args[2]; reflink_copy::reflink(src_file, tgt_file) } reflink-copy-0.1.23/examples/compare.rs000064400000000000000000000016171046102023000161300ustar 00000000000000use std::fs; use std::io::{self, Read}; use std::time::Instant; fn main() { let mut base_file = fs::File::create("base.txt").unwrap(); let mut src = io::repeat(65).take(100 * 1024 * 1024); // 100 MB io::copy(&mut src, &mut base_file).unwrap(); let before_reflink = Instant::now(); match reflink_copy::reflink("base.txt", "reflinked.txt") { Ok(()) => {} Err(e) => { println!("Error during reflinking:\n{:?}", e); fs::remove_file("base.txt").unwrap(); return; } }; println!("Time to reflink: {:?}", Instant::now() - before_reflink); let before_copy = Instant::now(); fs::copy("base.txt", "copied.txt").unwrap(); println!("Time to copy: {:?}", Instant::now() - before_copy); fs::remove_file("base.txt").unwrap(); fs::remove_file("reflinked.txt").unwrap(); fs::remove_file("copied.txt").unwrap(); } reflink-copy-0.1.23/examples/copy_folder.rs000064400000000000000000000035621046102023000170100ustar 00000000000000use reflink_copy::ReflinkSupport; use std::fs; use std::io; use std::path::Path; use walkdir::WalkDir; // cargo run --example copy_folder V:/1 V:/2 fn main() -> io::Result<()> { let args: Vec<_> = std::env::args().collect(); if args.len() < 3 { eprintln!("Usage: {} ", args[0]); return Ok(()); } let from = Path::new(&args[1]); let to = Path::new(&args[2]); let reflink_support = reflink_copy::check_reflink_support(from, to)?; println!("Reflink support: {reflink_support:?}"); let mut reflinked_count = 0u64; let mut copied_count = 0u64; for entry in WalkDir::new(from) { let entry = entry?; let relative_path = entry.path().strip_prefix(from).unwrap(); let target_path = to.join(relative_path); if entry.file_type().is_dir() { fs::create_dir_all(&target_path)?; } else { match reflink_support { ReflinkSupport::Supported => { reflink_copy::reflink(entry.path(), target_path)?; reflinked_count = reflinked_count.saturating_add(1); } ReflinkSupport::Unknown => { let result = reflink_copy::reflink_or_copy(entry.path(), target_path)?; if result.is_some() { copied_count = copied_count.saturating_add(1); } else { reflinked_count = reflinked_count.saturating_add(1); } } ReflinkSupport::NotSupported => { fs::copy(entry.path(), target_path)?; copied_count = copied_count.saturating_add(1); } } } } println!("reflinked files count: {reflinked_count}"); println!("copied files count: {copied_count}"); Ok(()) } reflink-copy-0.1.23/examples/reflink_block.rs000064400000000000000000000025141046102023000173030ustar 00000000000000use std::fs::File; use std::num::NonZeroU64; // cargo run --example reflink_block V:/file.bin V:/file_cow.bin 4096 fn main() -> std::io::Result<()> { let args: Vec<_> = std::env::args().collect(); let [_, src_file, tgt_file, cluster_size] = &args[..] else { eprintln!( "Usage: {} ", args[0] ); return Ok(()); }; let cluster_size: NonZeroU64 = cluster_size.parse().expect("cannot parse cluster size"); let from_file = File::open(src_file)?; let len = from_file.metadata()?.len(); let to_file = File::create(tgt_file)?; to_file.set_len(len)?; let mut offset = 0u64; while offset < len { let src_length = if cfg!(windows) { // Windows API clones the entire cluster regardless of the number of bytes actually used // by the file in that cluster. cluster_size } else { cluster_size.min(NonZeroU64::new(len - offset).unwrap()) }; println!("reflink {offset}, {src_length}"); reflink_copy::ReflinkBlockBuilder::new(&from_file, &to_file, src_length) .from_offset(offset) .to_offset(offset) .cluster_size(cluster_size) .reflink_block()?; offset += src_length.get(); } Ok(()) } reflink-copy-0.1.23/src/lib.rs000064400000000000000000000167731046102023000142320ustar 00000000000000//! Some file systems implement COW (copy on write) functionality in order to speed up file copies. //! On a high level, the new file does not actually get copied, but shares the same on-disk data //! with the source file. As soon as one of the files is modified, the actual copying is done by //! the underlying OS. //! //! This library exposes a single function, `reflink`, which attempts to copy a file using the //! underlying OSs' block cloning capabilities. The function signature is identical to `std::fs::copy`. //! //! At the moment Linux, Android, OSX, iOS, and Windows are supported. //! //! Note: On Windows, the integrity information features are only available on Windows Server editions //! starting from Windows Server 2012. Client versions of Windows do not support these features. //! [More Information](https://learn.microsoft.com/en-us/windows/win32/api/winioctl/ni-winioctl-fsctl_set_integrity_information) //! //! As soon as other OSes support the functionality, support will be added. mod reflink_block; mod sys; use std::fs; use std::io; use std::io::ErrorKind; use std::path::Path; /// Copies a file using COW semantics. /// /// For compatibility reasons with macOS, the target file will be created using `OpenOptions::create_new`. /// If you want to overwrite existing files, make sure you manually delete the target file first /// if it exists. /// /// ```rust /// match reflink_copy::reflink("src.txt", "dest.txt") { /// Ok(()) => println!("file has been reflinked"), /// Err(e) => println!("error while reflinking: {:?}", e) /// } /// ``` /// /// # Implementation details per platform /// /// ## Linux / Android /// /// Uses `ioctl_ficlone`. Supported file systems include btrfs and XFS (and maybe more in the future). /// NOTE that it generates a temporary file and is not atomic. /// /// ## MacOS / OS X / iOS /// /// Uses `clonefile` library function. This is supported on OS X Version >=10.12 and iOS version >= 10.0 /// This will work on APFS partitions (which means most desktop systems are capable). /// If src names a directory, the directory hierarchy is cloned as if each item was cloned individually. /// /// ## Windows /// /// Uses ioctl `FSCTL_DUPLICATE_EXTENTS_TO_FILE`. /// /// Supports ReFS on Windows Server and Windows Dev Drives. *Important note*: The windows implementation is currently /// untested and probably buggy. Contributions/testers with access to a Windows Server or Dev Drives are welcome. /// [More Information on Dev Drives](https://learn.microsoft.com/en-US/windows/dev-drive/#how-does-dev-drive-work) /// /// NOTE that it generates a temporary file and is not atomic. #[inline(always)] pub fn reflink(from: impl AsRef, to: impl AsRef) -> io::Result<()> { #[cfg_attr(feature = "tracing", tracing_attributes::instrument(name = "reflink"))] fn inner(from: &Path, to: &Path) -> io::Result<()> { sys::reflink(from, to).map_err(|err| { // Linux and Windows will return an inscrutable error when `from` is a directory or a // symlink, so add the real problem to the error. We need to use `fs::symlink_metadata` // here because `from.is_file()` traverses symlinks. // // According to https://www.manpagez.com/man/2/clonefile/, Macos otoh can reflink files, // directories and symlinks, so the original error is fine. if !cfg!(any( target_os = "macos", target_os = "ios", target_os = "tvos", target_os = "watchos" )) && !fs::symlink_metadata(from).map_or(false, |m| m.is_file()) { io::Error::new( io::ErrorKind::InvalidInput, format!("the source path is not an existing regular file: {}", err), ) } else { err } }) } inner(from.as_ref(), to.as_ref()) } /// Attempts to reflink a file. If the operation fails, a conventional copy operation is /// attempted as a fallback. /// /// If the function reflinked a file, the return value will be `Ok(None)`. /// /// If the function copied a file, the return value will be `Ok(Some(written))`. /// /// If target file already exists, operation fails with [`ErrorKind::AlreadyExists`]. /// /// ```rust /// match reflink_copy::reflink_or_copy("src.txt", "dest.txt") { /// Ok(None) => println!("file has been reflinked"), /// Ok(Some(written)) => println!("file has been copied ({} bytes)", written), /// Err(e) => println!("an error occured: {:?}", e) /// } /// ``` /// /// # Implementation details per platform /// /// ## MacOS / OS X / iOS /// /// If src names a directory, the directory hierarchy is cloned as if each item was cloned /// individually. This method does not provide a fallback for directories, so the fallback will also /// fail if reflinking failed. Macos supports reflinking symlinks, which is supported by the /// fallback. #[inline(always)] pub fn reflink_or_copy(from: impl AsRef, to: impl AsRef) -> io::Result> { #[cfg_attr( feature = "tracing", tracing_attributes::instrument(name = "reflink_or_copy") )] fn inner(from: &Path, to: &Path) -> io::Result> { if let Err(err) = sys::reflink(from, to) { match err.kind() { ErrorKind::NotFound | ErrorKind::PermissionDenied | ErrorKind::AlreadyExists => { return Err(err); } _ => {} } #[cfg(feature = "tracing")] tracing::warn!(?err, "Failed to reflink, fallback to fs::copy"); fs::copy(from, to).map(Some).map_err(|err| { // Both regular files and symlinks to regular files can be copied, so unlike // `reflink` we don't want to report invalid input on both files and symlinks if from.is_file() { err } else { io::Error::new( io::ErrorKind::InvalidInput, format!("the source path is not an existing regular file: {}", err), ) } }) } else { Ok(None) } } inner(from.as_ref(), to.as_ref()) } /// Checks whether reflink is supported on the filesystem for the specified source and target paths. /// /// This function verifies that both paths are on the same volume and that the filesystem supports /// reflink. /// /// > Note: Currently the function works only for windows. It returns `Ok(ReflinkSupport::Unknown)` /// > for any other platform. /// /// # Example /// ``` /// fn main() -> std::io::Result<()> { /// let support = reflink_copy::check_reflink_support("C:\\path\\to\\file", "C:\\path\\to\\another_file")?; /// println!("{support:?}"); /// let support = reflink_copy::check_reflink_support("path\\to\\folder", "path\\to\\another_folder")?; /// println!("{support:?}"); /// Ok(()) /// } /// ``` #[cfg_attr(not(windows), allow(unused_variables))] pub fn check_reflink_support( from: impl AsRef, to: impl AsRef, ) -> io::Result { #[cfg(windows)] return sys::check_reflink_support(from, to); #[cfg(not(windows))] Ok(ReflinkSupport::Unknown) } /// Enum indicating the reflink support status. #[derive(Debug, PartialEq, Eq)] pub enum ReflinkSupport { /// Reflink is supported. Supported, /// Reflink is not supported. NotSupported, /// Reflink support is unconfirmed. Unknown, } pub use reflink_block::ReflinkBlockBuilder; reflink-copy-0.1.23/src/reflink_block.rs000064400000000000000000000127521046102023000162610ustar 00000000000000use crate::sys; use std::fs::File; use std::io; use std::num::NonZeroU64; /// Creates a reflink of a specified block from one file to another. /// /// This functionality is designed to be highly performant and does not perform any extra API calls. /// It is expected that the user takes care of necessary preliminary checks and preparations. /// /// If you need to clone an entire file, consider using the [`reflink`] or [`reflink_or_copy`] /// functions instead. /// /// > Note: Currently the function works only for windows and linux platforms. It returns `Err` for /// any other platform. /// /// # General restrictions /// /// - The source and destination regions must begin and end at a cluster boundary. /// - If the source and destination regions are in the same file, they must not overlap. (The /// application may able to proceed by splitting up the block clone operation into multiple block /// clones that no longer overlap.) /// - `src_length` equal to 0 is not supported. /// /// # Linux specific restrictions and remarks /// /// - If the file size is not aligned to the cluster size, the reflink operation must not exceed /// the file length. For example, to reflink the whole file with size of 7000 bytes, `src_length` /// should be 7000 bytes. /// /// More information about block cloning on Linux can be found by the /// [link](https://www.man7.org/linux/man-pages/man2/ioctl_ficlonerange.2.html). /// /// # Windows specific restrictions and remarks /// /// - The destination region must not extend past the end of file. If the application wishes to /// extend the destination with cloned data, it must first call /// [`File::set_len`](fn@std::fs::File::set_len). /// - The source and destination files must be on the same ReFS volume. /// - The source and destination files must have the same Integrity Streams setting (that is, /// Integrity Streams must be enabled in both files, or disabled in both files). /// - If the source file is sparse, the destination file must also be sparse. /// - The block clone operation will break Shared Opportunistic Locks (also known as Level 2 /// Opportunistic Locks). /// - The ReFS volume must have been formatted with Windows Server 2016, and if Windows Failover /// Clustering is in use, the Clustering Functional Level must have been Windows Server 2016 or /// later at format time. /// - If the file size is not aligned to the cluster size, the reflink operation should still /// be aligned by the cluster size. For example, to reflink the whole file with size of 7000 bytes /// and a cluster size of 4096 bytes, `src_length` should be 8192 bytes. /// /// > Note: In order to handle blocks larger than 4GB, /// [`ReflinkBlockBuilder::reflink_block`] splits these big blocks into smaller ones. /// Each smaller block is 4GB minus the cluster size. This means there might be more than one API /// call needed for the larger blocks. /// /// More information about block cloning on Windows can be found by the /// [link](https://learn.microsoft.com/en-us/windows/win32/fileio/block-cloning). /// /// # Examples /// /// The example below demonstrates how to create a new file reusing blocks from another file. /// ```no_run /// use std::fs::File; /// use std::num::NonZeroU64; /// /// fn shuffle() -> std::io::Result<()> { /// let from_file = File::open("source.bin")?; /// let to_file = File::create("destination.bin")?; /// let cluster_size = NonZeroU64::new(4096).unwrap(); /// let len = cluster_size.get() * 2; /// /// to_file.set_len(len)?; /// /// reflink_copy::ReflinkBlockBuilder::new(&from_file, &to_file, cluster_size) /// .from_offset(0) /// .to_offset(cluster_size.get()) /// .reflink_block()?; /// /// reflink_copy::ReflinkBlockBuilder::new(&from_file, &to_file, cluster_size) /// .from_offset(cluster_size.get()) /// .to_offset(0) /// .reflink_block()?; /// /// Ok(()) /// } /// ``` /// [`reflink`]: crate::reflink /// [`reflink_or_copy`]: crate::reflink_or_copy #[derive(Debug)] pub struct ReflinkBlockBuilder<'from, 'to> { from: &'from File, from_offset: u64, to: &'to File, to_offset: u64, src_length: u64, cluster_size: Option, } impl<'from, 'to> ReflinkBlockBuilder<'from, 'to> { /// Creates a new instance of [`ReflinkBlockBuilder`]. pub fn new(from: &'from File, to: &'to File, src_length: NonZeroU64) -> Self { Self { from, from_offset: 0, to, to_offset: 0, src_length: src_length.get(), cluster_size: None, } } /// Sets the offset within the source file. #[must_use] pub fn from_offset(mut self, from_offset: u64) -> Self { self.from_offset = from_offset; self } /// Sets the offset within the destination file. #[must_use] pub fn to_offset(mut self, to_offset: u64) -> Self { self.to_offset = to_offset; self } /// Sets the cluster size. It is used to calculate the max block size of a single reflink call /// on Windows. #[must_use] pub fn cluster_size(mut self, cluster_size: NonZeroU64) -> Self { self.cluster_size = Some(cluster_size); self } /// Performs reflink operation for the specified block of data. pub fn reflink_block(self) -> io::Result<()> { sys::reflink_block( self.from, self.from_offset, self.to, self.to_offset, self.src_length, self.cluster_size, ) } } reflink-copy-0.1.23/src/sys/mod.rs000064400000000000000000000020051046102023000150400ustar 00000000000000use std::path::Path; use std::{fs, io}; use cfg_if::cfg_if; mod utility; cfg_if! { if #[cfg(unix)] { mod unix; pub use self::unix::reflink; pub(crate) use self::unix::reflink_block; } else if #[cfg(windows)] { mod windows_impl; pub use self::windows_impl::reflink; pub use self::windows_impl::check_reflink_support; pub(crate) use self::windows_impl::reflink_block; } else { pub use self::reflink_not_supported as reflink; pub(crate) use self::reflink_block_not_supported as reflink_block; } } #[allow(dead_code)] pub fn reflink_not_supported(_from: &Path, _to: &Path) -> std::io::Result<()> { Err(std::io::ErrorKind::Unsupported.into()) } #[allow(dead_code)] pub(crate) fn reflink_block_not_supported( _from: &fs::File, _from_offset: u64, _to: &fs::File, _to_offset: u64, _src_length: u64, _cluster_size: Option, ) -> io::Result<()> { Err(std::io::ErrorKind::Unsupported.into()) } reflink-copy-0.1.23/src/sys/unix/linux.rs000064400000000000000000000022431046102023000164070ustar 00000000000000use std::convert::TryInto; use std::os::unix::io::AsRawFd; use std::{fs, io, path::Path}; use crate::sys::utility::AutoRemovedFile; pub fn reflink(from: &Path, to: &Path) -> io::Result<()> { let src = fs::File::open(from)?; // pass O_EXCL to mimic macos behaviour let dest = AutoRemovedFile::create_new(to)?; rustix::fs::ioctl_ficlone(&dest, &src)?; dest.persist(); Ok(()) } #[cfg(target_os = "linux")] pub(crate) fn reflink_block( from: &fs::File, from_offset: u64, to: &fs::File, to_offset: u64, src_length: u64, _cluster_size: Option, ) -> io::Result<()> { let ret = unsafe { libc::ioctl( to.as_raw_fd(), libc::FICLONERANGE.try_into().unwrap(), &libc::file_clone_range { src_fd: from.as_raw_fd().into(), src_offset: from_offset, src_length, dest_offset: to_offset, }, ) }; if ret == -1 { Err(io::Error::last_os_error()) } else { Ok(()) } } #[cfg(target_os = "android")] pub(crate) use crate::sys::reflink_block_not_supported as reflink_block; reflink-copy-0.1.23/src/sys/unix/macos.rs000064400000000000000000000017251046102023000163560ustar 00000000000000use std::{ ffi::CString, io, os::{ raw::{c_char, c_int}, unix::ffi::OsStrExt, }, path::Path, }; fn cstr(path: &Path) -> io::Result { Ok(CString::new(path.as_os_str().as_bytes())?) } // const CLONE_NOFOLLOW: c_int = 0x0001; const CLONE_NOOWNERCOPY: c_int = 0x0002; extern "C" { // http://www.manpagez.com/man/2/clonefileat/ // https://github.com/apple/darwin-xnu/blob/0a798f6738bc1db01281fc08ae024145e84df927/bsd/sys/clonefile.h // TODO We need weak linkage here (OSX > 10.12, iOS > 10.0), otherwise compilation will fail on older versions fn clonefile(src: *const c_char, dest: *const c_char, flags: c_int) -> c_int; } pub fn reflink(from: &Path, to: &Path) -> io::Result<()> { let src = cstr(from)?; let dest = cstr(to)?; let ret = unsafe { clonefile(src.as_ptr(), dest.as_ptr(), CLONE_NOOWNERCOPY) }; if ret == -1 { Err(io::Error::last_os_error()) } else { Ok(()) } } reflink-copy-0.1.23/src/sys/unix/mod.rs000064400000000000000000000011101046102023000160170ustar 00000000000000use cfg_if::cfg_if; cfg_if! { if #[cfg(any(target_os = "linux", target_os = "android"))] { mod linux; pub use linux::reflink; pub(crate) use linux::reflink_block; } else if #[cfg(any(target_os = "macos", target_os = "ios", target_os = "tvos", target_os = "watchos"))] { mod macos; pub use macos::reflink; pub(crate) use super::reflink_block_not_supported as reflink_block; } else { pub use super::reflink_not_supported as reflink; pub(crate) use super::reflink_block_not_supported as reflink_block; } } reflink-copy-0.1.23/src/sys/utility.rs000064400000000000000000000026601046102023000157730ustar 00000000000000#![allow(dead_code)] use std::{ fs::{remove_file, File}, io, path::{Path, PathBuf}, }; #[cfg(unix)] use std::os::unix::io::{AsFd, AsRawFd, BorrowedFd, RawFd}; #[derive(Debug)] pub(super) struct AutoRemovedFile { // Option uses File's niche, so this is zero cost inner: Option, path: PathBuf, } impl AutoRemovedFile { pub fn create_new(path: &Path) -> io::Result { // pass O_EXCL to mimic macos behaviour let inner = File::options().write(true).create_new(true).open(path)?; Ok(Self { inner: Some(inner), path: path.into(), }) } #[cfg(unix)] pub fn as_raw_fd(&self) -> RawFd { self.as_inner_file().as_raw_fd() } pub fn persist(mut self) { self.inner.take(); } pub fn as_inner_file(&self) -> &File { self.inner.as_ref().unwrap() } } #[cfg(unix)] impl AsFd for AutoRemovedFile { fn as_fd(&self) -> BorrowedFd<'_> { self.as_inner_file().as_fd() } } impl Drop for AutoRemovedFile { fn drop(&mut self) { if self.inner.is_some() { if let Err(_err) = remove_file(&self.path) { #[cfg(feature = "tracing")] tracing::warn!( ?_err, "Failed to remove dest file {} on cleanup (failed to reflink)", self.path.display(), ); } } } } reflink-copy-0.1.23/src/sys/windows_impl.rs000064400000000000000000000345411046102023000170060ustar 00000000000000use super::utility::AutoRemovedFile; use crate::ReflinkSupport; use std::num::NonZeroU64; use std::{ convert::TryInto, ffi::c_void, fs::File, io, mem::{self, MaybeUninit}, os::windows::{ffi::OsStrExt, fs::MetadataExt, io::AsRawHandle}, path::Path, }; use windows::core::PCWSTR; use windows::Win32::{ Foundation::{HANDLE, MAX_PATH}, Storage::FileSystem::{ GetVolumeInformationByHandleW, GetVolumeInformationW, GetVolumeNameForVolumeMountPointW, GetVolumePathNameW, FILE_ATTRIBUTE_SPARSE_FILE, FILE_FLAGS_AND_ATTRIBUTES, }, System::{ Ioctl::{ DUPLICATE_EXTENTS_DATA, FSCTL_DUPLICATE_EXTENTS_TO_FILE, FSCTL_GET_INTEGRITY_INFORMATION, FSCTL_GET_INTEGRITY_INFORMATION_BUFFER, FSCTL_SET_INTEGRITY_INFORMATION, FSCTL_SET_INTEGRITY_INFORMATION_BUFFER, FSCTL_SET_SPARSE, }, SystemServices::FILE_SUPPORTS_BLOCK_REFCOUNTING, IO::DeviceIoControl, }, }; pub fn reflink(from: &Path, to: &Path) -> io::Result<()> { // Inspired by https://github.com/0xbadfca11/reflink/blob/master/reflink.cpp let src = File::open(from)?; let src_metadata = src.metadata()?; let src_file_size = src_metadata.file_size(); let src_is_sparse = (FILE_FLAGS_AND_ATTRIBUTES(src_metadata.file_attributes()) & FILE_ATTRIBUTE_SPARSE_FILE).0 != 0; let dest = AutoRemovedFile::create_new(to)?; // Set the destination to be sparse while we clone. // Important to avoid allocating zero-backed real storage when cloning // below which will just be released when cloning file extents. dest.set_sparse()?; let src_integrity_info = src.get_integrity_information()?; let cluster_size: i64 = src_integrity_info.ClusterSizeInBytes.into(); if cluster_size != 0 { if cluster_size != 4 * 1024 && cluster_size != 64 * 1024 { return Err(io::Error::new( io::ErrorKind::Other, "Cluster size of source must either be 4K or 64K (restricted by ReFS)", )); } // Copy over integrity information. Not sure if this is required. let mut dest_integrity_info = FSCTL_SET_INTEGRITY_INFORMATION_BUFFER { ChecksumAlgorithm: src_integrity_info.ChecksumAlgorithm, Reserved: src_integrity_info.Reserved, Flags: src_integrity_info.Flags, }; // ignore the error if it fails, the clone will still work if let Err(_e) = dest.set_integrity_information(&mut dest_integrity_info) { #[cfg(feature = "tracing")] tracing::warn!( ?_e, "Failed to set integrity information (probably on DevDriver), but the clone still works" ); } } // file_size must be sufficient to hold the data. // TODO test if the current implementation works: // Later on, we round up the bytes to copy in order to end at a cluster boundary. // This might very well result in us cloning past the file end. // Let's hope windows api sanitizes this, because otherwise a clean implementation is not really possible. dest.as_inner_file().set_len(src_file_size)?; // We must end at a cluster boundary let total_copy_len: i64 = { if cluster_size == 0 { src_file_size.try_into().unwrap() } else { // Round to the next cluster size round_up(src_file_size.try_into().unwrap(), cluster_size) } }; let cluster_size = if cluster_size != 0 { Some(NonZeroU64::new(cluster_size as u64).unwrap()) } else { None }; reflink_block( &src, 0, dest.as_inner_file(), 0, total_copy_len as u64, cluster_size, )?; if !src_is_sparse { dest.unset_sparse()?; } dest.persist(); Ok(()) } /// Additional functionality for windows files, needed for reflink trait FileExt { fn set_sparse(&self) -> io::Result<()>; fn unset_sparse(&self) -> io::Result<()>; fn get_integrity_information(&self) -> io::Result; fn set_integrity_information( &self, integrity_info: &mut FSCTL_SET_INTEGRITY_INFORMATION_BUFFER, ) -> io::Result<()>; fn is_block_cloning_supported(&self) -> io::Result; fn as_handle(&self) -> HANDLE; } impl FileExt for File { fn set_sparse(&self) -> io::Result<()> { let mut bytes_returned = 0u32; unsafe { DeviceIoControl( self.as_handle(), FSCTL_SET_SPARSE, None, 0, None, 0, Some(&mut bytes_returned as *mut _), None, ) }?; Ok(()) } fn unset_sparse(&self) -> io::Result<()> { let mut bytes_returned = 0u32; let mut sparse_flag: u32 = 0; unsafe { DeviceIoControl( self.as_handle(), FSCTL_SET_SPARSE, Some(&mut sparse_flag as *mut _ as *mut c_void), mem::size_of::() as u32, None, 0, Some(&mut bytes_returned as *mut _), None, ) }?; Ok(()) } fn get_integrity_information(&self) -> io::Result { let mut bytes_returned = 0u32; let mut integrity_info: MaybeUninit = MaybeUninit::uninit(); unsafe { DeviceIoControl( self.as_handle(), FSCTL_GET_INTEGRITY_INFORMATION, None, 0, Some(integrity_info.as_mut_ptr() as *mut c_void), mem::size_of::() .try_into() .unwrap(), Some(&mut bytes_returned as *mut _), None, )?; Ok(integrity_info.assume_init()) } } fn set_integrity_information( &self, integrity_info: &mut FSCTL_SET_INTEGRITY_INFORMATION_BUFFER, ) -> io::Result<()> { unsafe { DeviceIoControl( self.as_handle(), FSCTL_SET_INTEGRITY_INFORMATION, Some(integrity_info as *mut _ as *mut c_void), mem::size_of::() .try_into() .unwrap(), None, 0, None, None, ) }?; Ok(()) } fn is_block_cloning_supported(&self) -> io::Result { let mut flags = 0u32; unsafe { GetVolumeInformationByHandleW( self.as_handle(), None, None, None, Some(&mut flags as *mut _), None, ) }?; Ok((flags & FILE_SUPPORTS_BLOCK_REFCOUNTING) != 0) } fn as_handle(&self) -> HANDLE { HANDLE(self.as_raw_handle()) } } impl FileExt for AutoRemovedFile { fn set_sparse(&self) -> io::Result<()> { self.as_inner_file().set_sparse() } fn unset_sparse(&self) -> io::Result<()> { self.as_inner_file().unset_sparse() } fn get_integrity_information(&self) -> io::Result { self.as_inner_file().get_integrity_information() } fn set_integrity_information( &self, integrity_info: &mut FSCTL_SET_INTEGRITY_INFORMATION_BUFFER, ) -> io::Result<()> { self.as_inner_file() .set_integrity_information(integrity_info) } fn is_block_cloning_supported(&self) -> io::Result { self.as_inner_file().is_block_cloning_supported() } fn as_handle(&self) -> HANDLE { self.as_inner_file().as_handle() } } /// Rounds `num_to_round` to the next multiple of `multiple` /// /// # Precondition /// - `multiple` > 0 /// - `mutliple` is a power of 2 fn round_up(num_to_round: i64, multiple: i64) -> i64 { debug_assert!(multiple > 0); debug_assert_eq!((multiple & (multiple - 1)), 0); (num_to_round + multiple - 1) & -multiple } /// Checks whether reflink is supported on the filesystem for the specified source and target paths. /// /// This function verifies that both paths are on the same volume and that the filesystem supports /// reflink. pub fn check_reflink_support( from: impl AsRef, to: impl AsRef, ) -> io::Result { let from_volume = get_volume_path(from)?; let to_volume = get_volume_path(to)?; let from_guid = get_volume_guid_path(&from_volume)?; let to_guid = get_volume_guid_path(&to_volume)?; if from_guid != to_guid { // The source and destination files must be on the same volume return Ok(ReflinkSupport::NotSupported); } let volume_flags = get_volume_flags(&from_volume)?; if volume_flags & FILE_SUPPORTS_BLOCK_REFCOUNTING != 0 { Ok(ReflinkSupport::Supported) } else { Ok(ReflinkSupport::NotSupported) } } /// A wrapper function for /// [GetVolumePathNameW](https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getvolumepathnamew) /// that retrieves the volume mount point where the specified path is mounted. fn get_volume_path(path: impl AsRef) -> io::Result> { let path_wide: Vec = path .as_ref() .as_os_str() .encode_wide() .chain(Some(0)) .collect(); let mut volume_name_buffer = vec![0u16; MAX_PATH as usize]; unsafe { GetVolumePathNameW(PCWSTR(path_wide.as_ptr()), volume_name_buffer.as_mut()) }?; if let Some(pos) = volume_name_buffer.iter().position(|&c| c == 0) { volume_name_buffer.truncate(pos); } Ok(volume_name_buffer) } /// A wrapper function for /// [GetVolumeNameForVolumeMountPointW](https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getvolumenameforvolumemountpointw) /// that retrieves a volume GUID path for the volume that is associated with the specified volume /// mount point (drive letter, volume GUID path, or mounted folder). fn get_volume_guid_path(volume_path_w: &[u16]) -> io::Result> { let mut volume_guid_path = vec![0u16; 50usize]; unsafe { GetVolumeNameForVolumeMountPointW(PCWSTR(volume_path_w.as_ptr()), volume_guid_path.as_mut()) }?; if let Some(pos) = volume_guid_path.iter().position(|&c| c == 0) { volume_guid_path.truncate(pos); } Ok(volume_guid_path) } /// A wrapper function for /// [GetVolumeInformationW](https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getvolumeinformationw) /// that returns `FileSystemFlags`. fn get_volume_flags(volume_path_w: &[u16]) -> io::Result { let mut file_system_flags = 0u32; unsafe { GetVolumeInformationW( PCWSTR(volume_path_w.as_ptr()), None, None, None, Some(&mut file_system_flags as *mut _), None, ) }?; Ok(file_system_flags) } pub(crate) fn reflink_block( from: &File, from_offset: u64, to: &File, to_offset: u64, src_length: u64, cluster_size: Option, ) -> io::Result<()> { const GB: u64 = 1024u64 * 1024 * 1024; const MAX_REFS_CLUSTER_SIZE: u64 = 64 * 1024; // Must be smaller than 4GB; This is always a multiple of ClusterSize let max_io_size = 4u64 * GB - cluster_size .map(NonZeroU64::get) .unwrap_or(MAX_REFS_CLUSTER_SIZE); let mut bytes_copied = 0; while bytes_copied < src_length { let bytes_to_copy = max_io_size.min(src_length - bytes_copied); if let Some(cluster_size) = cluster_size { debug_assert_eq!(bytes_to_copy % cluster_size, 0); debug_assert_eq!(bytes_copied % cluster_size, 0); } duplicate_extent_to_file( from, from_offset + bytes_copied, to, to_offset + bytes_copied, bytes_to_copy, )?; bytes_copied += bytes_to_copy; } Ok(()) } fn duplicate_extent_to_file( from: &File, from_offset: u64, to: &File, to_offset: u64, src_length: u64, ) -> io::Result<()> { let mut dup_extent = DUPLICATE_EXTENTS_DATA { FileHandle: from.as_handle(), SourceFileOffset: from_offset as i64, TargetFileOffset: to_offset as i64, ByteCount: src_length as i64, }; let mut bytes_returned = 0u32; unsafe { DeviceIoControl( to.as_handle(), FSCTL_DUPLICATE_EXTENTS_TO_FILE, Some(&mut dup_extent as *mut _ as *mut c_void), size_of::().try_into().unwrap(), None, 0, Some(&mut bytes_returned as *mut _), None, ) }?; Ok(()) } #[cfg(test)] mod test { use super::*; #[test] fn test_round_up() { assert_eq!(round_up(0, 2), 0); assert_eq!(round_up(1, 2), 2); assert_eq!(round_up(2, 2), 2); assert_eq!(round_up(15, 8), 16); assert_eq!(round_up(17, 8), 24); assert_eq!(round_up(100000, 4096), 102400); assert_eq!(round_up(100000, 65536), 131072); } #[test] #[should_panic] fn test_invalid_multiple_zero() { round_up(10, 0); } #[test] #[should_panic] fn test_invalid_multiple_non_power_of_two() { round_up(10, 3); } #[test] fn test_get_volume_path_is_same() -> io::Result<()> { let src_volume_path = get_volume_path("./src")?; let tests_volume_path = get_volume_path("./tests")?; assert_eq!(src_volume_path, tests_volume_path); Ok(()) } #[test] fn test_get_volume_guid() -> io::Result<()> { let volume_path = get_volume_path(".")?; let re = regex::Regex::new(r"\\\\\?\\Volume\{.{8}-.{4}-.{4}-.{4}-.{12}\}\\").unwrap(); let volume_guid = get_volume_guid_path(&volume_path)?; let volume_guid = String::from_utf16(&volume_guid).unwrap(); assert!(re.is_match(&volume_guid)); Ok(()) } #[test] fn test_get_volume_flags() -> io::Result<()> { let volume_path = get_volume_path(".")?; let volume_flags = get_volume_flags(&volume_path)?; assert!(volume_flags > 0); Ok(()) } } reflink-copy-0.1.23/tests/reflink.rs000064400000000000000000000067671046102023000154730ustar 00000000000000use std::fs::{self, File}; use std::io; use std::path::Path; use tempfile::tempdir; use reflink_copy::{reflink, reflink_or_copy}; #[test] fn reflink_file_does_not_exist() { let from = Path::new("test/nonexistent-bogus-path"); let to = Path::new("test/other-bogus-path"); reflink(from, to).unwrap_err(); assert!(!from.exists()); assert!(!to.exists()); } #[test] fn reflink_src_does_not_exist() { let tmpdir = tempdir().unwrap(); let from = Path::new("test/nonexistent-bogus-path"); let to = tmpdir.path().join("out.txt"); fs::write(&to, b"hello").unwrap(); assert!(reflink(from, &to).is_err()); assert!(!from.exists()); assert_eq!(fs::read(&to).unwrap(), b"hello"); } #[test] fn reflink_dest_is_dir() { let dir = tempdir().unwrap(); let src_file_path = dir.path().join("src.txt"); let _src_file = File::create(&src_file_path).unwrap(); let err = reflink(&src_file_path, dir.path()).unwrap_err(); println!("{:?}", err); if cfg!(windows) { assert_eq!(err.kind(), io::ErrorKind::PermissionDenied); } else { assert_eq!(err.kind(), io::ErrorKind::AlreadyExists); } } #[cfg(not(any( target_os = "macos", target_os = "ios", target_os = "tvos", target_os = "watchos" )))] #[test] fn reflink_src_is_dir() { let dir = tempdir().unwrap(); let dest_file_path = dir.path().join("dest.txt"); let err = reflink(dir.path(), dest_file_path).unwrap_err(); println!("{:?}", err); assert_eq!(err.kind(), io::ErrorKind::InvalidInput); } #[test] fn reflink_existing_dest_dir_results_in_error() { let dir = tempdir().unwrap(); let src_file_path = dir.path().join("src"); let dest_file_path = dir.path().join("dest"); fs::create_dir(&src_file_path).unwrap(); fs::create_dir(&dest_file_path).unwrap(); let err = reflink(&src_file_path, &dest_file_path).unwrap_err(); println!("{:?}", err); if cfg!(any( target_os = "macos", target_os = "ios", target_os = "tvos", target_os = "watchos" )) { assert_eq!(err.kind(), io::ErrorKind::AlreadyExists); } else { assert_eq!(err.kind(), io::ErrorKind::InvalidInput); } } #[test] fn reflink_existing_dest_results_in_error() { let dir = tempdir().unwrap(); let src_file_path = dir.path().join("src.txt"); let dest_file_path = dir.path().join("dest.txt"); let _src_file = File::create(&src_file_path).unwrap(); let _dest_file = File::create(&dest_file_path).unwrap(); let err = reflink(&src_file_path, &dest_file_path).unwrap_err(); println!("{:?}", err); assert_eq!(err.kind(), io::ErrorKind::AlreadyExists) } #[test] fn reflink_ok() { let dir = tempdir().unwrap(); let src_file_path = dir.path().join("src.txt"); let dest_file_path = dir.path().join("dest.txt"); fs::write(&src_file_path, b"this is a test").unwrap(); let err = reflink(&src_file_path, dest_file_path); println!("{:?}", err); // do not panic for now, CI envs are old and will probably error out // assert_eq!(fs::read(&dest_file_path).unwrap(), b"this is a test"); } #[test] fn reflink_or_copy_ok() { let tmpdir = tempdir().unwrap(); let input = tmpdir.path().join("in.txt"); let out = tmpdir.path().join("out.txt"); fs::write(&input, b"hello").unwrap(); reflink_or_copy(&input, &out).unwrap(); assert_eq!(fs::read(&out).unwrap(), b"hello"); assert_eq!( input.metadata().unwrap().permissions(), out.metadata().unwrap().permissions() ); } reflink-copy-0.1.23/tests/reflink_win.rs000064400000000000000000000252111046102023000163310ustar 00000000000000#![cfg(windows)] use reflink_copy::{ check_reflink_support, reflink, reflink_or_copy, ReflinkBlockBuilder, ReflinkSupport, }; use std::fs::File; use std::io::{Read, Write}; use std::num::NonZeroU64; use std::path::{Path, PathBuf}; const FILE_SIZE: usize = 256 * 1024; const FILENAME: &str = "test_file.dat"; const CLUSTER_SIZE: usize = 4 * 1024; // paths are defined in build.yml fn temp_dir() -> PathBuf { PathBuf::from(std::env::var("RUNNER_TEMP").expect("RUNNER_TEMP is not set")) } fn refs1_dir() -> PathBuf { temp_dir().join("dev-drives").join("refs1") } fn refs2_dir() -> PathBuf { temp_dir().join("dev-drives").join("refs2") } fn ntfs_dir() -> PathBuf { temp_dir().join("dev-drives").join("ntfs") } fn make_subfolder(folder: &Path, line: u32) -> std::io::Result { let subfolder = folder.join(format!("subfolder_{line}")); std::fs::create_dir_all(&subfolder)?; Ok(subfolder) } fn create_test_file(path: &Path) -> std::io::Result<()> { if let Some(folder) = path.parent() { std::fs::create_dir_all(folder)?; } let mut file = File::create(path)?; file.write_all(&vec![0u8; FILE_SIZE])?; Ok(()) } #[test] #[ignore] fn test_correct_deployment() { assert!(temp_dir().join("dev-drives").join("ntfs.vhdx").exists()); } #[test] #[ignore] fn test_reflink_support_refs1_to_refs2() -> std::io::Result<()> { let result = check_reflink_support(refs1_dir(), refs2_dir())?; assert_eq!(result, ReflinkSupport::NotSupported); let from = make_subfolder(&refs1_dir(), line!())?; let to = make_subfolder(&refs2_dir(), line!())?; let result = check_reflink_support(from, to)?; assert_eq!(result, ReflinkSupport::NotSupported); Ok(()) } #[test] #[ignore] fn test_reflink_support_ntfs_to_refs1() -> std::io::Result<()> { let result = check_reflink_support(ntfs_dir(), refs1_dir())?; assert_eq!(result, ReflinkSupport::NotSupported); let from = make_subfolder(&ntfs_dir(), line!())?; let to = make_subfolder(&refs1_dir(), line!())?; let result = check_reflink_support(from, to)?; assert_eq!(result, ReflinkSupport::NotSupported); Ok(()) } #[test] #[ignore] fn test_reflink_support_refs1_to_ntfs() -> std::io::Result<()> { let result = check_reflink_support(refs1_dir(), ntfs_dir())?; assert_eq!(result, ReflinkSupport::NotSupported); let from = make_subfolder(&refs1_dir(), line!())?; let to = make_subfolder(&ntfs_dir(), line!())?; let result = check_reflink_support(from, to)?; assert_eq!(result, ReflinkSupport::NotSupported); Ok(()) } #[test] #[ignore] fn test_reflink_support_refs1() -> std::io::Result<()> { let result = check_reflink_support(refs1_dir(), refs1_dir())?; assert_eq!(result, ReflinkSupport::Supported); let from = make_subfolder(&refs1_dir(), line!())?; let to = make_subfolder(&refs1_dir(), line!())?; let result = check_reflink_support(from, to)?; assert_eq!(result, ReflinkSupport::Supported); Ok(()) } #[test] #[ignore] fn test_reflink_on_supported_config() -> std::io::Result<()> { let from = make_subfolder(&refs1_dir(), line!())?; let to = make_subfolder(&refs1_dir(), line!())?; create_test_file(&from.join(FILENAME))?; reflink(from.join(FILENAME), to.join(FILENAME)) } #[test] #[ignore] fn test_reflink_on_unsupported_config() -> std::io::Result<()> { let from = make_subfolder(&refs1_dir(), line!())?; let to = make_subfolder(&refs2_dir(), line!())?; create_test_file(&from.join(FILENAME))?; let _ = reflink(from.join(FILENAME), to.join(FILENAME)).unwrap_err(); Ok(()) } #[test] #[ignore] fn test_reflink_or_copy_on_supported_config() -> std::io::Result<()> { let from = make_subfolder(&refs1_dir(), line!())?; let to = make_subfolder(&refs1_dir(), line!())?; create_test_file(&from.join(FILENAME))?; let result = reflink_or_copy(from.join(FILENAME), to.join(FILENAME))?; assert_eq!(result, None); Ok(()) } #[test] #[ignore] fn test_reflink_or_copy_on_unsupported_config() -> std::io::Result<()> { let from = make_subfolder(&refs1_dir(), line!())?; let to = make_subfolder(&refs2_dir(), line!())?; create_test_file(&from.join(FILENAME))?; let result = reflink_or_copy(from.join(FILENAME), to.join(FILENAME))?; assert_eq!(result, Some(FILE_SIZE as u64)); Ok(()) } fn compare_files_eq(file1: &Path, file2: &Path) -> std::io::Result<()> { let mut f1 = File::open(file1)?; let mut f2 = File::open(file2)?; let block_size = f1.metadata()?.len().min(1024 * 1024) as usize; let mut buffer1 = vec![0; block_size]; let mut buffer2 = vec![0; block_size]; loop { let bytes_read1 = f1.read(&mut buffer1)?; let bytes_read2 = f2.read(&mut buffer2)?; assert_eq!(bytes_read1, bytes_read2); if bytes_read1 == 0 { break; } assert_eq!(&buffer1[..bytes_read1], &buffer2[..bytes_read1]); } Ok(()) } #[test] #[ignore] fn test_reflink_block_whole_file() -> std::io::Result<()> { let num_clusters = 3; let data_size = CLUSTER_SIZE * num_clusters; let from = make_subfolder(&refs2_dir(), line!())?.join(FILENAME); let to = make_subfolder(&refs2_dir(), line!())?.join(FILENAME); let mut source_file = File::create_new(&from)?; let data: Vec = (1..=num_clusters) .flat_map(|i| vec![i as u8; CLUSTER_SIZE]) .collect(); source_file.write_all(&data)?; source_file.flush()?; assert_eq!(source_file.metadata()?.len(), data_size as u64); let mut dest_file = File::create_new(&to)?; dest_file.set_len(data_size as u64)?; ReflinkBlockBuilder::new( &source_file, &dest_file, NonZeroU64::new(data_size as u64).unwrap(), ) .reflink_block()?; dest_file.flush()?; drop(source_file); drop(dest_file); compare_files_eq(&from, &to)?; Ok(()) } #[test] #[ignore] fn test_reflink_block_6gb() -> std::io::Result<()> { let data_size = 6u64 * 1024 * 1024 * 1024; let from = make_subfolder(&refs2_dir(), line!())?.join(FILENAME); let to = make_subfolder(&refs2_dir(), line!())?.join(FILENAME); let mut source_file = File::create_new(&from)?; source_file.set_len(data_size as u64)?; // to make test faster, we don't write anything to the file source_file.flush()?; assert_eq!(source_file.metadata()?.len(), data_size as u64); let mut dest_file = File::create_new(&to)?; dest_file.set_len(data_size as u64)?; ReflinkBlockBuilder::new( &source_file, &dest_file, NonZeroU64::new(data_size as u64).unwrap(), ) .reflink_block()?; dest_file.flush()?; drop(source_file); drop(dest_file); compare_files_eq(&from, &to)?; Ok(()) } #[test] #[ignore] fn test_reflink_unaligned_file() -> std::io::Result<()> { let num_clusters = 3; let data_size = (CLUSTER_SIZE * num_clusters + 1) as u64; let aligned_data_size = (CLUSTER_SIZE * num_clusters + CLUSTER_SIZE) as u64; let from = make_subfolder(&refs2_dir(), line!())?.join(FILENAME); let to = make_subfolder(&refs2_dir(), line!())?.join(FILENAME); let mut source_file = File::create_new(&from)?; let data: Vec = (1..=num_clusters) .flat_map(|i| vec![i as u8; CLUSTER_SIZE]) .collect(); source_file.write_all(&data)?; source_file.write("+".as_bytes())?; source_file.flush()?; assert_eq!(source_file.metadata()?.len(), data_size); let mut dest_file = File::create_new(&to)?; dest_file.set_len(data_size)?; println!( "reflink {}:0 -> {}:0, block {data_size}", from.display(), to.display() ); ReflinkBlockBuilder::new( &source_file, &dest_file, NonZeroU64::new(aligned_data_size as u64).unwrap(), ) .reflink_block()?; dest_file.flush()?; drop(source_file); drop(dest_file); compare_files_eq(&from, &to)?; Ok(()) } #[test] #[ignore] fn test_reflink_source_file() -> std::io::Result<()> { let num_clusters = 3; let data_size = (CLUSTER_SIZE * num_clusters) as u64; let from = make_subfolder(&refs2_dir(), line!())?.join(FILENAME); let mut source_file = File::create_new(&from)?; let data: Vec = (1..=num_clusters) .flat_map(|i| vec![i as u8; CLUSTER_SIZE]) .collect(); source_file.write_all(&data)?; source_file.flush()?; assert_eq!(source_file.metadata()?.len(), data_size); source_file.set_len(data_size * 2)?; println!( "reflink {}:0 -> {}:{data_size}, block {data_size}", from.display(), from.display() ); ReflinkBlockBuilder::new( &source_file, &source_file, NonZeroU64::new(data_size as u64).unwrap(), ) .to_offset(data_size as u64) .reflink_block()?; source_file.flush()?; assert_eq!(source_file.metadata()?.len(), data_size * 2); drop(source_file); let mut file = File::open(from)?; let mut buffer1 = vec![0u8; data_size as usize]; let mut buffer2 = vec![0u8; data_size as usize]; file.read_exact(buffer1.as_mut_slice())?; file.read_exact(buffer2.as_mut_slice())?; assert_eq!(buffer1, buffer2); Ok(()) } #[test] #[ignore] fn test_reflink_block_reverse() -> std::io::Result<()> { let num_clusters = 3; let data_size = CLUSTER_SIZE * num_clusters; let from = make_subfolder(&refs2_dir(), line!())?.join(FILENAME); let to = make_subfolder(&refs2_dir(), line!())?.join(FILENAME); let mut source_file = File::create_new(&from)?; let data: Vec> = (1..=num_clusters) .map(|i| vec![i as u8; CLUSTER_SIZE]) .collect(); for cluster in &data { source_file.write_all(&cluster)?; } source_file.flush()?; assert_eq!(source_file.metadata()?.len(), data_size as u64); let mut dest_file = File::create_new(&to)?; dest_file.set_len(data_size as u64)?; for i in 0..num_clusters { let r = num_clusters - 1 - i; let from_offset = i * CLUSTER_SIZE; let to_offset = r * CLUSTER_SIZE; println!( "reflink {}:{from_offset} -> {}:{to_offset}, block {CLUSTER_SIZE}", from.display(), to.display() ); ReflinkBlockBuilder::new( &source_file, &dest_file, NonZeroU64::new(CLUSTER_SIZE as u64).unwrap(), ) .from_offset(from_offset as u64) .to_offset(to_offset as u64) .reflink_block()?; } dest_file.flush()?; drop(source_file); drop(dest_file); let mut dest_file = std::fs::OpenOptions::new().read(true).open(&to)?; let mut buf = vec![0; CLUSTER_SIZE]; for i in num_clusters - 1..=0 { dest_file.read(buf.as_mut_slice())?; assert_eq!(buf, data[i as usize]); } Ok(()) }