- C 78.7%
- Python 18.9%
- Makefile 2.4%
| .gitignore | update gitignore | |
| deltacp.c | EST to ETA | |
| deltacp.scd | EST to ETA | |
| inspectextents | inspectextent: bool | |
| LICENSE | dm-era support and lot of changes | |
| Makefile | remove BLAKE3 | |
| README.md | EST to ETA | |
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-writeoption.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 (
--keepand--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.