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