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";
217 if output
="$( "$
(which ots
)" info "$1" | \
218 grep -E "^File sha256
hash: " | \
220 sed -E -e 's/(^File sha256 hash: )([0-9a-f]+$)/\2/g'; )" && \
221 [[ -n "$output" ]]; then
222 vbm
"STATUS:Read file digest (${output}) via ots from:$1";
223 printf "%s" "$output";
226 yell
"ERROR:Encountered problem getting file hash via ots from:$1";
229 }; # Gets hash of file from ots file
230 get_ots_oldestblock
() {
231 # Desc: Gets earliest Bitcoin block number from ots file
232 # Usage: get_ots_oldestblock FILE
233 # Example: get_ots_oldestblock foo.txt.ots
234 # Input: arg1 path OTS file path
235 # Output: stdout int Bitcoin block number
236 # Depends: OpenTimestamps 0.7.0, GNU grep 3.7, GNU Coreutils 8.32
240 vbm
"DEBUG:Starting get_ots_oldestblock() on:$1";
242 if output
="$( "$
(which ots
)" info "$1" | \
243 grep -E "verify BitcoinBlockHeaderAttestation\
([0-9]+\
)" | \
245 sed -E -e 's/(^ verify BitcoinBlockHeaderAttestation)\(([0-9]+)(\))/\2/g'; )" && \
246 [[ -n "$output" ]]; then
247 vbm
"STATUS:Retrieved Bitcoin block (${output}) via ots from:$1";
248 printf "%s" "$output";
251 yell
"ERROR:Encountered problem getting Bitcoin block number via ots from:$1";
254 }; # Gets oldest Bitcoin block from ots file
256 # Desc: Scans and stores an OTS file if none already stored
257 # Usage: store_ots_file FILE
258 # Example: store_ots_file foo.txt.ots
259 # Input: arg1 OTS file
262 vbm
"STATUS:Starting store_ots_file()";
264 # Check if provided OTS file exists
265 if [[ ! -f "$fin" ]]; then die
"FATAL:OTS file not found:$fin"; fi;
267 # Read file hash and oldest block from provided OTS file
268 if ! { fhash
="$(must get_ots_filehash "$fin")" && \
269 block
="$(must get_ots_oldestblock "$fin")"; }; then
270 yell
"ERROR:Problem analyzing file with OpenTimestamps:${fin}";
273 vbm
"STATUS:The provided OTS file at ${fin} has digest ${fhash} and block ${block}.";
275 # Copy provided OTS if no matching OTS stored
276 fout
="${fhash}_${block}.otsu"; # file name out
277 pout
="${pathOtsStore}/${fout}"; # file path out
278 if [[ ! -f "$pout" ]]; then
279 vbm
"STATUS:No matching stored OTS file found. Copying provided file to store at:${pout}";
280 must
cp -n "$fin" "$pout";
283 vbm
"STATUS:Stored OTS file with matching file hash and block number in file name found.";
286 # Get block number for provided and stored OTS files.
287 if ! { blk_provid
="$block"; blk_stored
="$(get_ots_oldestblock "$pout"; )"; }; then
288 yell
"ERROR:Could not read block numbers from OTS files: $(declare -p fhash block pout )";
291 # Copy provided OTS if matching OTS found stored but provided is older
292 if [[ "$blk_provid" -lt "$blk_stored" ]]; then
293 vbm
"WARNING:Provided OTS file somehow older despite having same name. Previous error in storing OTS file?";
294 must
mv "$pout" "${pout}--$(date +%s)";
295 must
cp "$fin" "$pout";
298 vbm
"STATUS:Stored OTS file has block number older than or as old as provided OTS file.";
300 }; # Stores provided OTS file is none already stored
301 get_sha256_digest
() {
302 # Depends: GNU Coreutils 8.32 (sha256sum)
303 # Input: arg1 path file path
304 # Output: stdout str sha256 digest (lowercase hexadecimal)
305 vbm
"DEBUG:Starting get_sha256_digest()";
306 sha256sum
"$1" |
head -n1 |
sed -E -e 's/(^[0-9a-f]{64})(.+)/\1/';
308 get_oldest_stored_ots_path
() {
309 # Desc: Lookup most recent OTS file from storage
310 # Input: pathOtsStore var path to OTS storage dir
311 # arg1 str sha256 digest (lowercase hexadecimal)
312 # Output: stdout path OTS file with matching sha256 digest
313 vbm
"DEBUG:Starting get_oldest_stored_ots_path()";
314 local -a otsStorePaths
;
318 mapfile
-t otsStorePaths
< <(find "$pathOtsStore" -type f
-name "${digest}*.otsu"; );
319 if [[ "${#otsStorePaths[@]}" -le 0 ]]; then
320 yell
"ERROR:No OTS file in OTS storage dir found. $(declare -p pathOtsStore digest otsStorePaths)";
324 blockNumOldest
="$( get_block_num_from_stored_ots_path "${otsStorePaths[0]}" )";
325 for ((i
=0; i
<"${#otsStorePaths[@]}"; i
++ )); do
326 blockNum
="$( get_block_num_from_stored_ots_path "${otsStorePaths[$i]}" )";
327 if [[ $blockNum -lt $blockNumOldest ]]; then
328 blockNumOldest
=$blockNum;
332 output
="$(readlink -f "${otsStorePaths[$i_oldest]}"; )";
334 if [[ -n "$output" ]] && [[ -f "$output" ]]; then
335 vbm
"STATUS:Found matching OTS file with digest ${digest} at:$output";
336 printf "%s" "$output";
339 yell
"ERROR:Could not find matching OTS file with digest ${digest} in ${pathOtsStore} .";
342 }; # Print to stdout path of OTS file with oldest block
343 get_block_num_from_stored_ots_path
() {
344 # Desc: Return block number from stored OTS path
345 # Input: arg1 input file path
346 # Output: stdout block number (int)
347 # Note: Assumes OTS file name pattern '{digest}_{blockNum}.otsu'.
348 local fin fbase block re
;
350 fbase
="$(basename "$fpath")";
351 block
="$(sed -E -e 's/^.+_([0-9]+).otsu$/\1/g' <<< "$fbase")";
353 if [[ "$block" =~
$re ]]; then
354 printf "%s" "$block";
356 yell
"ERROR:Invalid block number:$(declare -p fpath fbase block)";
359 }; # Print block number from stored OTS file
361 # Desc: Stores provided file's OTS files and retrieves older OTS files from storage if possible
362 # Usage: store_and_lookup [path]
363 # Depends: get_sha256_digest(), get_oldest_stored_ots_path(), get_ots_oldestblock()
364 local pathFileIn
="$1";
365 vbm
"DEBUG:Starting store_and_lookup() with provided file:${pathFileIn}";
368 if [[ ! -f "$pathFileIn" ]]; then yell
"ERROR:Not a file:${pathFileIn}"; return 1; fi;
370 # Check for and store any OTS file attached to provided file
371 ## Check if provided file is an OTS file itself
372 if [[ "$pathFileIn" =~ \.ots$
]]; then
373 vbm
"STATUS:The provided file is itself an OTS file. Store OTS file only.";
374 store_ots_file
"$pathFileIn" && vbm
"STATUS:Stored provided OTS file.";
377 ## Check if provided file has an accompanying OTS file
378 if [[ -f "${pathFileIn}.ots" ]]; then
379 vbm
"STATUS:The provided file is accompanied by an OTS file:${pathFileIn}.ots";
380 store_ots_file
"${pathFileIn}.ots" && vbm
"STATUS:Stored provided file's OTS file.";
383 # Lookup OTS file from archive for provided file.
385 fhash
="$(get_sha256_digest "$pathFileIn"; )";
387 ## Get stored OTS path if possible.
388 if ! path_stored_ots
="$(get_oldest_stored_ots_path "$fhash"; )"; then
389 yell
"STATUS:No stored OTS found. No action taken for:${pathFileIn}";
392 vbm
"STATUS:A stored OTS found with matching hash for provided file ${pathFileIn}.";
393 blk_stored
="$(get_ots_oldestblock "$path_stored_ots"; )";
394 vbm
"STATUS:The stored OTS file has block number ${blk_stored}.";
396 ## Check for OTS file accompanying provided file
397 if [[ -f "${pathFileIn}.ots" ]]; then
398 vbm
"STATUS:An OTS file is next to provided file ${pathFileIn}.";
399 blk_provid
="$(get_ots_oldestblock "${pathFileIn}.ots
"; )";
400 vbm
"STATUS:The provided file's OTS file has block number ${blk_provid}";
401 if [[ "$blk_stored" -lt "$blk_provid" ]]; then
402 vbm
"STATUS:An older timestamp in OTS store found. Replacing ${pathFileIn}.ots (block ${blk_provid}) with ${path_stored_ots} (block ${blk_stored}).";
403 if [[ ! -f "${pathFileIn}.ots.baku" ]]; then
404 must
mv "${pathFileIn}.ots" "${pathFileIn}.ots.baku" && \
405 vbm
"STATUS:Backed up existing OTS file.";
407 must
mv "${pathFileIn}.ots" "${pathFileIn}.ots.baku--$(date +%s)" && \
408 yell
"STATUS:Backed up existing OTS file with Unix epoch since backup OTS file already present.";
410 must
cp "$path_stored_ots" "${pathFileIn}.ots" && \
411 vbm
"STATUS:Replaced provided OTS file with stored OTS file.";
414 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} .";
417 vbm
"STATUS:No accompanying OTS file found and stored OTS file found with digest matching provided file. Copying ${path_stored_ots} to ${pathFileIn}.ots";
418 must
cp "$path_stored_ots" "${pathFileIn}.ots";
421 }; # stores provided OTS files and retrieves older OTS files if available
423 # Desc: Count and return total number of jobs
426 # Output: stdout integer number of jobs
427 # Depends: Bash 5.1.16
428 # Example: while [[$(count_jobs) -gt 0]]; do echo "Working..."; sleep 1; done;
432 job_count
="$(jobs -r | wc -l | tr -d ' ' )";
433 #yell "DEBUG:job_count:$job_count";
434 if [[ -z $job_count ]]; then job_count
="0"; fi;
436 }; # Return number of background jobs
440 vbm
"DEBUG:Starting rest of main()";
441 vbm
"$(declare -p pathsFilesIn)";
443 # Process files from provided input args
444 for fpath
in "${pathsFilesIn[@]}"; do
445 # throttle if too many jobs
446 if [[ "$(count_jobs)" -ge "$MAX_JOBS" ]]; then
451 must store_and_lookup
"$fpath" &
459 # Author: Steven Baltakatei Sandoval