style(bklog):Combine functions that parse '-r' and '-R'
[EVA-2020-02.git] / exec / bklog
1 #!/bin/bash
2 # Desc: Compresses, encrypts, and writes stdin every 5 seconds
3
4 #==BEGIN Define script parameters==
5 #===BEGIN Initialize variables===
6
7 # Logging Behavior parameters
8 bufferTTL="300"; # Time-to-live (seconds) for each buffer round
9 scriptTTL_TE="day"; # Time element at the end of which script terminates
10 dirTmpDefault="/dev/shm"; # Default parent of working directory
11
12 # Script Metadata
13 scriptName="bklog"; # Define basename of script file.
14 scriptVersion="0.1.15"; # Define version of script.
15 scriptURL="https://gitlab.com/baltakatei/ninfacyzga-01"; # Define wesite hosting this script.
16 scriptTimeStart="$(date +%Y%m%dT%H%M%S.%N)"; # YYYYmmddTHHMMSS.NNNNNNNNN
17 scriptHostname=$(hostname); # Save hostname of system running this script.
18 PATH="$HOME/.local/bin:$PATH"; # Add "$(systemd-path user-binaries)" path in case user apps saved there
19 ageVersion="1.0.0-beta2"; # Define version of age (encryption program)
20 ageURL="https://github.com/FiloSottile/age/releases/tag/v1.0.0-beta2"; # Define website hosting age.
21
22 # Arrays
23 declare -a buffer # array for storing while read buffer
24 declare -a argRecPubKeys # array for processArguments function
25 declare -a recPubKeysValid # array for storing both '-r' and '-R' recipient pubkeys
26 declare -a argProcStrings argProcFileExts # for storing buffer processing strings (ex: "gpsbabel -i nmea -f - -o gpx -F - ")
27 declare -Ag appRollCall # Associative array for storing app status
28 declare -Ag fileRollCall # Associative array for storing file status
29 declare -Ag dirRollCall # Associative array for storing dir status
30 declare -a procStrings procFileExts # Arrays for storing processing commands and resulting output file extensions
31
32 # Variables
33 optionVerbose=""; optionEncrypt=""; dirOut=""; optionEncrypt=""; dir_tmp="";
34 cmd_compress="";cmd_compress_suffix=""; cmd_encrypt=""; cmd_encrypt_suffix="";
35
36 #===END Initialize variables===
37
38 #===BEGIN Declare local script functions===
39 yell() { echo "$0: $*" >&2; } #o Yell, Die, Try Three-Fingered Claw technique
40 die() { yell "$*"; exit 111; } #o Ref/Attrib: https://stackoverflow.com/a/25515370
41 try() { "$@" || die "cannot $*"; } #o
42 processArguments() {
43 while [ ! $# -eq 0 ]; do # While number of arguments ($#) is not (!) equal to (-eq) zero (0).
44 case "$1" in
45 -v | --verbose) optionVerbose="true"; vbm "DEBUG :Verbose mode enabled.";; # Enable verbose mode.
46 -h | --help) showUsage; exit 1;; # Display usage.
47 --version) showVersion; exit 1;; # Show version
48 -o | --output) if [ -d "$2" ]; then dirOut="$2"; vbm "DEBUG :dirOut:$dirOut"; shift; fi ;; # Define output directory.
49 -e | --encrypt) optionEncrypt="true"; vbm "DEBUG :Encrypted output mode enabled.";; # Enable encryption
50 -r | --recipient) optionRecArg="true"; argRecPubKeys+=("$2"); vbm "STATUS:pubkey added:""$2"; shift;; # Add recipients
51 -R | --recipient-dir) optionRecDir="true" && argRecDir="$2"; shift;; # Add recipient watch dir
52 -c | --compress) optionCompress="true"; vbm "DEBUG :Compressed output mode enabled.";; # Enable compression
53 -z | --time-zone) try setTimeZoneEV "$2"; shift;; # Set timestamp timezone
54 -t | --temp-dir) optionTmpDir="true" && argTempDirPriority="$2"; shift;; # Set time zone
55 -b | --buffer-ttl) optionCustomBufferTTL="true" && argCustomBufferTTL="$2"; shift;; # Set custom buffer period (default: 300 seconds)
56 -B | --script-ttl) optionCustomScriptTTL_TE="true" && argCustomScriptTTL_TE="$2"; shift;; # Set custom script TTL (default: "day")
57 -p | --process-string) optionProcString="true" && argProcStrings+=("$2") && argProcFileExts+=("$3") && vbm "STATUS:file extension \"$2\" for output of processing string added:\"$3\""; shift; shift;;
58 -l | --label) optionLabel="true" && argLabel="$2"; vbm "DEBUG :Custom label received:$argLabel"; shift;;
59 -w | --store-raw) optionStoreRaw="true" && argRawFileExt="$2"; vbm "DEBUG :Raw stdin file extension received:$argRawFileExt"; shift;;
60 -W | --no-store-raw) optionNoStoreRaw="true"; vbm "DEBUG :Option selected to not store raw stdin data."; shift;;
61 *) yell "ERROR: Unrecognized argument: $1"; yell "STATUS:All arguments:$*"; exit 1;; # Handle unrecognized options.
62 esac
63 shift
64 done
65 } # Argument Processing
66 vbm() {
67 # Description: Prints verbose message ("vbm") to stderr if optionVerbose is set to "true".
68 # Usage: vbm "DEBUG :verbose message here"
69 # Version 0.1.2
70 # Input: arg1: string
71 # vars: optionVerbose
72 # Output: stderr
73 # Depends: bash 5.0.3, echo 8.30, date 8.30
74
75 if [ "$optionVerbose" = "true" ]; then
76 functionTime=$(date --iso-8601=ns); # Save current time in nano seconds.
77 echo "[$functionTime] ""$*" 1>&2; # Display argument text.
78 fi
79
80 # End function
81 return 0; # Function finished.
82 } # Displays message if optionVerbose true
83 checkapp() {
84 # Desc: If arg is a command, save result in assoc array 'appRollCall'
85 # Usage: checkapp arg1 arg2 arg3 ...
86 # Version: 0.1.1
87 # Input: global assoc. array 'appRollCall'
88 # Output: adds/updates key(value) to global assoc array 'appRollCall'
89 # Depends: bash 5.0.3
90 local returnState
91
92 #===Process Args===
93 for arg in "$@"; do
94 if command -v "$arg" 1>/dev/null 2>&1; then # Check if arg is a valid command
95 appRollCall[$arg]="true";
96 if ! [ "$returnState" = "false" ]; then returnState="true"; fi;
97 else
98 appRollCall[$arg]="false"; returnState="false";
99 fi;
100 done;
101
102 #===Determine function return code===
103 if [ "$returnState" = "true" ]; then
104 return 0;
105 else
106 return 1;
107 fi;
108 } # Check that app exists
109 checkfile() {
110 # Desc: If arg is a file path, save result in assoc array 'fileRollCall'
111 # Usage: checkfile arg1 arg2 arg3 ...
112 # Version: 0.1.1
113 # Input: global assoc. array 'fileRollCall'
114 # Output: adds/updates key(value) to global assoc array 'fileRollCall';
115 # Output: returns 0 if app found, 1 otherwise
116 # Depends: bash 5.0.3
117 local returnState
118
119 #===Process Args===
120 for arg in "$@"; do
121 if [ -f "$arg" ]; then
122 fileRollCall["$arg"]="true";
123 if ! [ "$returnState" = "false" ]; then returnState="true"; fi;
124 else
125 fileRollCall["$arg"]="false"; returnState="false";
126 fi;
127 done;
128
129 #===Determine function return code===
130 if [ "$returnState" = "true" ]; then
131 return 0;
132 else
133 return 1;
134 fi;
135 } # Check that file exists
136 checkdir() {
137 # Desc: If arg is a dir path, save result in assoc array 'dirRollCall'
138 # Usage: checkdir arg1 arg2 arg3 ...
139 # Version 0.1.1
140 # Input: global assoc. array 'dirRollCall'
141 # Output: adds/updates key(value) to global assoc array 'dirRollCall';
142 # Output: returns 0 if app found, 1 otherwise
143 # Depends: Bash 5.0.3
144 local returnState
145
146 #===Process Args===
147 for arg in "$@"; do
148 if [ -d "$arg" ]; then
149 dirRollCall["$arg"]="true";
150 if ! [ "$returnState" = "false" ]; then returnState="true"; fi
151 else
152 dirRollCall["$arg"]="false"; returnState="false";
153 fi
154 done
155
156 #===Determine function return code===
157 if [ "$returnState" = "true" ]; then
158 return 0;
159 else
160 return 1;
161 fi
162 } # Check that dir exists
163 displayMissing() {
164 # Desc: Displays missing apps, files, and dirs
165 # Usage: displayMissing
166 # Version 0.1.1
167 # Input: associative arrays: appRollCall, fileRollCall, dirRollCall
168 # Output: stderr: messages indicating missing apps, file, or dirs
169 # Depends: bash 5, checkAppFileDir()
170 local missingApps value appMissing missingFiles fileMissing
171 local missingDirs dirMissing
172
173 #==BEGIN Display errors==
174 #===BEGIN Display Missing Apps===
175 missingApps="Missing apps :";
176 #for key in "${!appRollCall[@]}"; do echo "DEBUG :$key => ${appRollCall[$key]}"; done
177 for key in "${!appRollCall[@]}"; do
178 value="${appRollCall[$key]}";
179 if [ "$value" = "false" ]; then
180 #echo "DEBUG :Missing apps: $key => $value";
181 missingApps="$missingApps""$key ";
182 appMissing="true";
183 fi;
184 done;
185 if [ "$appMissing" = "true" ]; then # Only indicate if an app is missing.
186 echo "$missingApps" 1>&2;
187 fi;
188 unset value;
189 #===END Display Missing Apps===
190
191 #===BEGIN Display Missing Files===
192 missingFiles="Missing files:";
193 #for key in "${!fileRollCall[@]}"; do echo "DEBUG :$key => ${fileRollCall[$key]}"; done
194 for key in "${!fileRollCall[@]}"; do
195 value="${fileRollCall[$key]}";
196 if [ "$value" = "false" ]; then
197 #echo "DEBUG :Missing files: $key => $value";
198 missingFiles="$missingFiles""$key ";
199 fileMissing="true";
200 fi;
201 done;
202 if [ "$fileMissing" = "true" ]; then # Only indicate if an app is missing.
203 echo "$missingFiles" 1>&2;
204 fi;
205 unset value;
206 #===END Display Missing Files===
207
208 #===BEGIN Display Missing Directories===
209 missingDirs="Missing dirs:";
210 #for key in "${!dirRollCall[@]}"; do echo "DEBUG :$key => ${dirRollCall[$key]}"; done
211 for key in "${!dirRollCall[@]}"; do
212 value="${dirRollCall[$key]}";
213 if [ "$value" = "false" ]; then
214 #echo "DEBUG :Missing dirs: $key => $value";
215 missingDirs="$missingDirs""$key ";
216 dirMissing="true";
217 fi;
218 done;
219 if [ "$dirMissing" = "true" ]; then # Only indicate if an dir is missing.
220 echo "$missingDirs" 1>&2;
221 fi;
222 unset value;
223 #===END Display Missing Directories===
224
225 #==END Display errors==
226 } # Display missing apps, files, dirs
227
228 appendFileTar(){
229 # Desc: Appends [processed] file to tar
230 # Usage: appendFileTar [file path] [name of file to be inserted] [tar path] [temp dir] ([process cmd])
231 # Version: 2.0.1
232 # Input: arg1: path of file to be (processed and) written
233 # arg2: name to use for file inserted into tar
234 # arg3: tar archive path (must exist first)
235 # arg4: temporary working dir
236 # arg5: (optional) command string to process file (ex: "gpsbabel -i nmea -f - -o kml -F - ")
237 # Output: file written to disk
238 # Example: decrypt multiple large files in parallel
239 # appendFileTar /tmp/largefile1.gpg "largefile1" $HOME/archive.tar /tmp "gpg --decrypt" &
240 # appendFileTar /tmp/largefile2.gpg "largefile2" $HOME/archive.tar /tmp "gpg --decrypt" &
241 # appendFileTar /tmp/largefile3.gpg "largefile3" $HOME/archive.tar /tmp "gpg --decrypt" &
242 # Depends: bash 5.0.3, tar 1.30, cat 8.30, yell()
243 local fn fileName tarPath tmpDir
244
245 # Save function name
246 fn="${FUNCNAME[0]}";
247 #yell "DEBUG :STATUS:$fn:Started appendFileTar()."
248
249 # Set file name
250 if ! [ -z "$2" ]; then fileName="$2"; else yell "ERROR:$fn:Not enough arguments."; exit 1; fi
251 # Check tar path is a file
252 if [ -f "$3" ]; then tarPath="$3"; else yell "ERROR:$fn:Tar archive arg not a file:$3"; exit 1; fi
253 # Check temp dir arg
254 if ! [ -z "$4" ]; then tmpDir="$4"; else yell "ERROR:$fn:No temporary working dir set."; exit 1; fi
255 # Set command strings
256 if ! [ -z "$5" ]; then cmd1="$5"; else cmd1="cat "; fi # command string
257
258 # Input command string
259 cmd0="cat \"\$1\"";
260
261 # Write to temporary working dir
262 eval "$cmd0 | $cmd1" > "$tmpDir"/"$fileName";
263
264 # Append to tar
265 try tar --append --directory="$tmpDir" --file="$tarPath" "$fileName";
266 #yell "DEBUG :STATUS:$fn:Finished appendFileTar()."
267 } # Append [processed] file to Tar archive
268 checkAgePubkey() {
269 # Desc: Checks if string is an age-compatible pubkey
270 # Usage: checkAgePubkey [str pubkey]
271 # Version: 0.1.2
272 # Input: arg1: string
273 # Output: return code 0: string is age-compatible pubkey
274 # return code 1: string is NOT an age-compatible pubkey
275 # age stderr (ex: there is stderr if invalid string provided)
276 # Depends: age (v0.1.0-beta2; https://github.com/FiloSottile/age/releases/tag/v1.0.0-beta2 )
277
278 argPubkey="$1";
279
280 if echo "test" | age -a -r "$argPubkey" 1>/dev/null; then
281 return 0;
282 else
283 return 1;
284 fi;
285 } # Check age pubkey
286 checkMakeTar() {
287 # Desc: Checks that a valid tar archive exists, creates one otherwise
288 # Usage: checkMakeTar [ path ]
289 # Version: 1.0.2
290 # Input: arg1: path of tar archive
291 # Output: exit code 0 : tar readable
292 # exit code 1 : tar missing; created
293 # exit code 2 : tar not readable; moved; replaced
294 # Depends: bash 5, date 8, tar 1, try()
295 local pathTar returnFlag0 returnFlag1 returnFlag2
296 pathTar="$1";
297
298 # Check if file is a valid tar archive
299 if tar --list --file="$pathTar" 1>/dev/null 2>&1; then
300 ## T1: return success
301 returnFlag0="tar valid";
302 else
303 ## F1: Check if file exists
304 if [[ -f "$pathTar" ]]; then
305 ### T: Rename file
306 try mv "$pathTar" "$pathTar""--broken--""$(date +%Y%m%dT%H%M%S)" && \
307 returnFlag1="tar moved";
308 else
309 ### F: -
310 :
311 fi;
312 ## F2: Create tar archive, return 0
313 try tar --create --file="$pathTar" --files-from=/dev/null && \
314 returnFlag2="tar created";
315 fi;
316
317 # Determine function return code
318 if [[ "$returnFlag0" = "tar valid" ]]; then
319 return 0;
320 elif [[ "$returnFlag2" = "tar created" ]] && ! [[ "$returnFlag1" = "tar moved" ]]; then
321 return 1; # tar missing so created
322 elif [[ "$returnFlag2" = "tar created" ]] && [[ "$returnFlag1" = "tar moved" ]]; then
323 return 2; # tar not readable so moved; replaced
324 fi;
325 } # checks if arg1 is tar; creates one otherwise
326 dateShort(){
327 # Desc: Date without separators (YYYYmmdd)
328 # Usage: dateShort ([str date])
329 # Version: 1.1.2
330 # Input: arg1: 'date'-parsable timestamp string (optional)
331 # Output: stdout: date (ISO-8601, no separators)
332 # Depends: bash 5.0.3, date 8.30, yell()
333 local argTime timeCurrent timeInput dateCurrentShort
334
335 argTime="$1";
336 # Get Current Time
337 timeCurrent="$(date --iso-8601=seconds)" ; # Produce `date`-parsable current timestamp with resolution of 1 second.
338 # Decide to parse current or supplied date
339 ## Check if time argument empty
340 if [[ -z "$argTime" ]]; then
341 ## T: Time argument empty, use current time
342 timeInput="$timeCurrent";
343 else
344 ## F: Time argument exists, validate time
345 if date --date="$argTime" 1>/dev/null 2>&1; then
346 ### T: Time argument is valid; use it
347 timeInput="$argTime";
348 else
349 ### F: Time argument not valid; exit
350 yell "ERROR:Invalid time argument supplied. Exiting."; exit 1;
351 fi;
352 fi;
353 # Construct and deliver separator-les date string
354 dateCurrentShort="$(date -d "$timeInput" +%Y%m%d)"; # Produce separator-less current date with resolution 1 day.
355 echo "$dateCurrentShort";
356 } # Get YYYYmmdd
357 dateTimeShort(){
358 # Desc: Timestamp without separators (YYYYmmddTHHMMSS+zzzz)
359 # Usage: dateTimeShort ([str date])
360 # Version 1.1.1
361 # Input: arg1: 'date'-parsable timestamp string (optional)
362 # Output: stdout: timestamp (ISO-8601, no separators)
363 # Depends: yell
364 local argTime timeCurrent timeInput timeCurrentShort
365
366 argTime="$1";
367 # Get Current Time
368 timeCurrent="$(date --iso-8601=seconds)" ; # Produce `date`-parsable current timestamp with resolution of 1 second.
369 # Decide to parse current or supplied date
370 ## Check if time argument empty
371 if [[ -z "$argTime" ]]; then
372 ## T: Time argument empty, use current time
373 timeInput="$timeCurrent";
374 else
375 ## F: Time argument exists, validate time
376 if date --date="$argTime" 1>/dev/null 2>&1; then
377 ### T: Time argument is valid; use it
378 timeInput="$argTime";
379 else
380 ### F: Time argument not valid; exit
381 yell "ERROR:Invalid time argument supplied. Exiting."; exit 1;
382 fi
383 fi
384 # Construct and deliver separator-les date string
385 timeCurrentShort="$(date -d "$timeInput" +%Y%m%dT%H%M%S%z)";
386 echo "$timeCurrentShort";
387 } # Get YYYYmmddTHHMMSS±zzzz
388 setTimeZoneEV(){
389 # Desc: Set time zone environment variable TZ
390 # Usage: setTimeZoneEV arg1
391 # Version 0.1.2
392 # Input: arg1: 'date'-compatible timezone string (ex: "America/New_York")
393 # TZDIR env var (optional; default: "/usr/share/zoneinfo")
394 # Output: exports TZ
395 # exit code 0 on success
396 # exit code 1 on incorrect number of arguments
397 # exit code 2 if unable to validate arg1
398 # Depends: yell(), printenv 8.30, bash 5.0.3
399 # Tested on: Debian 10
400 local tzDir returnState argTimeZone
401
402 argTimeZone="$1"
403 if ! [[ $# -eq 1 ]]; then
404 yell "ERROR:Invalid argument count.";
405 return 1;
406 fi
407
408 # Read TZDIR env var if available
409 if printenv TZDIR 1>/dev/null 2>&1; then
410 tzDir="$(printenv TZDIR)";
411 else
412 tzDir="/usr/share/zoneinfo";
413 fi
414
415 # Validate TZ string
416 if ! [[ -f "$tzDir"/"$argTimeZone" ]]; then
417 yell "ERROR:Invalid time zone argument.";
418 return 2;
419 else
420 # Export ARG1 as TZ environment variable
421 TZ="$argTimeZone" && export TZ && returnState="true";
422 fi
423
424 # Determine function return code
425 if [ "$returnState" = "true" ]; then
426 return 0;
427 fi
428 } # Exports TZ environment variable
429 showVersion() {
430 yell "$scriptVersion"
431 } # Display script version.
432 timeDuration(){
433 # Desc: Given seconds, output ISO-8601 duration string
434 # Ref/Attrib: ISO-8601:2004(E), §4.4.4.2 Representations of time intervals by duration and context information
435 # Note: "1 month" ("P1M") is assumed to be "30 days" (see ISO-8601:2004(E), §2.2.1.2)
436 # Usage: timeDuration [1:seconds] ([2:precision])
437 # Version: 1.0.4
438 # Input: arg1: seconds as base 10 integer >= 0 (ex: 3601)
439 # arg2: precision level (optional; default=2)
440 # Output: stdout: ISO-8601 duration string (ex: "P1H1S", "P2Y10M15DT10H30M20S")
441 # exit code 0: success
442 # exit code 1: error_input
443 # exit code 2: error_unknown
444 # Example: 'timeDuration 111111 3' yields 'P1DT6H51M'
445 # Depends: date 8, bash 5, yell,
446 local argSeconds argPrecision precision returnState remainder
447 local fullYears fullMonths fullDays fullHours fullMinutes fullSeconds
448 local hasYears hasMonths hasDays hasHours hasMinutes hasSeconds
449 local witherPrecision output
450 local displayYears displayMonths displayDays displayHours displayMinutes displaySeconds
451
452 argSeconds="$1"; # read arg1 (seconds)
453 argPrecision="$2"; # read arg2 (precision)
454 precision=2; # set default precision
455
456 # Check that between one and two arguments is supplied
457 if ! { [[ $# -ge 1 ]] && [[ $# -le 2 ]]; }; then
458 yell "ERROR:Invalid number of arguments:$# . Exiting.";
459 returnState="error_input"; fi
460
461 # Check that argSeconds provided
462 if [[ $# -ge 1 ]]; then
463 ## Check that argSeconds is a positive integer
464 if [[ "$argSeconds" =~ ^[[:digit:]]+$ ]]; then
465 :
466 else
467 yell "ERROR:argSeconds not a digit.";
468 returnState="error_input";
469 fi
470 else
471 yell "ERROR:No argument provided. Exiting.";
472 exit 1;
473 fi
474
475 # Consider whether argPrecision was provided
476 if [[ $# -eq 2 ]]; then
477 # Check that argPrecision is a positive integer
478 if [[ "$argPrecision" =~ ^[[:digit:]]+$ ]] && [[ "$argPrecision" -gt 0 ]]; then
479 precision="$argPrecision";
480 else
481 yell "ERROR:argPrecision not a positive integer. (is $argPrecision ). Leaving early.";
482 returnState="error_input";
483 fi;
484 else
485 :
486 fi;
487
488 remainder="$argSeconds" ; # seconds
489 ## Calculate full years Y, update remainder
490 fullYears=$(( remainder / (365*24*60*60) ));
491 remainder=$(( remainder - (fullYears*365*24*60*60) ));
492 ## Calculate full months M, update remainder
493 fullMonths=$(( remainder / (30*24*60*60) ));
494 remainder=$(( remainder - (fullMonths*30*24*60*60) ));
495 ## Calculate full days D, update remainder
496 fullDays=$(( remainder / (24*60*60) ));
497 remainder=$(( remainder - (fullDays*24*60*60) ));
498 ## Calculate full hours H, update remainder
499 fullHours=$(( remainder / (60*60) ));
500 remainder=$(( remainder - (fullHours*60*60) ));
501 ## Calculate full minutes M, update remainder
502 fullMinutes=$(( remainder / (60) ));
503 remainder=$(( remainder - (fullMinutes*60) ));
504 ## Calculate full seconds S, update remainder
505 fullSeconds=$(( remainder / (1) ));
506 remainder=$(( remainder - (remainder*1) ));
507 ## Check which fields filled
508 if [[ $fullYears -gt 0 ]]; then hasYears="true"; else hasYears="false"; fi
509 if [[ $fullMonths -gt 0 ]]; then hasMonths="true"; else hasMonths="false"; fi
510 if [[ $fullDays -gt 0 ]]; then hasDays="true"; else hasDays="false"; fi
511 if [[ $fullHours -gt 0 ]]; then hasHours="true"; else hasHours="false"; fi
512 if [[ $fullMinutes -gt 0 ]]; then hasMinutes="true"; else hasMinutes="false"; fi
513 if [[ $fullSeconds -gt 0 ]]; then hasSeconds="true"; else hasSeconds="false"; fi
514
515 ## Determine which fields to display (see ISO-8601:2004 §4.4.3.2)
516 witherPrecision="false"
517
518 ### Years
519 if $hasYears && [[ $precision -gt 0 ]]; then
520 displayYears="true";
521 witherPrecision="true";
522 else
523 displayYears="false";
524 fi;
525 if $witherPrecision; then ((precision--)); fi;
526
527 ### Months
528 if $hasMonths && [[ $precision -gt 0 ]]; then
529 displayMonths="true";
530 witherPrecision="true";
531 else
532 displayMonths="false";
533 fi;
534 if $witherPrecision && [[ $precision -gt 0 ]]; then
535 displayMonths="true";
536 fi;
537 if $witherPrecision; then ((precision--)); fi;
538
539 ### Days
540 if $hasDays && [[ $precision -gt 0 ]]; then
541 displayDays="true";
542 witherPrecision="true";
543 else
544 displayDays="false";
545 fi;
546 if $witherPrecision && [[ $precision -gt 0 ]]; then
547 displayDays="true";
548 fi;
549 if $witherPrecision; then ((precision--)); fi;
550
551 ### Hours
552 if $hasHours && [[ $precision -gt 0 ]]; then
553 displayHours="true";
554 witherPrecision="true";
555 else
556 displayHours="false";
557 fi;
558 if $witherPrecision && [[ $precision -gt 0 ]]; then
559 displayHours="true";
560 fi;
561 if $witherPrecision; then ((precision--)); fi;
562
563 ### Minutes
564 if $hasMinutes && [[ $precision -gt 0 ]]; then
565 displayMinutes="true";
566 witherPrecision="true";
567 else
568 displayMinutes="false";
569 fi;
570 if $witherPrecision && [[ $precision -gt 0 ]]; then
571 displayMinutes="true";
572 fi;
573 if $witherPrecision; then ((precision--)); fi;
574
575 ### Seconds
576
577 if $hasSeconds && [[ $precision -gt 0 ]]; then
578 displaySeconds="true";
579 witherPrecision="true";
580 else
581 displaySeconds="false";
582 fi;
583 if $witherPrecision && [[ $precision -gt 0 ]]; then
584 displaySeconds="true";
585 fi;
586 if $witherPrecision; then ((precision--)); fi;
587
588 ## Determine whether or not the "T" separator is needed to separate date and time elements
589 if ( $displayHours || $displayMinutes || $displaySeconds); then
590 displayDateTime="true"; else displayDateTime="false"; fi
591
592 ## Construct duration output string
593 output="P"
594 if $displayYears; then
595 output=$output$fullYears"Y"; fi
596 if $displayMonths; then
597 output=$output$fullMonths"M"; fi
598 if $displayDays; then
599 output=$output$fullDays"D"; fi
600 if $displayDateTime; then
601 output=$output"T"; fi
602 if $displayHours; then
603 output=$output$fullHours"H"; fi
604 if $displayMinutes; then
605 output=$output$fullMinutes"M"; fi
606 if $displaySeconds; then
607 output=$output$fullSeconds"S"; fi
608
609 ## Output duration string to stdout
610 echo "$output" && returnState="true";
611
612 #===Determine function return code===
613 if [ "$returnState" = "true" ]; then
614 return 0;
615 elif [ "$returnState" = "error_input" ]; then
616 yell "ERROR:input";
617 return 1;
618 else
619 yell "ERROR:Unknown";
620 return 2;
621 fi
622
623 } # Get duration (ex: PT10M4S )
624 timeUntilNextDay(){
625 # Desc: Report seconds until next day.
626 # Version: 1.0.2
627 # Output: stdout: integer seconds until next day
628 # Output: exit code 0 if stdout > 0; 1 if stdout = 0; 2 if stdout < 0
629 # Usage: timeUntilNextDay
630 # Usage: if ! myTTL="$(timeUntilNextDay)"; then yell "ERROR in if statement"; exit 1; fi
631 # Depends: date 8, echo 8, yell, try
632
633 local returnState timeCurrent timeNextDay secondsUntilNextDay returnState
634 timeCurrent="$(date --iso-8601=seconds)" ; # Produce `date`-parsable current timestamp with resolution of 1 second.
635 timeNextDay="$(date -d "$timeCurrent next day" --iso-8601=date)"; # Produce timestamp of beginning of tomorrow with resolution of 1 second.
636 secondsUntilNextDay="$(( $(date +%s -d "$timeNextDay") - $(date +%s -d "$timeCurrent") ))" ; # Calculate seconds until closest future midnight (res. 1 second).
637 if [[ "$secondsUntilNextDay" -gt 0 ]]; then
638 returnState="true";
639 elif [[ "$secondsUntilNextDay" -eq 0 ]]; then
640 returnState="warning_zero";
641 yell "WARNING:Reported time until next day exactly zero.";
642 elif [[ "$secondsUntilNextDay" -lt 0 ]]; then
643 returnState="warning_negative";
644 yell "WARNING:Reported time until next day is negative.";
645 fi
646
647 try echo "$secondsUntilNextDay"; # Report
648
649 # Determine function return code
650 if [[ "$returnState" = "true" ]]; then
651 return 0;
652 elif [[ "$returnState" = "warning_zero" ]]; then
653 return 1;
654 elif [[ "$returnState" = "warning_negative" ]]; then
655 return 2;
656 fi
657 } # Report seconds until next day
658 timeUntilNextHour(){
659 # Desc: Report seconds until next hour
660 # Version 1.0.1
661 # Output: stdout: integer seconds until next hour
662 # Output: exit code 0 if stdout > 0; 1 if stdout = 0; 2 if stdout < 0
663 # Usage: timeUntilNextHour
664 # Usage: if ! myTTL="$(timeUntilNextHour)"; then yell "ERROR in if statement"; exit 1; fi
665
666 local returnState timeCurrent timeNextHour secondsUntilNextHour
667 timeCurrent="$(date --iso-8601=seconds)"; # Produce `date`-parsable current timestamp with resolution of 1 second.
668 timeNextHour="$(date -d "$timeCurrent next hour" --iso-8601=hours)"; # Produce `date`-parsable current time stamp with resolution of 1 second.
669 secondsUntilNextHour="$(( $(date +%s -d "$timeNextHour") - $(date +%s -d "$timeCurrent") ))"; # Calculate seconds until next hour (res. 1 second).
670 if [[ "$secondsUntilNextHour" -gt 0 ]]; then
671 returnState="true";
672 elif [[ "$secondsUntilNextHour" -eq 0 ]]; then
673 returnState="warning_zero";
674 yell "WARNING:Reported time until next hour exactly zero.";
675 elif [[ "$secondsUntilNextHour" -lt 0 ]]; then
676 returnState="warning_negative";
677 yell "WARNING:Reported time until next hour is negative.";
678 fi;
679
680 try echo "$secondsUntilNextHour"; # Report
681
682 # Determine function return code
683 if [[ "$returnState" = "true" ]]; then
684 return 0;
685 elif [[ "$returnState" = "warning_zero" ]]; then
686 return 1;
687 elif [[ "$returnState" = "warning_negative" ]]; then
688 return 2;
689 fi;
690 } # Report seconds until next hour
691 validateInput() {
692 # Desc: Validates Input
693 # Usage: validateInput [str input] [str input type]
694 # Version: 0.3.1
695 # Input: arg1: string to validate
696 # arg2: string specifying input type (ex:"ssh_pubkey")
697 # Output: return code 0: if input string matched specified string type
698 # Depends: bash 5, yell()
699
700 local fn argInput argType
701
702 # Save function name
703 fn="${FUNCNAME[0]}";
704
705 # Process arguments
706 argInput="$1";
707 argType="$2";
708 if [[ $# -gt 2 ]]; then yell "ERROR:$0:$fn:Too many arguments."; exit 1; fi;
709
710 # Check for blank
711 if [[ -z "$argInput" ]]; then return 1; fi
712
713 # Define input types
714 ## ssh_pubkey
715 ### Check for alnum/dash base64 (ex: "ssh-rsa AAAAB3NzaC1yc2EAAA")
716 if [[ "$argType" = "ssh_pubkey" ]]; then
717 if [[ "$argInput" =~ ^[[:alnum:]-]*[\ ]*[[:alnum:]+/=]*$ ]]; then
718 return 0; fi; fi;
719
720 ## age_pubkey
721 ### Check for age1[:bech32:]
722 if [[ "$argType" = "age_pubkey" ]]; then
723 if [[ "$argInput" =~ ^age1[qpzry9x8gf2tvdw0s3jn54khce6mua7l]*$ ]]; then
724 return 0; fi; fi
725
726 ## integer
727 if [[ "$argType" = "integer" ]]; then
728 if [[ "$argInput" =~ ^[[:digit:]]*$ ]]; then
729 return 0; fi; fi;
730
731 ## time element (year, month, week, day, hour, minute, second)
732 if [[ "$argType" = "time_element" ]]; then
733 if [[ "$argInput" = "year" ]] || \
734 [[ "$argInput" = "month" ]] || \
735 [[ "$argInput" = "week" ]] || \
736 [[ "$argInput" = "day" ]] || \
737 [[ "$argInput" = "hour" ]] || \
738 [[ "$argInput" = "minute" ]] || \
739 [[ "$argInput" = "second" ]]; then
740 return 0; fi; fi;
741
742 # Return error if no condition matched.
743 return 1;
744 } # Validates strings
745
746 magicInitWorkingDir() {
747 # Desc: Determine temporary working directory from defaults or user input
748 # Usage: magicInitWorkingDir
749 # Input: vars: optionTmpDir, argTempDirPriority, dirTmpDefault
750 # Input: vars: scriptTimeStart
751 # Output: vars: dir_tmp
752 # Depends: bash 5.0.3, processArguments(), vbm(), yell()
753 # Parse '-t' option (user-specified temporary working dir)
754 ## Set dir_tmp_parent to user-specified value if specified
755 local fn dir_tmp_parent
756
757 # Save function name
758 fn="${FUNCNAME[0]}";
759
760 vbm "STATUS:$fn:Starting magicInitWorkingDir() function.";
761 if [[ "$optionTmpDir" = "true" ]]; then
762 if [[ -d "$argTempDirPriority" ]]; then
763 dir_tmp_parent="$argTempDirPriority";
764 else
765 yell "WARNING:$fn:Specified temporary working directory not valid:$argTempDirPriority";
766 exit 1; # Exit since user requires a specific temp dir and it is not available.
767 fi;
768 else
769 ## Set dir_tmp_parent to default or fallback otherwise
770 if [[ -d "$dirTmpDefault" ]]; then
771 dir_tmp_parent="$dirTmpDefault";
772 elif [[ -d /tmp ]]; then
773 yell "WARNING:$fn:$dirTmpDefault not available. Falling back to /tmp .";
774 dir_tmp_parent="/tmp";
775 else
776 yell "ERROR:$fn:No valid working directory available. Exiting.";
777 exit 1;
778 fi;
779 fi;
780 ## Set dir_tmp using dir_tmp_parent and nonce (scriptTimeStart)
781 dir_tmp="$dir_tmp_parent"/"$scriptTimeStart""..bkgpslog" && vbm "DEBUG :$fn:Set dir_tmp to:$dir_tmp"; # Note: removed at end of main().
782 vbm "STATUS:$fn:Finished magicInitWorkingDir() function.";
783 } # Sets working dir
784 magicInitCheckTar() {
785 # Desc: Initializes or checks output tar
786 # input: vars: dirOut, bufferTTL, cmd_encrypt_suffix, cmd_compress_suffix
787 # input: vars: scriptHostname
788 # output: vars: pathout_tar
789 # depends: Bash 5.0.3, vbm(), dateShort(), checkMakeTar(), magicWriteVersion()
790 local fn
791
792 # Save function name
793 fn="${FUNCNAME[0]}";
794
795 vbm "STATUS:$fn:Starting magicInitCheckTar() function.";
796 # Form pathout_tar
797 pathout_tar="$dirOut"/"$(dateShort "$(date --date="$bufferTTL seconds ago" --iso-8601=seconds)")".."$scriptHostname""$label""$cmd_compress_suffix""$cmd_encrypt_suffix".tar && \
798 vbm "STATUS:$fn:Set pathout_tar to:$pathout_tar";
799 # Validate pathout_tar as tar.
800 checkMakeTar "$pathout_tar";
801 ## Add VERSION file if checkMakeTar had to create a tar (exited 1) or replace one (exited 2)
802 vbm "STATUS:$fn:exit status before magicWriteVersion:$?"
803 if [[ $? -eq 1 ]] || [[ $? -eq 2 ]]; then magicWriteVersion; fi
804 vbm "STATUS:$fn:Finished magicInitCheckTar() function.";
805 } # Initialize tar, set pathout_tar
806 magicParseCompressionArg() {
807 # Desc: Parses compression arguments specified by '-c' option
808 # Input: vars: optionCompress
809 # Output: cmd_compress, cmd_compress_suffix
810 # Depends: processArguments(), vbm(), checkapp(), gzip 1.9
811 local fn
812
813 # Save function name
814 fn="${FUNCNAME[0]}";
815
816 vbm "STATUS:$fn:Starting magicParseCompressionArg() function.";
817 if [[ "$optionCompress" = "true" ]]; then # Check if compression option active
818 if checkapp gzip; then # Check if gzip available
819 cmd_compress="gzip " && vbm "STATUS:$fn:cmd_compress:$cmd_compress";
820 cmd_compress_suffix=".gz" && vbm "STATUS:$fn:cmd_compress_suffix:$cmd_compress_suffix";
821 else
822 yell "ERROR:$fn:Compression enabled but \"gzip\" not found. Exiting."; exit 1;
823 fi;
824 else
825 cmd_compress="tee /dev/null " && vbm "STATUS:$fn:cmd_compress:$cmd_compress";
826 cmd_compress_suffix="" && vbm "STATUS:$fn:cmd_compress_suffix:$cmd_compress_suffix";
827 vbm "DEBUG :$fn:Compression not enabled.";
828 fi;
829 vbm "STATUS:$fn:Starting magicParseCompressionArg() function.";
830 } # Form compression cmd string and filename suffix
831 magicParseCustomTTL() {
832 # Desc: Set user-specified TTLs for buffer and script
833 # Usage: magicParseCustomTTL
834 # Input: vars: argCustomBufferTTL (integer), argCustomScriptTTL_TE (string)
835 # Input: vars: optionCustomBufferTTL, optionCustomScriptTTL_TE
836 # Input: vars: bufferTTL (integer), scriptTTL_TE (string)
837 # Output: bufferTTL (integer), scriptTTL_TE (string)
838 # Depends: Bash 5.0.3, yell(), vbm(), validateInput(), showUsage()
839 local fn
840
841 # Save function name
842 fn="${FUNCNAME[0]}";
843
844 vbm "STATUS:$fn:Starting magicParseCustomTTL() function.";
845 # React to '-b, --buffer-ttl' option
846 if [[ "$optionCustomBufferTTL" = "true" ]]; then
847 ## T: Check if argCustomBufferTTL is an integer
848 if validateInput "$argCustomBufferTTL" "integer"; then
849 ### T: argCustomBufferTTL is an integer
850 bufferTTL="$argCustomBufferTTL" && vbm "STATUS:$fn:Custom bufferTTL from -b:$bufferTTL";
851 else
852 ### F: argcustomBufferTTL is not an integer
853 yell "ERROR:$fn:Invalid integer argument for custom buffer time-to-live."; showUsage; exit 1;
854 fi;
855 ## F: do not change bufferTTL
856 fi;
857
858 # React to '-B, --script-ttl' option
859 if [[ "$optionCustomScriptTTL_TE" = "true" ]]; then
860 ## T: Check if argCustomScriptTTL is a time element (ex: "day", "hour")
861 if validateInput "$argCustomScriptTTL_TE" "time_element"; then
862 ### T: argCustomScriptTTL is a time element
863 scriptTTL_TE="$argCustomScriptTTL_TE" && vbm "STATUS:$fn:Custom scriptTTL_TE from -B:$scriptTTL_TE";
864 else
865 ### F: argcustomScriptTTL is not a time element
866 yell "ERROR:$fn:Invalid time element argument for custom script time-to-live."; showUsage; exit 1;
867 fi;
868 ## F: do not change scriptTTL_TE
869 fi;
870 vbm "STATUS:$fn:Starting magicParseCustomTTL() function.";
871 } # Sets custom script or buffer TTL if specified
872 magicParseLabel() {
873 # Desc: Parses -l option to set label
874 # In : optionLabel, argLabel
875 # Out: vars: label
876 # Depends: Bash 5.0.3, vbm(), yell()
877 local fn
878
879 # Save function name
880 fn="${FUNCNAME[0]}";
881
882 vbm "STATUS:$fn:Started magicParseLabel() function.";
883 # Do nothing if optionLabel not set to true.
884 if [[ ! "$optionLabel" = "true" ]]; then
885 vbm "STATUS:$fn:optionlabel not set to 'true'. Returning early.";
886 return;
887 fi;
888 # Set label if optionLabel is true
889 if [[ "$optionLabel" = "true" ]]; then
890 label="_""$argLabel";
891 vbm "STATUS:$fn:Set label:$label";
892 fi;
893 vbm "STATUS:$fn:Finished magicParseLabel() function.";
894 } # Set label used in output file name
895 magicParseProcessStrings() {
896 # Desc: Processes user-supplied process strings into process commands for appendFileTar().
897 # Usage: magicParseProcessStrings
898 # In : vars: optionProcString optionNoStoreRaw optionStoreRaw argRawFileExt
899 # arry: argProcStrings, argProcFileExts
900 # Out: arry: procStrings, procFileExts
901 # Depends Bash 5.0.3, yell(), vbm()
902 local fn rawFileExt
903
904 # Save function name
905 fn="${FUNCNAME[0]}";
906
907 vbm "STATUS:$fn:Starting magicParseProcessStrings() function.";
908 vbm "STATUS:$fn:var:optionProcString:$optionProcString";
909 vbm "STATUS:$fn:var:optionNoStoreRaw:$optionNoStoreRaw";
910 vbm "STATUS:$fn:var:optionStoreRaw:$optionStoreRaw";
911 vbm "STATUS:$fn:var:argRawFileExt:$argRawFileExt";
912 vbm "STATUS:$fn:ary:argProcStrings:${argProcStrings[*]}";
913 vbm "STATUS:$fn:ary:argProcFileExts:${argProcFileExts[*]}"
914 # Validate input
915 ## Validate argRawFileExt
916 if [[ "$argRawFileExt" =~ ^[.][[:alnum:]]*$ ]]; then
917 rawFileExt="$argRawFileExt";
918 fi;
919
920 # Add default stdin output file entries for procStrings, procFileExts
921 ## Check if user specified that no raw stdin be saved.
922 if [[ ! "$optionNoStoreRaw" = "true" ]]; then
923 ### T: --no-store-raw not set. Store raw. Append procStrings with cat.
924 #### Append procStrings array
925 procStrings+=("cat ");
926 #### Check if --store-raw set.
927 if [[ "$optionStoreRaw" = "true" ]]; then
928 ##### T: --store-raw set. Append procFileExts with user-specified file ext
929 procFileExts+=("$rawFileExt");
930 else
931 ##### F: --store-raw not set. Append procFileExts with default ".stdin" file ext
932 ###### Append procFileExts array
933 procFileExts+=(".stdin");
934 fi;
935 else
936 ### F: --no-store-raw set. Do not store raw.
937 #### Do not append procStrings or procFileExts arrays.
938 :
939 fi;
940
941 # Do nothing more if optionProcString not set to true.
942 if [[ ! "$optionProcString" = "true" ]]; then
943 vbm "STATUS:$fn:optionProcString not set to 'true'. Returning early.";
944 return; fi;
945 # Validate input array indices
946 ## Make sure that argProcStrings and argProcFileExts have same index counts
947 if ! [[ "${#argProcStrings[@]}" -eq "${#argProcFileExts[@]}" ]]; then
948 yell "ERROR:$fn:Mismatch in number of elements in arrays argProcStrings and argProcFileExts:${#argProcStrings[@]} DNE ${#argProcFileExts[@]}";
949 yell "STATUS:$fn:argProcStrings:${argProcStrings[*]}"; yell "STATUS:$fn:argProcFileExts:${argProcFileExts[*]}"; exit 1; fi;
950 ## Make sure that no array elements are blank
951 for element in "${argProcStrings[@]}"; do
952 if [[ -z "$element" ]]; then yell "ERROR:$fn:Empty process string specified. Exiting."; exit 1; fi; done
953 for element in "${argProcFileExts[@]}"; do
954 if [[ -z "$element" ]]; then yell "ERROR:$fn:Empty output file extension specified. Exiting."; exit 1; fi; done
955 ## Make sure that no process string starts with '-' (ex: if only one arg supplied after '-p' option)
956 for element in "${argProcStrings[@]}"; do
957 if [[ "$element" =~ ^[-][[:print:]]*$ ]] && [[ ! "$element" =~ ^[[:print:]]*$ ]]; then
958 yell "ERROR:$fn:Illegal character '-' at start of process string element:\"$element\"";
959 exit 1; fi; done;
960 vbm "STATUS:$fn:Quick check shows argProcStrings and argProcFileExts appear to have valid contents.";
961 procStrings=("${argProcStrings[@]}"); # Export process command strings
962 procFileExts=("${argProcFileExts[@]}"); # Export process command strings
963 vbm "STATUS:$fn:Finished magicParseProcessStrings() function.";
964 } # Validate and save process strings and file extensions to arrays procStrings, procFileExts
965 magicParseRecipients() {
966 # Desc: Parses recipient arguments specified by '-r' or '-R' options
967 # Usage: magicParseRecipients
968 # In : vars: optionEncrypt, optionRecArg, optionRecDir
969 # arry: argRecPubKeys (-r), argRecDir (-R)
970 # Out: vars: cmd_encrypt, cmd_encrypt_suffix
971 # Depends: head 8.30, checkapp(), checkAgePubkey(), validateInput()
972 local fn recipients recipientDir recFileLine updateRecipients
973 local -a recPubKeysValid candRecPubKeysValid
974
975 # Save function name
976 fn="${FUNCNAME[0]}";
977 vbm "STATUS:$fn:Starting magicParseRecipients() function.";
978
979 # Catch illegal option combinations
980 ## Catch case if '-e' is set but neither '-r' nor '-R' is set
981 if [[ "$optionEncrypt" = "true" ]] && \
982 ! { [[ "$optionRecArg" = "true" ]] || [[ "$optionRecDir" = "true" ]]; }; then
983 yell "ERROR:$fn:\\'-e\\' set but no \\'-r\\' or \\'-R\\' set."; exit 1; fi;
984 ## Catch case if '-r' or '-R' set but '-e' is not
985 if [[ ! "$optionEncrypt" = "true" ]] && \
986 { [[ "$optionRecArg" = "true" ]] || [[ "$optionRecDir" = "true" ]]; }; then
987 yell "ERROR:$fn:\\'-r\\' or \\'-R\\' set but \\'-e\\' is not set."; exit 1; fi;
988
989 # Handle no encryption cases
990 if [[ ! "$optionEncrypt" = "true" ]]; then
991 cmd_encrypt="cat " && vbm "STATUS:$fn:cmd_encrypt:$cmd_encrypt";
992 cmd_encrypt_suffix="" && vbm "STATUS:$fn:cmd_encrypt_suffix:$cmd_encrypt_suffix";
993 vbm "DEBUG :$fn:Encryption not enabled.";
994 return; fi;
995
996 # Handle encryption cases
997 ## Check age availability
998 if ! checkapp age; then yell "ERROR:$fn:age not available. Exiting."; exit 1; fi
999 ## Parse '-r' options: validate and append pubkeys from argRecPubKeys to recPubKeysValid
1000 if [[ "$optionRecArg" = "true" ]]; then
1001 for pubkey in "${argRecPubKeys[@]}"; do # Validate recipient pubkey strings by forming test message
1002 vbm "DEBUG :$fn:Testing pubkey string:$pubkey";
1003 if checkAgePubkey "$pubkey" && \
1004 ( validateInput "$pubkey" "ssh_pubkey" || validateInput "$pubkey" "age_pubkey"); then
1005 #### Add validated pubkey to recPubKeysValid array
1006 recPubKeysValid+=("$pubkey") && \
1007 vbm "DEBUG :$fn:recPubkeysValid:pubkey added:$pubkey";
1008 else
1009 yell "ERROR:$fn:Exit code ""$?"". Invalid recipient pubkey string. Exiting."; exit 1;
1010 fi;
1011 done;
1012 vbm "STATUS:$fn:Finished processing argRecPubKeys array";
1013 vbm "DEBUG :$fn:Array of validated pubkeys:${recPubKeysValid[*]}";
1014 fi;
1015 ## Parse '-R' options: validate and append pubkeys in argRecDir to recPubKeysValid
1016 if [[ "$optionRecDir" = "true" ]]; then
1017 ### Check that argRecDir is a directory
1018 if [[ -d "$argRecDir" ]]; then
1019 recipientDir="$argRecDir" && \
1020 vbm "STATUS:$fn:Recipient watch directory detected:\"$recipientDir\"";
1021 #### Initialize variable indicating outcome of pubkey review
1022 unset updateRecipients
1023 #### Add existing recipients from '-r' option
1024 candRecPubKeysValid=("${recPubKeysValid[@]}");
1025 #### Parse files in recipientDir
1026 for file in "$recipientDir"/*; do
1027 ##### Read first line of each file
1028 recFileLine="$(head -n1 "$file")" && \
1029 vbm "STATUS:$fn:Checking if pubkey:\"$recFileLine\"";
1030 ##### check if first line is a valid pubkey
1031 if checkAgePubkey "$recFileLine" && \
1032 ( validateInput "$recFileLine" "ssh_pubkey" || validateInput "$recFileLine" "age_pubkey"); then
1033 ###### T: add candidate pubkey to candRecPubKeysValid
1034 candRecPubKeysValid+=("$recFileLine") && \
1035 vbm "STATUS:$fn:RecDir pubkey is valid pubkey:\"$recFileLine\"";
1036 else
1037 ###### F: throw warning;
1038 yell "ERROR:$fn:Invalid recipient file detected. Not modifying recipient list:$recFileLine";
1039 updateRecipients="false";
1040 fi;
1041 done
1042 #### Write candRecPubKeysValid array to recPubKeysValid if no invalid key detected
1043 if ! [[ "$updateRecipients" = "false" ]]; then
1044 recPubKeysValid=("${candRecPubKeysValid[@]}") && \
1045 vbm "STATUS:$fn:Wrote candRecPubkeysValid to recPubKeysValid:\"${recPubKeysValid[*]}\"";
1046 fi;
1047 fi;
1048 fi;
1049
1050 ## Form age recipient string from recPubKeysValid
1051 for pubkey in "${recPubKeysValid[@]}"; do
1052 recipients="$recipients""-r '$pubkey' ";
1053 vbm "STATUS:$fn:Added pubkey for forming age recipient string:""$pubkey";
1054 vbm "DEBUG :$fn:recipients:""$recipients";
1055 done;
1056
1057 ## Output cmd_encrypt, cmd_encrypt_suffix from recipients
1058 cmd_encrypt="age ""$recipients " && vbm "STATUS:$fn:cmd_encrypt:$cmd_encrypt";
1059 cmd_encrypt_suffix=".age" && vbm "STATUS:$fn:cmd_encrypt_suffix:$cmd_encrypt_suffix";
1060
1061 vbm "STATUS:$fn:Finished magicParseRecipients() function.";
1062 } # Sets cmd_encrypt, cmd_encrypt_suffix from -r, -R args
1063 magicSetScriptTTL() {
1064 #Desc: Sets script_TTL seconds from provided time_element string argument
1065 #Usage: magicSetScriptTTL [str time_element]
1066 #Input: arg1: string (Ex: scriptTTL_TE; "day" or "hour")
1067 #Output: var: scriptTTL (integer seconds)
1068 #Depends: timeUntilNextHour, timeUntilNextDay
1069 local fn argTimeElement
1070
1071 # Save function name
1072 fn="${FUNCNAME[0]}";
1073
1074 vbm "STATUS:$fn:Starting magicSetScriptTTL() function.";
1075 argTimeElement="$1";
1076 if [[ "$argTimeElement" = "day" ]]; then
1077 # Set script lifespan to end at start of next day
1078 if ! scriptTTL="$(timeUntilNextDay)"; then # sets scriptTTL, then checks exit code
1079 if [[ "$scriptTTL" -eq 0 ]]; then
1080 ((scriptTTL++)); # Add 1 because 0 would cause 'timeout' to never timeout.
1081 else
1082 yell "ERROR:$fn:timeUntilNextDay exit code $?"; exit 1;
1083 fi;
1084 fi;
1085 elif [[ "$argTimeElement" = "hour" ]]; then
1086 # Set script lifespan to end at start of next hour
1087 if ! scriptTTL="$(timeUntilNextHour)"; then # sets scriptTTL, then checks exit code
1088 if [[ "$scriptTTL" -eq 0 ]]; then
1089 ((scriptTTL++)); # Add 1 because 0 would cause 'timeout' to never timeout.
1090 else
1091 yell "ERROR:$fn:timeUntilNextHour exit code $?"; exit 1;
1092 fi;
1093 fi;
1094 else
1095 yell "ERROR:$fn:Invalid argument for setScriptTTL function:$argTimeElement"; exit 1;
1096 fi;
1097 vbm "STATUS:$fn:Finished magicSetScriptTTL() function.";
1098 } # Set scriptTTL in seconds until next (day|hour).
1099 magicWriteVersion() {
1100 # Desc: Appends time-stamped VERSION to pathout_tar
1101 # Usage: magicWriteVersion
1102 # Input: vars: pathout_tar, dir_tmp
1103 # Input: vars: scriptVersion, scriptURL, ageVersion, ageURL, scriptHostname
1104 # Input: array: recPubKeysValid
1105 # Output: appends tar (pathout_tar)
1106 # Depends: bash 5.0.3, dateTimeShort(), appendArgTar()
1107 local fn fileoutVersion contentVersion pubKeyIndex pubKeyIndex
1108
1109 # Save function name
1110 fn="${FUNCNAME[0]}";
1111
1112 vbm "STATUS:$fn:Starting magicWriteVersion() function.";
1113 # Set VERSION file name
1114 fileoutVersion="$(dateTimeShort)..VERSION";
1115
1116 # Gather VERSION data in contentVersion
1117 contentVersion="scriptVersion=$scriptVersion";
1118 #contentVersion="$contentVersion""\\n";
1119 contentVersion="$contentVersion""\\n""scriptName=$scriptName";
1120 contentVersion="$contentVersion""\\n""scriptURL=$scriptURL";
1121 contentVersion="$contentVersion""\\n""ageVersion=$ageVersion";
1122 contentVersion="$contentVersion""\\n""ageURL=$ageURL";
1123 contentVersion="$contentVersion""\\n""date=$(date --iso-8601=seconds)";
1124 contentVersion="$contentVersion""\\n""hostname=$scriptHostname";
1125 ## Add list of recipient pubkeys
1126 for pubkey in "${recPubKeysValid[@]}"; do
1127 ((pubKeyIndex++))
1128 contentVersion="$contentVersion""\\n""PUBKEY_$pubKeyIndex=$pubkey";
1129 done
1130 ## Process newline escapes
1131 contentVersion="$(echo -e "$contentVersion")"
1132
1133 # Write contentVersion as file fileoutVersion and write-append to pathout_tar
1134 appendArgTar "$contentVersion" "$fileoutVersion" "$pathout_tar" "$dir_tmp" && \
1135 vbm "STATUS:$fn:Appended $fileoutVersion to $pathout_tar";
1136 vbm "STATUS:$fn:Finished magicWriteVersion() function.";
1137 } # write version data to pathout_tar via appendArgTar()
1138 magicProcessWriteBuffer() {
1139 # Desc: process and write buffer
1140 # In : vars: bufferTTL bufferTTL_STR scriptHostname label dir_tmp SECONDS
1141 # : arry: buffer
1142 # Out: file:(pathout_tar)
1143 # Depends: Bash 5.0.3, date 8.30, yell(), vbm(), dateTimeShort(),
1144 ### Note: These arrays should all have the same number of elements:
1145 ### pathouts, fileouts, procFileExts, procStrings
1146
1147 local fn timeBufferStartLong timeBufferStart fileoutBasename
1148 local -a fileouts pathouts
1149 local writeCmd1 writeCmd2 writeCmd3 writeCmd4
1150
1151 # Debug:Get function name
1152 fn="${FUNCNAME[0]}";
1153
1154 vbm "STATUS:$fn:Started magicProcessWriteBuffer().";
1155 # Determine file paths (time is start of buffer period)
1156 ## Calculate start time
1157 timeBufferStartLong="$(date --date="$bufferTTL seconds ago" --iso-8601=seconds)" && \
1158 vbm "DEBUG :$fn:timeBufferStartLong:$timeBufferStartLong";
1159 timeBufferStart="$(dateTimeShort "$timeBufferStartLong" )" && \
1160 vbm "DEBUG :$fn:timeBufferStart:$timeBufferStart"; # Note start time YYYYmmddTHHMMSS+zzzz (no separators)
1161 ## Set common basename
1162 fileoutBasename="$timeBufferStart""--""$bufferTTL_STR""..""$scriptHostname""$label" && \
1163 vbm "STATUS:$fn:Set fileoutBasename to:$fileoutBasename";
1164 ## Determine output file name array
1165 ### in: fileOutBasename cmd_compress_suffix cmd_encrypt_suffix procFileExts
1166 for fileExt in "${procFileExts[@]}"; do
1167 fileouts+=("$fileoutBasename""$fileExt""$cmd_compress_suffix""$cmd_encrypt_suffix") && \
1168 vbm "STATUS:$fn:Added $fileExt to fileouts:${fileouts[*]}";
1169 done;
1170 for fileName in "${fileouts[@]}"; do
1171 pathouts+=("$dir_tmp"/"$fileName") && \
1172 vbm "STATUS:$fn:Added $fileName to pathouts:${pathouts[*]}";
1173 done;
1174 ## Update pathout_tar
1175 magicInitCheckTar;
1176
1177 # Process and write buffers to dir_tmp
1178 ## Prepare command strings
1179 writeCmd1="printf \"%s\\\\n\" \"\${buffer[@]}\""; # printf "%s\\n" "${buffer[@]}"
1180 #writeCmd2="" # NOTE: Specified by parsing array procStrings
1181 writeCmd3="$cmd_compress";
1182 writeCmd4="$cmd_encrypt";
1183
1184 ## Process buffer and write to dir_tmp
1185 for index in "${!pathouts[@]}"; do
1186 writeCmd2="${procStrings[$index]}";
1187 writeCmdAll="$writeCmd1 | $writeCmd2 | $writeCmd3 | $writeCmd4" && vbm "STATUS:$fn:Assembled command:\"$writeCmdAll\"";
1188 eval "$writeCmdAll" >> "${pathouts[$index]}" && vbm "STATUS:$fn:Wrote command output to ${pathouts[$index]}";
1189 done;
1190
1191 # Append dir_tmp files to pathout_tar
1192 wait; # Wait to avoid collision with older magicProcessWriteBuffer() instances (see https://www.tldp.org/LDP/abs/html/x9644.html )
1193 for index in "${!pathouts[@]}"; do
1194 appendFileTar "${pathouts[$index]}" "${fileouts[$index]}" "$pathout_tar" "$dir_tmp" && \
1195 vbm "STATUS:$fn:Appended ${pathouts[$index]} to $pathout_tar";
1196 done;
1197
1198 # Remove secured chunks from dir_tmp
1199 for path in "${pathouts[@]}"; do
1200 rm "$path" && vbm "STATUS:$fn:Removed:$path";
1201 done;
1202
1203 vbm "STATUS:$fn:Finished magicProcessWriteBuffer().";
1204 } # Process and Write buffer
1205 showUsage() {
1206 cat <<'EOF'
1207 USAGE:
1208 cmd | bklog [ options ]
1209
1210 OPTIONS:
1211 -h, --help
1212 Display help information.
1213 --version
1214 Display script version.
1215 -v, --verbose
1216 Display debugging info.
1217 -e, --encrypt
1218 Encrypt output.
1219 -r, --recipient [ string pubkey ]
1220 Specify recipient. May be age or ssh pubkey.
1221 May be specified multiple times for multiple pubkeys.
1222 See https://github.com/FiloSottile/age
1223 -o, --output [ path dir ]
1224 Specify output directory to save logs. This option is required
1225 to save log data.
1226 -p, --process-string [ filter command ] [ output file extension]
1227 Specify how to create and name a processed version of the stdin.
1228 For example, if stdin is 'nmea' location data:
1229
1230 -p "gpsbabel -i nmea -f - -o gpx -F - " ".gpx"
1231
1232 This option would cause the stdin to 'bklog' to be piped into
1233 the 'gpsbabel' command, interpreted as 'nmea' data, converted
1234 into 'gpx' format, and then appended to the output tar file
1235 as a file with a '.gpx' extension.
1236 This option may be specified multiple times in order to output
1237 results of multiple different processing methods.
1238 -l, --label [ string ]
1239 Specify a label to be included in all output file names.
1240 Ex: 'location' if stdin is location data.
1241 -w, --store-raw [ file extension ]
1242 Specify file extension of file within output tar that contains
1243 raw stdin data. The default behavior is to always save raw stdin
1244 data in a '.stdin' file. Example usage when 'bklog' receives
1245 'nmea' data from 'gpspipe -r':
1246
1247 -w ".nmea"
1248
1249 Stdin data is saved in a '.nmea' file within the output tar.
1250 -W, --no-store-raw
1251 Do not store raw stdin in output tar.
1252 -c, --compress
1253 Compress output with gzip (before encryption if enabled).
1254 -z, --time-zone
1255 Specify time zone. (ex: "America/New_York")
1256 -t, --temp-dir [path dir]
1257 Specify parent directory for temporary working directory.
1258 Default: "/dev/shm"
1259 -R, --recipient-dir [path dir]
1260 Specify directory containing files whose first lines are
1261 to be interpreted as pubkey strings (see '-r' option). Only
1262 one directory may be specified.
1263 -b, --buffer-ttl [integer]
1264 Specify custom buffer period in seconds (default: 300 seconds)
1265 -B, --script-ttl [time element string]
1266 Specify custom script time-to-live in seconds (default: "day")
1267 Valid values: "day", "hour"
1268
1269 EXAMPLE: (bash script lines)
1270 $ gpspipe -r | /bin/bash bklog -v -e -c -z "UTC" -t "/dev/shm" \
1271 -r age1mrmfnwhtlprn4jquex0ukmwcm7y2nxlphuzgsgv8ew2k9mewy3rs8u7su5 \
1272 -r age1ala848kqrvxc88rzaauc6vc5v0fqrvef9dxyk79m0vjea3hagclswu0lgq \
1273 -R ~/.config/bklog/recipients -w ".nmea" -b 300 -B "day" \
1274 -o ~/Sync/Logs -l "location" \
1275 -p "gpsbabel -i nmea -f - -o gpx -F - " ".gpx" \
1276 -p "gpsbabel -i nmea -f - -o kml -F - " ".kml"
1277 EOF
1278 } # Display information on how to use this script.
1279
1280 main() {
1281 # Desc: Main function
1282 # Usage: main "$@"
1283 # Inputs: many
1284 # Outputs: file (pathout_tar)
1285 # Depends: many
1286 local fn
1287
1288 # Debug:Get function name
1289 fn="${FUNCNAME[0]}";
1290
1291 vbm "STATUS:$fn:Started function main().";
1292 # Process arguments
1293 processArguments "$@";
1294 ## Determine working directory
1295 magicInitWorkingDir; # Sets dir_tmp from argTempDirPriority
1296 ## Set output encryption and compression option strings
1297 ### React to "-e", "-r", and "-R" (encryption recipients) options
1298 magicParseRecipients; # Update cmd_encrypt, cmd_encrypt_suffix
1299 ### React to "-c" ("compression") option
1300 magicParseCompressionArg; # Updates cmd_compress[_suffix]
1301 ## React to "-b" and "-B" (custom buffer and script TTL) options
1302 magicParseCustomTTL; # Sets custom scriptTTL_TE and/or bufferTTL if specified
1303 ## React to "-p" (user-supplied process command and file extension strings) options
1304 magicParseProcessStrings; # Sets arrays: procStrings, procFileExts
1305 ## React to "-l" (output file label) option
1306 magicParseLabel; # sets label (ex: "_location")
1307
1308 # Perform secondary setup operations
1309 ## Set script lifespan (scriptTTL from scriptTTL_TE)
1310 magicSetScriptTTL "$scriptTTL_TE";
1311 ## File name substring (ISO-8601 duration from bufferTTL)
1312 bufferTTL_STR="$(timeDuration "$bufferTTL")" && vbm "DEBUG :$fn:bufferTTL_STR:$bufferTTL_STR";
1313 ## Init temp working dir
1314 try mkdir "$dir_tmp" && vbm "DEBUG :$fn:Working dir created at dir_tmp:$dir_tmp";
1315 ## Initialize output tar (set pathout_tar)
1316 magicInitCheckTar;
1317
1318 # Check vital apps, files, dirs
1319 if ! checkapp tar && ! checkdir "$dirOut" "dir_tmp"; then
1320 yell "ERROR:$fn:Critical components missing.";
1321 displayMissing; yell "Exiting."; exit 1; fi
1322
1323 # MAIN LOOP: Run until script TTL seconds pass
1324 bufferRound=0;
1325 while [[ $SECONDS -lt "scriptTTL" ]]; do
1326 vbm "STATUS:$fn:Starting buffer round:$bufferRound";
1327 bufferTOD="$((SECONDS + bufferTTL))"; # Set buffer round time-of-death
1328 # Consume stdin to fill buffer until buffer time-of-death (TOD) arrives
1329 while read -r -t "$bufferTTL" line && [[ $SECONDS -lt "$bufferTOD" ]]; do
1330 # Append line to buffer array
1331 buffer+=("$line");
1332 done;
1333 # Create dir_tmp if missing
1334 if ! [[ -d "$dir_tmp" ]]; then
1335 yell "ERROR:$fn:dir_tmp existence failure:$dir_tmp";
1336 try mkdir "$dir_tmp" && vbm "DEBUG :$fn:Working dir recreated dir_tmp:$dir_tmp"; fi
1337 # Update cmd_encrypt, cmd_encrypt_suffix
1338 magicParseRecipients;
1339 # Export buffer to asynchronous processing.
1340 magicProcessWriteBuffer &
1341 unset buffer; # Clear buffer array for next bufferRound
1342 # Increment buffer round
1343 ((bufferRound++));
1344 done;
1345
1346 # Cleanup
1347 ## Remove dir_tmp
1348 try rm -r "$dir_tmp" && vbm "STATUS:$fn:Removed dir_tmp:$dir_tmp";
1349
1350 vbm "STATUS:$fn:Finished function main().";
1351 } # Main function
1352
1353 #===END Declare local script functions===
1354 #==END Define script parameters==
1355
1356 #==BEGIN Perform work and exit==
1357 main "$@" # Run main function.
1358 exit 0;
1359 #==END Perform work and exit==
1360
1361 # Author: Steven Baltakatei Sandoval;
1362 # License: GPLv3+