diff --git a/CHANGELOG.d/20251116T170044--add-denotebak-backend__added.md b/CHANGELOG.d/20251116T170044--add-denotebak-backend__added.md new file mode 100644 index 0000000..23b768d --- /dev/null +++ b/CHANGELOG.d/20251116T170044--add-denotebak-backend__added.md @@ -0,0 +1,4 @@ + + +### Added +- [DeNotebak](https://sr.ht/~plattfot/DeNotebak/) backend support. [#45](https://github.com/plattfot/baksnapper/issues/45) diff --git a/baksnapper.sh b/baksnapper.sh index 2bbd850..b77ce11 100755 --- a/baksnapper.sh +++ b/baksnapper.sh @@ -466,6 +466,16 @@ then fi fi + if [[ $sender_version -ge 4 && $($sender snapshot-type) != "snapper" ]] + then + error "config interface only supports snapper" + fi + + if [[ $receiver_version -ge 4 && $($receiver snapshot-type) != "snapper" ]] + then + error "config interface only supports snapper" + fi + src_root=${subvolume:?}/.snapshots dest_root="$dest/$p_config" elif [[ -n "$p_src" && -n "$p_dest" ]] @@ -523,6 +533,40 @@ else error "You need to specify the SOURCE and DEST or the config name to backup!" fi +# Get what snapshot type the daemon supports +# 1 [out]: variable where it will save the snapshot type. +# 2 [in]: variable that contains the baksnapperd backend +# 3 [in]: the version of the baksnapperd backend +function get-snapshot-type { + declare -n snapshot_type=$1 + if [[ $3 -ge 4 ]] + then + snapshot_type=$(${!2} snapshot-type) + else + # shellcheck disable=SC2034 + backend=snapper + fi +} +get-snapshot-type sender_snapshot_type sender "$sender_version" +get-snapshot-type receiver_snapshot_type receiver "$receiver_version" + +# shellcheck disable=SC2154 +if [[ "$sender_snapshot_type" != "$receiver_snapshot_type" ]] +then + error "Sender and receiver expects different snapshot types: sender: \ +$sender_snapshot_type, receiver: $receiver_snapshot_type" +fi + +snapshot_type=$sender_snapshot_type + +# Using a bitmap to represent where a snapshot is located: +# 0b01 → exist at the source +# 0b10 → exist at the destination +# 0b11 → exist at both locations, i.e. common +declare -A common +only_in_src=() +only_in_dest=() + ## Setup signals ############################################################### # EXIT --> if notify-send is installed, inform the user; always cleanup if hash notify-send 2> /dev/null @@ -597,28 +641,20 @@ function gather-receiver-snapshots { # only_in_src: only exist at the source # only_in_dest: only exist at the destination function compare-snapshots { - idx_src=0 idx_dest=0 - - # Using a bitmap to represent where a snapshot is located: - # 0b01 → exist at the source - # 0b10 → exist at the destination - # 0b11 → exist at both locations, i.e. common - common=() only_in_src=() only_in_dest=() - # NOTE: the snapshots must be sorted in ascending order hence the # "sort -g" when getting the snapshots. while (( idx_src < num_src_snapshots && idx_dest < num_dest_snapshots )) do - if [ "${src_snapshots[idx_src]}" -eq "${dest_snapshots[idx_dest]}" ] + if [[ "${src_snapshots[idx_src]}" == "${dest_snapshots[idx_dest]}" ]] then common["${src_snapshots[idx_src]}"]=0b11 ((++idx_src)) ((++idx_dest)) - elif [ "${src_snapshots[idx_src]}" -lt "${dest_snapshots[idx_dest]}" ] + elif [[ "${src_snapshots[idx_src]}" < "${dest_snapshots[idx_dest]}" ]] then only_in_src+=("${src_snapshots[idx_src]}") ((++idx_src)) @@ -645,15 +681,17 @@ function single-backup { local start_time start_time=$(date +%s) - $receiver create-snapshot "$dest_root" "$1" || - error "Failed to create snapshot at backup location!" - - if ! $sender send-info "$src_root" "$1" | $receiver receive-info "$dest_root" "$1" + if [[ "$snapshot_type" == "snapper" ]] then - $receiver remove-broken-snapshot "$dest_root" "$1" - error "Failed to send snapshot info!" - fi + $receiver create-snapshot "$dest_root" "$1" || + error "Failed to create snapshot at backup location!" + if ! $sender send-info "$src_root" "$1" | $receiver receive-info "$dest_root" "$1" + then + $receiver remove-broken-snapshot "$dest_root" "$1" + error "Failed to send snapshot info!" + fi + fi printf "%s\t\t%s\t" "${dest_root}" "${1}" >>"${p_summary}" exec 4>"${p_temp_dir}/${1}" if ! $sender send-snapshot "$src_root" "$1" | tee >( wc -c >&4 ) | $receiver receive-snapshot "$dest_root" "$1" @@ -676,16 +714,17 @@ function incremental-backup { start_time=$(date +%s) echo "Incremental backup $1 $2" - - $receiver create-snapshot "$dest_root" "$2" || - error "Failed to create snapshot at backup location!" - - if ! $sender send-info "$src_root" "$2" | $receiver receive-info "$dest_root" "$2" + if [[ "$snapshot_type" == "snapper" ]] then - $receiver remove-broken-snapshot "$dest_root" "$1" - error "Failed to send snapshot info!" - fi + $receiver create-snapshot "$dest_root" "$2" || + error "Failed to create snapshot at backup location!" + if ! $sender send-info "$src_root" "$2" | $receiver receive-info "$dest_root" "$2" + then + $receiver remove-broken-snapshot "$dest_root" "$1" + error "Failed to send snapshot info!" + fi + fi printf "%s\t%s\t%s\t" "${dest_root}" "${1}" "${2}" >>"${p_summary}" exec 4>"${p_temp_dir}/${2}" if ! $sender send-incremental-snapshot "$src_root/"{"$1","$2"} \ @@ -767,62 +806,59 @@ function backup { return 0 fi + # Find the first common snapshot that is lower than the specified + # snapshot. + # + # 1 [out]: parent snapshot + # 2 [in]: snapshot + function find-parent { + declare -n out=$1 + local snapshot=$2 + local idx=${num_src_snapshots}-1 + for (( ; idx >= 0; --idx )) + do + if [[ "${src_snapshots[idx]}" < "$snapshot" && \ + ${common[${src_snapshots[idx]}]} == 0b11 ]] + then + break + fi + done + out=${src_snapshots[idx]} + } + if [[ $p_all == 0 ]] then - local common_ids=("${!common[@]}") - local common_last=${common_ids[-1]} # Check that it's not already synced - if [[ "$common_last" == "$p_snapshot" ]] + if [[ ${common[$p_snapshot]} == 0b11 ]] then - error "Already synced the last snapshot." + error "Already synced snapshot $p_snapshot." fi - incremental-backup "$common_last" "$p_snapshot" + local parent + find-parent parent "$p_snapshot" + incremental-backup "$parent" "$p_snapshot" return 0 else - local -a src_and_common - for snapshot in "${!common[@]}" - do - # These exist at both locations → 0b01 - src_and_common["$snapshot"]=0b11 - done - for snapshot in "${only_in_src[@]}" - do - # These only exist at the source → 0b01 - src_and_common["$snapshot"]=0b01 - done - local num_snapshots=${#src_and_common[@]} # Find the first common snapshot that is lower than the first # source only snapshot. This will be the start of the incremental # backup. If no one is found it will use the lowest common one. - local first_src_snapshot=${only_in_src[0]} - local snapshot_ids=("${!src_and_common[@]}") - local idx=${num_snapshots}-1 - for (( ; idx >= 0; --idx )) - do - if [[ ${snapshot_ids[idx]} -lt $first_src_snapshot ]] - then - if [[ ${src_and_common[${snapshot_ids[idx]}]} == 0b11 ]] - then - break - fi - fi - done - incremental-backup "${snapshot_ids[idx]}" "$first_src_snapshot" - src_and_common["${snapshot_ids[idx]}"]=0b11 + first_src_snapshot="${only_in_src[0]}" + find-parent parent "$first_src_snapshot" + incremental-backup "$parent" "$first_src_snapshot" + common["${src_snapshots[idx]}"]=0b11 for (( src_idx = 1; src_idx < num_src_only; ++src_idx )) do - # Find the source only snapshot in the snapshot_ids + # Find the next source snapshot to send local src_snapshot=${only_in_src[src_idx]} - for (( ; idx < num_snapshots; ++idx )) + for (( ; idx < num_src_snapshots; ++idx )) do - if [[ ${snapshot_ids[idx]} -eq $src_snapshot ]] + if [[ "${src_snapshots[idx]}" == "$src_snapshot" ]] then break fi done # Use the common snapshot previous to it as the parent - incremental-backup "${snapshot_ids[idx-1]}" "$src_snapshot" - src_and_common["$src_snapshot"]=0b11 + incremental-backup "${src_snapshots[idx-1]}" "$src_snapshot" + common["$src_snapshot"]=0b11 done fi } diff --git a/baksnapperd-denotebak.sh b/baksnapperd-denotebak.sh new file mode 100644 index 0000000..65244a3 --- /dev/null +++ b/baksnapperd-denotebak.sh @@ -0,0 +1,164 @@ +#! /usr/bin/env bash + +# Baksnapperd-denotbak - Daemon used by baksnapper when backup DeNotebak snapshots + +# SPDX-FileCopyrightText: 2025 Fredrik Salomonsson +# +# SPDX-License-Identifier: GPL-3.0-or-later + +read -rd '' version <. +This is free software: you are free to change and redistribute it. +There is NO WARRANTY, to the extent permitted by law. +EOF + +shopt -s extglob + +function error { + echo "[ERROR] $1" 1>&2 + exit 1 +} + +function warning { + echo -e "[Warning] $1" 1>&2 +} + +case "$1" in + version) + # Return what version of the API this is using. + # + # It always a single integer. + # No input. + echo 4 + ;; + snapshot-type) + # Return what type of snapshot this backend supports + # + # No input. + echo denotebak + ;; + list-snapshots) + # List snapshots at location. + # + # Stripping any tags as they are volatile. + # 1 [in]: Path to the directory the snapshots are expected to be in. + shift + find "$1" -mindepth 1 -maxdepth 1 -type d -printf "%f\n" | sed 's/__.*//' | sort -g + ;; + verify-snapshot) + # Verify that a snapshot exist. + # + # 1 [in]: File path to a snapshot — sans tags. + # Return 0 if it exist, non-zero otherwise. + shift + test -d "$1"?(__*) || error "Snapshot $1 doesn't exist." + ;; + incomplete-snapshot) + # Check if a snapshot is incomplete. + # + # 1 [in]: Path to the directory the snapshot is in. + # 2 [in]: Name of the snapshot — sans tags. + # Return 0 if the snapshot is incomplete, non-zero otherwise. + shift + [[ $(btrfs property get "$1/$2"?(__*) ro) != "ro=true" ]] + ;; + create-location) + # Prepare destination to receive a snapshot. + # + # 1 [in]: Path to the directory the snapshots will be in. + shift + mkdir -p "$1" + ;; + receive-snapshot) + # Receive a snapshot. + # + # 1 [in]: Path to the directory the snapshot will be in. + # 2 [in]: Name of the snapshot — sans tags. + shift + btrfs receive "$1" + ;; + send-snapshot) + # Send a snapshot. + # + # 1 [in]: Path to the directory the snapshot is in. + # 2 [in]: Name of the snapshot — sans tags. + shift + btrfs send "$1/$2"?(__*) + ;; + send-incremental-snapshot) + # Only send what is different from the parent snapshot + # + # 1 [in]: File path to the parent snapshot — sans tags. + # 2 [in]: File path to the snapshot — sans tags. + shift + btrfs send -p "$1"?(__*) "$2"?(__*) + ;; + remove-snapshots) + # Remove all snapshots listed in the input. + # + # 1 [in]: Path to the directory the snapshots are in. + # 2+ [in]: Name of the snapshots — sans tags. + shift + dest_root=$1 + shift + for snapshot in "$@" + do + if btrfs subvolume show "$dest_root/$snapshot"?(__*) &> /dev/null + then + echo "Deleting snapshot $snapshot" + btrfs subvolume delete "$dest_root/$snapshot"?(__*) + else + warning "Snapshot $snapshot does not match a DeNotebak snapshot, ignoring it." + fi + done + ;; + remove-broken-snapshot) + # Remove a broken snapshot. + # + # 1 [in]: Path to the directory the snapshot is in. + # 2 [in]: Name of the snapshot — sans tags. + shift + dest_root=$1 + snapshot=$2 + if btrfs subvolume show "$dest_root/$snapshot"?(__*) &> /dev/null + then + btrfs subvolume delete "$dest_root/$snapshot"?(__*) + fi + ;; + link-latest) + # Create symlink `latest` to the latest snapshot. + # + # 1 [in]: Path to the directory the snapshots are in. + set -x + shift + declare -a snapshots + if [[ -e "$1/latest" ]]; then + if [[ -h "$1/latest" ]]; then + rm "$1/latest" + else + error "$1/latest exists and is not a symbolic link. Link is not created." + fi + fi + for dir in $(find "$1" -maxdepth 1 -mindepth 1 -type d -printf "%P\n"|sort) + do + if btrfs subvolume show "$1/$dir" &> /dev/null && [[ ! -h "$1/$dir" ]] + then + snapshots+=("$dir") + fi + done + if [ ${#snapshots[@]} -ne 0 ]; then + ln -sfn "${snapshots[-1]}" "$1/latest" + else + error "link-latest: No suitable snapshots at $1 (${#snapshots[@]})" + fi + ;; + --version) + echo -e "$version" + exit 0 + ;; + *) + error "Unrecognized command '$1', bailing out!" + ;; +esac diff --git a/baksnapperd.sh b/baksnapperd.sh index 61f3469..9cdbb9c 100755 --- a/baksnapperd.sh +++ b/baksnapperd.sh @@ -35,6 +35,12 @@ case "$1" in # No input. echo 4 ;; + snapshot-type) + # Return what type of snapshot this backend supports + # + # No input. + echo snapper + ;; list-snapshots) # List snapshots at location. # diff --git a/meson.build b/meson.build index 49d520c..1e46ca3 100644 --- a/meson.build +++ b/meson.build @@ -30,6 +30,16 @@ baksnapperd = custom_target( install_tag: 'runtime', ) +baksnapperd_denotebak = custom_target( + command: [install_cmd, '@INPUT@', '@OUTPUT@'], + input: 'baksnapperd-denotebak.sh', + output: 'baksnapperd-denotebak', + install: true, + install_dir: 'bin', + install_mode: 'rwxr-xr-x', + install_tag: 'runtime', +) + bsconf_root = get_option('bsconf-root') if bsconf_root == '' bsconf_root = get_option('sysconfdir') @@ -82,6 +92,9 @@ test_env.set('MOCK_SSH_CONNECTION', '0') test_env.set('MOCK_SSH_KEY', 'foo.key=foo.example.com:bar.key=bar.example.com') test_env.set('MOCK_SSH_BAKSNAPPERD', baksnapperd[0].full_path()) +denotebak_test_env = test_env +denotebak_test_env.set('MOCK_SSH_BAKSNAPPERD', baksnapperd_denotebak[0].full_path()) + runner = find_program('./tests/runner.scm') test_configuration = { @@ -106,191 +119,473 @@ test_configuration = { 'ALL': 'YES', 'VERBOSE': 'NO', }, + 'denotebak-default': { + 'DAEMON': baksnapperd_denotebak[0].full_path(), + }, + 'denotebak-latest': { + 'DAEMON': baksnapperd_denotebak[0].full_path(), + 'PRUNE': 'NO', + 'ALL': 'NO', + 'VERBOSE': 'NO', + 'LINK': 'YES', + }, + 'denotebak-prune': { + 'DAEMON': baksnapperd_denotebak[0].full_path(), + 'PRUNE': 'YES', + 'ALL': 'NO', + 'VERBOSE': 'NO', + }, + 'denotebak-all': { + 'DAEMON': baksnapperd_denotebak[0].full_path(), + 'ALL': 'YES', + 'VERBOSE': 'NO', + }, } -foreach conf: [ +foreach test_entry: [ { - 'suffix': '-config', - 'runner-args': [], - 'baksnapper-args': ['--config', 'root'], - 'configfile': false, - 'suite': ['config'], + 'name': 'default', + 'runner-args': [ + '--sender', '1,2,3', + '--expected', '3', + ], + 'baksnapper-args': [ + baksnapper[0].full_path(), + '--daemon', baksnapperd[0].full_path(), + ], + 'configfile': 'default', + 'configfile-baksnapper-args': [], + 'suite': ['backup'], + 'depends': [baksnapperd], + 'env': test_env, }, { - 'suffix': '-src-dest', - 'runner-args': ['--src-dest'], - 'baksnapper-args': [], - 'configfile': false, - 'suite': ['src-dest'], + 'name': 'latest', + 'runner-args': [ + '--sender', '1,2,3', + '--expected', '3', + '--expected-latest', '3', + ], + 'baksnapper-args': [ + baksnapper[0].full_path(), + '--daemon', baksnapperd[0].full_path(), + '--link', + ], + 'configfile': 'latest', + 'configfile-baksnapper-args': [], + 'suite': ['latest'], + 'depends': [baksnapperd], + 'env': test_env, }, { - 'suffix': '-configfile', - 'runner-args': [], - 'baksnapper-args': [], - 'configfile': true, - 'suite': ['configfile'], + 'name': 'prune', + 'runner-args': [ + '--sender', '1,2,3,6,7,8', + '--receiver', '1,2,3,4,5,6', + '--expected', '1,2,3,6,8:p=6', + ], + 'baksnapper-args': [ + baksnapper[0].full_path(), + '--daemon', baksnapperd[0].full_path(), + '--prune', + ], + 'configfile': 'prune', + 'configfile-baksnapper-args': [], + 'suite': ['prune'], + 'depends': [baksnapperd], + 'env': test_env, }, { - 'suffix': '-configfile-path', - 'runner-args': ['--path-config'], - 'baksnapper-args': [], - 'configfile': true, - 'suite': ['configfile', 'config'], + 'name': 'backup-2', + 'runner-args': [ + '--sender', '1,2,3', + '--expected', '2', + ], + 'baksnapper-args': [ + baksnapper[0].full_path(), + '--daemon', baksnapperd[0].full_path(), + '--snapshot', '2', + ], + 'configfile': 'default', + 'configfile-baksnapper-args': [], + 'suite': ['backup'], + 'depends': [baksnapperd], + 'env': test_env, }, { - 'suffix': '-configfile-src-dest', - 'runner-args': ['--src-dest'], - 'baksnapper-args': [], - 'configfile': true, - 'suite': ['configfile', 'src-dest'], + 'name': 'delete-1', + 'runner-args': [ + '--sender', '1,2,3', + '--receiver', '1,2,3', + '--expected', '2,3', + ], + 'baksnapper-args': [ + baksnapper[0].full_path(), + '--daemon', baksnapperd[0].full_path(), + '--delete', '1', + ], + 'configfile': 'default', + 'configfile-baksnapper-args': [], + 'suite': ['delete'], + 'depends': [baksnapperd], + 'env': test_env, }, { - 'suffix': '-configfile-src', - 'runner-args': ['--src-dest', '--src-config'], - 'baksnapper-args': [], - 'configfile': true, - 'suite': ['configfile', 'src-dest'], + 'name': 'delete-1,3', + 'runner-args': [ + '--sender', '1,2,3', + '--receiver', '1,2,3', + '--expected', '2', + ], + 'baksnapper-args': [ + baksnapper[0].full_path(), + '--daemon', baksnapperd[0].full_path(), + '--delete', '1,3', + ], + 'configfile': 'default', + 'configfile-baksnapper-args': [], + 'suite': ['delete'], + 'depends': [baksnapperd], + 'env': test_env, }, { - 'suffix': '-configfile-dest', - 'runner-args': ['--src-dest', '--dest-config'], - 'baksnapper-args': [], - 'configfile': true, - 'suite': ['configfile', 'src-dest'], + 'name': 'backup-with-right-parent', + 'runner-args': [ + '--sender', '64,65,76,83,91,92,93', + '--receiver', '64,66:p=64,76:p=66,83:p=76,91:p=83,92:p=83', + '--expected', '64,66:p=64,76:p=66,83:p=76,91:p=83,92:p=83,93:p=92', + ], + 'baksnapper-args': [ + baksnapper[0].full_path(), + '--daemon', baksnapperd[0].full_path(), + ], + 'configfile': 'default', + 'configfile-baksnapper-args': [], + 'suite': ['backup'], + 'depends': [baksnapperd], + 'env': test_env, }, + # https://github.com/plattfot/baksnapper/issues/27 { - 'suffix': '-configfile-both', - 'runner-args': ['--src-dest', '--src-config', '--dest-config'], - 'baksnapper-args': [], - 'configfile': true, - 'suite': ['configfile', 'src-dest'], + 'name': 'backup-all-with-right-parent', + 'runner-args': [ + '--sender', '64,65,76,83,91,92,93', + '--receiver', '64,66:p=64,76:p=66,83:p=76,91:p=83,92:p=83', + '--expected', '64,65:p=64,66:p=64,76:p=66,83:p=76,91:p=83,92:p=83,93:p=92', + ], + 'baksnapper-args': [ + baksnapper[0].full_path(), + '--daemon', baksnapperd[0].full_path(), + '--all', + ], + 'configfile': 'all', + 'configfile-baksnapper-args': [], + 'suite': ['backup'], + 'depends': [baksnapperd], + 'env': test_env, + }, + # DeNotebak + { + 'name': 'denotebak-default', + 'runner-args': [ + '--sender', ','.join([ + '20251023T130000:t=dailies', + '20251025T120000', + '20251025T160000:t=hourly']), + '--expected', ','.join([ + '20251025T160000']), + '--type', 'denotebak', + ], + 'baksnapper-args': [ + baksnapper[0].full_path(), + '--daemon', baksnapperd_denotebak[0].full_path(), + ], + 'configfile': 'denotebak-default', + 'configfile-baksnapper-args': [], + 'suite': ['denotebak', 'backup'], + 'depends': [baksnapperd_denotebak], + 'env': denotebak_test_env, + }, + { + 'name': 'denotebak-latest', + 'runner-args': [ + '--sender', ','.join([ + '20251023T130000:t=dailies', + '20251025T120000', + '20251025T160000:t=hourly']), + '--expected', ','.join([ + '20251025T160000']), + '--expected-latest', '20251025T160000', + '--type', 'denotebak', + ], + 'baksnapper-args': [ + baksnapper[0].full_path(), + '--daemon', baksnapperd_denotebak[0].full_path(), + '--link', + ], + 'configfile': 'denotebak-latest', + 'configfile-baksnapper-args': [], + 'suite': ['denotebak', 'latest'], + 'depends': [baksnapperd_denotebak], + 'env': denotebak_test_env, + }, + { + 'name': 'denotebak-prune', + 'runner-args': [ + '--sender', ','.join([ + '20251023T130000:t=dailies', + '20251025T120000', + '20251025T130000:t=dailies', + '20251025T140000:t=hourly', + '20251025T150000:t=hourly', + '20251025T160000:t=hourly']), + '--receiver', ','.join([ + '20251023T130000', + '20251023T180000', + '20251024T090000', + '20251024T130000', + '20251024T180000', + '20251025T120000', + '20251025T130000']), + '--expected', ','.join([ + '20251023T130000', + '20251025T120000', + '20251025T130000', + '20251025T160000:p=20251025T130000']), + '--type', 'denotebak', + ], + 'baksnapper-args': [ + baksnapper[0].full_path(), + '--daemon', baksnapperd_denotebak[0].full_path(), + '--prune', + ], + 'configfile': 'denotebak-prune', + 'configfile-baksnapper-args': [], + 'suite': ['denotebak', 'prune'], + 'depends': [baksnapperd_denotebak], + 'env': denotebak_test_env, + }, + { + 'name': 'denotebak-backup-20251025T150000', + 'runner-args': [ + '--sender', ','.join([ + '20251025T140000:t=hourly', + '20251025T150000:t=hourly', + '20251025T160000:t=hourly']), + '--expected', ','.join([ + '20251025T150000']), + '--type', 'denotebak', + ], + 'baksnapper-args': [ + baksnapper[0].full_path(), + '--daemon', baksnapperd_denotebak[0].full_path(), + '--snapshot', '20251025T150000', + ], + 'configfile': 'denotebak-default', + 'configfile-baksnapper-args': [], + 'suite': ['denotebak', 'backup'], + 'depends': [baksnapperd_denotebak], + 'env': denotebak_test_env, + }, + { + 'name': 'denotebak-delete-20251025T140000', + 'runner-args': [ + '--sender', ','.join([ + '20251025T140000:t=hourly', + '20251025T150000:t=hourly', + '20251025T160000:t=hourly']), + '--receiver', ','.join([ + '20251025T140000', + '20251025T150000', + '20251025T160000']), + '--expected', ','.join([ + '20251025T150000', + '20251025T160000']), + '--type', 'denotebak', + ], + 'baksnapper-args': [ + baksnapper[0].full_path(), + '--daemon', baksnapperd_denotebak[0].full_path(), + '--delete', '20251025T140000', + ], + 'configfile': 'denotebak-default', + 'configfile-baksnapper-args': [], + 'suite': ['denotebak', 'delete'], + 'depends': [baksnapperd_denotebak], + 'env': denotebak_test_env, + }, + { + 'name': 'denotebak-delete-20251025T140000,20251025T160000', + 'runner-args': [ + '--sender', ','.join([ + '20251025T140000:t=hourly', + '20251025T150000:t=hourly', + '20251025T160000:t=hourly']), + '--receiver', ','.join([ + '20251025T140000:t=hourly', + '20251025T150000', + '20251025T160000']), + '--expected', ','.join([ + '20251025T150000']), + '--type', 'denotebak', + ], + 'baksnapper-args': [ + baksnapper[0].full_path(), + '--daemon', baksnapperd_denotebak[0].full_path(), + '--delete', '20251025T140000,20251025T160000', + ], + 'configfile': 'denotebak-default', + 'configfile-baksnapper-args': [], + 'suite': ['denotebak', 'delete'], + 'depends': [baksnapperd_denotebak], + 'env': denotebak_test_env, + }, + { + 'name': 'denotebak-backup-with-right-parent', + 'runner-args': [ + '--sender', ','.join([ + '20251023T130000:t=dailies', + '20251023T180000:t=dailies', + '20251024T090000:t=dailies', + '20251024T130000:t=dailies', + '20251024T180000:t=dailies', + '20251025T120000', + '20251025T130000:t=dailies', + '20251025T140000:t=hourly', + '20251025T150000:t=hourly', + '20251025T160000:t=hourly']), + '--receiver', ','.join([ + '20251023T130000', + '20251023T180000:p=20251023T130000', + '20251024T090000:p=20251023T180000', + '20251024T130000:p=20251024T090000', + '20251024T180000:p=20251024T130000', + '20251025T120000:p=20251024T180000', + '20251025T130000:p=20251025T120000']), + '--expected', ','.join([ + '20251023T130000', + '20251023T180000:p=20251023T130000', + '20251024T090000:p=20251023T180000', + '20251024T130000:p=20251024T090000', + '20251024T180000:p=20251024T130000', + '20251025T120000:p=20251024T180000', + '20251025T130000:p=20251025T120000', + '20251025T160000:p=20251025T130000']), + '--type', 'denotebak', + ], + 'baksnapper-args': [ + baksnapper[0].full_path(), + '--daemon', baksnapperd_denotebak[0].full_path(), + ], + 'configfile': 'denotebak-default', + 'configfile-baksnapper-args': [], + 'suite': ['denotebak', 'backup'], + 'depends': [baksnapperd_denotebak], + 'env': denotebak_test_env, + }, + { + 'name': 'denotebak-backup-all-with-right-parent', + 'runner-args': [ + '--sender', ','.join([ + '20251023T130000:t=dailies', + '20251023T180000:t=dailies', + '20251024T090000:t=dailies', + '20251024T130000:t=dailies', + '20251024T180000:t=dailies', + '20251025T120000', + '20251025T130000:t=dailies', + '20251025T140000:t=hourly', + '20251025T150000:t=hourly', + '20251025T160000:t=hourly']), + '--receiver', ','.join([ + '20251023T130000', + '20251023T180000:p=20251023T130000', + '20251024T090000:p=20251023T180000', + '20251024T130000:p=20251024T090000', + '20251024T180000:p=20251024T130000', + '20251025T120000:p=20251024T180000', + '20251025T130000:p=20251025T120000']), + '--expected', ','.join([ + '20251023T130000', + '20251023T180000:p=20251023T130000', + '20251024T090000:p=20251023T180000', + '20251024T130000:p=20251024T090000', + '20251024T180000:p=20251024T130000', + '20251025T120000:p=20251024T180000', + '20251025T130000:p=20251025T120000', + '20251025T140000:p=20251025T130000', + '20251025T150000:p=20251025T140000', + '20251025T160000:p=20251025T150000']), + '--type', 'denotebak', + ], + 'baksnapper-args': [ + baksnapper[0].full_path(), + '--daemon', baksnapperd_denotebak[0].full_path(), + '--all', + ], + 'configfile': 'denotebak-all', + 'configfile-baksnapper-args': [], + 'suite': ['denotebak', 'backup'], + 'depends': [baksnapperd_denotebak], + 'env': denotebak_test_env, }, ] - foreach test_entry: [ + foreach conf: [ { - 'name': 'default', - 'runner-args': [ - '--sender', '1,2,3', - '--expected', '3', - ], - 'baksnapper-args': [ - baksnapper[0].full_path(), - '--daemon', baksnapperd[0].full_path(), - ], - 'configfile': 'default', - 'configfile-baksnapper-args': [], - 'suite': ['backup'], + 'suffix': '-config', + 'runner-args': [], + 'baksnapper-args': ['--config', 'root'], + 'configfile': false, + 'suite': ['config'], }, { - 'name': 'latest', - 'runner-args': [ - '--sender', '1,2,3', - '--expected', '3', - '--expected-latest', '3', - ], - 'baksnapper-args': [ - baksnapper[0].full_path(), - '--daemon', baksnapperd[0].full_path(), - '--link', - ], - 'configfile': 'latest', - 'configfile-baksnapper-args': [], - 'suite': ['latest'], + 'suffix': '-src-dest', + 'runner-args': ['--src-dest'], + 'baksnapper-args': [], + 'configfile': false, + 'suite': ['src-dest'], }, { - 'name': 'prune', - 'runner-args': [ - '--sender', '1,2,3,6,7,8', - '--receiver', '1,2,3,4,5,6', - '--expected', '1,2,3,6,8:p=6', - ], - 'baksnapper-args': [ - baksnapper[0].full_path(), - '--daemon', baksnapperd[0].full_path(), - '--prune', - ], - 'configfile': 'prune', - 'configfile-baksnapper-args': [], - 'suite': ['prune'], + 'suffix': '-configfile', + 'runner-args': [], + 'baksnapper-args': [], + 'configfile': true, + 'suite': ['configfile'], }, { - 'name': 'backup-2', - 'runner-args': [ - '--sender', '1,2,3', - '--expected', '2', - ], - 'baksnapper-args': [ - baksnapper[0].full_path(), - '--daemon', baksnapperd[0].full_path(), - '--snapshot', '2', - ], - 'configfile': 'default', - 'configfile-baksnapper-args': [], - 'suite': ['backup'], + 'suffix': '-configfile-path', + 'runner-args': ['--path-config'], + 'baksnapper-args': [], + 'configfile': true, + 'suite': ['configfile', 'config'], }, { - 'name': 'delete-1', - 'runner-args': [ - '--sender', '1,2,3', - '--receiver', '1,2,3', - '--expected', '2,3', - ], - 'baksnapper-args': [ - baksnapper[0].full_path(), - '--daemon', baksnapperd[0].full_path(), - '--delete', '1', - ], - 'configfile': 'default', - 'configfile-baksnapper-args': [], - 'suite': ['delete'], + 'suffix': '-configfile-src-dest', + 'runner-args': ['--src-dest'], + 'baksnapper-args': [], + 'configfile': true, + 'suite': ['configfile', 'src-dest'], }, { - 'name': 'delete-1,3', - 'runner-args': [ - '--sender', '1,2,3', - '--receiver', '1,2,3', - '--expected', '2', - ], - 'baksnapper-args': [ - baksnapper[0].full_path(), - '--daemon', baksnapperd[0].full_path(), - '--delete', '1,3', - ], - 'configfile': 'default', - 'configfile-baksnapper-args': [], - 'suite': ['delete'], + 'suffix': '-configfile-src', + 'runner-args': ['--src-dest', '--src-config'], + 'baksnapper-args': [], + 'configfile': true, + 'suite': ['configfile', 'src-dest'], }, { - 'name': 'backup-with-right-parent', - 'runner-args': [ - '--sender', '64,65,76,83,91,92,93', - '--receiver', '64,66:p=64,76:p=66,83:p=76,91:p=83,92:p=83', - '--expected', '64,66:p=64,76:p=66,83:p=76,91:p=83,92:p=83,93:p=92', - ], - 'baksnapper-args': [ - baksnapper[0].full_path(), - '--daemon', baksnapperd[0].full_path(), - ], - 'configfile': 'default', - 'configfile-baksnapper-args': [], - 'suite': ['backup'], + 'suffix': '-configfile-dest', + 'runner-args': ['--src-dest', '--dest-config'], + 'baksnapper-args': [], + 'configfile': true, + 'suite': ['configfile', 'src-dest'], }, - # https://github.com/plattfot/baksnapper/issues/27 { - 'name': 'backup-all-with-right-parent', - 'runner-args': [ - '--sender', '64,65,76,83,91,92,93', - '--receiver', '64,66:p=64,76:p=66,83:p=76,91:p=83,92:p=83', - '--expected', '64,65:p=64,66:p=64,76:p=66,83:p=76,91:p=83,92:p=83,93:p=92', - ], - 'baksnapper-args': [ - baksnapper[0].full_path(), - '--daemon', baksnapperd[0].full_path(), - '--all', - ], - 'configfile': 'all', - 'configfile-baksnapper-args': [], - 'suite': ['backup'], - } + 'suffix': '-configfile-both', + 'runner-args': ['--src-dest', '--src-config', '--dest-config'], + 'baksnapper-args': [], + 'configfile': true, + 'suite': ['configfile', 'src-dest'], + }, ] foreach ssh: [ { @@ -350,10 +645,16 @@ foreach conf: [ baksnapper_args += test_entry['configfile-baksnapper-args'] test_depends += [test_config] endif + + # The DeNotebak backend does not support --config + if 'denotebak' in test_entry['suite'] and 'src-dest' not in conf['suite'] + continue + endif + test( test_entry['name']+conf['suffix']+ssh['suffix']+ssh_key['suffix'], runner, - env: test_env, + env: test_entry['env'], args: test_entry['runner-args'] + conf['runner-args'] + ssh['args'] + runner_args + ['--'] + @@ -361,8 +662,7 @@ foreach conf: [ ssh_key['args'] + baksnapper_args, depends: [ baksnapper, - baksnapperd, - ] + test_depends, + ] + test_entry['depends'] + test_depends, suite: conf['suite'] + test_entry['suite'], ) endforeach diff --git a/tests/bin/btrfs b/tests/bin/btrfs index 2ddb258..f8e8f5c 100755 --- a/tests/bin/btrfs +++ b/tests/bin/btrfs @@ -60,11 +60,13 @@ do ;; esac done - function subvolume { case "$1" in + show) + test -d "$2" + ;; delete) - rm -rf -- "$1" + rm -rf -- "$2" ;; *) >&2 echo "unsupported subcommand to subvolume: $1" @@ -82,16 +84,21 @@ function receive { snapshot_parent=${data%:*} if [[ -n "$snapshot_parent" ]] then - snapshot_parent=$(basename "$(dirname "$snapshot_parent")") + if [[ "$(basename "$snapshot_parent")" == "snapshot" ]] + then + snapshot_parent=$(basename "$(dirname "$snapshot_parent")") + else + snapshot_parent=$(basename "$snapshot_parent") + fi fi snapshot=${data#*:} - - mkdir -p "$1/snapshot" + path="$1/$(basename "$snapshot")" + mkdir -p "$path" { echo "parent=$snapshot_parent" echo "snapshot=$snapshot" echo "ro=true" - } > "$1/snapshot/data" + } > "$path/data" } function property { diff --git a/tests/runner.scm b/tests/runner.scm index 5ad621f..d3ed138 100755 --- a/tests/runner.scm +++ b/tests/runner.scm @@ -23,11 +23,12 @@ (string-join paths file-name-separator-string)) (define-record-type - (make-snapshot id parent state) + (make-snapshot id parent state tags) snapshot? (id snapshot-id) (parent snapshot-parent) - (state snapshot-state)) + (state snapshot-state) + (tags snapshot-tags)) (define (make-snapshot-from input) "Parse INPUT and create a snapshot from it." @@ -37,12 +38,13 @@ (map (lambda (metadatum) (match (string-split metadatum #\=) - (("p" parent) (cons 'parent parent)) - (("s" state) - (let ((state-sym (string->symbol state))) - (match state-sym + (("p" parent) (cons 'parent parent)) + (("s" state) + (let ((state-sym (string->symbol state))) + (match state-sym ((or 'valid 'empty 'no-info 'no-snapshot 'incomplete) - (cons 'state state-sym))))))) + (cons 'state state-sym))))) + (("t" tags) (cons 'tags tags)))) (cdr info)))) (make-snapshot id @@ -51,7 +53,64 @@ (_ #f)) (match (assoc 'state metadata) (('state . s) s) - (_ 'valid))))) + (_ 'valid)) + (match (assoc 'state metadata) + (('tags . t) t) + (_ #f))))) + +(define* (create-snapshot path snapshot #:key valid?) + "Create a dummy snapshot at PATH. + +It will have the following structure: + +PATH[__] +└── data + +The file `data` will contain; snapshot=ID, where ID is the id from +SNAPSHOT; parent=PARENT, where PARAENT is the parent from SNAPSHOT. +If parent is #f it will skip it. + +It will append the tags string to the path, separated by `__`, if that +does not evaluate to false. + +It will also contain `ro=true` if VALID? evaluates to true." +(mkdir + (match (snapshot-tags snapshot) + (#f path) + (tags (string-append path "__" tags)))) + +(let ((port (open-output-file + (string-append path file-name-separator-string "data")))) + (and-let* ((parent (snapshot-parent snapshot))) + (format port "parent=~a~%" parent)) + (format port "snapshot=~a~%" (snapshot-id snapshot)) + (when valid? + (format port "ro=true~%")) + (close-port port))) + +(define (create-denotebak-snapshot path snapshot) + "Create dummy DeNotebak snapshot at PATH for SNAPSHOT. + +A DeNotebak snapshot is just a btrfs snapshot. This will mock the +structure with the following: + +PATH[__] +└── data + +The state in SNAPSHOT specify what state it should create the +snapshot, accepted values are: + +- valid: a complete snapshot as described above. With `ro=true` added +to the `data` file. + +- incomplete: a broken snapshot which snapshot directory is +incomplete. Which is indicated by the `ro=true` attribute will be +missing in the `data` file." + (match (snapshot-state snapshot) + ('valid + (create-snapshot path snapshot #:valid? #t)) + ('incomplete + (create-snapshot path snapshot #:valid? #f)))) (define (create-snapper-snapshot path snapshot) "Create dummy snapper snapshot at PATH for SNAPSHOT. @@ -93,35 +152,24 @@ incomplete. It will have both `info.xml` and `snapshot`. But the (create-info.xml (lambda () (close-port (open-output-file - (string-append path "/info.xml"))))) - (create-snapshot (lambda* (#:key valid?) - (mkdir snapshot-dir) - (let ((port (open-output-file - (string-append snapshot-dir - file-name-separator-string "data")))) - (and-let* ((parent (snapshot-parent snapshot))) - (format port "parent=~a~%" parent)) - (format port "snapshot=~a~%" (snapshot-id snapshot)) - (when valid? - (format port "ro=true~%")) - (close-port port))))) + (string-append path "/info.xml")))))) (match (snapshot-state snapshot) ('valid (mkdir path) (create-info.xml) - (create-snapshot #:valid? #t)) + (create-snapshot snapshot-dir snapshot #:valid? #t)) ('empty (mkdir path)) ('no-info (mkdir path) - (create-snapshot #:valid? #t)) + (create-snapshot snapshot-dir snapshot #:valid? #t)) ('no-snapshot (mkdir path) (create-info.xml)) ('incomplete (mkdir path) (create-info.xml) - (create-snapshot #:valid? #f))))) + (create-snapshot snapshot-dir snapshot #:valid? #f))))) (define (check-snapshot-input value) "Verify the input for receiver or expected." @@ -137,6 +185,7 @@ incomplete. It will have both `info.xml` and `snapshot`. But the (_ (format (current-error-port) "unsupported state: ~a~%" state) #f))) + (("t" tags) #t) (_ (format (current-error-port) "invalid snapshot syntax: ~a~%" metadata) #f)) @@ -171,7 +220,7 @@ incomplete. It will have both `info.xml` and `snapshot`. But the (read-snapshots (cons (path-join parent name) child))) children))))) -(define (make-snapshot-from-read data) +(define (make-snapshot-from-snapper-read data) "Create a snapshot from DATA. Where DATA is in the format of what you get calling `read-snapshots'." @@ -202,7 +251,38 @@ Where DATA is in the format of what you get calling `read-snapshots'." ((#t () _) 'no-snapshot) (_ - 'incomplete))))) + 'incomplete)) + #f))) + +(define (make-snapshot-from-denotebak-read data) + "Create a snapshot from DATA. + +Where DATA is in the format of what you get calling `read-snapshots'." + (match data + (('latest . #t) (make-snapshot 'latest #f 'valid #f)) + (_ + (let* ((id (car data)) + (get (lambda (key alist default) + (match (assoc key alist) + ((key . v) v) + (_ default)))) + (metadata (cadr data)) + (snapshot-info (get "snapshot" metadata #f)) + (read-only (match (assoc "ro" metadata) + (("ro" . "true") #t) + (_ #f))) + (parent (get "parent" metadata #f))) + (make-snapshot + id + parent + (match (list snapshot-info read-only) + ((#f _) + 'no-snapshot) + ((snapshot #t) + 'valid) + (_ + 'incomplete)) + #f))))) (define (check-latest expected-latest receiver-dir) "Verify that EXPECTED-LATEST points to the expected snapshot. @@ -260,7 +340,7 @@ Return the path to the modified CONFIGFILE if it is defined otherwise #f." (define (main args) (let* ((option-spec `((config (single-char #\c) (value #t)) - (sender (single-char #\s) (value #t)) + (sender (single-char #\s) (value #t) (predicate ,check-snapshot-input)) (latest (single-char #\l) (value #t)) (receiver (single-char #\r) (value #t) (predicate ,check-snapshot-input)) (expected (single-char #\e) (value #t) (predicate ,check-snapshot-input)) @@ -332,6 +412,7 @@ Fredrik \"PlaTFooT\" Salomonsson (let* ((parse-comma-option (lambda (input) (remove string-null? (string-split (option-ref options input "") #\,)))) + (type (string->symbol (option-ref options 'type "snapper"))) (config (option-ref options 'config "root")) (sender (parse-comma-option 'sender)) (receiver (parse-comma-option 'receiver)) @@ -343,7 +424,13 @@ Fredrik \"PlaTFooT\" Salomonsson (sender-dir (path-join test-dir config ".snapshots")) (receiver-root-dir (path-join test-dir "receiver")) (receiver-dir (path-join receiver-root-dir config)) - (configfile (option-ref options 'configfile #f))) + (configfile (option-ref options 'configfile #f)) + (create-snapshot-func (match type + ('denotebak create-denotebak-snapshot) + (_ create-snapper-snapshot))) + (make-snapshot-from-read (match type + ('denotebak make-snapshot-from-denotebak-read) + (_ make-snapshot-from-snapper-read)))) ;; Needed for nftw to be able to read the directory (chmod test-dir #o744) (format #t "Test ~a~%" test-dir) @@ -352,8 +439,8 @@ Fredrik \"PlaTFooT\" Salomonsson (format #t " expected: ~a~%" expected) (let ((exit-status 0) (sender-snapshots - (map (lambda (id) - (make-snapshot id #f 'valid)) + (map (lambda (input) + (make-snapshot-from input)) sender)) (receiver-snapshots (map (lambda (input) @@ -371,7 +458,7 @@ Fredrik \"PlaTFooT\" Salomonsson (mkdir sender-dir) (for-each (lambda (snapshot) - (create-snapper-snapshot + (create-snapshot-func (path-join sender-dir (snapshot-id snapshot)) snapshot)) sender-snapshots) @@ -380,7 +467,7 @@ Fredrik \"PlaTFooT\" Salomonsson (mkdir receiver-dir) (for-each (lambda (snapshot) - (create-snapper-snapshot + (create-snapshot-func (path-join receiver-dir (snapshot-id snapshot)) snapshot)) receiver-snapshots)