2 # Desc: Compresses, encrypts, and writes stdin every 5 seconds
4 #==BEGIN Define script parameters==
5 #===BEGIN Initialize variables===
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
13 scriptName
="bklog"; # Define basename of script file.
14 scriptVersion
="0.1.21"; # 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.
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
33 optionVerbose
=""; optionEncrypt
=""; dirOut
=""; optionEncrypt
=""; dir_tmp
="";
34 cmd_compress
="";cmd_compress_suffix
=""; cmd_encrypt
=""; cmd_encrypt_suffix
="";
36 #===END Initialize variables===
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
43 while [ ! $# -eq 0 ]; do # While number of arguments ($#) is not (!) equal to (-eq) zero (0).
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.
65 } # Argument Processing
67 # Description: Prints verbose message ("vbm") to stderr if optionVerbose is set to "true".
68 # Usage: vbm "DEBUG :verbose message here"
73 # Depends: bash 5.0.3, echo 8.30, date 8.30
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.
81 return 0; # Function finished.
82 } # Displays message if optionVerbose true
84 # Desc: If arg is a command, save result in assoc array 'appRollCall'
85 # Usage: checkapp arg1 arg2 arg3 ...
87 # Input: global assoc. array 'appRollCall'
88 # Output: adds/updates key(value) to global assoc array 'appRollCall'
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;
98 appRollCall
[$arg]="false"; returnState
="false";
102 #===Determine function return code===
103 if [ "$returnState" = "true" ]; then
108 } # Check that app exists
110 # Desc: If arg is a file path, save result in assoc array 'fileRollCall'
111 # Usage: checkfile arg1 arg2 arg3 ...
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
121 if [ -f "$arg" ]; then
122 fileRollCall
["$arg"]="true";
123 if ! [ "$returnState" = "false" ]; then returnState
="true"; fi;
125 fileRollCall
["$arg"]="false"; returnState
="false";
129 #===Determine function return code===
130 if [ "$returnState" = "true" ]; then
135 } # Check that file exists
137 # Desc: If arg is a dir path, save result in assoc array 'dirRollCall'
138 # Usage: checkdir arg1 arg2 arg3 ...
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
148 if [ -d "$arg" ]; then
149 dirRollCall
["$arg"]="true";
150 if ! [ "$returnState" = "false" ]; then returnState
="true"; fi
152 dirRollCall
["$arg"]="false"; returnState
="false";
156 #===Determine function return code===
157 if [ "$returnState" = "true" ]; then
162 } # Check that dir exists
164 # Desc: Displays missing apps, files, and dirs
165 # Usage: displayMissing
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
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 ";
185 if [ "$appMissing" = "true" ]; then # Only indicate if an app is missing.
186 echo "$missingApps" 1>&2;
189 #===END Display Missing Apps===
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 ";
202 if [ "$fileMissing" = "true" ]; then # Only indicate if an app is missing.
203 echo "$missingFiles" 1>&2;
206 #===END Display Missing Files===
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 ";
219 if [ "$dirMissing" = "true" ]; then # Only indicate if an dir is missing.
220 echo "$missingDirs" 1>&2;
223 #===END Display Missing Directories===
225 #==END Display errors==
226 } # Display missing apps, files, dirs
229 # Desc: Writes first argument to temporary file with arguments as options, then appends file to tar
230 # Usage: appendArgTar "$(echo "Data to be written.")" [name of file to be inserted] [tar path] [temp dir] ([cmd1] [cmd2] [cmd3] [cmd4]...)
232 # Input: arg1: data to be written
233 # arg2: file name of file to be inserted into tar
234 # arg3: tar archive path (must exist first)
235 # arg4: temporary working dir
236 # arg5+: command strings (ex: "gpsbabel -i nmea -f - -o kml -F - ")
237 # Output: file written to disk
238 # Example: decrypt multiple large files in parallel
239 # appendArgTar "$(cat /tmp/largefile1.gpg)" "largefile1" $HOME/archive.tar /tmp "gpg --decrypt" &
240 # appendArgTar "$(cat /tmp/largefile2.gpg)" "largefile2" $HOME/archive.tar /tmp "gpg --decrypt" &
241 # appendArgTar "$(cat /tmp/largefile3.gpg)" "largefile3" $HOME/archive.tar /tmp "gpg --decrypt" &
242 # Depends: bash 5, tar 1, yell()
243 # Ref/Attrib: Using 'eval' to construct command strings https://askubuntu.com/a/476533
245 local fn fileName tarPath tmpDir cmd0 cmd1 cmd2 cmd3 cmd4
249 #yell "DEBUG:STATUS:$fn:Finished appendArgTar()."
252 if ! [ -z "$2" ]; then fileName
="$2"; else yell
"ERROR:$fn:Not enough arguments."; exit 1; fi
254 # Check tar path is a file
255 if [ -f "$3" ]; then tarPath
="$3"; else yell
"ERROR:$fn:Tar archive arg not a file."; exit 1; fi
258 if ! [ -z "$4" ]; then tmpDir
="$4"; else yell
"ERROR:$fn:No temporary working dir set."; exit 1; fi
260 # Set command strings
261 if ! [ -z "$5" ]; then cmd1
="$5"; else cmd1
="cat "; fi # command string 1
262 if ! [ -z "$6" ]; then cmd2
="$6"; else cmd2
="cat "; fi # command string 2
263 if ! [ -z "$7" ]; then cmd3
="$7"; else cmd3
="cat "; fi # command string 3
264 if ! [ -z "$8" ]; then cmd4
="$8"; else cmd4
="cat "; fi # command string 4
270 # yell "DEBUG:STATUS:$fn:cmd0:$cmd0"
271 # yell "DEBUG:STATUS:$fn:cmd1:$cmd1"
272 # yell "DEBUG:STATUS:$fn:cmd2:$cmd2"
273 # yell "DEBUG:STATUS:$fn:cmd3:$cmd3"
274 # yell "DEBUG:STATUS:$fn:cmd4:$cmd4"
275 # yell "DEBUG:STATUS:$fn:fileName:$fileName"
276 # yell "DEBUG:STATUS:$fn:tarPath:$tarPath"
277 # yell "DEBUG:STATUS:$fn:tmpDir:$tmpDir"
279 # Write to temporary working dir
280 eval "$cmd0 | $cmd1 | $cmd2 | $cmd3 | $cmd4" > "$tmpDir"/"$fileName";
283 try
tar --append --directory="$tmpDir" --file="$tarPath" "$fileName";
284 #yell "DEBUG:STATUS:$fn:Finished appendArgTar()."
285 } # Append Bash var to file appended to Tar archive
287 # Desc: Appends [processed] file to tar
288 # Usage: appendFileTar [file path] [name of file to be inserted] [tar path] [temp dir] ([process cmd])
290 # Input: arg1: path of file to be (processed and) written
291 # arg2: name to use for file inserted into tar
292 # arg3: tar archive path (must exist first)
293 # arg4: temporary working dir
294 # arg5: (optional) command string to process file (ex: "gpsbabel -i nmea -f - -o kml -F - ")
295 # Output: file written to disk
296 # Example: decrypt multiple large files in parallel
297 # appendFileTar /tmp/largefile1.gpg "largefile1" $HOME/archive.tar /tmp "gpg --decrypt" &
298 # appendFileTar /tmp/largefile2.gpg "largefile2" $HOME/archive.tar /tmp "gpg --decrypt" &
299 # appendFileTar /tmp/largefile3.gpg "largefile3" $HOME/archive.tar /tmp "gpg --decrypt" &
300 # Depends: bash 5.0.3, tar 1.30, cat 8.30, yell()
301 local fn fileName tarPath tmpDir
305 #yell "DEBUG :STATUS:$fn:Started appendFileTar()."
308 if ! [ -z "$2" ]; then fileName
="$2"; else yell
"ERROR:$fn:Not enough arguments."; exit 1; fi
309 # Check tar path is a file
310 if [ -f "$3" ]; then tarPath
="$3"; else yell
"ERROR:$fn:Tar archive arg not a file:$3"; exit 1; fi
312 if ! [ -z "$4" ]; then tmpDir
="$4"; else yell
"ERROR:$fn:No temporary working dir set."; exit 1; fi
313 # Set command strings
314 if ! [ -z "$5" ]; then cmd1
="$5"; else cmd1
="cat "; fi # command string
316 # Input command string
319 # Write to temporary working dir
320 eval "$cmd0 | $cmd1" > "$tmpDir"/"$fileName";
323 try
tar --append --directory="$tmpDir" --file="$tarPath" "$fileName";
324 #yell "DEBUG :STATUS:$fn:Finished appendFileTar()."
325 } # Append [processed] file to Tar archive
327 # Desc: Checks if string is an age-compatible pubkey
328 # Usage: checkAgePubkey [str pubkey]
330 # Input: arg1: string
331 # Output: return code 0: string is age-compatible pubkey
332 # return code 1: string is NOT an age-compatible pubkey
333 # age stderr (ex: there is stderr if invalid string provided)
334 # Depends: age (v0.1.0-beta2; https://github.com/FiloSottile/age/releases/tag/v1.0.0-beta2 )
338 if echo "test" | age
-a -r "$argPubkey" 1>/dev
/null
; then
345 # Desc: Checks that a valid tar archive exists, creates one otherwise
346 # Usage: checkMakeTar [ path ]
348 # Input: arg1: path of tar archive
349 # Output: exit code 0 : tar readable
350 # exit code 1 : tar missing; created
351 # exit code 2 : tar not readable; moved; replaced
352 # Depends: bash 5, date 8, tar 1, try()
353 local pathTar returnFlag0 returnFlag1 returnFlag2
356 # Check if file is a valid tar archive
357 if tar --list --file="$pathTar" 1>/dev
/null
2>&1; then
358 ## T1: return success
359 returnFlag0
="tar valid";
361 ## F1: Check if file exists
362 if [[ -f "$pathTar" ]]; then
364 try
mv "$pathTar" "$pathTar""--broken--""$(date +%Y%m%dT%H%M%S)" && \
365 returnFlag1
="tar moved";
370 ## F2: Create tar archive, return 0
371 try
tar --create --file="$pathTar" --files-from=/dev
/null
&& \
372 returnFlag2
="tar created";
375 # Determine function return code
376 if [[ "$returnFlag0" = "tar valid" ]]; then
378 elif [[ "$returnFlag2" = "tar created" ]] && ! [[ "$returnFlag1" = "tar moved" ]]; then
379 return 1; # tar missing so created
380 elif [[ "$returnFlag2" = "tar created" ]] && [[ "$returnFlag1" = "tar moved" ]]; then
381 return 2; # tar not readable so moved; replaced
383 } # checks if arg1 is tar; creates one otherwise
385 # Desc: Date without separators (YYYYmmdd)
386 # Usage: dateShort ([str date])
388 # Input: arg1: 'date'-parsable timestamp string (optional)
389 # Output: stdout: date (ISO-8601, no separators)
390 # Depends: bash 5.0.3, date 8.30, yell()
391 local argTime timeCurrent timeInput dateCurrentShort
395 timeCurrent
="$(date --iso-8601=seconds)" ; # Produce `date`-parsable current timestamp with resolution of 1 second.
396 # Decide to parse current or supplied date
397 ## Check if time argument empty
398 if [[ -z "$argTime" ]]; then
399 ## T: Time argument empty, use current time
400 timeInput
="$timeCurrent";
402 ## F: Time argument exists, validate time
403 if date --date="$argTime" 1>/dev
/null
2>&1; then
404 ### T: Time argument is valid; use it
405 timeInput
="$argTime";
407 ### F: Time argument not valid; exit
408 yell
"ERROR:Invalid time argument supplied. Exiting."; exit 1;
411 # Construct and deliver separator-les date string
412 dateCurrentShort
="$(date -d "$timeInput" +%Y%m%d)"; # Produce separator-less current date with resolution 1 day.
413 echo "$dateCurrentShort";
416 # Desc: Timestamp without separators (YYYYmmddTHHMMSS+zzzz)
417 # Usage: dateTimeShort ([str date])
419 # Input: arg1: 'date'-parsable timestamp string (optional)
420 # Output: stdout: timestamp (ISO-8601, no separators)
422 local argTime timeCurrent timeInput timeCurrentShort
426 timeCurrent
="$(date --iso-8601=seconds)" ; # Produce `date`-parsable current timestamp with resolution of 1 second.
427 # Decide to parse current or supplied date
428 ## Check if time argument empty
429 if [[ -z "$argTime" ]]; then
430 ## T: Time argument empty, use current time
431 timeInput
="$timeCurrent";
433 ## F: Time argument exists, validate time
434 if date --date="$argTime" 1>/dev
/null
2>&1; then
435 ### T: Time argument is valid; use it
436 timeInput
="$argTime";
438 ### F: Time argument not valid; exit
439 yell
"ERROR:Invalid time argument supplied. Exiting."; exit 1;
442 # Construct and deliver separator-les date string
443 timeCurrentShort
="$(date -d "$timeInput" +%Y%m%dT%H%M%S%z)";
444 echo "$timeCurrentShort";
445 } # Get YYYYmmddTHHMMSS±zzzz
447 # Desc: Set time zone environment variable TZ
448 # Usage: setTimeZoneEV arg1
450 # Input: arg1: 'date'-compatible timezone string (ex: "America/New_York")
451 # TZDIR env var (optional; default: "/usr/share/zoneinfo")
453 # exit code 0 on success
454 # exit code 1 on incorrect number of arguments
455 # exit code 2 if unable to validate arg1
456 # Depends: yell(), printenv 8.30, bash 5.0.3
457 # Tested on: Debian 10
458 local tzDir returnState argTimeZone
461 if ! [[ $# -eq 1 ]]; then
462 yell
"ERROR:Invalid argument count.";
466 # Read TZDIR env var if available
467 if printenv TZDIR
1>/dev
/null
2>&1; then
468 tzDir
="$(printenv TZDIR)";
470 tzDir
="/usr/share/zoneinfo";
474 if ! [[ -f "$tzDir"/"$argTimeZone" ]]; then
475 yell
"ERROR:Invalid time zone argument.";
478 # Export ARG1 as TZ environment variable
479 TZ
="$argTimeZone" && export TZ
&& returnState
="true";
482 # Determine function return code
483 if [ "$returnState" = "true" ]; then
486 } # Exports TZ environment variable
488 yell
"$scriptVersion"
489 } # Display script version.
491 # Desc: Given seconds, output ISO-8601 duration string
492 # Ref/Attrib: ISO-8601:2004(E), §4.4.4.2 Representations of time intervals by duration and context information
493 # Note: "1 month" ("P1M") is assumed to be "30 days" (see ISO-8601:2004(E), §2.2.1.2)
494 # Usage: timeDuration [1:seconds] ([2:precision])
496 # Input: arg1: seconds as base 10 integer >= 0 (ex: 3601)
497 # arg2: precision level (optional; default=2)
498 # Output: stdout: ISO-8601 duration string (ex: "P1H1S", "P2Y10M15DT10H30M20S")
499 # exit code 0: success
500 # exit code 1: error_input
501 # exit code 2: error_unknown
502 # Example: 'timeDuration 111111 3' yields 'P1DT6H51M'
503 # Depends: date 8, bash 5, yell,
504 local argSeconds argPrecision precision returnState remainder
505 local fullYears fullMonths fullDays fullHours fullMinutes fullSeconds
506 local hasYears hasMonths hasDays hasHours hasMinutes hasSeconds
507 local witherPrecision output
508 local displayYears displayMonths displayDays displayHours displayMinutes displaySeconds
510 argSeconds
="$1"; # read arg1 (seconds)
511 argPrecision
="$2"; # read arg2 (precision)
512 precision
=2; # set default precision
514 # Check that between one and two arguments is supplied
515 if ! { [[ $# -ge 1 ]] && [[ $# -le 2 ]]; }; then
516 yell
"ERROR:Invalid number of arguments:$# . Exiting.";
517 returnState
="error_input"; fi
519 # Check that argSeconds provided
520 if [[ $# -ge 1 ]]; then
521 ## Check that argSeconds is a positive integer
522 if [[ "$argSeconds" =~ ^
[[:digit
:]]+$
]]; then
525 yell
"ERROR:argSeconds not a digit.";
526 returnState
="error_input";
529 yell
"ERROR:No argument provided. Exiting.";
533 # Consider whether argPrecision was provided
534 if [[ $# -eq 2 ]]; then
535 # Check that argPrecision is a positive integer
536 if [[ "$argPrecision" =~ ^
[[:digit
:]]+$
]] && [[ "$argPrecision" -gt 0 ]]; then
537 precision
="$argPrecision";
539 yell
"ERROR:argPrecision not a positive integer. (is $argPrecision ). Leaving early.";
540 returnState
="error_input";
546 remainder
="$argSeconds" ; # seconds
547 ## Calculate full years Y, update remainder
548 fullYears
=$
(( remainder
/ (365*24*60*60) ));
549 remainder
=$
(( remainder
- (fullYears
*365*24*60*60) ));
550 ## Calculate full months M, update remainder
551 fullMonths
=$
(( remainder
/ (30*24*60*60) ));
552 remainder
=$
(( remainder
- (fullMonths
*30*24*60*60) ));
553 ## Calculate full days D, update remainder
554 fullDays
=$
(( remainder
/ (24*60*60) ));
555 remainder
=$
(( remainder
- (fullDays
*24*60*60) ));
556 ## Calculate full hours H, update remainder
557 fullHours
=$
(( remainder
/ (60*60) ));
558 remainder
=$
(( remainder
- (fullHours
*60*60) ));
559 ## Calculate full minutes M, update remainder
560 fullMinutes
=$
(( remainder
/ (60) ));
561 remainder
=$
(( remainder
- (fullMinutes
*60) ));
562 ## Calculate full seconds S, update remainder
563 fullSeconds
=$
(( remainder
/ (1) ));
564 remainder
=$
(( remainder
- (remainder
*1) ));
565 ## Check which fields filled
566 if [[ $fullYears -gt 0 ]]; then hasYears
="true"; else hasYears
="false"; fi
567 if [[ $fullMonths -gt 0 ]]; then hasMonths
="true"; else hasMonths
="false"; fi
568 if [[ $fullDays -gt 0 ]]; then hasDays
="true"; else hasDays
="false"; fi
569 if [[ $fullHours -gt 0 ]]; then hasHours
="true"; else hasHours
="false"; fi
570 if [[ $fullMinutes -gt 0 ]]; then hasMinutes
="true"; else hasMinutes
="false"; fi
571 if [[ $fullSeconds -gt 0 ]]; then hasSeconds
="true"; else hasSeconds
="false"; fi
573 ## Determine which fields to display (see ISO-8601:2004 §4.4.3.2)
574 witherPrecision
="false"
577 if $hasYears && [[ $precision -gt 0 ]]; then
579 witherPrecision
="true";
581 displayYears
="false";
583 if $witherPrecision; then ((precision--
)); fi;
586 if $hasMonths && [[ $precision -gt 0 ]]; then
587 displayMonths
="true";
588 witherPrecision
="true";
590 displayMonths
="false";
592 if $witherPrecision && [[ $precision -gt 0 ]]; then
593 displayMonths
="true";
595 if $witherPrecision; then ((precision--
)); fi;
598 if $hasDays && [[ $precision -gt 0 ]]; then
600 witherPrecision
="true";
604 if $witherPrecision && [[ $precision -gt 0 ]]; then
607 if $witherPrecision; then ((precision--
)); fi;
610 if $hasHours && [[ $precision -gt 0 ]]; then
612 witherPrecision
="true";
614 displayHours
="false";
616 if $witherPrecision && [[ $precision -gt 0 ]]; then
619 if $witherPrecision; then ((precision--
)); fi;
622 if $hasMinutes && [[ $precision -gt 0 ]]; then
623 displayMinutes
="true";
624 witherPrecision
="true";
626 displayMinutes
="false";
628 if $witherPrecision && [[ $precision -gt 0 ]]; then
629 displayMinutes
="true";
631 if $witherPrecision; then ((precision--
)); fi;
635 if $hasSeconds && [[ $precision -gt 0 ]]; then
636 displaySeconds
="true";
637 witherPrecision
="true";
639 displaySeconds
="false";
641 if $witherPrecision && [[ $precision -gt 0 ]]; then
642 displaySeconds
="true";
644 if $witherPrecision; then ((precision--
)); fi;
646 ## Determine whether or not the "T" separator is needed to separate date and time elements
647 if ( $displayHours ||
$displayMinutes ||
$displaySeconds); then
648 displayDateTime
="true"; else displayDateTime
="false"; fi
650 ## Construct duration output string
652 if $displayYears; then
653 output
=$output$fullYears"Y"; fi
654 if $displayMonths; then
655 output
=$output$fullMonths"M"; fi
656 if $displayDays; then
657 output
=$output$fullDays"D"; fi
658 if $displayDateTime; then
659 output
=$output"T"; fi
660 if $displayHours; then
661 output
=$output$fullHours"H"; fi
662 if $displayMinutes; then
663 output
=$output$fullMinutes"M"; fi
664 if $displaySeconds; then
665 output
=$output$fullSeconds"S"; fi
667 ## Output duration string to stdout
668 echo "$output" && returnState
="true";
670 #===Determine function return code===
671 if [ "$returnState" = "true" ]; then
673 elif [ "$returnState" = "error_input" ]; then
677 yell
"ERROR:Unknown";
681 } # Get duration (ex: PT10M4S )
683 # Desc: Report seconds until next day.
685 # Output: stdout: integer seconds until next day
686 # Output: exit code 0 if stdout > 0; 1 if stdout = 0; 2 if stdout < 0
687 # Usage: timeUntilNextDay
688 # Usage: if ! myTTL="$(timeUntilNextDay)"; then yell "ERROR in if statement"; exit 1; fi
689 # Depends: date 8, echo 8, yell, try
691 local returnState timeCurrent timeNextDay secondsUntilNextDay returnState
692 timeCurrent
="$(date --iso-8601=seconds)" ; # Produce `date`-parsable current timestamp with resolution of 1 second.
693 timeNextDay
="$(date -d "$timeCurrent next day
" --iso-8601=date)"; # Produce timestamp of beginning of tomorrow with resolution of 1 second.
694 secondsUntilNextDay
="$(( $(date +%s -d "$timeNextDay") - $(date +%s -d "$timeCurrent") ))" ; # Calculate seconds until closest future midnight (res. 1 second).
695 if [[ "$secondsUntilNextDay" -gt 0 ]]; then
697 elif [[ "$secondsUntilNextDay" -eq 0 ]]; then
698 returnState
="warning_zero";
699 yell
"WARNING:Reported time until next day exactly zero.";
700 elif [[ "$secondsUntilNextDay" -lt 0 ]]; then
701 returnState
="warning_negative";
702 yell
"WARNING:Reported time until next day is negative.";
705 try
echo "$secondsUntilNextDay"; # Report
707 # Determine function return code
708 if [[ "$returnState" = "true" ]]; then
710 elif [[ "$returnState" = "warning_zero" ]]; then
712 elif [[ "$returnState" = "warning_negative" ]]; then
715 } # Report seconds until next day
717 # Desc: Report seconds until next hour
719 # Output: stdout: integer seconds until next hour
720 # Output: exit code 0 if stdout > 0; 1 if stdout = 0; 2 if stdout < 0
721 # Usage: timeUntilNextHour
722 # Usage: if ! myTTL="$(timeUntilNextHour)"; then yell "ERROR in if statement"; exit 1; fi
724 local returnState timeCurrent timeNextHour secondsUntilNextHour
725 timeCurrent
="$(date --iso-8601=seconds)"; # Produce `date`-parsable current timestamp with resolution of 1 second.
726 timeNextHour
="$(date -d "$timeCurrent next hour
" --iso-8601=hours)"; # Produce `date`-parsable current time stamp with resolution of 1 second.
727 secondsUntilNextHour
="$(( $(date +%s -d "$timeNextHour") - $(date +%s -d "$timeCurrent") ))"; # Calculate seconds until next hour (res. 1 second).
728 if [[ "$secondsUntilNextHour" -gt 0 ]]; then
730 elif [[ "$secondsUntilNextHour" -eq 0 ]]; then
731 returnState
="warning_zero";
732 yell
"WARNING:Reported time until next hour exactly zero.";
733 elif [[ "$secondsUntilNextHour" -lt 0 ]]; then
734 returnState
="warning_negative";
735 yell
"WARNING:Reported time until next hour is negative.";
738 try
echo "$secondsUntilNextHour"; # Report
740 # Determine function return code
741 if [[ "$returnState" = "true" ]]; then
743 elif [[ "$returnState" = "warning_zero" ]]; then
745 elif [[ "$returnState" = "warning_negative" ]]; then
748 } # Report seconds until next hour
750 # Desc: Validates Input
751 # Usage: validateInput [str input] [str input type]
753 # Input: arg1: string to validate
754 # arg2: string specifying input type (ex:"ssh_pubkey")
755 # Output: return code 0: if input string matched specified string type
756 # Depends: bash 5, yell()
758 local fn argInput argType
766 if [[ $# -gt 2 ]]; then yell
"ERROR:$0:$fn:Too many arguments."; exit 1; fi;
769 if [[ -z "$argInput" ]]; then return 1; fi
773 ### Check for alnum/dash base64 (ex: "ssh-rsa AAAAB3NzaC1yc2EAAA")
774 if [[ "$argType" = "ssh_pubkey" ]]; then
775 if [[ "$argInput" =~ ^
[[:alnum
:]-]*[\
]*[[:alnum
:]+/=]*$
]]; then
779 ### Check for age1[:bech32:]
780 if [[ "$argType" = "age_pubkey" ]]; then
781 if [[ "$argInput" =~ ^age1
[qpzry9x8gf2tvdw0s3jn54khce6mua7l
]*$
]]; then
785 if [[ "$argType" = "integer" ]]; then
786 if [[ "$argInput" =~ ^
[[:digit
:]]*$
]]; then
789 ## time element (year, month, week, day, hour, minute, second)
790 if [[ "$argType" = "time_element" ]]; then
791 if [[ "$argInput" = "year" ]] || \
792 [[ "$argInput" = "month" ]] || \
793 [[ "$argInput" = "week" ]] || \
794 [[ "$argInput" = "day" ]] || \
795 [[ "$argInput" = "hour" ]] || \
796 [[ "$argInput" = "minute" ]] || \
797 [[ "$argInput" = "second" ]]; then
800 # Return error if no condition matched.
802 } # Validates strings
804 magicInitWorkingDir
() {
805 # Desc: Determine temporary working directory from defaults or user input
806 # Usage: magicInitWorkingDir
807 # Input: vars: optionTmpDir, argTempDirPriority, dirTmpDefault
808 # Input: vars: scriptTimeStart
809 # Output: vars: dir_tmp
810 # Depends: bash 5.0.3, processArguments(), vbm(), yell()
811 # Parse '-t' option (user-specified temporary working dir)
812 ## Set dir_tmp_parent to user-specified value if specified
813 local fn dir_tmp_parent
818 vbm
"STATUS:$fn:Starting magicInitWorkingDir() function.";
819 if [[ "$optionTmpDir" = "true" ]]; then
820 if [[ -d "$argTempDirPriority" ]]; then
821 dir_tmp_parent
="$argTempDirPriority";
823 yell
"WARNING:$fn:Specified temporary working directory not valid:$argTempDirPriority";
824 exit 1; # Exit since user requires a specific temp dir and it is not available.
827 ## Set dir_tmp_parent to default or fallback otherwise
828 if [[ -d "$dirTmpDefault" ]]; then
829 dir_tmp_parent
="$dirTmpDefault";
830 elif [[ -d /tmp
]]; then
831 yell
"WARNING:$fn:$dirTmpDefault not available. Falling back to /tmp .";
832 dir_tmp_parent
="/tmp";
834 yell
"ERROR:$fn:No valid working directory available. Exiting.";
838 ## Set dir_tmp using dir_tmp_parent and nonce (scriptTimeStart)
839 dir_tmp
="$dir_tmp_parent"/"$scriptTimeStart""..bkgpslog" && vbm
"DEBUG :$fn:Set dir_tmp to:$dir_tmp"; # Note: removed at end of main().
840 vbm
"STATUS:$fn:Finished magicInitWorkingDir() function.";
842 magicInitCheckTar
() {
843 # Desc: Initializes or checks output tar
844 # input: vars: dirOut, bufferTTL, cmd_encrypt_suffix, cmd_compress_suffix
845 # input: vars: scriptHostname
846 # output: vars: pathout_tar
847 # depends: Bash 5.0.3, vbm(), dateShort(), checkMakeTar(), magicWriteVersion()
848 local fn checkMakeTarES
853 vbm
"STATUS:$fn:Starting magicInitCheckTar() function.";
855 pathout_tar
="$dirOut"/"$(dateShort "$
(date --date="$bufferTTL seconds ago" --iso-8601=seconds
)")"..
"$scriptHostname""$label""$cmd_compress_suffix""$cmd_encrypt_suffix".
tar && \
856 vbm
"STATUS:$fn:Set pathout_tar to:$pathout_tar";
857 # Validate pathout_tar as tar.
858 checkMakeTar
"$pathout_tar"; checkMakeTarES
="$?";
859 ## Add VERSION file if checkMakeTar had to create a tar (exited 1) or replace one (exited 2)
860 vbm
"STATUS:$fn:exit status before magicWriteVersion:$checkMakeTarES"
861 if [[ "$checkMakeTarES" -eq 1 ]] ||
[[ "$checkMakeTarES" -eq 2 ]]; then magicWriteVersion
; fi
862 vbm
"STATUS:$fn:Finished magicInitCheckTar() function.";
863 } # Initialize tar, set pathout_tar
864 magicParseCompressionArg
() {
865 # Desc: Parses compression arguments specified by '-c' option
866 # Input: vars: optionCompress
867 # Output: cmd_compress, cmd_compress_suffix
868 # Depends: processArguments(), vbm(), checkapp(), gzip 1.9
874 vbm
"STATUS:$fn:Starting magicParseCompressionArg() function.";
875 if [[ "$optionCompress" = "true" ]]; then # Check if compression option active
876 if checkapp
gzip; then # Check if gzip available
877 cmd_compress
="gzip " && vbm
"STATUS:$fn:cmd_compress:$cmd_compress";
878 cmd_compress_suffix
=".gz" && vbm
"STATUS:$fn:cmd_compress_suffix:$cmd_compress_suffix";
880 yell
"ERROR:$fn:Compression enabled but \"gzip\" not found. Exiting."; exit 1;
883 cmd_compress
="tee /dev/null " && vbm
"STATUS:$fn:cmd_compress:$cmd_compress";
884 cmd_compress_suffix
="" && vbm
"STATUS:$fn:cmd_compress_suffix:$cmd_compress_suffix";
885 vbm
"DEBUG :$fn:Compression not enabled.";
887 vbm
"STATUS:$fn:Starting magicParseCompressionArg() function.";
888 } # Form compression cmd string and filename suffix
889 magicParseCustomTTL
() {
890 # Desc: Set user-specified TTLs for buffer and script
891 # Usage: magicParseCustomTTL
892 # Input: vars: argCustomBufferTTL (integer), argCustomScriptTTL_TE (string)
893 # Input: vars: optionCustomBufferTTL, optionCustomScriptTTL_TE
894 # Input: vars: bufferTTL (integer), scriptTTL_TE (string)
895 # Output: bufferTTL (integer), scriptTTL_TE (string)
896 # Depends: Bash 5.0.3, yell(), vbm(), validateInput(), showUsage()
902 vbm
"STATUS:$fn:Starting magicParseCustomTTL() function.";
903 # React to '-b, --buffer-ttl' option
904 if [[ "$optionCustomBufferTTL" = "true" ]]; then
905 ## T: Check if argCustomBufferTTL is an integer
906 if validateInput
"$argCustomBufferTTL" "integer"; then
907 ### T: argCustomBufferTTL is an integer
908 bufferTTL
="$argCustomBufferTTL" && vbm
"STATUS:$fn:Custom bufferTTL from -b:$bufferTTL";
910 ### F: argcustomBufferTTL is not an integer
911 yell
"ERROR:$fn:Invalid integer argument for custom buffer time-to-live."; showUsage
; exit 1;
913 ## F: do not change bufferTTL
916 # React to '-B, --script-ttl' option
917 if [[ "$optionCustomScriptTTL_TE" = "true" ]]; then
918 ## T: Check if argCustomScriptTTL is a time element (ex: "day", "hour")
919 if validateInput
"$argCustomScriptTTL_TE" "time_element"; then
920 ### T: argCustomScriptTTL is a time element
921 scriptTTL_TE
="$argCustomScriptTTL_TE" && vbm
"STATUS:$fn:Custom scriptTTL_TE from -B:$scriptTTL_TE";
923 ### F: argcustomScriptTTL is not a time element
924 yell
"ERROR:$fn:Invalid time element argument for custom script time-to-live."; showUsage
; exit 1;
926 ## F: do not change scriptTTL_TE
928 vbm
"STATUS:$fn:Starting magicParseCustomTTL() function.";
929 } # Sets custom script or buffer TTL if specified
931 # Desc: Parses -l option to set label
932 # In : optionLabel, argLabel
934 # Depends: Bash 5.0.3, vbm(), yell()
940 vbm
"STATUS:$fn:Started magicParseLabel() function.";
941 # Do nothing if optionLabel not set to true.
942 if [[ ! "$optionLabel" = "true" ]]; then
943 vbm
"STATUS:$fn:optionlabel not set to 'true'. Returning early.";
946 # Set label if optionLabel is true
947 if [[ "$optionLabel" = "true" ]]; then
948 label
="_""$argLabel";
949 vbm
"STATUS:$fn:Set label:$label";
951 vbm
"STATUS:$fn:Finished magicParseLabel() function.";
952 } # Set label used in output file name
953 magicParseProcessStrings
() {
954 # Desc: Processes user-supplied process strings into process commands for appendFileTar().
955 # Usage: magicParseProcessStrings
956 # In : vars: optionProcString optionNoStoreRaw optionStoreRaw argRawFileExt
957 # arry: argProcStrings, argProcFileExts
958 # Out: arry: procStrings, procFileExts
959 # Depends Bash 5.0.3, yell(), vbm()
965 vbm
"STATUS:$fn:Starting magicParseProcessStrings() function.";
966 vbm
"STATUS:$fn:var:optionProcString:$optionProcString";
967 vbm
"STATUS:$fn:var:optionNoStoreRaw:$optionNoStoreRaw";
968 vbm
"STATUS:$fn:var:optionStoreRaw:$optionStoreRaw";
969 vbm
"STATUS:$fn:var:argRawFileExt:$argRawFileExt";
970 vbm
"STATUS:$fn:ary:argProcStrings:${argProcStrings[*]}";
971 vbm
"STATUS:$fn:ary:argProcFileExts:${argProcFileExts[*]}"
973 ## Validate argRawFileExt
974 if [[ "$argRawFileExt" =~ ^
[.
][[:alnum
:]]*$
]]; then
975 rawFileExt
="$argRawFileExt" && \
976 vbm
"DEBUG :$fn:Set rawFileExt to \"$argRawFileExt\"";
978 vbm
"DEBUG :$fn:Validation failure for $argRawFileExt . Not set to rawFileExt.";
981 # Add default stdin output file entries for procStrings, procFileExts
982 ## Check if user specified that no raw stdin be saved.
983 if [[ ! "$optionNoStoreRaw" = "true" ]]; then
984 ### T: --no-store-raw not set. Store raw. Append procStrings with cat.
985 vbm
"DEBUG :$fn:--no-store-raw not set. Storing raw.";
986 #### Append procStrings array
987 procStrings
+=("cat ") && \
988 vbm
"DEBUG :$fn:Appended \"cat \" to procStrings";
989 vbm
"DEBUG :$fn:procStrings array:${procStrings[*]}";
990 #### Check if --store-raw set.
991 if [[ "$optionStoreRaw" = "true" ]]; then
992 ##### T: --store-raw set. Append procFileExts with user-specified file ext
993 vbm
"DEBUG :$fn:--store-raw set.";
994 procFileExts
+=("$rawFileExt") && \
995 vbm
"DEBUG :$fn:Appended $rawFileExt to procFileExts";
996 vbm
"STATUS:$fn:procFileExts array:${procFileExts[*]}";
998 ##### F: --store-raw not set. Append procFileExts with default ".stdin" file ext
999 ###### Append procFileExts array
1000 procFileExts
+=(".stdin") && \
1001 vbm
"DEBUG :$fn:Appended \".stdin\" to procFileExts";
1002 vbm
"STATUS:$fn:procFileExts array:${procFileExts[*]}";
1005 ### F: --no-store-raw set. Do not store raw.
1006 #### Do not append procStrings or procFileExts arrays.
1007 vbm
"STATUS:$fn:--no-store-raw set. Not storing raw.";
1008 vbm
"STATUS:$fn:procFileExts array:${procFileExts[*]}";
1011 # Do nothing more if optionProcString not set to true.
1012 if [[ ! "$optionProcString" = "true" ]]; then
1013 vbm
"STATUS:$fn:optionProcString not set to 'true'. Returning early.";
1015 # Validate input array indices
1016 ## Make sure that argProcStrings and argProcFileExts have same index counts
1017 if ! [[ "${#argProcStrings[@]}" -eq "${#argProcFileExts[@]}" ]]; then
1018 yell
"ERROR:$fn:Mismatch in number of elements in arrays argProcStrings and argProcFileExts:${#argProcStrings[@]} DNE ${#argProcFileExts[@]}";
1019 yell
"STATUS:$fn:argProcStrings:${argProcStrings[*]}"; yell
"STATUS:$fn:argProcFileExts:${argProcFileExts[*]}"; exit 1; fi;
1020 ## Make sure that no array elements are blank
1021 for element
in "${argProcStrings[@]}"; do
1022 if [[ -z "$element" ]]; then yell
"ERROR:$fn:Empty process string specified. Exiting."; exit 1; fi; done
1023 for element
in "${argProcFileExts[@]}"; do
1024 if [[ -z "$element" ]]; then yell
"ERROR:$fn:Empty output file extension specified. Exiting."; exit 1; fi; done
1025 ## Make sure that no process string starts with '-' (ex: if only one arg supplied after '-p' option)
1026 for element
in "${argProcStrings[@]}"; do
1027 if [[ "$element" =~ ^
[-][[:print
:]]*$
]] && [[ ! "$element" =~ ^
[[:print
:]]*$
]]; then
1028 yell
"ERROR:$fn:Illegal character '-' at start of process string element:\"$element\"";
1030 vbm
"STATUS:$fn:Quick check shows argProcStrings and argProcFileExts appear to have valid contents.";
1031 vbm
"STATUS:$fn:argProcStrings:${argProcStrings[*]}"
1032 vbm
"STATUS:$fn:argProcStrings:${argProcFileExts[*]}"
1033 procStrings
+=("${argProcStrings[@]}"); # Export process command strings
1034 procFileExts
+=("${argProcFileExts[@]}"); # Export process command strings
1035 vbm
"STATUS:$fn:Finished magicParseProcessStrings() function.";
1036 } # Validate and save process strings and file extensions to arrays procStrings, procFileExts
1037 magicParseRecipients
() {
1038 # Desc: Parses recipient arguments specified by '-r' or '-R' options
1039 # Usage: magicParseRecipients
1040 # In : vars: optionEncrypt, optionRecArg, optionRecDir
1041 # arry: argRecPubKeys (-r), argRecDir (-R)
1042 # Out: vars: cmd_encrypt, cmd_encrypt_suffix
1043 # Depends: head 8.30, checkapp(), checkAgePubkey(), validateInput()
1044 local fn recipients recipientDir recFileLine updateRecipients
1045 local -a recPubKeysValid candRecPubKeysValid
1047 # Save function name
1048 fn
="${FUNCNAME[0]}";
1049 vbm
"STATUS:$fn:Starting magicParseRecipients() function.";
1051 # Catch illegal option combinations
1052 ## Catch case if '-e' is set but neither '-r' nor '-R' is set
1053 if [[ "$optionEncrypt" = "true" ]] && \
1054 ! { [[ "$optionRecArg" = "true" ]] ||
[[ "$optionRecDir" = "true" ]]; }; then
1055 yell
"ERROR:$fn:\\'-e\\' set but no \\'-r\\' or \\'-R\\' set."; exit 1; fi;
1056 ## Catch case if '-r' or '-R' set but '-e' is not
1057 if [[ ! "$optionEncrypt" = "true" ]] && \
1058 { [[ "$optionRecArg" = "true" ]] ||
[[ "$optionRecDir" = "true" ]]; }; then
1059 yell
"ERROR:$fn:\\'-r\\' or \\'-R\\' set but \\'-e\\' is not set."; exit 1; fi;
1061 # Handle no encryption cases
1062 if [[ ! "$optionEncrypt" = "true" ]]; then
1063 cmd_encrypt
="cat " && vbm
"STATUS:$fn:cmd_encrypt:$cmd_encrypt";
1064 cmd_encrypt_suffix
="" && vbm
"STATUS:$fn:cmd_encrypt_suffix:$cmd_encrypt_suffix";
1065 vbm
"DEBUG :$fn:Encryption not enabled.";
1068 # Handle encryption cases
1069 ## Check age availability
1070 if ! checkapp age
; then yell
"ERROR:$fn:age not available. Exiting."; exit 1; fi
1071 ## Parse '-r' options: validate and append pubkeys from argRecPubKeys to recPubKeysValid
1072 if [[ "$optionRecArg" = "true" ]]; then
1073 for pubkey
in "${argRecPubKeys[@]}"; do # Validate recipient pubkey strings by forming test message
1074 vbm
"DEBUG :$fn:Testing pubkey string:$pubkey";
1075 if checkAgePubkey
"$pubkey" && \
1076 ( validateInput
"$pubkey" "ssh_pubkey" || validateInput
"$pubkey" "age_pubkey"); then
1077 #### Add validated pubkey to recPubKeysValid array
1078 recPubKeysValid
+=("$pubkey") && \
1079 vbm
"DEBUG :$fn:recPubkeysValid:pubkey added:$pubkey";
1081 yell
"ERROR:$fn:Exit code ""$?"". Invalid recipient pubkey string. Exiting."; exit 1;
1084 vbm
"STATUS:$fn:Finished processing argRecPubKeys array";
1085 vbm
"DEBUG :$fn:Array of validated pubkeys:${recPubKeysValid[*]}";
1087 ## Parse '-R' options: validate and append pubkeys in argRecDir to recPubKeysValid
1088 if [[ "$optionRecDir" = "true" ]]; then
1089 ### Check that argRecDir is a directory
1090 if [[ -d "$argRecDir" ]]; then
1091 recipientDir
="$argRecDir" && \
1092 vbm
"STATUS:$fn:Recipient watch directory detected:\"$recipientDir\"";
1093 #### Initialize variable indicating outcome of pubkey review
1094 unset updateRecipients
1095 #### Add existing recipients from '-r' option
1096 candRecPubKeysValid
=("${recPubKeysValid[@]}");
1097 #### Parse files in recipientDir
1098 for file in "$recipientDir"/*; do
1099 ##### Read first line of each file
1100 recFileLine
="$(head -n1 "$file")" && \
1101 vbm
"STATUS:$fn:Checking if pubkey:\"$recFileLine\"";
1102 ##### check if first line is a valid pubkey
1103 if checkAgePubkey
"$recFileLine" && \
1104 ( validateInput
"$recFileLine" "ssh_pubkey" || validateInput
"$recFileLine" "age_pubkey"); then
1105 ###### T: add candidate pubkey to candRecPubKeysValid
1106 candRecPubKeysValid
+=("$recFileLine") && \
1107 vbm
"STATUS:$fn:RecDir pubkey is valid pubkey:\"$recFileLine\"";
1109 ###### F: throw warning;
1110 yell
"ERROR:$fn:Invalid recipient file detected. Not modifying recipient list:$recFileLine";
1111 updateRecipients
="false";
1114 #### Write candRecPubKeysValid array to recPubKeysValid if no invalid key detected
1115 if ! [[ "$updateRecipients" = "false" ]]; then
1116 recPubKeysValid
=("${candRecPubKeysValid[@]}") && \
1117 vbm
"STATUS:$fn:Wrote candRecPubkeysValid to recPubKeysValid:\"${recPubKeysValid[*]}\"";
1122 ## Form age recipient string from recPubKeysValid
1123 for pubkey
in "${recPubKeysValid[@]}"; do
1124 recipients
="$recipients""-r '$pubkey' ";
1125 vbm
"STATUS:$fn:Added pubkey for forming age recipient string:""$pubkey";
1126 vbm
"DEBUG :$fn:recipients:""$recipients";
1129 ## Output cmd_encrypt, cmd_encrypt_suffix from recipients
1130 cmd_encrypt
="age ""$recipients " && vbm
"STATUS:$fn:cmd_encrypt:$cmd_encrypt";
1131 cmd_encrypt_suffix
=".age" && vbm
"STATUS:$fn:cmd_encrypt_suffix:$cmd_encrypt_suffix";
1133 vbm
"STATUS:$fn:Finished magicParseRecipients() function.";
1134 } # Sets cmd_encrypt, cmd_encrypt_suffix from -r, -R args
1135 magicSetScriptTTL
() {
1136 #Desc: Sets script_TTL seconds from provided time_element string argument
1137 #Usage: magicSetScriptTTL [str time_element]
1138 #Input: arg1: string (Ex: scriptTTL_TE; "day" or "hour")
1139 #Output: var: scriptTTL (integer seconds)
1140 #Depends: timeUntilNextHour, timeUntilNextDay
1141 local fn argTimeElement
1143 # Save function name
1144 fn
="${FUNCNAME[0]}";
1146 vbm
"STATUS:$fn:Starting magicSetScriptTTL() function.";
1147 argTimeElement
="$1";
1148 if [[ "$argTimeElement" = "day" ]]; then
1149 # Set script lifespan to end at start of next day
1150 if ! scriptTTL
="$(timeUntilNextDay)"; then # sets scriptTTL, then checks exit code
1151 if [[ "$scriptTTL" -eq 0 ]]; then
1152 ((scriptTTL
++)); # Add 1 because 0 would cause 'timeout' to never timeout.
1154 yell
"ERROR:$fn:timeUntilNextDay exit code $?"; exit 1;
1157 elif [[ "$argTimeElement" = "hour" ]]; then
1158 # Set script lifespan to end at start of next hour
1159 if ! scriptTTL
="$(timeUntilNextHour)"; then # sets scriptTTL, then checks exit code
1160 if [[ "$scriptTTL" -eq 0 ]]; then
1161 ((scriptTTL
++)); # Add 1 because 0 would cause 'timeout' to never timeout.
1163 yell
"ERROR:$fn:timeUntilNextHour exit code $?"; exit 1;
1167 yell
"ERROR:$fn:Invalid argument for setScriptTTL function:$argTimeElement"; exit 1;
1169 vbm
"STATUS:$fn:Finished magicSetScriptTTL() function.";
1170 } # Set scriptTTL in seconds until next (day|hour).
1171 magicWriteVersion
() {
1172 # Desc: Appends time-stamped VERSION to pathout_tar
1173 # Usage: magicWriteVersion
1174 # Input: vars: pathout_tar, dir_tmp
1175 # Input: vars: scriptVersion, scriptURL, ageVersion, ageURL, scriptHostname
1176 # Input: array: recPubKeysValid
1177 # Output: appends tar (pathout_tar)
1178 # Depends: bash 5.0.3, dateTimeShort(), appendArgTar()
1179 local fn fileoutVersion contentVersion pubKeyIndex pubKeyIndex
1181 # Save function name
1182 fn
="${FUNCNAME[0]}";
1184 vbm
"STATUS:$fn:Starting magicWriteVersion() function.";
1185 # Set VERSION file name
1186 fileoutVersion
="$(dateTimeShort)..VERSION";
1188 # Gather VERSION data in contentVersion
1189 contentVersion
="scriptVersion=$scriptVersion";
1190 #contentVersion="$contentVersion""\\n";
1191 contentVersion
="$contentVersion""\\n""scriptName=$scriptName";
1192 contentVersion
="$contentVersion""\\n""scriptURL=$scriptURL";
1193 contentVersion
="$contentVersion""\\n""ageVersion=$ageVersion";
1194 contentVersion
="$contentVersion""\\n""ageURL=$ageURL";
1195 contentVersion
="$contentVersion""\\n""date=$(date --iso-8601=seconds)";
1196 contentVersion
="$contentVersion""\\n""hostname=$scriptHostname";
1197 ## Add list of recipient pubkeys
1198 for pubkey
in "${recPubKeysValid[@]}"; do
1200 contentVersion
="$contentVersion""\\n""PUBKEY_$pubKeyIndex=$pubkey";
1202 ## Process newline escapes
1203 contentVersion
="$(echo -e "$contentVersion")"
1205 # Write contentVersion as file fileoutVersion and write-append to pathout_tar
1206 appendArgTar
"$contentVersion" "$fileoutVersion" "$pathout_tar" "$dir_tmp" && \
1207 vbm
"STATUS:$fn:Appended $fileoutVersion to $pathout_tar";
1208 vbm
"STATUS:$fn:Finished magicWriteVersion() function.";
1209 } # write version data to pathout_tar via appendArgTar()
1210 magicProcessWriteBuffer
() {
1211 # Desc: process and write buffer
1212 # In : vars: bufferTTL bufferTTL_STR scriptHostname label dir_tmp SECONDS
1214 # Out: file:(pathout_tar)
1215 # Depends: Bash 5.0.3, date 8.30, yell(), vbm(), dateTimeShort(),
1216 ### Note: These arrays should all have the same number of elements:
1217 ### pathouts, fileouts, procFileExts, procStrings
1219 local fn timeBufferStartLong timeBufferStart fileoutBasename
1220 local -a fileouts pathouts
1221 local writeCmd1 writeCmd2 writeCmd3 writeCmd4
1223 # Debug:Get function name
1224 fn
="${FUNCNAME[0]}";
1226 vbm
"STATUS:$fn:Started magicProcessWriteBuffer().";
1227 vbm
"DEBUG :$fn:buffer array element count:${#buffer[@]}";
1228 vbm
"DEBUG :$fn:buffer array first element:${buffer[0]}";
1229 vbm
"DEBUG :$fn:buffer array last element :${buffer[-1]}";
1231 # Determine file paths (time is start of buffer period)
1232 ## Calculate start time
1233 timeBufferStartLong
="$(date --date="$bufferTTL seconds ago
" --iso-8601=seconds)" && \
1234 vbm
"DEBUG :$fn:timeBufferStartLong:$timeBufferStartLong";
1235 timeBufferStart
="$(dateTimeShort "$timeBufferStartLong" )" && \
1236 vbm
"DEBUG :$fn:timeBufferStart:$timeBufferStart"; # Note start time YYYYmmddTHHMMSS+zzzz (no separators)
1237 ## Set common basename
1238 fileoutBasename
="$timeBufferStart""--""$bufferTTL_STR""..""$scriptHostname""$label" && \
1239 vbm
"STATUS:$fn:Set fileoutBasename to:$fileoutBasename";
1240 ## Determine output file name array
1241 ### in: fileOutBasename cmd_compress_suffix cmd_encrypt_suffix procFileExts
1242 for fileExt
in "${procFileExts[@]}"; do
1243 fileouts
+=("$fileoutBasename""$fileExt""$cmd_compress_suffix""$cmd_encrypt_suffix") && \
1244 vbm
"STATUS:$fn:Added $fileExt to fileouts:${fileouts[*]}";
1246 for fileName
in "${fileouts[@]}"; do
1247 pathouts
+=("$dir_tmp"/"$fileName") && \
1248 vbm
"STATUS:$fn:Added $fileName to pathouts:${pathouts[*]}";
1250 ## Update pathout_tar
1253 # Process and write buffers to dir_tmp
1254 ## Prepare command strings
1255 writeCmd1
="printf \"%s\\\\n\" \"\${buffer[@]}\""; # printf "%s\\n" "${buffer[@]}"
1256 #writeCmd2="" # NOTE: Specified by parsing array procStrings
1257 writeCmd3
="$cmd_compress";
1258 writeCmd4
="$cmd_encrypt";
1260 ## Process buffer and write to dir_tmp
1261 vbm
"DEBUG :$fn:fileouts element count:${#fileouts[@]}";
1262 vbm
"DEBUG :$fn:pathouts element count:${#pathouts[@]}";
1263 vbm
"DEBUG :$fn:procStrings element count:${#pathouts[@]}";
1264 vbm
"DEBUG :$fn:fileouts contents:${fileouts[*]}";
1265 vbm
"DEBUG :$fn:pathouts contents:${pathouts[*]}";
1266 vbm
"DEBUG :$fn:procStrings contents:${pathouts[*]}";
1267 for index
in "${!pathouts[@]}"; do
1268 writeCmd2
="${procStrings[$index]}";
1269 writeCmdAll
="$writeCmd1 | $writeCmd2 | $writeCmd3 | $writeCmd4" && vbm
"STATUS:$fn:Assembled command:\"$writeCmdAll\"";
1270 eval "$writeCmd1 | $writeCmd2 | $writeCmd3 | $writeCmd4" > "${pathouts[$index]}" && vbm
"STATUS:$fn:Wrote command output to ${pathouts[$index]}";
1273 # Append dir_tmp files to pathout_tar
1274 wait; # Wait to avoid collision with older magicProcessWriteBuffer() instances (see https://www.tldp.org/LDP/abs/html/x9644.html )
1275 for index
in "${!pathouts[@]}"; do
1276 tar --apend --directory="$dir_tmp" --file="$pathout_tar" "${fileouts[$index]}" && \
1277 vbm
"STATUS:$fn:Appended ${pathouts[$index]} to $pathout_tar";
1278 #appendFileTar "${pathouts[$index]}" "${fileouts[$index]}" "$pathout_tar" "$dir_tmp" && \
1281 # Remove secured chunks from dir_tmp
1282 for path
in "${pathouts[@]}"; do
1283 rm "$path" && vbm
"STATUS:$fn:Removed:$path";
1286 vbm
"STATUS:$fn:Finished magicProcessWriteBuffer().";
1287 } # Process and Write buffer
1291 cmd | bklog [ options ]
1295 Display help information.
1297 Display script version.
1299 Display debugging info.
1302 -r, --recipient [ string pubkey ]
1303 Specify recipient. May be age or ssh pubkey.
1304 May be specified multiple times for multiple pubkeys.
1305 See https://github.com/FiloSottile/age
1306 -o, --output [ path dir ]
1307 Specify output directory to save logs. This option is required
1309 -p, --process-string [ filter command ] [ output file extension]
1310 Specify how to create and name a processed version of the stdin.
1311 For example, if stdin is 'nmea' location data:
1313 -p "gpsbabel -i nmea -f - -o gpx -F - " ".gpx"
1315 This option would cause the stdin to 'bklog' to be piped into
1316 the 'gpsbabel' command, interpreted as 'nmea' data, converted
1317 into 'gpx' format, and then appended to the output tar file
1318 as a file with a '.gpx' extension.
1319 This option may be specified multiple times in order to output
1320 results of multiple different processing methods.
1321 -l, --label [ string ]
1322 Specify a label to be included in all output file names.
1323 Ex: 'location' if stdin is location data.
1324 -w, --store-raw [ file extension ]
1325 Specify file extension of file within output tar that contains
1326 raw stdin data. The default behavior is to always save raw stdin
1327 data in a '.stdin' file. Example usage when 'bklog' receives
1328 'nmea' data from 'gpspipe -r':
1332 Stdin data is saved in a '.nmea' file within the output tar.
1334 Do not store raw stdin in output tar.
1336 Compress output with gzip (before encryption if enabled).
1338 Specify time zone. (ex: "America/New_York")
1339 -t, --temp-dir [path dir]
1340 Specify parent directory for temporary working directory.
1342 -R, --recipient-dir [path dir]
1343 Specify directory containing files whose first lines are
1344 to be interpreted as pubkey strings (see '-r' option). Only
1345 one directory may be specified.
1346 -b, --buffer-ttl [integer]
1347 Specify custom buffer period in seconds (default: 300 seconds)
1348 -B, --script-ttl [time element string]
1349 Specify custom script time-to-live in seconds (default: "day")
1350 Valid values: "day", "hour"
1352 EXAMPLE: (bash script lines)
1353 $ gpspipe -r | /bin/bash bklog -v -e -c -z "UTC" -t "/dev/shm" \
1354 -r age1mrmfnwhtlprn4jquex0ukmwcm7y2nxlphuzgsgv8ew2k9mewy3rs8u7su5 \
1355 -r age1ala848kqrvxc88rzaauc6vc5v0fqrvef9dxyk79m0vjea3hagclswu0lgq \
1356 -R ~/.config/bklog/recipients -w ".nmea" -b 300 -B "day" \
1357 -o ~/Sync/Logs -l "location" \
1358 -p "gpsbabel -i nmea -f - -o gpx -F - " ".gpx" \
1359 -p "gpsbabel -i nmea -f - -o kml -F - " ".kml"
1361 } # Display information on how to use this script.
1364 # Desc: Main function
1367 # Outputs: file (pathout_tar)
1371 # Debug:Get function name
1372 fn
="${FUNCNAME[0]}";
1374 vbm
"STATUS:$fn:Started function main().";
1376 processArguments
"$@";
1377 ## Determine working directory
1378 magicInitWorkingDir
; # Sets dir_tmp from argTempDirPriority
1379 ## Set output encryption and compression option strings
1380 ### React to "-e", "-r", and "-R" (encryption recipients) options
1381 magicParseRecipients
; # Update cmd_encrypt, cmd_encrypt_suffix
1382 ### React to "-c" ("compression") option
1383 magicParseCompressionArg
; # Updates cmd_compress[_suffix]
1384 ## React to "-b" and "-B" (custom buffer and script TTL) options
1385 magicParseCustomTTL
; # Sets custom scriptTTL_TE and/or bufferTTL if specified
1386 ## React to "-p" (user-supplied process command and file extension strings) options
1387 magicParseProcessStrings
; # Sets arrays: procStrings, procFileExts
1388 ## React to "-l" (output file label) option
1389 magicParseLabel
; # sets label (ex: "_location")
1391 # Perform secondary setup operations
1392 ## Set script lifespan (scriptTTL from scriptTTL_TE)
1393 magicSetScriptTTL
"$scriptTTL_TE";
1394 ## File name substring (ISO-8601 duration from bufferTTL)
1395 bufferTTL_STR
="$(timeDuration "$bufferTTL")" && vbm
"DEBUG :$fn:bufferTTL_STR:$bufferTTL_STR";
1396 ## Init temp working dir
1397 try mkdir
"$dir_tmp" && vbm
"DEBUG :$fn:Working dir created at dir_tmp:$dir_tmp";
1398 ## Initialize output tar (set pathout_tar)
1400 ## Append VERSION file to tar
1403 # Check vital apps, files, dirs
1404 if ! checkapp
tar && ! checkdir
"$dirOut" "dir_tmp"; then
1405 yell
"ERROR:$fn:Critical components missing.";
1406 displayMissing
; yell
"Exiting."; exit 1; fi
1408 # MAIN LOOP: Run until script TTL seconds pass
1410 while [[ $SECONDS -lt "scriptTTL" ]]; do
1411 vbm
"STATUS:$fn:Starting buffer round:$bufferRound";
1412 bufferTOD
="$((SECONDS + bufferTTL))"; # Set buffer round time-of-death
1413 # Consume stdin to fill buffer until buffer time-of-death (TOD) arrives
1414 while read -r -t "$bufferTTL" line
&& [[ $SECONDS -lt "$bufferTOD" ]]; do
1415 # Append line to buffer array
1418 # Create dir_tmp if missing
1419 if ! [[ -d "$dir_tmp" ]]; then
1420 yell
"ERROR:$fn:dir_tmp existence failure:$dir_tmp";
1421 try mkdir
"$dir_tmp" && vbm
"DEBUG :$fn:Working dir recreated dir_tmp:$dir_tmp"; fi
1422 # Update cmd_encrypt, cmd_encrypt_suffix
1423 magicParseRecipients
;
1424 # Export buffer to asynchronous processing.
1425 magicProcessWriteBuffer
&
1426 unset buffer
; # Clear buffer array for next bufferRound
1427 # Increment buffer round
1433 try
rm -r "$dir_tmp" && vbm
"STATUS:$fn:Removed dir_tmp:$dir_tmp";
1435 vbm
"STATUS:$fn:Finished function main().";
1438 #===END Declare local script functions===
1439 #==END Define script parameters==
1441 #==BEGIN Perform work and exit==
1442 main
"$@" # Run main function.
1444 #==END Perform work and exit==
1446 # Author: Steven Baltakatei Sandoval;