#!/usr/bin/env bash # Define variables declare -Ag appRollCall # Associative array for storing app status declare -Ag fileRollCall # Associative array for storing file status declare -Ag dirRollCall # Associative array for storing dir status declare -ag arrayPosArgs # Associative array for processArgs() function declare -g ots_delay; ots_delay=1 # minimum time in seconds between ots operations declare -ag calendars; calendars+=("https://finney.calendar.eternitywall.com"); calendars+=("https://btc.calendar.catallaxy.com"); calendars+=("https://alice.btc.calendar.opentimestamps.org"); calendars+=("https://bob.btc.calendar.opentimestamps.org"); # Declare functions yell() { echo "$0: $*" >&2; } # print script path and all args to stderr die() { yell "$*"; exit 111; } # same as yell() but non-zero exit status try() { "$@" || die "cannot $*"; } # runs args as command, reports args if command fails checkapp() { # Desc: If arg is a command, save result in assoc array 'appRollCall' # Usage: checkapp arg1 arg2 arg3 ... # Version: 0.1.1 # Input: global assoc. array 'appRollCall' # Output: adds/updates key(value) to global assoc array 'appRollCall' # Depends: bash 5.0.3 local returnState #===Process Args=== for arg in "$@"; do if command -v "$arg" 1>/dev/null 2>&1; then # Check if arg is a valid command appRollCall[$arg]="true"; if ! [ "$returnState" = "false" ]; then returnState="true"; fi; else appRollCall[$arg]="false"; returnState="false"; fi; done; #===Determine function return code=== if [ "$returnState" = "true" ]; then return 0; else return 1; fi; } # Check that app exists checkfile() { # Desc: If arg is a file path, save result in assoc array 'fileRollCall' # Usage: checkfile arg1 arg2 arg3 ... # Version: 0.1.1 # Input: global assoc. array 'fileRollCall' # Output: adds/updates key(value) to global assoc array 'fileRollCall'; # Output: returns 0 if app found, 1 otherwise # Depends: bash 5.0.3 local returnState #===Process Args=== for arg in "$@"; do if [ -f "$arg" ]; then fileRollCall["$arg"]="true"; if ! [ "$returnState" = "false" ]; then returnState="true"; fi; else fileRollCall["$arg"]="false"; returnState="false"; fi; done; #===Determine function return code=== if [ "$returnState" = "true" ]; then return 0; else return 1; fi; } # Check that file exists checkdir() { # Desc: If arg is a dir path, save result in assoc array 'dirRollCall' # Usage: checkdir arg1 arg2 arg3 ... # Version 0.1.2 # Input: global assoc. array 'dirRollCall' # Output: adds/updates key(value) to global assoc array 'dirRollCall'; # Output: returns 0 if all args are dirs; 1 otherwise # Depends: Bash 5.0.3 local returnState #===Process Args=== for arg in "$@"; do if [ -z "$arg" ]; then dirRollCall["(Unspecified Dirname(s))"]="false"; returnState="false"; elif [ -d "$arg" ]; then dirRollCall["$arg"]="true"; if ! [ "$returnState" = "false" ]; then returnState="true"; fi else dirRollCall["$arg"]="false"; returnState="false"; fi done #===Determine function return code=== if [ "$returnState" = "true" ]; then return 0; else return 1; fi } # Check that dir exists displayMissing() { # Desc: Displays missing apps, files, and dirs # Usage: displayMissing # Version 1.0.0 # Input: associative arrays: appRollCall, fileRollCall, dirRollCall # Output: stderr: messages indicating missing apps, file, or dirs # Output: returns exit code 0 if nothing missing; 1 otherwise # Depends: bash 5, checkAppFileDir() local missingApps value appMissing missingFiles fileMissing local missingDirs dirMissing #==BEGIN Display errors== #===BEGIN Display Missing Apps=== missingApps="Missing apps :"; #for key in "${!appRollCall[@]}"; do echo "DEBUG:$key => ${appRollCall[$key]}"; done for key in "${!appRollCall[@]}"; do value="${appRollCall[$key]}"; if [ "$value" = "false" ]; then #echo "DEBUG:Missing apps: $key => $value"; missingApps="$missingApps""$key "; appMissing="true"; fi; done; if [ "$appMissing" = "true" ]; then # Only indicate if an app is missing. echo "$missingApps" 1>&2; fi; unset value; #===END Display Missing Apps=== #===BEGIN Display Missing Files=== missingFiles="Missing files:"; #for key in "${!fileRollCall[@]}"; do echo "DEBUG:$key => ${fileRollCall[$key]}"; done for key in "${!fileRollCall[@]}"; do value="${fileRollCall[$key]}"; if [ "$value" = "false" ]; then #echo "DEBUG:Missing files: $key => $value"; missingFiles="$missingFiles""$key "; fileMissing="true"; fi; done; if [ "$fileMissing" = "true" ]; then # Only indicate if an app is missing. echo "$missingFiles" 1>&2; fi; unset value; #===END Display Missing Files=== #===BEGIN Display Missing Directories=== missingDirs="Missing dirs:"; #for key in "${!dirRollCall[@]}"; do echo "DEBUG:$key => ${dirRollCall[$key]}"; done for key in "${!dirRollCall[@]}"; do value="${dirRollCall[$key]}"; if [ "$value" = "false" ]; then #echo "DEBUG:Missing dirs: $key => $value"; missingDirs="$missingDirs""$key "; dirMissing="true"; fi; done; if [ "$dirMissing" = "true" ]; then # Only indicate if an dir is missing. echo "$missingDirs" 1>&2; fi; unset value; #===END Display Missing Directories=== #==END Display errors== #==BEGIN Determine function return code=== if [ "$appMissing" == "true" ] || [ "$fileMissing" == "true" ] || [ "$dirMissing" == "true" ]; then return 1; else return 0; fi #==END Determine function return code=== } # Display missing apps, files, dirs vbm() { # Description: Prints verbose message ("vbm") to stderr if opVerbose is set to "true". # Usage: vbm "DEBUG :verbose message here" # Version 0.2.0 # Input: arg1: string # vars: opVerbose # Output: stderr # Depends: bash 5.0.3, GNU-coreutils 8.30 (echo, date) if [ "$opVerbose" = "true" ]; then functionTime="$(date --iso-8601=ns)"; # Save current time in nano seconds. echo "[$functionTime]:$0:""$*" 1>&2; # Display argument text. fi # End function return 0; # Function finished. } # Displays message if opVerbose true showVersion() { # Desc: Displays script version and license information. # Usage: showVersion # Version: 0.0.1 # Input: scriptVersion var containing version string # Output: stdout # Depends: vbm(), yell, GNU-coreutils 8.30 # Initialize function vbm "DEBUG:showVersion function called." cat <<'EOF' bkots 0.0.4 Copyright (C) 2022 Steven Baltakatei Sandoval License GPLv3: GNU GPL version 3 This is free software; you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. GNU Coreutils 8.32 Copyright (C) 2020 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later . This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. EOF # End function vbm "DEBUG:showVersion function ended." return 0; # Function finished. } # Display script version. showUsage() { # Desc: Display script usage information # Usage: showUsage # Version 0.0.1 # Input: none # Output: stdout # Depends: GNU-coreutils 8.30 (cat) cat <<'EOF' USAGE: bkots [ options ] [PATH...] POSITIONAL ARGUMENTS: PATH Path(s) of file(s) or directory(ies) OPTIONS: --dry-run Do everything except run 'ots' commands. -h, --help Display help information. --include-dotfiles Include files and directories starting with '.' (not included by default). -r, --recursive Consider files in dirs recursively. --version Display script version. -v, --verbose Display debugging info. -- Mark end of options. Interpret remaining arguments as positional arguments. DESCRIPTION: Scans files by file paths or directory paths provided by positional arguments to see if Open Timestamps '.ots' file exists. If so, attempt to upgrade and verify the '.ots' file. If no '.ots' file exists, attempt to create one. Files with a dotfile parent directory located anywhere in the file path are ignored by default. (e.g. 'HEAD' in '/home/user/diary/.git/logs/HEAD' because of '.git'). Dotfiles themselves are also ignored by default (e.g. '/home/user/.gitconfig'). EXAMPLES: bkots -v foo.txt bkots foo.txt bar.pdf /home/username/Pictures/ EOF } # Display information on how to use this script. processArgs() { # Desc: Processes arguments provided to script. # Usage: processArgs "$@" # Version: 1.0.0 # Input: "$@" (list of arguments provided to the function) # Output: Sets following variables used by other functions: # opVerbose Indicates verbose mode enable status. (ex: "true", "false") # arrayPosArgs Array of remaining positional argments # Depends: # yell() Displays messages to stderr. # vbm() Displays messsages to stderr if opVerbose set to "true". # showUsage() Displays usage information about parent script. # showVersion() Displays version about parent script. # arrayPosArgs Global array for storing non-option positional arguments (i.e. arguments following the `--` option). # External dependencies: bash (5.1.16), echo # Ref./Attrib.: # [1]: Marco Aurelio (2014-05-08). "echo that outputs to stderr". https://stackoverflow.com/a/23550347 # [2]: "Handling positional parameters" (2018-05-12). https://wiki.bash-hackers.org/scripting/posparams # Initialize function vbm "DEBUG:processArgs function called." # Perform work while [ ! $# -eq 0 ]; do # While number of arguments ($#) is not (!) equal to (-eq) zero (0). #yell "DEBUG:Starting processArgs while loop." # Debug stderr message. See [1]. #yell "DEBUG:Provided arguments are:""$*" # Debug stderr message. See [1]. case "$1" in --dry-run) # Do not run ots commands option_dry_run="true"; vbm "DEBUG:Option enabled:dry run";; -h | --help) showUsage; exit 1;; # Display usage. --include-dotfiles) # Include dotfiles option_include_dotfiles="true"; vbm "DEBUG:Option enabled:include dotfiles";; -r | --recursive) # Specify recursive option option_recursive="true"; vbm "DEBUG:option enabled:include files in dirs recursively";; --version) showVersion; exit 1;; # Show version -v | --verbose) opVerbose="true"; vbm "DEBUG:Verbose mode enabled.";; # Enable verbose mode. See [1]. --) # End of all options. See [2]. shift; for arg in "$@"; do vbm "DEBUG:adding to arrayPosArgs:$arg"; arrayPosArgs+=("$arg"); done; break;; -*) showUsage; yell "ERROR: Unrecognized option."; exit 1;; # Display usage *) # Assume remaining arguments are positional arguments for arg in "$@"; do vbm "DEBUG:adding to arrayPosArgs:$arg"; arrayPosArgs+=("$arg"); done; break;; #*) showUsage; yell "ERROR: Unrecognized argument."; exit 1;; # Handle unrecognized options. See [1]. esac shift done # End function vbm "DEBUG:processArgs function ended." return 0; # Function finished. }; # Evaluate script options from positional arguments (ex: $1, $2, $3, etc.). get_parent_dirnames() { # Desc: Provides newline-delimited list of each parent dir of a file or dir # Usage: get_parent_dirnames arg1 # Input: arg1 input path # Output: stdout newline-delimited list of parent dirs # Version: 0.0.1 # Depends: yell(), die(), try() local path # Check input if [[ $# -ne 1 ]]; then die "FATAL:Incorrect number of arguments:$#"; fi; if ! { [[ -f $1 ]] || [[ -d $1 ]]; }; then die "FATAL:Not a file or dir:$1"; fi; # Process path path="$1"; while [[ -f $path ]] || [[ -d $path ]]; do path="$(dirname "$path")"; name_base_previous="$name_base"; name_base="$(basename "$path")"; ## Check for stop condition (dirname returns same result as previous iteration) if [[ $name_base == "$name_base_previous" ]]; then break; fi; echo "$name_base"; done; }; # Output parent dirnames to stdout main() { # Desc: Creates `.ots` file: # - for each file specified in arrayPosArgs array # - for each file in each dir specified in arrayPosArgs array # Output file created alongside each file or in output directory specified by pathDirIn1 # Usage: main "$@"; # Input: arrayPosArgs array with positional arguments # pathDirOut1 path for output `.ots` files (if pathDirOut1 is specified and is a path) # Output: file(s) creates `.ots` file alongside specified files # Depends: find (GNU findutils) 4.8.0, GNU Coreutils 8.32 (sort) # Ref/Attrib: [1] How to create an array of unique elements from a string/array in bash https://unix.stackexchange.com/a/167194 # [2] How to find files containing newlines in their names https://stackoverflow.com/a/21727028 local -a file_list file_list_pruned; local -a files_to_verify files_to_upgrade files_to_stamp local -a files_to_verify_pruned files_to_upgrade_pruned files_to_stamp_pruned # Process args processArgs "$@"; # Check dependencies if ! checkapp ots find; then displayMissing; die "FATAL:Missing dependencies."; fi; # Check arguments ## Mark if output dir option specified if [[ -v pathDirOut1 ]]; then vbm "DEBUG:output directory specified:pathDirOut1:$pathDirOut1"; if [[ -d $pathDirOut1 ]]; then vbm "DEBUG:pathDirOut1:$pathDirOut1"; config_output_dir="true"; else die "ERROR:Not a dir:$pathDirOut1"; fi; fi; # Display ots details vbm "$(type ots)"; # show how 'ots' is defined #TODO: add option to define 'ots' as a bash function that #populates the ots option '--bitcoin-node FILE' with a #user-specified FILE. # Populate file_list vbm "DEBUG:begin populate file_list array"; for item in "${arrayPosArgs[@]}"; do vbm "DEBUG:adding to file list:item:$item"; ## Get full canonicalized path (follow symlinks) item="$(readlink -f "$item")"; vbm "DEBUG:item full path:item:$item"; ## Add to list: files if [[ -f $item ]]; then vbm "DEBUG:is a file:item:$item"; file_list+=("$item"); vbm "DEBUG:added to file_list:$item"; ## Add to list: files in dirs elif [[ -d $item ]]; then vbm "DEBUG:is a dir:item:$item"; ### Check for recursive flag if [[ "$option_recursive" == "true" ]]; then vbm "DEBUG:option_recursive:$option_recursive"; while read -r line; do file_list+=("$line"); vbm "DEBUG:added to file_list:$line"; done < <(find "$item" -type f); else while read -r line; do file_list+=("$line"); vbm "DEBUG:added to file_list:$line"; done < <(find "$item" -maxdepth 1 -type f); fi; else die "ERROR:Not a file or dir:item:$item"; fi; done; if [[ $opVerbose == "true" ]]; then vbm "DEBUG:file_list:"; printf "%s\n" "${file_list[@]}"; fi; # Prune file_list for item in "${file_list[@]}"; do if ! [[ $option_include_dotfiles == "true" ]]; then ## Ignore files located beneath a dotfile directory (e.g. '/home/my_repo/.git/config') unset flag_contains_dotfile_parent; while read -r line; do ### Check line from output of get_parent_dirnames pattern="^\."; if [[ $line =~ $pattern ]]; then #### line starts with '.' vbm "DEBUG:Dotfile parent detected. Not including in file_list_pruned:$item"; vbm "DEBUG:Dotfile in path:item:$item"; vbm "DEBUG:Dotfile parent:line:$line"; flag_contains_dotfile_parent="true"; break fi; done < <(get_parent_dirnames "$item"); if [[ $flag_contains_dotfile_parent == "true" ]]; then unset flag_contains_dotfile_parent; continue; # skip to next item (i.e. don't add to file_list_pruned) fi; ## Ignore dotfiles themselves item_basename="$(basename "$item")"; pattern="^\."; if [[ $item_basename =~ $pattern ]]; then vbm "INFO :Skipping dotfile:item:$item"; continue; # skip to next item fi; fi; ## Ignore files with newlines present in filename. See [2]. if [[ $item =~ $'\n' ]]; then yell "INFO :Skipping file name with newline:$item"; continue; # skip to next item fi; ## Ignore files that end in '~'. if [[ $item =~ ~$ ]]; then yell "INFO :Skipping file ending in tilde:$item"; continue; # skip to next item fi; ## Add item to file_list_pruned file_list_pruned+=("$item"); done; if [[ $opVerbose == "true" ]]; then vbm "DEBUG:file_list_pruned:"; printf "%s\n" "${file_list_pruned[@]}"; fi; # Decide what actions to take for items in file_list_pruned for item in "${file_list_pruned[@]}"; do vbm "DEBUG:considering action to take for item:$item"; unset path_src path_prf dir_parent dir_source; ## Check file extension if [[ $item =~ .ots$ ]]; then ### item ends in '.ots'. Item is proof file. vbm "DEBUG:item ends in '.ots'. Item is proof file:item:$item"; if [[ -f ${item%.ots} ]]; then #### Proof file (item) is adjacent to source file vbm "DEBUG:Proof file (item) is adjacent to source file."; ##### Upgrade and verify proof file against adjacent source file vbm "DEBUG:Marking proof file to be upgraded and verified."; path_src="${item%.ots}"; path_prf="$item"; files_to_upgrade+=("$(printf "%s" "$path_prf")"); files_to_verify+=("$(printf "%s\n%s" "$path_src" "$path_prf")"); else #### Proof file (item) is not adjacent to source file vbm "DEBUG:Proof file (item) is not adjacent to source file."; #### Check if source file in parent dir dir_parent="$(dirname "$(dirname "$item")" )"; cand_src_filename="$(basename "$item")"; cand_src_path="$dir_parent/$cand_src_filename"; if [[ -f "$cand_src_path" ]]; then ##### source file in parent dir vbm "DEBUG:found source file in parent:cand_src_path:$cand_src_path"; path_src="$cand_src_path"; path_prf="$item"; files_to_upgrade+=("$(printf "%s" "$path_prf")"); files_to_verify+=("$(printf "%s\n%s" "$path_src" "$path_prf")"); else #### Throw non-fatal error vbm "DEBUG:Source file not found for proof file:item:$item"; yell "ERROR:Item is proof file but source filei not adjacent in parent dir. item:$item"; #### Attempt upgrade only vbm "DEBUG:Marking proof file to be upgraded."; path_prf="$item"; files_to_upgrade+=("$(printf "%s" "$path_prf")"); fi; fi; else ### item does not end in '.ots'. Item is source file. vbm "DEBUG:item does NOT end in '.ots'. Item is source file."; if [[ -f "$item".ots ]]; then #### Proof file is adjacent to source file (item). vbm "DEBUG:Proof file is adjacent to source file (item)."; ##### Upgrade and verify proof file against adjacent source file. vbm "DEBUG:Marking proof file to be upgraded and verified."; path_src="$item"; path_prf="$item.ots"; files_to_upgrade+=("$(printf "%s" "$path_prf")"); files_to_verify+=("$(printf "%s\n%s" "$path_src" "$path_prf")"); else #### Proof file is not adjacent to source file (item). #### Check if proof file is in subdir vbm "DEBUG:checking if proof file for source file (item) is in subdir:item:$item"; unset flag_proof_in_subdir; dir_item="$(dirname "$item")"; cand_prf_filename="$(basename "$item")".ots; while read -r line; do line_basename="$(basename "$line")"; if [[ $line_basename == "$cand_prf_filename" ]]; then flag_proof_in_subdir="true"; path_prf="$line"; vbm "DEBUG:proof found in subdir at:line:$line"; break; fi; done < <(find "$dir_item" -mindepth 2 -maxdepth 2 -type f) if [[ $flag_proof_in_subdir == "true" ]]; then ##### Proof file is in subdir vbm "DEBUG:Proof file detected in subdir relative to source file (item)"; #path_prf="$path_prf"; # set in while loop path_src="$item"; files_to_upgrade+=("$(printf "%s" "$path_prf")"); files_to_verify+=("$(printf "%s\n%s" "$path_src" "$path_prf")"); else ##### Proof file is not in subdir vbm "DEBUG:Proof file not detected in subdir relative to source file (item)."; #### Stamp source file vbm "DEBUG:Marking source file to be stamped."; path_src="$item"; files_to_stamp+=("$(printf "%s" "$path_src")") fi; unset flag_proof_in_subdir; fi; fi; done; unset path_src path_prf dir_item dir_parent cand_prf_filename cand_src_filename line_basename cand_src_path # Prune action lists. ## Sort and prune file action arrays ### files to upgrade while read -r -d $'\0' line; do vbm "DEBUG:adding to files_to_upgrade_pruned:line:$line"; files_to_upgrade_pruned+=("$line"); done < <(printf "%s\0" "${files_to_upgrade[@]}" | sort -zu | shuf -z); # See [1] if [[ $opVerbose == "true" ]]; then vbm "DEBUG:files_to_upgrade_pruned:"; printf "%s\n" "${files_to_upgrade_pruned[@]}"; fi; ### files to verify while read -r -d $'\0' line; do vbm "DEBUG:adding to files_to_verify_pruned:line:$line"; files_to_verify_pruned+=("$line"); done < <(printf "%s\0" "${files_to_verify[@]}" | sort -zu | shuf -z); # See [1] if [[ $opVerbose == "true" ]]; then vbm "DEBUG:files_to_verify_pruned:"; printf "%s\n\n" "${files_to_verify_pruned[@]}"; fi; ### files to stamp while read -r -d $'\0' line; do vbm "DEBUG:adding to files_to_stamp_pruned:line:$line"; files_to_stamp_pruned+=("$line"); done < <(printf "%s\0" "${files_to_stamp[@]}" | sort -zu | shuf -z); # See [1] if [[ $opVerbose == "true" ]]; then vbm "DEBUG:files_to_stamp_pruned:"; printf "%s\n" "${files_to_stamp_pruned[@]}"; fi; # Act on files ## Upgrade files for item in "${files_to_upgrade_pruned[@]}"; do path_prf="$(cut -d $'\n' -f1 < <(echo "$item"))"; if [[ -z "$path_prf" ]]; then yell "ERROR:blank upgrade item encountered. Skipping:item:$item"; continue; fi; vbm "DEBUG:Attempting to upgrade proof file:path_prf:$path_prf"; if [[ ! $option_dry_run == "true" ]]; then #ots upgrade "$path_prf"; for url in "${calendars[@]}"; do ots -l "$url" --no-default-whitelist upgrade "$path_prf"; done; else yell "DEBUG:DRY RUN:Not running:\"ots upgrade $path_prf\""; fi; sleep "$ots_delay"; done; ## Verify files for item in "${files_to_verify_pruned[@]}"; do path_src="$(cut -d $'\n' -f1 < <(echo "$item"))"; path_prf="$(cut -d $'\n' -f2 < <(echo "$item"))"; if [[ -z "$path_src" ]] || [[ -z "$path_prf" ]]; then yell "ERROR:blank verify item encountered. Skipping:item:$item"; continue; fi; vbm "DEBUG:Attempting to verify source file:path_src:$path_src"; vbm "DEBUG: against proof file: path_prf:$path_prf"; if [[ ! $option_dry_run == "true" ]]; then #ots verify -f "$path_src" "$path_prf"; for url in "${calendars[@]}"; do ots -l "$url" --no-default-whitelist verify -f "$path_src" "$path_prf"; done; else yell "DEBUG:DRY RUN:Not running:\"ots verify -f $path_src $path_prf\""; fi; sleep "$ots_delay"; done; ## Stamp files for item in "${files_to_stamp_pruned[@]}"; do path_src="$(cut -d $'\n' -f1 < <(echo "$item"))"; if [[ -z "$path_src" ]]; then yell "ERROR:blank stamp item encountered. Skipping:item:$item"; continue; fi; vbm "DEBUG:Attempting to stamp source file:path_src:$path_src"; if [[ ! $option_dry_run == "true" ]]; then #ots stamp "$item"; for url in "${calendars[@]}"; do ots -l "$url" --no-default-whitelist stamp "$item"; done; else yell "DEBUG:DRY RUN:Not running:\"ots stamp $item\""; fi; sleep "$ots_delay"; done; }; # main program # Run program main "$@";