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