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