2 # Desc: Utility for backing up and retrieving ots files 
   3 # Usage: bkotslu -I [dir] 
   5 # Depends: OpenTimestamps 0.7.0 (see https://opentimestamps.org ) 
   7 # NOTE: This script does not verify OTS files; it assumes the contents of OTS files fed to it are valid. 
   9 OTS_FCACHE_DIR
="$HOME/.cache/bkotslu/"; 
  13 declare -a pathsFilesIn
; 
  15 yell
() { echo "$0: $*" >&2; } # print script path and all args to stderr 
  16 die
() { yell 
"$*"; exit 111; } # same as yell() but non-zero exit status 
  17 must
() { "$@" || die 
"cannot $*"; } # runs args as command, reports args if command fails 
  19     # Description: Prints verbose message ("vbm") to stderr if opVerbose is set to "true". 
  20     # Usage: vbm "DEBUG :verbose message here" 
  25     # Depends: bash 5.1.16, GNU-coreutils 8.30 (echo, date) 
  27     if [ "$opVerbose" = "true" ]; then 
  28         functionTime
="$(date --iso-8601=ns)"; # Save current time in nano seconds. 
  29         echo "[$functionTime]:$0:""$*" 1>&2;  # Display argument text. 
  33     return 0; # Function finished. 
  34 }; # Displays message if opVerbose true 
  36     # Desc: Display script usage information 
  41     # Depends: GNU-coreutils 8.30 (cat) 
  44         bkotslu [ options ] [FILE...] 
  48                 Display help information. 
  50                 Display script version. 
  52                 Display debugging info. 
  53         -i, --input-file [FILE] 
  54                 Provide path for file to submit/lookup OTS files for 
  56                 Provide dir containing files to submit/lookup OTS files for 
  58                 Define output directory path for storing ots backup files. 
  59                   DEFAULT: $HOME/.cache/bkotslu/ 
  61                 Follow subdirectories in provided directories. 
  63                 Indicate end of options. 
  66       Hash foo.txt and lookup matching ots file 
  69       Store and lookup older ots files of a directory 
  73 }; # Display information on how to use this script. 
  75     # Desc: Displays script version and license information. 
  78     # Input: scriptVersion   var containing version string 
  80     # Depends: vbm(), yell, GNU-coreutils 8.30 
  83     vbm 
"DEBUG:showVersion function called." 
  87 Copyright (C) 2024 Steven Baltakatei Sandoval 
  88 License GPLv3: GNU GPL version 3 
  89 This is free software; you are free to change and redistribute it. 
  90 There is NO WARRANTY, to the extent permitted by law. 
  93     Copyright (C) 2020 Free Software Foundation, Inc. 
  94     License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>. 
  95     This is free software: you are free to change and redistribute it. 
  96     There is NO WARRANTY, to the extent permitted by law. 
 100     vbm 
"DEBUG:showVersion function ended." 
 101     return 0; # Function finished. 
 102 }; # Display script version. 
 104     vbm 
"STATUS:Starting checkDepends()"; 
 105     if ! command -v sha256sum 
1>/dev
/urandom 
2>&1; then 
 106         die 
"FATAL:sha256sum not available."; fi; 
 107     if ! command -v grep 1>/dev
/urandom 
2>&1; then 
 108         die 
"FATAL:grep not available."; fi; 
 109     if ! command -v find 1>/dev
/urandom 
2>&1; then 
 110         die 
"FATAL:find not available."; fi; 
 113     # Desc: Processes arguments provided to script. 
 114     # Usage: processArgs "$@" 
 116     # Input: "$@"             (list of arguments provided to the function) 
 117     # Output: Sets following variables used by other functions: 
 118     #   opVerbose            Indicates verbose mode enable status.  (ex: "true", "false") 
 119     #   pathOtsStore          Path to output directory. 
 120     #X   pathDirIn1           Path to input directory. 
 121     #X   pathFileIn1          Path to input file. 
 122     #   arrayPosArgs         Array of remaining positional argments 
 123     #   pathsFilesIn         input files 
 125     #   yell()           Displays messages to stderr. 
 126     #   vbm()            Displays messsages to stderr if opVerbose set to "true". 
 127     #   showUsage()      Displays usage information about parent script. 
 128     #   showVersion()    Displays version about parent script. 
 129     #   arrayPosArgs     Global array for storing non-option positional arguments (i.e. arguments following the `--` option). 
 130     # External dependencies: bash (5.1.16), echo 
 132     #  [1]: Marco Aurelio (2014-05-08). "echo that outputs to stderr". https://stackoverflow.com/a/23550347 
 133     #  [2]: "Handling positional parameters" (2018-05-12). https://wiki.bash-hackers.org/scripting/posparams 
 135     # Initialize function 
 136     vbm 
"DEBUG:processArgs function called." 
 139     if [ $# -le 0 ]; then yell 
"FATAL:No arguments provided."; showUsage
; fi; 
 140     while [ ! $# -eq 0 ]; do   # While number of arguments ($#) is not (!) equal to (-eq) zero (0). 
 141         #yell "DEBUG:Starting processArgs while loop." # Debug stderr message. See [1]. 
 142         #yell "DEBUG:Provided arguments are:""$*"      # Debug stderr message. See [1]. 
 144             -h | 
--help) showUsage
; exit 1;; # Display usage. 
 145             --version) showVersion
; exit 1;; # Show version 
 146             -v | 
--verbose) opVerbose
="true"; vbm 
"DEBUG:Verbose mode enabled.";; # Enable verbose mode. See [1]. 
 147             -r | 
--recursive) MAX_FIND_DEPTH
=12; vbm 
"DEBUG:Recursive mode enabled.";; 
 148             -i | 
--input-file) # Define input file path 
 149                 if [ -f "$2" ]; then # If $2 is file that exists, add $2 to pathsFilesIn array, then pop $2. 
 150                     pathsFilesIn
+=("$(readlink -f "$2")"); 
 151                     vbm 
"DEBUG:Added to pathsFilesIn array:$2"; 
 154                     die 
"FATAL:The provided input file does not exist:$2"; 
 156             -I | 
--input-dir) # Define input directory path 
 157                 if [ -d "$2" ]; then # If $2 is dir that exists, find and save files in $2 to pathsFilesIn array, then pop $2. 
 158                     while read -r file; do 
 159                         file="$(readlink -f "$file")"; 
 160                         vbm 
"STATUS:Added to pathsFilesIn array:${file}"; 
 161                         pathsFilesIn
+=("$(readlink -f "$file")"); 
 162                     done < <(find "$2" -maxdepth "$MAX_FIND_DEPTH" -type f
); 
 163                     vbm 
"DEBUG:Added to pathsFilesIn array the contents of dir:$2"; 
 165                 else # Display error if $2 is not a valid dir. 
 166                     die 
"FATAL:The specified input directory does not exist:$2"; 
 168             -O | 
--output-dir) # Define output directory path 
 169                 if [ -d "$2" ]; then # If $2 is dir that exists, set pathOtsStore to $2, pop $2 
 171                     vbm 
"DEBUG:Output directory pathOtsStore set to:${pathOtsStore}"; 
 174                     die 
"FATAL:The specified output directory is not valid:$2"; 
 176             --) # End of all options. See [2]. 
 179                     vbm 
"DEBUG:adding to arrayPosArgs:$arg"; 
 180                     arrayPosArgs
+=("$arg"); 
 183             -*) showUsage
; die 
"FATAL: Unrecognized option.";; # Display usage 
 184             *) showUsage
; die 
"FATAL: Unrecognized argument.";; # Handle unrecognized options. See [1]. 
 189     ## Identify ots file cache dir 
 190     if [[ -z "$pathOtsStore" ]]; then 
 191         vbm 
"STATUS:No output directory for caching OTS files specified."; 
 192         pathOtsStore
="$OTS_FCACHE_DIR"; 
 193         vbm 
"STATUS:Assuming OTS files to be cached in:${pathOtsStore}"; 
 195     ## Create cache dir if necessary 
 196     if [[ ! -d "$pathOtsStore" ]]; then 
 197         vbm 
"STATUS:Creating OTS file cache directory:${pathOtsStore}"; 
 198         must mkdir 
-p "$pathOtsStore"; 
 202     vbm 
"DEBUG:processArgs function ended."; 
 203     return  0; # Function finished. 
 204 }; # Evaluate script options from positional arguments (ex: $1, $2, $3, etc.). 
 206     # Desc: Gets hash of an opentimestamp'd file from ots file 
 207     # Usage: get_ots_filehash FILE 
 208     # Example: get_ots_filehash foo.txt.ots 
 209     # Depends: ots 0.7.0, GNU grep 3.7, GNU Coreutils 8.32 
 210     #          vbm()              verbose output 
 212     # Input:  arg1    OTS file path 
 213     # Output: stdout  sha256 file hash (lowercase) 
 215     vbm 
"DEBUG:Starting get_ots_filehash() on:$1"; 
 218     if output
="$( "$
(which ots
)" info "$1" | \ 
 219             grep -E "^File sha256 
hash: " | \ 
 221             sed -E -e 's/(^File sha256 hash: )([0-9a-f]+$)/\2/g'; )" && \
 
 222             [[ -n "$output" ]] && \
 
 223             [[ "$output" =~ 
$re ]]; then 
 224         vbm 
"STATUS:Read file digest (${output}) via ots from:$1"; 
 225         printf "%s" "$output"; 
 228         die 
"ERROR:Encountered problem getting file hash via ots from:$1"; 
 230 }; # Gets hash of file from ots file 
 231 get_ots_oldestblock
() { 
 232     # Desc: Gets earliest Bitcoin block number from ots file 
 233     # Usage: get_ots_oldestblock FILE 
 234     # Example: get_ots_oldestblock foo.txt.ots 
 235     # Input: arg1     path  OTS file path 
 236     # Output: stdout  int   Bitcoin block number 
 237     # Depends: OpenTimestamps 0.7.0, GNU grep 3.7, GNU Coreutils 8.32 
 241     vbm 
"DEBUG:Starting get_ots_oldestblock() on:$1"; 
 244     if output
="$( "$
(which ots
)" info "$1" | \ 
 245             grep -E "verify BitcoinBlockHeaderAttestation\
([0-9]+\
)" | \ 
 247             sed -E -e 's/(^    verify BitcoinBlockHeaderAttestation)\(([0-9]+)(\))/\2/g'; )" && \
 
 248             [[ -n "$output" ]] && \
 
 249             [[ "$output" =~ 
$re ]]; then 
 250         vbm 
"STATUS:Retrieved Bitcoin block (${output}) via ots from:$1"; 
 251         printf "%s" "$output"; 
 254         die 
"ERROR:Encountered problem getting Bitcoin block number via ots from:$1"; 
 256 }; # Gets oldest Bitcoin block from ots file 
 258     # Desc: Scans and stores an OTS file if none already stored 
 259     # Usage: store_ots_file FILE 
 260     # Example: store_ots_file foo.txt.ots 
 261     # Input:  arg1  OTS file 
 264     vbm 
"STATUS:Starting store_ots_file()"; 
 266     # Check if provided OTS file exists 
 267     if [[ ! -f "$fin" ]]; then die 
"FATAL:OTS file not found:$fin"; fi; 
 269     # Read file hash and oldest block from provided OTS file 
 270     if ! { fhash
="$(must get_ots_filehash "$fin")" && \
 
 271                block
="$(must get_ots_oldestblock "$fin")"; }; then 
 272         yell 
"ERROR:Problem analyzing file with OpenTimestamps:${fin}"; 
 275     vbm 
"STATUS:The provided OTS file at ${fin} has digest ${fhash} and block ${block}."; 
 277     # Copy provided OTS if no matching OTS stored 
 278     fout
="${fhash}_${block}.otsu"; # file name out 
 279     pout
="${pathOtsStore}/${fout}"; # file path out     
 280     if [[ ! -f "$pout" ]]; then 
 281         vbm 
"STATUS:No matching stored OTS file found. Copying provided file to store at:${pout}"; 
 282         must 
cp -n "$fin" "$pout"; 
 285         vbm 
"STATUS:Stored OTS file with matching file hash and block number in file name found."; 
 288     # Get block number for provided and stored OTS files. 
 289     if ! { blk_provid
="$block"; blk_stored
="$(get_ots_oldestblock "$pout"; )"; }; then 
 290         yell 
"ERROR:Could not read block numbers from OTS files: $(declare -p fhash block pout )"; 
 293     if [[ ! "$blk_stored" =~ 
$re ]] || 
[[ ! "$blk_provid" =~ 
$re ]]; then 
 294         die 
"FATAL:Invalid block number(s):$(declare -p blk_stored blk_provid)"; 
 297     # Copy provided OTS if matching OTS found stored but provided is older 
 298     if [[ "$blk_provid" -lt "$blk_stored" ]]; then 
 299         vbm 
"WARNING:Provided OTS file somehow older despite having same name. Previous error in storing OTS file?"; 
 300         must 
mv "$pout" "${pout}--$(date +%s)"; 
 301         must 
cp "$fin" "$pout"; 
 304         vbm 
"STATUS:Stored OTS file has block number older than or as old as provided OTS file."; 
 306 }; # Stores provided OTS file is none already stored 
 307 get_sha256_digest
() { 
 308     # Depends: GNU Coreutils 8.32 (sha256sum) 
 309     # Input:  arg1    path  file path 
 310     # Output: stdout  str   sha256 digest (lowercase hexadecimal) 
 311     vbm 
"DEBUG:Starting get_sha256_digest()"; 
 312     sha256sum 
"$1" | 
head -n1 | 
sed -E -e 's/(^[0-9a-f]{64})(.+)/\1/'; 
 314 get_oldest_stored_ots_path
() { 
 315     # Desc: Lookup most recent OTS file from storage 
 316     # Input:  pathOtsStore  var  path to OTS storage dir 
 317     #         arg1         str  sha256 digest (lowercase hexadecimal) 
 318     # Output: stdout       path OTS file with matching sha256 digest 
 319     vbm 
"DEBUG:Starting get_oldest_stored_ots_path()"; 
 320     local -a otsStorePaths
; 
 324     mapfile 
-t otsStorePaths 
< <(find "$pathOtsStore" -type f 
-name "${digest}*.otsu"; ); 
 325     if [[ "${#otsStorePaths[@]}" -le 0 ]]; then 
 326         yell 
"NOTICE:No OTS file for digest ${digest} found in ${pathOtsStore}."; 
 331     blockNumOldest
="$( get_block_num_from_stored_ots_path "${otsStorePaths[0]}" )"; 
 332     if ! [[ "$blockNumOldest" =~ 
$re ]]; then die 
"FATAL:Invalid block number:${blockNumOldest}"; fi; 
 333     for ((i
=0; i
<"${#otsStorePaths[@]}"; i
++ )); do 
 334         blockNum
="$( get_block_num_from_stored_ots_path "${otsStorePaths[$i]}" )"; 
 335         if ! [[ "$blockNum" =~ 
$re ]]; then die 
"FATAL:Invalid block number:${blockNum}"; fi; 
 336         if [[ $blockNum -lt $blockNumOldest ]]; then 
 337             blockNumOldest
=$blockNum; 
 341     output
="$(readlink -f "${otsStorePaths[$i_oldest]}"; )"; 
 343     if [[ -n "$output" ]] && [[ -f "$output" ]]; then 
 344         vbm 
"STATUS:Found matching OTS file with digest ${digest} at:$output"; 
 345         printf "%s" "$output"; 
 348         yell 
"ERROR:Could not find matching OTS file with digest ${digest} in ${pathOtsStore} ."; 
 351 }; # Print to stdout path of OTS file with oldest block 
 352 get_block_num_from_stored_ots_path
() { 
 353     # Desc: Return block number from stored OTS path 
 354     # Input:  arg1    input file path 
 355     # Output: stdout  block number (int) 
 356     # Note: Assumes OTS file name pattern '{digest}_{blockNum}.otsu'. 
 357     local fin fbase block re
; 
 359     fbase
="$(basename "$fpath")"; 
 360     block
="$(sed -E -e 's/^.+_([0-9]+).otsu$/\1/g' <<< "$fbase")"; 
 362     if [[ "$block" =~ 
$re ]]; then 
 363         printf "%s" "$block"; 
 365         yell 
"ERROR:Invalid block number:$(declare -p fpath fbase block)"; 
 368 }; # Print block number from stored OTS file 
 370     # Desc: Stores provided file's OTS files and retrieves older OTS files from storage if possible 
 371     # Usage: store_and_lookup [path] 
 372     # Depends: get_sha256_digest(), get_oldest_stored_ots_path(), get_ots_oldestblock() 
 373     local pathFileIn
="$1"; 
 374     vbm 
"DEBUG:Starting store_and_lookup() with provided file:${pathFileIn}"; 
 377     if [[ ! -f "$pathFileIn" ]]; then yell 
"ERROR:Not a file:${pathFileIn}"; return 1; fi; 
 379     # Check for and store any OTS file attached to provided file 
 380     ## Check if provided file is an OTS file itself 
 381     if [[ "$pathFileIn" =~ \.ots$ 
]]; then 
 382         vbm 
"STATUS:The provided file is itself an OTS file. Store OTS file only."; 
 383         store_ots_file 
"$pathFileIn" && vbm 
"STATUS:Stored provided OTS file."; 
 386     ## Check if provided file has an accompanying OTS file 
 387     if [[ -f "${pathFileIn}.ots" ]]; then 
 388         vbm 
"STATUS:The provided file is accompanied by an OTS file:${pathFileIn}.ots"; 
 389         store_ots_file 
"${pathFileIn}.ots" && vbm 
"STATUS:Stored provided file's OTS file."; 
 392     # Lookup OTS file from archive for provided file. 
 394     fhash
="$(get_sha256_digest "$pathFileIn"; )"; 
 396     ## Get stored OTS path if possible. 
 397     if ! path_stored_ots
="$(get_oldest_stored_ots_path "$fhash"; )"; then 
 398         yell 
"STATUS:No stored OTS found. No action taken for:${pathFileIn}"; 
 401     vbm 
"STATUS:A stored OTS found with matching hash for provided file ${pathFileIn}."; 
 402     blk_stored
="$(get_ots_oldestblock "$path_stored_ots"; )"; 
 403     vbm 
"STATUS:The stored OTS file has block number ${blk_stored}."; 
 405     ## Check for OTS file accompanying provided file 
 406     if [[ -f "${pathFileIn}.ots" ]]; then 
 407         vbm 
"STATUS:An OTS file is next to provided file ${pathFileIn}."; 
 408         blk_provid
="$(must get_ots_oldestblock "${pathFileIn}.ots
"; )"; 
 409         vbm 
"STATUS:The provided file's OTS file has block number ${blk_provid}"; 
 411         if [[ ! "$blk_stored" =~ 
$re ]] || 
[[ ! "$blk_provid" =~ 
$re ]]; then 
 412             die 
"FATAL:Invalid block number(s):$(declare -p blk_stored blk_provid)"; 
 414         if [[ "$blk_stored" -lt "$blk_provid" ]]; then 
 415             vbm 
"STATUS:An older timestamp in OTS store found. Replacing ${pathFileIn}.ots (block ${blk_provid}) with ${path_stored_ots} (block ${blk_stored})."; 
 416             if [[ ! -f "${pathFileIn}.ots.baku" ]]; then 
 417                 must 
mv "${pathFileIn}.ots" "${pathFileIn}.ots.baku" && \
 
 418                     vbm 
"STATUS:Backed up existing OTS file."; 
 420                 must 
mv "${pathFileIn}.ots" "${pathFileIn}.ots.baku--$(date +%s)" && \
 
 421                     yell 
"STATUS:Backed up existing OTS file with Unix epoch since backup OTS file already present."; 
 423             must 
cp "$path_stored_ots" "${pathFileIn}.ots" && \
 
 424                 vbm 
"STATUS:Replaced provided OTS file with stored OTS file."; 
 427             yell 
"STATUS:The stored OTS file (block ${blk_stored}) is not older than provided file's OTS file (block ${blk_provid}). No action taken for:${pathFileIn} ."; 
 430         vbm 
"STATUS:No accompanying OTS file found and stored OTS file found with digest matching provided file. Copying ${path_stored_ots} to ${pathFileIn}.ots"; 
 431         must 
cp "$path_stored_ots" "${pathFileIn}.ots"; 
 434 }; # stores provided OTS files and retrieves older OTS files if available 
 436     # Desc: Count and return total number of jobs 
 439     # Output: stdout   integer number of jobs 
 440     # Depends: Bash 5.1.16 
 441     # Example: while [[$(count_jobs) -gt 0]]; do echo "Working..."; sleep 1; done; 
 445     job_count
="$(jobs -r | wc -l | tr -d ' ' )"; 
 446     #yell "DEBUG:job_count:$job_count"; 
 447     if [[ -z $job_count ]]; then job_count
="0"; fi; 
 449 }; # Return number of background jobs 
 453     vbm 
"DEBUG:Starting rest of main()"; 
 454     vbm 
"$(declare -p pathsFilesIn)"; 
 456     # Process files from provided input args 
 457     for fpath 
in "${pathsFilesIn[@]}"; do 
 458         # throttle if too many jobs 
 459         if [[ "$(count_jobs)" -ge "$MAX_JOBS" ]]; then 
 464         must store_and_lookup 
"$fpath" & 
 472 # Author: Steven Baltakatei Sandoval