#!/usr/bin/env bash # ___ ___ ___ ___ # /\__\ /\ \ /\ \ /\__\ # /:/ / \:\ \ \:\ \ /::| | # /:/__/ \:\ \ \:\ \ /:|:| | # /::\ \ ___ /::\ \ /::\ \ /:/|:|__|__ # /:/\:\ /\__\ /:/\:\__\ /:/\:\__\ /:/ |::::\__\ # \/__\:\/:/ / /:/ \/__/ /:/ \/__/ \/__/~~/:/ / # \::/ / /:/ / /:/ / /:/ / # /:/ / \/__/ \/__/ /:/ / # /:/ / /:/ / # \/__/ \/__/ # # Copyright (c) 2023, Robert Swinford gmail.com> # # For the full copyright and license information, please view the LICENSE file # that was distributed with this source code. set -euf -o pipefail #set -x print_version() { printf "\ nicotine $(httm --version | cut -f2 -d' ') " 1>&2 exit 0 } print_usage() { local nicotine="\e[31mnicotine\e[0m" local httm="\e[31mhttm\e[0m" local git="\e[31mgit\e[0m" local tar="\e[31mtar\e[0m" printf "\ $nicotine is a wrapper script for $httm which converts unique snapshot file versions to a $git archive. USAGE: nicotine [OPTIONS]... [file1 file2...] OPTIONS: --output-dir: Select the output directory. Default is the current working directory. --no-archive Disable archive creation. Create a new $git respository (e.g. named \"\$file1-git\") in the output directory. --debug: Show $git and $tar command output. Default is to complete silence both. --help: Display this dialog. --version: Display script version. " 1>&2 exit 1 } print_err_exit() { print_err "$@" exit 1 } print_err() { printf "%s\n" "Error: $*" 1>&2 } prep_exec() { [[ -n "$( command -v find exit 0 )" ]] || print_err_exit "'find' is required to execute 'nicotine'. Please check that 'find' is in your path." [[ -n "$( command -v readlink exit 0 )" ]] || print_err_exit "'readlink' is required to execute 'nicotine'. Please check that 'readlink' is in your path." [[ -n "$( command -v git exit 0 )" ]] || print_err_exit "'git' is required to execute 'nicotine'. Please check that 'git' is in your path." [[ -n "$( command -v tar exit 0 )" ]] || print_err_exit "'tar' is required to execute 'nicotine'. Please check that 'targit' is in your path." [[ -n "$( command -v mktemp exit 0 )" ]] || print_err_exit "'mktemp' is required to execute 'nicotine'. Please check that 'mktemp' is in your path." [[ -n "$( command -v mkdir exit 0 )" ]] || print_err_exit "'mkdir' is required to execute 'nicotine'. Please check that 'mkdir' is in your path." [[ -n "$( command -v httm exit 0 )" ]] || print_err_exit "'httm' is required to execute 'nicotine'. Please check that 'httm' is in your path." } function copy_add_commit { local debug=$1 shift local path="$1" shift local dest_dir="$1" shift if [[ -d "$path" ]]; then cp -a "$path" "$dest_dir/" # return early -- only commit files not directories return 0 else cp -a "$path" "$dest_dir" fi if [[ $debug = true ]]; then git add --all "$dest_dir" git commit -m "httm commit from ZFS snapshot" --date "$(date -d "$(stat -c %y $path)")" || true else git add --all "$dest_dir" > /dev/null git commit -q -m "httm commit from ZFS snapshot" --date "$(date -d "$(stat -c %y $path)")" > /dev/null || true fi } function get_unique_versions { local debug=$1 shift local path="$1" shift local dest_dir="$1" shift local -a version_list # all we care about is unique file versions, unique directory versions aren't useful # in this context, as we are recursing and commiting every file we find, we can skip # commiting all directory versions we find [[ -d "$path" ]] || while read -r line; do version_list+=("$line") done <<<"$(httm -n --omit-ditto "$path")" # see above, one/zero version indicates $path has no snaps if [[ -d "$path" ]] || [[ ${#version_list[@]} -eq 0 ]] || [[ ${#version_list[@]} -eq 1 ]]; then copy_add_commit $debug "$path" "$dest_dir" else for version in "${version_list[@]}"; do copy_add_commit $debug "$version" "$dest_dir" done fi } function traverse { local debug=$1 shift local path="$1" shift local dest_dir="$1" shift get_unique_versions $debug "$path" "$dest_dir" # return early - if is file, can't traverse [[ -d "$path" ]] || return 0 local -a dir_entries=() local basename="$(basename "$path")" while read -r line; do [[ -z "$line" ]] || dir_entries+=("$line") done <<<"$(find "$path" -mindepth 1 -maxdepth 1)" # return early - empty dir [[ ${#dir_entries[@]} -ne 0 ]] || return 0 for entry in "${dir_entries[@]}"; do if [[ -d "$entry" ]]; then traverse $debug "$entry" "$dest_dir/$basename" else get_unique_versions $debug "$entry" "$dest_dir/$basename" fi done } function convert_to_git { local debug=$1 shift local no_archive=$1 shift local working_dir="$1" shift local tmp_dir="$1" shift local output_dir="$1" shift local path="$1" shift local archive_dir="" local basename="" # create dir for file raw="$(basename "$path")" # Use parameter expansion to remove leading dot: "${FILENAME#.}" basename="${raw#.}" # git requires a dir to init archive_dir="$tmp_dir/$basename" mkdir "$archive_dir" || print_err_exit "nicotine could not create a temporary directory. Check you have permissions to create." # ... and we must enter the dir to have git work cd "$archive_dir" || print_err_exit "nicotine could not enter a temporary directory: $archive_dir. Check you have permissions to enter." # create git repo if [[ $debug = true ]]; then git init || print_err_exit "git could not initialize directory" else git init -q >/dev/null || print_err_exit "git could not initialize directory" fi # copy, add, and commit to git repo in loop # why branch? because git requires a dir and # for files we create a dir specifically for the file if [[ -d "$path" ]]; then traverse $debug "$path" "$tmp_dir" else traverse $debug "$path" "$archive_dir" fi if [[ $no_archive = true ]]; then cp -ra "$archive_dir" "$output_dir/$basename-git" else # tar works with relative paths, so make certain we are in our base tmp_dir cd "$tmp_dir" # create archive local output_file="$output_dir/$basename-git.tar.gz" if [[ $debug = true ]]; then tar -zcvf "$output_file" "./$basename" || print_err_exit "Archive creation failed. Quitting." else tar -zcvf "$output_file" "./$basename" > /dev/null || print_err_exit "Archive creation failed. Quitting." fi fi # cleanup safely [[ ! -e "$tmp_dir/$basename" ]] || rm -rf "$tmp_dir/$basename" [[ -e "$tmp_dir/$basename" ]] || rm -rf "$tmp_dir/*" if [[ $no_archive = true ]]; then printf "nicotine git repository created successfully: $basename-git\n" else printf "nicotine git archive created successfully: $output_file\n" fi } function nicotine { # do we have commands to execute? prep_exec local debug=false local no_archive=false local working_dir local output_dir working_dir="$(realpath "$( pwd )" 2>/dev/null || true)" [[ -e "$working_dir" ]] || print_err_exit "Could not obtain current working directory. Quitting." output_dir="$working_dir" [[ $# -ge 1 ]] || print_usage [[ "$1" != "-h" && "$1" != "--help" ]] || print_usage [[ "$1" != "-V" && "$1" != "--version" ]] || print_version while [[ $# -ge 1 ]]; do if [[ "$1" == "--output-dir" ]]; then shift [[ $# -ge 1 ]] || print_err_exit "output-dir argument is empty" output_dir="$(realpath "$1" 2>/dev/null || true)" [[ -e "$output_dir" ]] || print_err_exit "Could not obtain output directory from path given. Quitting." shift elif [[ "$1" == "--debug" ]]; then shift debug=true elif [[ "$1" == "--no-archive" ]]; then shift no_archive=true else break fi done local tmp_dir="$( mktemp -d )" trap "[[ ! -d "$tmp_dir" ]] || rm -rf "$tmp_dir"" EXIT [[ -e "$tmp_dir" ]] || print_err_exit "Could not create a temporary directory for scratch work. Quitting." [[ $# -ne 0 ]] || print_err_exit "User must specify at least one input file. Quitting." for a; do canonical_path="$( realpath "$a" 2>/dev/null [[ $? -eq 0 ]] || print_err "Could not determine canonical path for: $a" )" [[ -n "$canonical_path" ]] || continue # check if file exists if [[ ! -e "$canonical_path" ]]; then printf "$canonical_path does not exist. Skipping.\n" continue fi # ... and tar will not create an archive for an empty dir if [[ -d "$canonical_path" ]] && [[ -z "$(find "$canonical_path" -mindepth 1 -maxdepth 1)" ]]; then printf "$canonical_path is an empty directory. Skipping.\n" continue fi # if not a file, directory, or symlink, we can't use so also skip if [[ ! -f "$canonical_path" && ! -d "$canonical_path" && ! -L "$canonical_path" ]]; then printf "$canonical_path is not a file, directory, or symlink. Skipping.\n" continue fi convert_to_git $debug $no_archive "$working_dir" "$tmp_dir" "$output_dir" "$canonical_path" done } nicotine "$@"