1
0
Fork
You've already forked deltacp
0
A copy tool designed for incremental backup of block device by utilizing reflink (CoW).
  • C 78.7%
  • Python 18.9%
  • Makefile 2.4%
2026年06月24日 20:23:19 +09:00
.gitignore update gitignore 2026年03月23日 19:39:59 +09:00
deltacp.c EST to ETA 2026年06月24日 20:23:19 +09:00
deltacp.scd EST to ETA 2026年06月24日 20:23:19 +09:00
inspectextents inspectextent: bool 2026年06月22日 21:14:28 +09:00
LICENSE dm-era support and lot of changes 2026年05月28日 18:25:49 +09:00
Makefile remove BLAKE3 2026年06月17日 13:38:23 +09:00
README.md EST to ETA 2026年06月24日 20:23:19 +09:00

AUR

deltacp - A copy tool designed for incremental and differential backups of block devices or large files by utilizing reflink (CoW).

SYNOPSIS

deltacp [options] init SOURCE DEST

deltacp [options] delta REFERENCE SOURCE DEST

deltacp [options] inplace SOURCE DEST

deltacp [options] verify FILE

deltacp [options] calc-meta FILE

deltacp [options] make-zero <size> FILE

deltacp [options] help

DESCRIPTION

deltacp (delta-copy) is a copy tool designed to optimize incremental backups by utilizing reflink. Unlike traditional backups, it performs incremental backups without fragmenting or encoding the backup image. As a result, restoring or recovering works the same as when copying with dd.

Limitations

- Require CoW (with reflink support) filesystems for backup storage such as XFS, Btrfs (except inplace).

- deltacp's heavy use of CoW can lead to substantial extent fragmentaton.

Usage

The initial backup is always a full backup, regardless of the strategy used.

 $ deltacp init /dev/vg/vm_image vm_image_backup01
 ...
 $
 $ ls
 vm_image_backup01 vm_image_backup01.dcmeta

Or when SOURCE is -, read standard input.

 $ vzdump 111 --stdout | deltacp init - vm_image_backup01
 ...

'vm_image_backup01.dcmeta' is a file that contain the necessary metadata.

deltacp can perform differential copying based on earlier backups.

 $ deltacp delta vm_image_backup01 /dev/vg/vm_image vm_image_backup02
 [3:38] [100.00%] [92.00GiB] [new:16k, shared:359k, hole:29] [ETA: 0:00]
 size: 94208 MiB
 new chunk: 16959 (4239 MiB)
 increment: 4.50%
 $
 $ ls
 vm_image_backup01 vm_image_backup01.dcmeta vm_image_backup02 vm_image_backup02.dcmeta
 $

In this example, the additional space consumption is about 4GB. The first argument is called the reference (in this case 'vm_image_backup01'). You may specify any previous generation as the reference. This depends on whether you want to perform differential or incremental backups.

The following is an example of an incremental backup:

 $ deltacp delta vm_image_backup02 /dev/vg/vm_image vm_image_backup03
 ...

deltacp uses reflinks (shared extents) to share data. Once you understand this, you'll realize that you can freely delete any unnecessary backup regardless of the backup strategy. A backup chain cannot be destroyed due to the use of reflink.

 $ rm vm_image_backup01 vm_image_backup01.dcmeta
 $
 $ deltacp delta vm_image_backup03 /dev/vg/vm_image vm_image_backup04
 ...
 $ ls
 vm_image_backup02 vm_image_backup02.dcmeta vm_image_backup03 vm_image_backup03.dcmeta
 vm_image_backup04 vm_image_backup04.dcmeta
 $

These backup files contain the complete block device data. Every backup is a synthetic full backup. There are no special command for restore/recovery, unlike many other backup tools (eg: extract, receive, merge, etc). You can restore them with dd or mount them using losetup and mount.

The SOURCE can be a file such as a virtual machine image (.qcow2, .raw), a database file (.sqlite, .db), or a backup file generated by deltacp. If you specify a backup file that have metadata (FILE.dcmeta) as the SOURCE, may achieve fast copying by utilizing that metadata. This is advantageous when creating multiple backups, such as in the 3-2-1 rule.

COMMANDS

  • init

    Perform full-backup. Aside from generating metadata, it behaves like copying with dd, but zero-filled chunks become holes. Since holes do not consume extents, this reduces actual space usage. If you want to avoid creating holes, use the --prefer-write option.

    If the target resides on an HDD, filling the free space with zeros before init can reduce space usage. Most SSDs reads zero after discard (Deterministic read ZEROs after TRIM), so invoking fstrim achieves the same effect. Same for thin-provisioning device.

  • delta

    Perform incremental/differential backups.

    After cloning REFERENCE to DEST, only the differing data blocks in DEST are overwritten with those from SOURCE.

    It supports growing and shrinking the SOURCE size.

  • inplace

    Unlike delta, inplace overwrites DEST (other tools call this sync). Since inplace does not use reflink (CoW), it can also be used on targets mounted over a network or via FUSE.

    Zero data is written as-is, but it can be turned into holes with the '--prefer-punch-hole'.

    If it's interrupted by SIGINT, it can't be resumed, but you can restart the inplace from the beginning.

  • verify

    Verify the metadata against the backup data.

  • calc-meta

    Calculate metadata (.dcmeta) in case of accidentaly delete metadata.

  • make-zero

    Make zero data and metadata (.dcmeta). <size> in bytes.

  • help

    Same as -h, --help.

OPTIONS

-h, --help Show help and exit.

-v, --verbose Increase verbosity.

-v, --version Show version and exit.

-q,--quiet Suppress messages except errors.

options for 'init', 'delta' and 'inplace'

-k,--keep Keep DEST when interrupted.

-r,--resume Resume from interrupted. --resume enables --keep.

-H,--hashsum Simultaneously computes the hash sum of SOURCE (.xxh128sum).

-s,--fsync Forces fsync() before close.

-D,--directio Enable DirectIO for SOURCE.

--prefer-write Prefer write over skip (seek); only for init. The default behavior converts zero-filled data into holes (sparse file). If specified, use fallocate() to obtain the least-fragmented allocation.

--prefer-punch-hole Prefer punch-hole over shared zeroes. the effect depends on the filesystem. The default behavior is try to share REFERECE's extents. It has no awareness of why the region reads as zeroes (HOLE, UNWRITTEN or actually filled by zero).

When this option is specified with `inplace`, zero writes are treated as holes.

--disable-punch-hole If punch-hole is enabled (default), zero new data over non-zero old data results in a punch-hole; disabling it forces zero-write.

--chunk-run <run-length> Set the run-length for update chunks (default: 1). Since each chunk is fixed at 256KiB, setting the run-length to 8 results in a 2MiB update chunk. When extent fragmentation is severe, one can either perform a full backup again or increase this run-length parameter to mitigate fragmentation effects.

--era [/dev/mapper/xxx_era_meta_access] Use dm-era. see CBT section.

--thin [/dev/mapper/xxx_tmeta:-] Use lvm-thin delta. see CBT section.

EXAMPLES

  • Resumable operations (--keep and --resume):
 # deltacp --keep init /dev/vg/lv_very_large /mnt/backup_01
 [44:36] [85.97%] [249.31GiB] [new:22k, shared:920k, hole:78k] [EST: 7:16]^C
 SIGINT Received
 #
 # deltacp --resume init /dev/vg/lv_very_large /mnt/backup_01
 ...
  • backup from physical device:
 # deltacp delta backup/sdc2_2026_01_01 /dev/sdc2 backup/sdc2_2026_01_06
 ...
  • backup from LV-snapshots.
 # sync -f /home; lvcreate --size 2G --snapshot --name home_snap_2026_01_28 vg/home
 Logical volume "home_snap_2026_01_28" created.
 # deltacp delta backup/home_2026_01_27 /dev/vg/home_snap_2026_01_28 backup/home_2026_01_28
(Note: Taking an LVM snapshot of a mounted filesystem may capture in-flight writes, resulting in inconsistent file contents)
  • incremental copy of the local backup to an external drive:
 # deltacp delta /mnt/backup_01 ./backup_02 /mnt/backup_02
This operation can reference ./backup_02.dcmeta, allowing fast copy.
If all files are located under the same mount point, using _FICLONERANGE_(2) allows even faster copying. This is useful when, after taking a new, non-fragmented full backup, you can merge several older generations into the new generation's shared block.
 # deltacp delta ./vm_image_2026_01_31 ./vm_image_2026_02_01_new ./vm_image_2026_01_31_decrement
 [0:20] [100.00%] [290.00GiB] [new:0, shared:1006k, hole:181k] [EST: 0:00]
 size: 296960 MiB
 new chunk: 0 (0 MiB)
 increment: 0.00%
  • inplace:
 # deltacp inplace /dev/vg/vm_image /mnt/vm_image
*inplace* operation does not reuire CoW filesystem.
  • verify:
 # xxh128sum -c backup_2026_01_28.xxh128sum
 ./backup_2026_01_28: OK
 # deltacp verify backup_2026_01_28
 verify: 100.0%
 verify ok
  • block device recovery:
 # dd if=backup_02 of=/dev/vg/destination bs=64M status=progress
  • read only file recovery (show an example in the XFS case):
 # losetup -r /dev/loop0 backup_02
 # mount -t xfs -o ro,nouuid,norecovery /dev/loop0 /mnt
 # ...
XFS requires the _nouuid_ option because the UUID is duplicated.
If you backup a device while it's mounted, the _norecovery_ option is also required.
  • incremental backup over SSH-tunneled NBD to a LUKS-encrypted volume
 # ssh -4 -M -N -f -S /tmp/ssh-nbd-sock -L 10809:127.0.0.1:10809 user@remote
 # nbd-client -N backup1 127.0.0.1 10809 /dev/nbd0
 #
 # cryptsetup open /dev/nbd0 remotevault --key-file <path-to-key>
 # mount /dev/mapper/remotevault /mnt
 # deltacp -q delta /mnt/volume_backup_01 ./volume_backup_02 /mnt/volume_backup_02
 # umount /mnt
 # cryptsetup close remotevault
 #
 # nbd-client -d /dev/nbd0 
 # ssh -S /tmp/ssh-nbd-sock -O exit user@remote
  • make zero data for thin-provisioning device
 # lvchange --zero y vg/mypool
 # lvcreate --thin -V 100G -n thin_dev vg/mypool
 ...
 # deltacp make-zero $(blockdev --getsize64 /dev/vg/thin_dev) ./backup_thin_device_init
 ...
  • inspect extents
 $ inspectextents groups vm_data_2026_03_*
 Group 0: 202,216 MiB shared by all 9 files
 vm_data_2026_03_20 (non-shared: 1,906 MiB)
 vm_data_2026_03_21 (non-shared: 702 MiB)
 vm_data_2026_03_22 (non-shared: 734 MiB)
 vm_data_2026_03_23 (non-shared: 953 MiB)
 vm_data_2026_03_24 (non-shared: 927 MiB)
 vm_data_2026_03_25 (non-shared: 866 MiB)
 vm_data_2026_03_26 (non-shared: 738 MiB)
 vm_data_2026_03_27 (non-shared: 674 MiB)
 vm_data_2026_03_28 (non-shared: 7,215 MiB)
 Orphans: 10
 $
 $ inspectextents stat vm_data_2026_03_28
 vm_data_2026_03_28:
 unique : 7,215 MiB ( 7,564,951,552 B)
 shared : 205,391 MiB ( 215,367,811,072 B)
 hole : 84,355 MiB ( 88,452,366,336 B)
 unwritten : 0 MiB ( 0 B)
 stat.st_size : 296,960 MiB ( 311,385,128,960 B)
 stat.st_blocks: 435,416,672
 extents : 25,432
 $
 $ inspectextents --deltacp ls
 -----------------------------------------------------------
 Size Unique Shared Hole Extent Name
 -----------------------------------------------------------
 290.0GiB 1.9GiB 207.3GiB 80.8GiB 20.2K vm_data_2026_06_07i
 290.0GiB 720.5MiB 208.5GiB 80.7GiB 21.2K vm_data_2026_06_08i
 290.0GiB 602.2MiB 208.7GiB 80.7GiB 24.0K vm_data_2026_06_09i
 290.0GiB 669.8MiB 208.6GiB 80.7GiB 24.7K vm_data_2026_06_10i
 290.0GiB 879.8MiB 208.4GiB 80.7GiB 24.9K vm_data_2026_06_11i
 290.0GiB 1.6GiB 207.7GiB 80.7GiB 16.9K vm_data_2026_06_12i
 290.0GiB 720.2MiB 208.6GiB 80.7GiB 22.7K vm_data_2026_06_13i
 290.0GiB 601.0MiB 208.7GiB 80.7GiB 23.8K vm_data_2026_06_14i
 290.0GiB 593.2MiB 208.7GiB 80.7GiB 24.1K vm_data_2026_06_15i
 290.0GiB 749.0MiB 208.6GiB 80.7GiB 22.1K vm_data_2026_06_16i
 290.0GiB 1.0GiB 208.3GiB 80.7GiB 24.3K vm_data_2026_06_17i

CBT SUPPORT

CBT (Changed Block Tracking) is a mechanism that identifies which blocks have changed since a previous snapshot or timestamp (era), so only those changed blocks are transferred or stored. This makes incremental backups much faster and reduces I/O. deltacp supports two mechanisms for CBT: dm-era and LVM thin provisioning. CBT support requires the thin-provisioning-tools package.

When using CBT, the --hashsum option cannot be used. Calculating a hashsum requires reading all of the SOURCE blocks. CBT is designed to prevent all SOURCE blocks from being read.

Both CBD mechanisms can be enabled either from the beginning or at any point later.

dm-era

dm-era is a CBT mechanism that can be applied to any block devices. It maintains metadata that assigns a monotonically increasing number (era) to each block. Because of this design, it requires a small amount of metadata storage.

 # DEVNAME=test
 # DEVPATH=/dev/vg/test
 # METADEVPATH=/dev/vg/test_meta
 #
 # dmsetup create era_${DEVNAME}_meta --table "0 $(blockdev --getsz ${METADEVPATH}) linear ${METADEVPATH} 0"
 # dmsetup create era_${DEVNAME} --table "0 $(blockdev --getsz {DEVPATH}) era /dev/mapper/era_${DEVNAME}_meta ${DEVPATH} 512"
 # dmsetup create era_${DEVNAME}_meta_access --table "0 $(blockdev --getsz ${METADEVPATH}) linear /dev/mapper/era_${DEVNAME}_meta 0"
 #
 # dmsetup status era_${DEVNAME}
 0 608174080 era 8 1220/65536 51 -
...

In this example, the granularity is set to 512. Since the device-mapper sector size is 512 bytes, this results in 512*512 = 256KiB, which aligns exactly with the deltacp's chunk size. However, the granularity value does not have to be 512.

Since dm-era does not provide any mechanism to set itself up automatically at boot, users must configure it through their own scripts. After setting it up, any write-involving operations should be performed on /dev/mapper/era_xxx.

Suppose you already have a backup named ./test01. To use dm-era with it, you first assign the era number of that backup. If it was just created, the number will be 0.

 # echo 0 > test01.era
 # ls
 test01 test01.dcmeta test01.era

To apply some changes to /dev/mapper/era_test and backup the differences, you need to send a take_metadata_snap message. The actual backup process will look like the following

 # dmsetup message era_test 0 take_metadata_snap
 # deltacp --era /dev/mapper/era_test_meta_access delta ./test01 /dev/mapper/era_test ./test02
 ...
 # dmsetup message era_test 0 drop_metadata_snap

In a practical setup, it is generally safer to take a snapshot of /dev/vg/test and backingup snapshot instead.

 # lvcreate --size 8G --snapshot --name test_snap_02 vg/test
 # dmsetup message era_test 0 take_metadata_snap
 # deltacp --era /dev/mapper/era_test_meta_access delta ./test01 /dev/vg/test_snap_02 ./test02
 ...
 # dmsetup message era_test 0 drop_metadata_snap
 # lvremove /dev/vg/test_snap_01
 # ls
 test01 test01.dcmeta test01.era test02 test02.dcmeta test02.era

This works as long as you send the take_metadata_snap message after taking the snapshot.

LVM thin provisioning

Thin-provisioning is a type of CBT that calculates the difference between two snapshots. To create a incremental/differential backup, you must retain a snapshot from the previous backup or an earlier one. To use this CBT, specify the path to the pool's metadata followed by a colon and two thinIDs in the --thin option.

Author hope the following examples are self-explanatory.

 # lvcreate --snapshot --ignoreactivationskip -n thin_test_snap_2026_06_17 vg/thin_test
 ...
 # lvs -o name,thin_id
 LV ThId
 pool0
 thin_test 1
 thin_test_snap_2026_06_08 4
 thin_test_snap_2026_06_17 5
 ...
 #
 # dmsetup message /dev/mapper/vg-pool0-tpool 0 reserve_metadata_snap
 # deltacp -v --thin /dev/mapper/vg-pool0_tmeta:4-5 delta ./thin_test_2026_06_08 /dev/vg/thin_test_snap_2026_06_17 ./thin_test_2026_06_17
 ...
 # dmsetup message /dev/mapper/vg-pool0-tpool 0 release_metadata_snap
 #
 # lvremove /dev/vg/thin_test_snap_2026_06_08
...

Unlike dm-era, thin provisioning is not require setting up the metadata storage. Plus, you don't need to configure it manually at each boot. The downside is that it can only be used with LVM thin-provisioning.

SIGNALS

SIGINT (CTRL+c)

calcel backup and delete unfinished file unless --keep specified.

SIGUSR1

print current progress even --quiet specified.

SIGUSR2

cancel and keep unfinished file even --keep was not specified.

NOTE

  • XFS has good resilience to low free-space conditions, so even at 95% usage it remains stable.
  • Do NOT defrag backup files; it will unshare extents.
  • Do NOT attempt to compress backup files hoping to save even more spaces.
  • The usage reported by du becomes inaccurate because of shared extents. Use df or inspectextents.