doc(user/bkphotorights): Add exiftool.org XMP tag reference
[BK-2020-03.git] / user / bksum
... / ...
CommitLineData
1#!/usr/bin/env bash
2# Desc: Create and sign a checksum
3# Usage: bksum file.txt
4# Input: stdin: file list (newline delimited)
5# arg(s): file paths (IFS delimited)
6# Output: file containing sha256 hashes and file paths
7# Depends: bash v5.1.16, date (GNU Coreutils 8.32), gpg v2.2.27, ots v0.7.0
8# Version: 0.1.1
9
10declare -Ag appRollCall # Associative array for storing app status
11declare -Ag fileRollCall # Associative array for storing file status
12declare -Ag dirRollCall # Associative array for storing dir status
13declare -ag arrPosArgs; # positional arguments
14declare -ag arrStdin; # standard input lines
15declare -ag arrInFiles; # input files
16
17yell() { echo "$0: $*" >&2; } # print script path and all args to stderr
18die() { yell "$*"; exit 111; } # same as yell() but non-zero exit status
19try() { "$@" || die "cannot $*"; } # runs args as command, reports args if command fails
20vbm() {
21 # Description: Prints verbose message ("vbm") to stderr if opVerbose is set to "true".
22 # Usage: vbm "DEBUG :verbose message here"
23 # Version 0.2.0
24 # Input: arg1: string
25 # vars: opVerbose
26 # Output: stderr
27 # Depends: bash 5.1.16, GNU-coreutils 8.30 (echo, date)
28
29 if [ "$opVerbose" = "true" ]; then
30 functionTime="$(date --iso-8601=ns)"; # Save current time in nano seconds.
31 echo "[$functionTime]:$0:""$*" 1>&2; # Display argument text.
32 fi
33
34 # End function
35 return 0; # Function finished.
36} # Displays message if opVerbose true
37showUsage() {
38 # Desc: Display script usage information
39 # Usage: showUsage
40 # Version 0.0.2
41 # Input: none
42 # Output: stdout
43 # Depends: GNU-coreutils 8.30 (cat)
44 cat <<'EOF'
45 USAGE:
46 bksum [ options ] [FILE...]
47
48 DESCRIPTION:
49 Creates sha256 checksum of files.
50
51 OPTIONS:
52 -h, --help
53 Display help information.
54 --version
55 Display script version.
56 -v, --verbose
57 Display debugging info.
58 -o, --output-file
59 Define output file path. By default, the file
60 name includes the full ISO-8601 timestamp
61 without separators, e.g.:
62 20220920T2117+0000..SHA256SUM
63 -s, --sign
64 Sign with GnuPG the checksum file.
65 -t, --timestamp
66 Timestamp with OpenTimestamps the checksum file
67 (and GnuPG signature if -s/--sign specified).
68 -T, --timestamp-wait
69 Same as '-t/--timestamp' except that
70 OpenTimestamps will run as a background process
71 until a calendar server returns a completed
72 timestamp.
73 --
74 Indicate end of options.
75
76 EXAMPLE:
77 bksum file.txt
78 bksum file1.txt file2.txt
79 bksum -v -- file1.txt file2.txt
80 bksum -v -t -- file1.txt file2.txt
81 find . -type f | bksum
82 find . -type f | bksum -v -s -t -- file.txt
83
84 NOTE:
85 If GNU Coreutils 8.32 `sha256sum` used, checksum file
86 can be verified using:
87 sha256sum --check 20220920T2117+0000..SHA256SUM
88EOF
89} # Display information on how to use this script.
90showVersion() {
91 # Desc: Displays script version and license information.
92 # Usage: showVersion
93 # Version: 0.0.2
94 # Input: scriptVersion var containing version string
95 # Output: stdout
96 # Depends: vbm(), yell, GNU-coreutils 8.30
97
98 # Initialize function
99 vbm "DEBUG:showVersion function called."
100
101 cat <<'EOF'
102bksum 0.0.1
103Copyright (C) 2022 Steven Baltakatei Sandoval
104License GPLv3: GNU GPL version 3
105This is free software; you are free to change and redistribute it.
106There is NO WARRANTY, to the extent permitted by law.
107
108EOF
109
110 # End function
111 vbm "DEBUG:showVersion function ended."
112 return 0; # Function finished.
113} # Display script version.
114checkapp() {
115 # Desc: If arg is a command, save result in assoc array 'appRollCall'
116 # Usage: checkapp arg1 arg2 arg3 ...
117 # Version: 0.1.1
118 # Input: global assoc. array 'appRollCall'
119 # Output: adds/updates key(value) to global assoc array 'appRollCall'
120 # Depends: bash 5.0.3
121 local returnState
122
123 #===Process Args===
124 for arg in "$@"; do
125 if command -v "$arg" 1>/dev/null 2>&1; then # Check if arg is a valid command
126 appRollCall[$arg]="true";
127 if ! [ "$returnState" = "false" ]; then returnState="true"; fi;
128 else
129 appRollCall[$arg]="false"; returnState="false";
130 fi;
131 done;
132
133 #===Determine function return code===
134 if [ "$returnState" = "true" ]; then
135 return 0;
136 else
137 return 1;
138 fi;
139} # Check that app exists
140checkfile() {
141 # Desc: If arg is a file path, save result in assoc array 'fileRollCall'
142 # Usage: checkfile arg1 arg2 arg3 ...
143 # Version: 0.1.2
144 # Input: global assoc. array 'fileRollCall'
145 # Output: adds/updates key(value) to global assoc array 'fileRollCall';
146 # Output: returns 0 if app found, 1 otherwise
147 # Depends: bash 5.0.3
148 local returnState
149
150 #===Process Args===
151 for arg in "$@"; do
152 if [ -f "$arg" ]; then
153 fileRollCall["$arg"]="true";
154 if ! [ "$returnState" = "false" ]; then returnState="true"; fi;
155 elif [ -z "$arg" ]; then
156 fileRollCall["(no name)"]="false"; returnState="false";
157 else
158 fileRollCall["$arg"]="false"; returnState="false";
159 fi;
160 done;
161
162 #===Determine function return code===
163 if [ "$returnState" = "true" ]; then
164 return 0;
165 else
166 return 1;
167 fi;
168} # Check that file exists
169checkdir() {
170 # Desc: If arg is a dir path, save result in assoc array 'dirRollCall'
171 # Usage: checkdir arg1 arg2 arg3 ...
172 # Version 0.1.2
173 # Input: global assoc. array 'dirRollCall'
174 # Output: adds/updates key(value) to global assoc array 'dirRollCall';
175 # Output: returns 0 if all args are dirs; 1 otherwise
176 # Depends: Bash 5.0.3
177 local returnState
178
179 #===Process Args===
180 for arg in "$@"; do
181 if [ -z "$arg" ]; then
182 dirRollCall["(Unspecified Dirname(s))"]="false"; returnState="false";
183 elif [ -d "$arg" ]; then
184 dirRollCall["$arg"]="true";
185 if ! [ "$returnState" = "false" ]; then returnState="true"; fi
186 else
187 dirRollCall["$arg"]="false"; returnState="false";
188 fi
189 done
190
191 #===Determine function return code===
192 if [ "$returnState" = "true" ]; then
193 return 0;
194 else
195 return 1;
196 fi
197} # Check that dir exists
198displayMissing() {
199 # Desc: Displays missing apps, files, and dirs
200 # Usage: displayMissing
201 # Version 1.0.0
202 # Input: associative arrays: appRollCall, fileRollCall, dirRollCall
203 # Output: stderr: messages indicating missing apps, file, or dirs
204 # Output: returns exit code 0 if nothing missing; 1 otherwise
205 # Depends: bash 5, checkAppFileDir()
206 local missingApps value appMissing missingFiles fileMissing
207 local missingDirs dirMissing
208
209 #==BEGIN Display errors==
210 #===BEGIN Display Missing Apps===
211 missingApps="Missing apps :";
212 #for key in "${!appRollCall[@]}"; do echo "DEBUG:$key => ${appRollCall[$key]}"; done
213 for key in "${!appRollCall[@]}"; do
214 value="${appRollCall[$key]}";
215 if [ "$value" = "false" ]; then
216 #echo "DEBUG:Missing apps: $key => $value";
217 missingApps="$missingApps""$key ";
218 appMissing="true";
219 fi;
220 done;
221 if [ "$appMissing" = "true" ]; then # Only indicate if an app is missing.
222 echo "$missingApps" 1>&2;
223 fi;
224 unset value;
225 #===END Display Missing Apps===
226
227 #===BEGIN Display Missing Files===
228 missingFiles="Missing files:";
229 #for key in "${!fileRollCall[@]}"; do echo "DEBUG:$key => ${fileRollCall[$key]}"; done
230 for key in "${!fileRollCall[@]}"; do
231 value="${fileRollCall[$key]}";
232 if [ "$value" = "false" ]; then
233 #echo "DEBUG:Missing files: $key => $value";
234 missingFiles="$missingFiles""$key ";
235 fileMissing="true";
236 fi;
237 done;
238 if [ "$fileMissing" = "true" ]; then # Only indicate if an app is missing.
239 echo "$missingFiles" 1>&2;
240 fi;
241 unset value;
242 #===END Display Missing Files===
243
244 #===BEGIN Display Missing Directories===
245 missingDirs="Missing dirs:";
246 #for key in "${!dirRollCall[@]}"; do echo "DEBUG:$key => ${dirRollCall[$key]}"; done
247 for key in "${!dirRollCall[@]}"; do
248 value="${dirRollCall[$key]}";
249 if [ "$value" = "false" ]; then
250 #echo "DEBUG:Missing dirs: $key => $value";
251 missingDirs="$missingDirs""$key ";
252 dirMissing="true";
253 fi;
254 done;
255 if [ "$dirMissing" = "true" ]; then # Only indicate if an dir is missing.
256 echo "$missingDirs" 1>&2;
257 fi;
258 unset value;
259 #===END Display Missing Directories===
260
261 #==END Display errors==
262 #==BEGIN Determine function return code===
263 if [ "$appMissing" == "true" ] || [ "$fileMissing" == "true" ] || [ "$dirMissing" == "true" ]; then
264 return 1;
265 else
266 return 0;
267 fi
268 #==END Determine function return code===
269} # Display missing apps, files, dirs
270processArgs() {
271 # Desc: Processes arguments provided to script
272 # Usage: processArgs "$@"
273 # Version: 1.0.0 (modified)
274 # Input: "$@" (list of arguments provided to the function)
275 # Output: Sets following variables used by other functions:
276 # opVerbose Indicates verbose mode enable status. (ex: "true", "false")
277 # pathDirOut1 Path to output directory.
278 # pathFileOut1 Path to output file.
279 # opFileOut1_overwrite Indicates whether file pathFileOut1 should be overwritten (ex: "true", "false").
280 # opTs Indicates timestamp mode
281 # opTsWait Indicates timestamp mode with wait option
282 # arrPosArgs Array of remaining positional argments
283 # Depends:
284 # yell() Displays messages to stderr.
285 # vbm() Displays messsages to stderr if opVerbose set to "true".
286 # showUsage() Displays usage information about parent script.
287 # showVersion() Displays version about parent script.
288 # arrPosArgs Global array for storing non-option positional arguments (i.e. arguments following the `--` option).
289 # External dependencies: bash (5.1.16), echo
290 # Ref./Attrib.:
291 # [1]: Marco Aurelio (2014-05-08). "echo that outputs to stderr". https://stackoverflow.com/a/23550347
292 # [2]: "Handling positional parameters" (2018-05-12). https://wiki.bash-hackers.org/scripting/posparams
293
294 # Initialize function
295 vbm "DEBUG:processArgs function called."
296
297 # Perform work
298 while [ ! $# -eq 0 ]; do # While number of arguments ($#) is not (!) equal to (-eq) zero (0).
299 #yell "DEBUG:Starting processArgs while loop." # Debug stderr message. See [1].
300 #yell "DEBUG:Provided arguments are:""$*" # Debug stderr message. See [1].
301 case "$1" in
302 -h | --help) showUsage; exit 1;; # Display usage.
303 --version) showVersion; exit 1;; # Show version
304 -v | --verbose) opVerbose="true"; vbm "DEBUG:Verbose mode enabled.";; # Enable verbose mode. See [1].
305 -o | --output-file) # Define output file path
306 if [ -f "$2" ]; then # If $2 is file that exists, prompt user to continue to overwrite, set pathFileOut1 to $2, pop $2.
307 yell "Specified output file $2 already exists. Overwrite? (y/n):"
308 read -r m; case $m in
309 y | Y | yes) opFileOut1_overwrite="true";;
310 n | N | no) opFileOut1_overwrite="false";;
311 *) yell "Invalid selection. Exiting."; exit 1;;
312 esac
313 if [ "$opFileOut1_overwrite" == "true" ]; then
314 pathFileOut1="$2";
315 shift;
316 vbm "DEBUG:Output file pathFileOut1 set to:""$2";
317 else
318 yell "ERORR:Exiting in order to not overwrite output file:""$pathFileOut1";
319 exit 1;
320 fi
321 else
322 pathFileOut1="$2";
323 shift;
324 vbm "DEBUG:Output file pathFileOut1 set to:""$2";
325 fi ;;
326 -s | --sign) # sign with gpg
327 opSign="true"; vbm "DEBUG:Signing mode enabled.";;
328 -t | --timestamp) # timestamp with ots
329 opTs="true"; vbm "DEBUG:Timestamp mode enabled.";;
330 -T | --timestamp-wait) # timestamp with ots with wait option
331 opTs="true"; vbm "DEBUG:Timestamp mode enabled.";
332 opTsWait="true"; vbm "DEBUG:Timestamp wait mode enabled.";;
333 --) # End of all options. See [2].
334 shift;
335 for arg in "$@"; do
336 vbm "DEBUG:adding to arrPosArgs:$arg";
337 arrPosArgs+=("$arg");
338 done;
339 break;;
340 -*) showUsage; yell "ERROR: Unrecognized option."; exit 1;; # Display usage
341 *)
342 for arg in "$@"; do
343 vbm "DEBUG:adding to arrPosArgs:$arg";
344 arrPosArgs+=("$arg");
345 done;
346 break;;
347 esac;
348 shift;
349 done;
350
351 # End function
352 vbm "DEBUG:processArgs function ended."
353 return 0; # Function finished.
354} # Evaluate script options from positional arguments (ex: $1, $2, $3, etc.).
355processStdin() {
356 # Desc: Save stdin lines to array
357 # Input: stdin standard input
358 # arrStdin array for storing stdin lines
359 # Output: arrStdin array for storing stdin lines
360 # Ref/Attrib: [1] https://unix.stackexchange.com/a/484643 Check if no command line arguments and STDIN is empty
361
362 if [[ -t 0 ]]; then
363 return 1; # error if file descriptor 0 (stdin) open
364 else
365 while read -r line; do
366 arrStdin+=("$line"); done;
367 return 0;
368 fi;
369}; # Save stdin to array
370checkInput() {
371 # Desc: Check that files (specified by positional arguments and
372 # standard input lines) exist and are in the same directory.
373 # Input: arrPosArgs[@] positional arguments
374 # arrStdin[@] standard input lines
375 # Output: return code 0 success
376 # return code 1 failure
377 # arrInFiles list of verified files
378 # Depends: displayMissing(), checkfile();
379 local flagMissing flagDirErr workDir;
380
381 # Check that positional arguments are files
382 for elem in "${arrPosArgs[@]}"; do
383 if checkfile "$elem"; then
384 arrInFiles+=("$elem");
385 else
386 flagMissing="true";
387 fi;
388 done;
389
390 # Check that stdin lines are files
391 for elem in "${arrStdin[@]}"; do
392 if checkfile "$elem"; then
393 arrInFiles+=("$elem");
394 else
395 flagMissing="true";
396 fi;
397 done;
398
399 # Check that all files are in the same directory
400 if [[ "$flagMissing" != "true" ]]; then
401 workDir="$( dirname "$( readlink -f "${arrInFiles[0]}" )" )";
402 for elem in "${arrInFiles[@]}"; do
403 elem="$(readlink -f "$elem")"; # full path
404 if [[ "$(dirname "$elem")" != "$workDir" ]]; then
405 flagDirErr="true";
406 fi;
407 done;
408 fi;
409
410 # Return non-zero if displayMissing() reports files missing.
411 if [[ "$flagMissing" == "true" ]]; then
412 displayMissing; return 1; fi;
413 if [[ "$flagDirErr" == "true" ]]; then
414 yell "ERROR:All files not in same directory.";
415 return 1; fi;
416 return 0;
417}; # Check positional arguments
418checkDepends() {
419 # Desc: Check if expected commands available
420
421 checkapp date sha256sum;
422 if [[ $opSign == "true" ]]; then checkapp gpg; fi;
423 if [[ $opTs == "true" ]]; then checkapp ots; fi;
424
425 # Return failure is displayMissing() reports apps missing.
426 if ! displayMissing; then return 1; else return 0; fi;
427}; # Check dependencies
428count_jobs() {
429 # Desc: Count and return total number of jobs
430 # Usage: count_jobs
431 # Input: None.
432 # Output: stdout integer number of jobs
433 # Depends: Bash 5.1.16
434 # Example: while [[$(count_jobs) -gt 0]]; do echo "Working..."; sleep 1; done;
435 # Version: 0.0.1
436
437 local job_count;
438 job_count="$(jobs -r | wc -l | tr -d ' ' )";
439 #yell "DEBUG:job_count:$job_count";
440 if [[ -z $job_count ]]; then job_count="0"; fi;
441 echo "$job_count";
442}; # Return number of background jobs
443main() {
444 # Input: pathFileOut1 option-specified output file path
445 # appRollCall assoc-array for checkapp(), displayMissing()
446 # fileRollCall assoc-array for checkfile(), displayMissing()
447 # dirRollCall assoc-array for checkdir(), displayMissing()
448 # arrPosArgs array for processArgs()
449 # arrStdin array for processStdin()
450 # arrInFiles array for checkInput()
451 # (args) for processArgs()
452 # (stdin) for processStdin()
453 # Output: file written to $pathSumOut
454 # file written to $pathSigOut
455 #
456 local fileSumOut dirOut pathSumOut pathSigOut;
457
458 # Check dependencies and input
459 if ! checkDepends; then die "FATAL:Missing apps."; fi;
460 processArgs "$@";
461 processStdin;
462 vbm "DEBUG:$(declare -p arrPosArgs)";
463 vbm "DEBUG:$(declare -p arrStdin)";
464 if ! [[ "${#arrPosArgs[@]}" -ge 1 || "${#arrStdin[@]}" -ge 1 ]]; then
465 yell "ERROR:No positional arguments or stdin lines.";
466 showUsage; exit 1; fi;
467 if ! checkInput; then die "FATAL:Invalid file list."; fi;
468 vbm "DEBUG:$(declare -p arrInFiles)";
469
470 # Do work
471 if [[ -n "$pathFileOut1" ]]; then
472 pathSumOut="$pathFileOut1";
473 else
474 dirOut="$( dirname "${arrInFiles[0]}" )";
475 fileSumOut="$(date +%Y%m%dT%H%M%S%z)..SHA256SUMS";
476 pathSumOut="$dirOut"/"$fileSumOut";
477 fi;
478 pathSigOut="$pathSumOut".asc;
479 for file in "${arrInFiles[@]}"; do
480 sha256sum "$file" >> "$pathSumOut";
481 done;
482
483 # Do optional work
484 ## Sign checksum file.
485 if [[ $opSign == "true" ]]; then
486 yell "STATUS:Signing checksum file...";
487 try gpg --detach-sign --armor --output "$pathSigOut" "$pathSumOut" && yell "STATUS:Checksum created.";
488 fi;
489 ## Timestamp checksum file.
490 if [[ $opTs == "true" ]]; then
491 yell "STATUS:Timestamping checksum file...";
492 if [[ $opTsWait != "true" ]]; then
493 try ots s "$pathSumOut" &
494 elif [[ $opTsWait == "true" ]]; then
495 yell "NOTICE:Waiting for calendar server response in background. (This may take 8 to 24 hours)...";
496 yell "ADVICE:Do not close or suspend this terminal.";
497 try ots --wait s "$pathSumOut" 1>/dev/random 2>&1 &
498 fi;
499 fi;
500 ## Timestamp checksum signature file.
501 if [[ $opTs == "true" && $opSign == "true" ]]; then
502 yell "STATUS:Timestamping signature file...";
503 if [[ $opTsWait != "true" ]]; then
504 try ots s "$pathSigOut" && yell "STATUS:Timestamp of checksum signature file created.";
505 elif [[ $opTsWait == "true" ]]; then
506 yell "NOTICE:Waiting for calendar server response in background. (This may take 8 to 24 hours)...";
507 yell "ADVICE:Do not close or suspend this terminal.";
508 try ots --wait s "$pathSigOut" 1>/dev/random 2>&1 &
509 fi;
510 fi;
511
512 ## Wait until background jobs (if any) completed
513 for (( n = 1; "$(count_jobs)" > 0; n++ )); do
514 if ! [[ $((n % 60)) -eq 0 ]]; then
515 printf ".";
516 else
517 printf ".%05ds passed.\n" "$SECONDS";
518 fi;
519 sleep 60;
520 done;
521
522 yell "Done.";
523 return 0;
524}; # main program
525
526main "$@";
527
528# Author: Steven Baltakatei Sandoval
529# License: GPLv3+