#!/bin/sh set -eu prev_hash= prev_bk_file= # Back up a database and output a SHA-256 hash of its SQL dump. backup() { local db_file="${1}" local bk_file="${2}" shift 2 sqlite3 "${db_file}" ".backup ${bk_file}~" sqlite3 "${bk_file}~" '.dump' | sha256sum | cut -d ' ' -f 1 return 0 } # Read previous backup's hash and file name. # Returns 0 if the new backup's hash is equal to the previous backup's hash, or # 1 otherwise. compare_hashes() { local db_file="${1}" local bk_hash="${2}" shift 2 { read -r prev_hash prev_bk_file 0<"${db_file}.bakhash"; } 2>/dev/null \ || : case "${prev_hash}" in "${bk_hash}") return 0;; esac return 1 } # Make a symbolic link with the new backup file name to the previous backup # file. # No path resolution is performed, so either absolute paths should be used or # all backup files should be in the same directory. # The linking of the final backup file and the unlinking of the temporary file # are not atomic, but as long as they're done in this order, the only possible # consequence is that the linking is done but an extraneous temporary file # remains. link_to_prev() { local bk_file="${1}" shift 1 ln -s "${prev_bk_file}.xz" "${bk_file}.xz" rm "${bk_file}~" return 0 } # Compress and save a backup file. save_backup() { local db_file="${1}" shift 1 xz "${bk_file}~" mv "${bk_file}~.xz" "${bk_file}.xz" return 0 } # Save a backup file's name and hash for future comparison and linking. save_hash() { local db_file="${1}" local bk_file="${2}" local bk_hash="${3}" shift 3 printf '%s %s\n' "${bk_hash}" "${bk_file}" 1>"${db_file}.bakhash~" mv "${db_file}.bakhash~" "${db_file}.bakhash" return 0 } usage() { printf 'Usage: %s \n' "${0}" return 0 } main() { local db_file= local bk_file= local bk_hash= if [ ${#} -ne 2 ]; then usage 1>&2 return 1 fi db_file="${1}" bk_file="${2}" shift 2 bk_hash="$(backup "${db_file}" "${bk_file}")" if compare_hashes "${db_file}" "${bk_hash}"; then link_to_prev "${bk_file}" else save_backup "${bk_file}" save_hash "${db_file}" "${bk_file}" "${bk_hash}" fi return 0 } main "${@}"