Skip to content

Instantly share code, notes, and snippets.

@mprymek
Last active December 10, 2025 14:13
Show Gist options
  • Select an option

  • Save mprymek/427aa070d83ddf1f8775f3a0840938e1 to your computer and use it in GitHub Desktop.

Select an option

Save mprymek/427aa070d83ddf1f8775f3a0840938e1 to your computer and use it in GitHub Desktop.
Script to efficiently dump/restore whole disks using `sfdisk`&`fsarchiver` - useful for RasPi SD cards backup
#!/bin/bash
# Dumps and restores disk partition table and all disk partitions contents
# using fsarchiver. Useful for backing up and restoring complete SD cards
# in a size-efficient way.
#
# Supports testing that the dumped archives are restorable.
# Bash strict mode.
# see https://olivergondza.github.io/2019/10/01/bash-strict-mode.html
set -euo pipefail
trap 's=$?; $echo -e >&2 "\nERROR: line "$LINENO": $BASH_COMMAND\n"; exit $s' ERR
PTABLE_FILE=ptable.sfdisk
DISK_START_FILE=disk_start.img
DISK_START_LEN_MB=5
MD5SUM_FILE=md5sums.txt
USE_CORES=$(($(nproc) - 1))
#FSA_VERBOSE="-v"
FSA_VERBOSE=""
FSA_OPTS="-j$USE_CORES $FSA_VERBOSE"
# Define maximum disk size (in bytes) to avoid accidental operations on large disks.
MAX_DISK_SIZE=$((32 * 1024 * 1024 * 1024)) # 32GB
dd=/bin/dd
echo="/bin/echo -e"
lsblk=/bin/lsblk
sfdisk=/sbin/sfdisk
fsarchiver=/sbin/fsarchiver
wipefs=/sbin/wipefs
losetup=/sbin/losetup
truncate=/bin/truncate
md5sum=/bin/md5sum
title() {
MSG="$1"
$echo "**** $MSG"
}
debug() {
MSG="$1"
if [ ! -z "${DEBUG-}" ]; then
$echo "D: $MSG"
fi
}
check_device() {
DEVICE="$1"
if [ ! -b "$DEVICE" ]; then
bad_arg "Device $DEVICE is not a block device."
fi
DEV_SIZE=$($lsblk -dnb -o size "$DEVICE")
debug "Device size: ${DEV_SIZE}B"
if [ "$DEV_SIZE" -gt "$MAX_DISK_SIZE" ]; then
panic "Device $DEVICE size ${DEV_SIZE}B is greater than ${MAX_DISK_SIZE}B => probably not an SD card!"
fi
}
check_command() {
CMD="$1"
if [ ! -x "$CMD" ]; then
panic "Command $CMD not found or not executable."
fi
}
usage() {
$echo "Usage:"
$echo
$echo "$0 dump DEVICE"
$echo "$0 restore DEVICE"
$echo "$0 test"
$echo
$echo "dump - dump partition table and start of disk"
$echo "restore - restore partition table and start of disk"
$echo "test - test that archives are restorable"
$echo "DEVICE - block device to dump or restore to (e.g. /dev/sda)"
$echo
$echo "Examples:"
$echo
$echo "sudo $0 dump /dev/sdd 2>&1 | tee dump.log"
$echo "sudo $0 restore /dev/sdd 2>&1 | tee restore.log"
$echo "sudo $0 test 2>&1 | tee test.log"
}
bad_arg() {
MSG="$1"
$echo "\nERROR: $MSG\n"
usage
exit 1
}
panic() {
MSG="$1"
$echo "\nERROR: $MSG\n"
exit 1
}
args_num_at_least() {
AT_LEAST="$1"
ACTUAL="$2"
MSG="$3"
if [ "$ACTUAL" -lt $AT_LEAST ]; then
bad_arg "$MSG"
fi
}
dump() {
args_num_at_least 1 $# "DEVICE argument missing."
DEVICE=$1
check_command "$sfdisk"
check_command "$dd"
check_command "$fsarchiver"
check_command "$md5sum"
check_device "$DEVICE"
title "Dumping partition table of $DEVICE to $PTABLE_FILE"
$sfdisk -d "$DEVICE" > "$PTABLE_FILE"
title "Dumping first ${DISK_START_LEN_MB}MB of $DEVICE to $DISK_START_FILE"
$dd "if=$DEVICE" "of=$DISK_START_FILE" bs=1M "count=$DISK_START_LEN_MB"
PARTITIONS=$($lsblk -n -o path "$DEVICE" | tail -n +2 | xargs)
debug "Partitions found: $PARTITIONS"
debug "Will use $USE_CORES cores for fsarchiver."
for PARTITION in $PARTITIONS; do
PARTNUM=$(echo "$PARTITION" | tr -d -c 0-9 )
DUMPFILE="partition-${PARTNUM}.fsa"
title "Dumping partition $PARTITION to $DUMPFILE"
$fsarchiver savefs $FSA_OPTS -o "$DUMPFILE" "$PARTITION"
done
title "Storing MD5 checksums of dump files to $MD5SUM_FILE"
$md5sum "$PTABLE_FILE" "$DISK_START_FILE" partition-*.fsa > "$MD5SUM_FILE"
# Print date in case script output is logged to file.
title "Dump OK. Date: $(date)"
}
restore() {
args_num_at_least 1 $# "DEVICE argument missing."
DEVICE=$1
# Determine partition number prefix.
# Needed for loop devices used in test_archives() where device name is
# something like /dev/loop0 and partitions are /dev/loop0p1, /dev/loop0p2, ...
# => prefix is "p".
# For regular devices like /dev/sda, partitions are /dev/sda1, /dev/sda2, ...
# => prefix is "".
PARTNUM_PREFIX="${2-}"
check_command "$wipefs"
check_command "$dd"
check_command "$sfdisk"
check_command "$fsarchiver"
check_device "$DEVICE"
title "Wiping all partitions on $DEVICE"
$wipefs -a "$DEVICE"
title "Restoring first ${DISK_START_LEN_MB}MB of $DISK_START_FILE to $DEVICE"
$dd "if=$DISK_START_FILE" "of=$DEVICE"
title "Restoring partition table from $PTABLE_FILE to $DEVICE"
$sfdisk "$DEVICE" < "$PTABLE_FILE"
DUMPFILES=$(ls partition-*.fsa | sort | xargs)
debug "Dump files found: $DUMPFILES"
for DUMPFILE in $DUMPFILES; do
PARTNUM=$(echo "$DUMPFILE" | tr -d -c 0-9 )
PARTITION_DEVICE="${DEVICE}${PARTNUM_PREFIX}${PARTNUM}"
title "Restoring $DUMPFILE to $PARTITION_DEVICE"
$fsarchiver restfs "$DUMPFILE" "id=0,dest=$PARTITION_DEVICE"
done
# Print date in case script output is logged to file.
title "Restore OK. Date: $(date)"
}
test_archives_cleanup() {
IMAGE_FILE="$1"
LOOP_DEVICE="$2"
title "Destroying loop device $LOOP_DEVICE"
$losetup -d "$LOOP_DEVICE"
title "Deleting virtual disk image $IMAGE_FILE"
rm "$IMAGE_FILE"
}
test_archives() {
check_command "$md5sum"
check_command "$truncate"
check_command "$losetup"
check_command "$sfdisk"
title "Verifying MD5 checksums of dump files from $MD5SUM_FILE"
$md5sum -c "$MD5SUM_FILE"
IMAGE_FILE="tmp-virtual-disk.img"
LOOP_DEVICE=$($losetup -f)
title "Creating temporary virtual disk image $IMAGE_FILE"
$truncate -s $MAX_DISK_SIZE "$IMAGE_FILE"
title "Setting up loop device $LOOP_DEVICE for $IMAGE_FILE"
$losetup "$LOOP_DEVICE" "$IMAGE_FILE"
trap "test_archives_cleanup '$IMAGE_FILE' '$LOOP_DEVICE'" EXIT
# We must dump partition table first to create partitions on the loop device
# because partprobe /dev/loopX does not work reliably.
title "Restoring partition table from $PTABLE_FILE and reattaching loop device"
$sfdisk "$LOOP_DEVICE" < "$PTABLE_FILE"
$losetup -d "$LOOP_DEVICE"
# Wait a bit to ensure the device is fully detached.
sleep 1
$losetup -P "$LOOP_DEVICE" "$IMAGE_FILE"
restore "$LOOP_DEVICE" p
# Print date in case script output is logged to file.
title "Test OK. Date: $(date)"
}
args_num_at_least 1 $# "Command not specified."
COMMAND=$1
shift
case $COMMAND in
dump)
dump $@
;;
restore)
restore $@
;;
test)
test_archives $@
;;
*)
bad_arg "Invalid command: $COMMAND"
esac
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment