3 # Desc: Records gps data until midnight
4 # Author: Steven Baltakatei Sandoval; License: GPLv3+
5 # Usage: bkgpslog -o [output dir]
7 #==BEGIN Define script parameters==
8 ## Logging Behavior parameters
9 BUFFER_TTL
="300"; # time between file writes
10 SCRIPT_TTL_TE
="day"; # (day|hour)
11 #### TZ="UTC"; export TZ; # Default time zone; overridden by '--time-zone=[str]' option
12 DIR_TMP_DEFAULT
="/dev/shm"; # Default parent of working directory
14 SCRIPT_TIME_START
=$
(date +%Y
%m
%dT
%H
%M
%S.
%N
);
15 PATH
="$HOME/.local/bin:$PATH"; # Add "$(systemd-path user-binaries)" path in case apps saved there
16 SCRIPT_HOSTNAME
=$
(hostname
); # Save hostname of system running this script.
17 SCRIPT_VERSION
="0.4.4-alpha"; # Define version of script.
18 SCRIPT_NAME
="bkgpslog"; # Define basename of script file.
19 SCRIPT_URL
="https://gitlab.com/baltakatei/ninfacyzga-01"; # Define wesite hosting this script.
20 AGE_VERSION
="1.0.0-beta2"; # Define version of age (encryption program)
21 AGE_URL
="https://github.com/FiloSottile/age/releases/tag/v1.0.0-beta2"; # Define website hosting age.
23 declare -Ag appRollCall
# Associative array for storing app status
24 declare -Ag fileRollCall
# Associative array for storing file status
25 declare -Ag dirRollCall
# Associative array for storing dir status
26 declare -a argRecPubKeys
# for processArguments function
27 # declare -a errorHistory # for correcting buffer lag
29 ## Initialize variables
30 OPTION_VERBOSE
=""; OPTION_ENCRYPT
=""; OPTION_COMPRESS
=""; OPTION_TMPDIR
="";
31 errResetx10e3
=0; BUFFER_TTL_ADJ_FLOAT
="";
32 ### PID Control factors
33 K_P
=10; # Gain for compensating buffer round lag
34 T_I
=2; # Consider this number of past buffer rounds to eliminate error
35 T_D
=1; # Predict value this number of buffer rounds into the future
37 #===BEGIN Declare local script functions===
39 # Desc: If arg is a command, save result in assoc array 'appRollCall'
40 # Usage: checkapp arg1 arg2 arg3 ...
41 # Input: global assoc. array 'appRollCall'
42 # Output: adds/updates key(value) to global assoc array 'appRollCall'
44 #echo "DEBUG:$(date +%S.%N)..Starting checkapp function."
45 #echo "DEBUG:args: $@"
46 #echo "DEBUG:returnState:$returnState"
50 #echo "DEBUG:processing arg:$arg"
51 if command -v "$arg" 1>/dev
/null
2>&1; then # Check if arg is a valid command
52 appRollCall
[$arg]="true";
53 #echo "DEBUG:appRollCall[$arg]:"${appRollCall[$arg]}
54 if ! [ "$returnState" = "false" ]; then returnState
="true"; fi
56 appRollCall
[$arg]="false"; returnState
="false";
60 #for key in "${!appRollCall[@]}"; do echo "DEBUG:$key => ${appRollCall[$key]}"; done
61 #echo "DEBUG:evaluating returnstate. returnState:"$returnState
63 #===Determine function return code===
64 if [ "$returnState" = "true" ]; then
65 #echo "DEBUG:checkapp returns true for $arg";
68 #echo "DEBUG:checkapp returns false for $arg";
71 } # Check that app exists
73 # Desc: If arg is a file path, save result in assoc array 'fileRollCall'
74 # Usage: checkfile arg1 arg2 arg3 ...
75 # Input: global assoc. array 'fileRollCall'
76 # Output: adds/updates key(value) to global assoc array 'fileRollCall';
77 # Output: returns 0 if app found, 1 otherwise
82 #echo "DEBUG:processing arg:$arg"
83 if [ -f "$arg" ]; then
84 fileRollCall
["$arg"]="true";
85 #echo "DEBUG:fileRollCall[\"$arg\"]:"${fileRollCall["$arg"]}
86 if ! [ "$returnState" = "false" ]; then returnState
="true"; fi
88 fileRollCall
["$arg"]="false"; returnState
="false";
92 #for key in "${!fileRollCall[@]}"; do echo "DEBUG:fileRollCall key [$key] is:${fileRollCall[$key]}"; done
93 #echo "DEBUG:evaluating returnstate. returnState:"$returnState
95 #===Determine function return code===
96 if [ "$returnState" = "true" ]; then
97 #echo "DEBUG:checkapp returns true for $arg";
100 #echo "DEBUG:checkapp returns false for $arg";
103 } # Check that file exists
105 # Desc: If arg is a dir path, save result in assoc array 'dirRollCall'
106 # Usage: checkdir arg1 arg2 arg3 ...
107 # Input: global assoc. array 'dirRollCall'
108 # Output: adds/updates key(value) to global assoc array 'dirRollCall';
109 # Output: returns 0 if app found, 1 otherwise
114 #echo "DEBUG:processing arg:$arg"
115 if [ -d "$arg" ]; then
116 dirRollCall
["$arg"]="true";
117 #echo "DEBUG:dirRollCall[\"$arg\"]:"${dirRollCall["$arg"]}
118 if ! [ "$returnState" = "false" ]; then returnState
="true"; fi
119 elif [ "$arg" = "" ]; then
120 dirRollCall
["$arg"]="false"; returnState
="false";
126 #for key in "${!dirRollCall[@]}"; do echo "DEBUG:dirRollCall key [$key] is:${dirRollCall[$key]}"; done
127 #echo "DEBUG:evaluating returnstate. returnState:"$returnState
129 #===Determine function return code===
130 if [ "$returnState" = "true" ]; then
131 #echo "DEBUG:checkapp returns true for $arg";
134 #echo "DEBUG:checkapp returns false for $arg";
137 } # Check that dir exists
139 # Yell, Die, Try Three-Fingered Claw technique
140 # Ref/Attrib: https://stackoverflow.com/a/25515370
141 yell
() { echo "$0: $*" >&2; }
142 die
() { yell
"$*"; exit 111; }
143 try
() { "$@" || die
"cannot $*"; }
146 echo "$@" 1>&2; # Define stderr echo function.
147 } # Define stderr message function.
150 echoerr
" bkgpslog [ options ]"
153 echoerr
" -h, --help"
154 echoerr
" Display help information."
156 echoerr
" Display script version."
157 echoerr
" -v, --verbose"
158 echoerr
" Display debugging info."
159 echoerr
" -e, --encrypt"
160 echoerr
" Encrypt output."
161 echoerr
" -r, --recipient [ string pubkey ]"
162 echoerr
" Specify recipient. May be age or ssh pubkey."
163 echoerr
" May be specified multiple times for multiple pubkeys."
164 echoerr
" See https://github.com/FiloSottile/age"
165 echoerr
" -o, --output [ path dir ]"
166 echoerr
" Specify output directory to save logs."
167 echoerr
" -c, --compress"
168 echoerr
" Compress output with gzip (before encryption if enabled)."
169 echoerr
" -z, --time-zone"
170 echoerr
" Specify time zone. (ex: \"America/New_York\")"
171 echoerr
" -t, --temp-dir [path dir]"
172 echoerr
" Specify parent directory for temporary working directory."
173 echoerr
" Default: \"/dev/shm\""
174 echoerr
" -R, --recipient-dir [path dir]"
175 echoerr
" Specify directory containing files whose first lines are"
176 echoerr
" to be interpreted as pubkey strings (see \\'-r\\' option)."
177 echoerr
" -b, --buffer-ttl [integer]"
178 echoerr
" Specify custom buffer period in seconds (default: 300 seconds)"
179 echoerr
" -B, --script-ttl [integer]"
180 echoerr
" Specify custom script time-to-live in seconds (default: \"day\")"
182 echoerr
"EXAMPLE: (bash script lines)"
183 echoerr
"/bin/bash bkgpslog -v -e -c \\"
184 echoerr
"-z \"UTC\" -t \"/dev/shm\" \\"
185 echoerr
"-r age1mrmfnwhtlprn4jquex0ukmwcm7y2nxlphuzgsgv8ew2k9mewy3rs8u7su5 \\"
186 echoerr
"-r age1ala848kqrvxc88rzaauc6vc5v0fqrvef9dxyk79m0vjea3hagclswu0lgq \\"
187 echoerr
"-o ~/Sync/Location"
188 } # Display information on how to use this script.
190 echoerr
"$SCRIPT_VERSION"
191 } # Display script version.
193 # Usage: vbm "DEBUG:verbose message here"
194 # Description: Prints verbose message ("vbm") to stderr if OPTION_VERBOSE is set to "true".
196 # - OPTION_VERBOSE variable set by processArguments function. (ex: "true", "false")
197 # - "$@" positional arguments fed to this function.
199 # Script function dependencies: echoerr
200 # External function dependencies: echo
201 # Last modified: 2020-04-11T23:57Z
202 # Last modified by: Steven Baltakatei Sandoval
206 if [ "$OPTION_VERBOSE" = "true" ]; then
207 FUNCTION_TIME
=$
(date --iso-8601=ns
); # Save current time in nano seconds.
208 echoerr
"[$FUNCTION_TIME] ""$*"; # Display argument text.
212 return 0; # Function finished.
213 } # Verbose message display function.
215 while [ ! $# -eq 0 ]; do # While number of arguments ($#) is not (!) equal to (-eq) zero (0).
216 #echoerr "DEBUG:Starting processArguments while loop."
217 #echoerr "DEBUG:Provided arguments are:""$*"
219 -h |
--help) showUsage
; exit 1;; # Display usage.
220 --version) showVersion
; exit 1;; # Show version
221 -v |
--verbose) OPTION_VERBOSE
="true"; vbm
"DEBUG:Verbose mode enabled.";; # Enable verbose mode.
222 -o |
--output) if [ -d "$2" ]; then DIR_OUT
="$2"; vbm
"DEBUG:DIR_OUT:$DIR_OUT"; shift; fi ;; # Define output directory.
223 -e |
--encrypt) OPTION_ENCRYPT
="true"; vbm
"DEBUG:Encrypted output mode enabled.";; # Enable encryption
224 -r |
--recipient) OPTION_RECIPIENTS
="true"; argRecPubKeys
+=("$2"); vbm
"STATUS:pubkey added:""$2"; shift;; # Add recipients
225 -c |
--compress) OPTION_COMPRESS
="true"; vbm
"DEBUG:Compressed output mode enabled.";; # Enable compression
226 -z |
--time-zone) try setTimeZoneEV
"$2"; shift;; # Set timestamp timezone
227 -t |
--temp-dir) OPTION_TMPDIR
="true" && argTempDirPriority
="$2"; shift;; # Set time zone
228 -R |
--recipient-dir) OPTION_RECIPIENTS
="true"; OPTION_RECDIR
="true" && argRecDir
="$2"; shift;; # Add recipient watch dir
229 -b |
--buffer-ttl) OPTION_CUSTOM_BUFFERTTL
="true" && argCustomBufferTTL
="$2"; shift;; # Set custom buffer period (default: 300 seconds)
230 -B |
--script-ttl) OPTION_CUSTOM_SCRIPTTTL_TE
="true" && argCustomScriptTTL
="$2"; shift;; # Set custom script TTL (default: "day")
231 *) echoerr
"ERROR: Unrecognized argument: $1"; echoerr
"STATUS:All arguments:$*"; exit 1;; # Handle unrecognized options.
235 } # Argument Processing
237 # Desc: Set time zone environment variable TZ
238 # Usage: setTimeZoneEV arg1
239 # Input: arg1: 'date'-compatible timezone string (ex: "America/New_York")
240 # TZDIR env var (optional; default: "/usr/share/zoneinfo")
242 # exit code 0 on success
243 # exit code 1 on incorrect number of arguments
244 # exit code 2 if unable to validate arg1
245 # Depends: yell, printenv, bash 5
246 # Tested on: Debian 10
248 local tzDir returnState
249 if ! [[ $# -eq 1 ]]; then
250 yell
"ERROR:Invalid argument count.";
254 # Read TZDIR env var if available
255 if printenv TZDIR
1>/dev
/null
2>&1; then
256 tzDir
="$(printenv TZDIR)";
258 tzDir
="/usr/share/zoneinfo";
262 if ! [[ -f "$tzDir"/"$ARG1" ]]; then
263 yell
"ERROR:Invalid time zone argument.";
266 # Export ARG1 as TZ environment variable
267 TZ
="$ARG1" && export TZ
&& returnState
="true";
270 # Determine function return code
271 if [ "$returnState" = "true" ]; then
274 } # Exports TZ environment variable
276 # Desc: Report seconds until next day.
278 # Output: stdout: integer seconds until next day
279 # Output: exit code 0 if stdout > 0; 1 if stdout = 0; 2 if stdout < 0
280 # Usage: timeUntilNextDay
281 # Usage: if ! myTTL="$(timeUntilNextDay)"; then yell "ERROR in if statement"; exit 1; fi
282 # Depends: date 8, echo 8, yell, try
284 local returnState TIME_CURRENT TIME_NEXT_DAY SECONDS_UNTIL_NEXT_DAY
286 TIME_CURRENT
="$(date --iso-8601=seconds)" ; # Produce `date`-parsable current timestamp with resolution of 1 second.
287 TIME_NEXT_DAY
="$(date -d "$TIME_CURRENT next day
" --iso-8601=date)"; # Produce timestamp of beginning of tomorrow with resolution of 1 second.
288 SECONDS_UNTIL_NEXT_DAY
="$(( $(date +%s -d "$TIME_NEXT_DAY") - $(date +%s -d "$TIME_CURRENT") ))" ; # Calculate seconds until closest future midnight (res. 1 second).
289 if [[ "$SECONDS_UNTIL_NEXT_DAY" -gt 0 ]]; then
291 elif [[ "$SECONDS_UNTIL_NEXT_DAY" -eq 0 ]]; then
292 returnState
="warning_zero";
293 yell
"WARNING:Reported time until next day exactly zero.";
294 elif [[ "$SECONDS_UNTIL_NEXT_DAY" -lt 0 ]]; then
295 returnState
="warning_negative";
296 yell
"WARNING:Reported time until next day is negative.";
299 try
echo "$SECONDS_UNTIL_NEXT_DAY"; # Report
301 # Determine function return code
302 if [[ "$returnState" = "true" ]]; then
304 elif [[ "$returnState" = "warning_zero" ]]; then
306 elif [[ "$returnState" = "warning_negative" ]]; then
309 } # Report seconds until next day
311 # Desc: Report seconds until next hour
313 # Output: stdout: integer seconds until next hour
314 # Output: exit code 0 if stdout > 0; 1 if stdout = 0; 2 if stdout < 0
315 # Usage: timeUntilNextHour
316 # Usage: if ! myTTL="$(timeUntilNextHour)"; then yell "ERROR in if statement"; exit 1; fi
318 local returnState TIME_CURRENT TIME_NEXT_HOUR SECONDS_UNTIL_NEXT_HOUR
319 TIME_CURRENT
="$(date --iso-8601=seconds)"; # Produce `date`-parsable current timestamp with resolution of 1 second.
320 TIME_NEXT_HOUR
="$(date -d "$TIME_CURRENT next hour
" --iso-8601=hours)"; # Produce `date`-parsable current time stamp with resolution of 1 second.
321 SECONDS_UNTIL_NEXT_HOUR
="$(( $(date +%s -d "$TIME_NEXT_HOUR") - $(date +%s -d "$TIME_CURRENT") ))"; # Calculate seconds until next hour (res. 1 second).
322 if [[ "$SECONDS_UNTIL_NEXT_HOUR" -gt 0 ]]; then
324 elif [[ "$SECONDS_UNTIL_NEXT_HOUR" -eq 0 ]]; then
325 returnState
="warning_zero";
326 yell
"WARNING:Reported time until next hour exactly zero.";
327 elif [[ "$SECONDS_UNTIL_NEXT_HOUR" -lt 0 ]]; then
328 returnState
="warning_negative";
329 yell
"WARNING:Reported time until next hour is negative.";
332 try
echo "$SECONDS_UNTIL_NEXT_HOUR"; # Report
334 # Determine function return code
335 if [[ "$returnState" = "true" ]]; then
337 elif [[ "$returnState" = "warning_zero" ]]; then
339 elif [[ "$returnState" = "warning_negative" ]]; then
342 } # Report seconds until next hour
344 # Desc: Timestamp without separators (YYYYmmddTHHMMSS+zzzz)
345 # Usage: dateTimeShort ([str date])
347 # Input: arg1: 'date'-parsable timestamp string (optional)
348 # Output: stdout: timestamp (ISO-8601, no separators)
350 local TIME_CURRENT TIME_CURRENT_SHORT
354 TIME_CURRENT
="$(date --iso-8601=seconds)" ; # Produce `date`-parsable current timestamp with resolution of 1 second.
355 # Decide to parse current or supplied date
356 ## Check if time argument empty
357 if [[ -z "$argTime" ]]; then
358 ## T: Time argument empty, use current time
359 TIME_INPUT
="$TIME_CURRENT";
361 ## F: Time argument exists, validate time
362 if date --date="$argTime" 1>/dev
/null
2>&1; then
363 ### T: Time argument is valid; use it
364 TIME_INPUT
="$argTime";
366 ### F: Time argument not valid; exit
367 yell
"ERROR:Invalid time argument supplied. Exiting."; exit 1;
370 # Construct and deliver separator-les date string
371 TIME_CURRENT_SHORT
="$(date -d "$TIME_INPUT" +%Y%m%dT%H%M%S%z)";
372 echo "$TIME_CURRENT_SHORT";
373 } # Get YYYYmmddTHHMMSS±zzzz
375 # Desc: Date without separators (YYYYmmdd)
376 # Usage: dateShort ([str date])
378 # Input: arg1: 'date'-parsable timestamp string (optional)
379 # Output: stdout: date (ISO-8601, no separators)
381 local TIME_CURRENT DATE_CURRENT_SHORT
385 TIME_CURRENT
="$(date --iso-8601=seconds)" ; # Produce `date`-parsable current timestamp with resolution of 1 second.
386 # Decide to parse current or supplied date
387 ## Check if time argument empty
388 if [[ -z "$argTime" ]]; then
389 ## T: Time argument empty, use current time
390 TIME_INPUT
="$TIME_CURRENT";
392 ## F: Time argument exists, validate time
393 if date --date="$argTime" 1>/dev
/null
2>&1; then
394 ### T: Time argument is valid; use it
395 TIME_INPUT
="$argTime";
397 ### F: Time argument not valid; exit
398 yell
"ERROR:Invalid time argument supplied. Exiting."; exit 1;
401 # Construct and deliver separator-les date string
402 DATE_CURRENT_SHORT
="$(date -d "$TIME_INPUT" +%Y%m%d)"; # Produce separator-less current date with resolution 1 day.
403 echo "$DATE_CURRENT_SHORT";
406 # Desc: Given seconds, output ISO-8601 duration string
407 # Ref/Attrib: ISO-8601:2004(E), §4.4.4.2 Representations of time intervals by duration and context information
408 # Note: "1 month" ("P1M") is assumed to be "30 days" (see ISO-8601:2004(E), §2.2.1.2)
409 # Usage: timeDuration [1:seconds] ([2:precision])
411 # Input: arg1: seconds as base 10 integer >= 0 (ex: 3601)
412 # arg2: precision level (optional; default=2)
413 # Output: stdout: ISO-8601 duration string (ex: "P1H1S", "P2Y10M15DT10H30M20S")
414 # exit code 0: success
415 # exit code 1: error_input
416 # exit code 2: error_unknown
417 # Example: 'timeDuration 111111 3' yields 'P1DT6H51M'
418 # Depends: date 8 (gnucoreutils), yell,
419 local returnState argSeconds argPrecision remainder precision witherPrecision
420 local fullYears fullMonths fullDays fullHours fullMinutes fullSeconds
421 local displayYears displayMonths displayDays displayHours displayMinutes displaySeconds
422 local hasYears hasMonths hasDays hasHours hasMinutes hasSeconds
424 argSeconds
="$1"; # read arg1 (seconds)
425 argPrecision
="$2"; # read arg2 (precision)
426 precision
=2; # set default precision
428 # Check that between one and two arguments is supplied
429 if ! { [[ $# -ge 1 ]] && [[ $# -le 2 ]]; }; then
430 yell
"ERROR:Invalid number of arguments:$# . Exiting.";
431 returnState
="error_input"; fi
433 # Check that argSeconds provided
434 if [[ $# -ge 1 ]]; then
435 ## Check that argSeconds is a positive integer
436 if [[ "$argSeconds" =~ ^
[[:digit
:]]+$
]]; then
439 yell
"ERROR:argSeconds not a digit.";
440 returnState
="error_input";
443 yell
"ERROR:No argument provided. Exiting.";
447 # Consider whether argPrecision was provided
448 if [[ $# -eq 2 ]]; then
449 # Check that argPrecision is a positive integer
450 if [[ "$argPrecision" =~ ^
[[:digit
:]]+$
]] && [[ "$argPrecision" -gt 0 ]]; then
451 precision
="$argPrecision";
453 yell
"ERROR:argPrecision not a positive integer. (is $argPrecision ). Leaving early.";
454 returnState
="error_input";
460 remainder
="$argSeconds" ; # seconds
461 ## Calculate full years Y, update remainder
462 fullYears
=$
(( remainder
/ (365*24*60*60) ));
463 remainder
=$
(( remainder
- (fullYears
*365*24*60*60) ));
464 ## Calculate full months M, update remainder
465 fullMonths
=$
(( remainder
/ (30*24*60*60) ));
466 remainder
=$
(( remainder
- (fullMonths
*30*24*60*60) ));
467 ## Calculate full days D, update remainder
468 fullDays
=$
(( remainder
/ (24*60*60) ));
469 remainder
=$
(( remainder
- (fullDays
*24*60*60) ));
470 ## Calculate full hours H, update remainder
471 fullHours
=$
(( remainder
/ (60*60) ));
472 remainder
=$
(( remainder
- (fullHours
*60*60) ));
473 ## Calculate full minutes M, update remainder
474 fullMinutes
=$
(( remainder
/ (60) ));
475 remainder
=$
(( remainder
- (fullMinutes
*60) ));
476 ## Calculate full seconds S, update remainder
477 fullSeconds
=$
(( remainder
/ (1) ));
478 remainder
=$
(( remainder
- (remainder
*1) ));
479 ## Check which fields filled
480 if [[ $fullYears -gt 0 ]]; then hasYears
="true"; else hasYears
="false"; fi
481 if [[ $fullMonths -gt 0 ]]; then hasMonths
="true"; else hasMonths
="false"; fi
482 if [[ $fullDays -gt 0 ]]; then hasDays
="true"; else hasDays
="false"; fi
483 if [[ $fullHours -gt 0 ]]; then hasHours
="true"; else hasHours
="false"; fi
484 if [[ $fullMinutes -gt 0 ]]; then hasMinutes
="true"; else hasMinutes
="false"; fi
485 if [[ $fullSeconds -gt 0 ]]; then hasSeconds
="true"; else hasSeconds
="false"; fi
487 ## Determine which fields to display (see ISO-8601:2004 §4.4.3.2)
488 witherPrecision
="false"
491 if $hasYears && [[ $precision -gt 0 ]]; then
493 witherPrecision
="true";
495 displayYears
="false";
497 if $witherPrecision; then ((precision--
)); fi;
500 if $hasMonths && [[ $precision -gt 0 ]]; then
501 displayMonths
="true";
502 witherPrecision
="true";
504 displayMonths
="false";
506 if $witherPrecision && [[ $precision -gt 0 ]]; then
507 displayMonths
="true";
509 if $witherPrecision; then ((precision--
)); fi;
512 if $hasDays && [[ $precision -gt 0 ]]; then
514 witherPrecision
="true";
518 if $witherPrecision && [[ $precision -gt 0 ]]; then
521 if $witherPrecision; then ((precision--
)); fi;
524 if $hasHours && [[ $precision -gt 0 ]]; then
526 witherPrecision
="true";
528 displayHours
="false";
530 if $witherPrecision && [[ $precision -gt 0 ]]; then
533 if $witherPrecision; then ((precision--
)); fi;
536 if $hasMinutes && [[ $precision -gt 0 ]]; then
537 displayMinutes
="true";
538 witherPrecision
="true";
540 displayMinutes
="false";
542 if $witherPrecision && [[ $precision -gt 0 ]]; then
543 displayMinutes
="true";
545 if $witherPrecision; then ((precision--
)); fi;
549 if $hasSeconds && [[ $precision -gt 0 ]]; then
550 displaySeconds
="true";
551 witherPrecision
="true";
553 displaySeconds
="false";
555 if $witherPrecision && [[ $precision -gt 0 ]]; then
556 displaySeconds
="true";
558 if $witherPrecision; then ((precision--
)); fi;
560 ## Determine whether or not the "T" separator is needed to separate date and time elements
561 if ( $displayHours ||
$displayMinutes ||
$displaySeconds); then
562 displayDateTime
="true"; else displayDateTime
="false"; fi
564 ## Construct duration output string
566 if $displayYears; then
567 OUTPUT
=$OUTPUT$fullYears"Y"; fi
568 if $displayMonths; then
569 OUTPUT
=$OUTPUT$fullMonths"M"; fi
570 if $displayDays; then
571 OUTPUT
=$OUTPUT$fullDays"D"; fi
572 if $displayDateTime; then
573 OUTPUT
=$OUTPUT"T"; fi
574 if $displayHours; then
575 OUTPUT
=$OUTPUT$fullHours"H"; fi
576 if $displayMinutes; then
577 OUTPUT
=$OUTPUT$fullMinutes"M"; fi
578 if $displaySeconds; then
579 OUTPUT
=$OUTPUT$fullSeconds"S"; fi
581 ## Output duration string to stdout
582 echo "$OUTPUT" && returnState
="true";
584 #===Determine function return code===
585 if [ "$returnState" = "true" ]; then
587 elif [ "$returnState" = "error_input" ]; then
591 yell
"ERROR:Unknown";
595 } # Get duration (ex: PT10M4S )
597 # Desc: Displays missing apps, files, and dirs
598 # Usage: displayMissing
599 # Input: associative arrays: appRollCall, fileRollCall, dirRollCall
600 # Output: stderr messages
601 #==BEGIN Display errors==
602 #===BEGIN Display Missing Apps===
603 missingApps
="Missing apps :"
604 #for key in "${!appRollCall[@]}"; do echo "DEBUG:$key => ${appRollCall[$key]}"; done
605 for key
in "${!appRollCall[@]}"; do
606 value
="${appRollCall[$key]}"
607 if [ "$value" = "false" ]; then
608 #echo "DEBUG:Missing apps: $key => $value";
609 missingApps
="$missingApps""$key "
613 if [ "$appMissing" = "true" ]; then # Only indicate if an app is missing.
614 echo "$missingApps" 1>&2;
616 #===END Display Missing Apps===
618 #===BEGIN Display Missing Files===
619 missingFiles
="Missing files:"
620 #for key in "${!fileRollCall[@]}"; do echo "DEBUG:$key => ${fileRollCall[$key]}"; done
621 for key
in "${!fileRollCall[@]}"; do
622 value
="${fileRollCall[$key]}"
623 if [ "$value" = "false" ]; then
624 #echo "DEBUG:Missing files: $key => $value";
625 missingFiles
="$missingFiles""$key "
629 if [ "$fileMissing" = "true" ]; then # Only indicate if an app is missing.
630 echo "$missingFiles" 1>&2;
632 #===END Display Missing Files===
634 #===BEGIN Display Missing Directories===
635 missingDirs
="Missing dirs:"
636 #for key in "${!dirRollCall[@]}"; do echo "DEBUG:$key => ${dirRollCall[$key]}"; done
637 for key
in "${!dirRollCall[@]}"; do
638 value
="${dirRollCall[$key]}"
639 if [ "$value" = "false" ]; then
640 #echo "DEBUG:Missing dirs: $key => $value";
641 missingDirs
="$missingDirs""$key "
645 if [ "$dirMissing" = "true" ]; then # Only indicate if an dir is missing.
646 echo "$missingDirs" 1>&2;
648 #===END Display Missing Directories===
650 #==END Display errors==
651 } # Display missing apps, files, dirs
652 magicSetScriptTTL
() {
653 #Desc: Sets script_TTL seconds from provided time_element string argument
654 #Usage: magicSetScriptTTL [str time_element]
655 #Input: arg1: string (Ex: SCRIPT_TTL_TE; "day" or "hour")
656 #Output: var: SCRIPT_TTL (integer seconds)
657 #Depends: timeUntilNextHour, timeUntilNextDay
660 if [[ "$argTimeElement" = "day" ]]; then
661 # Set script lifespan to end at start of next day
662 if ! SCRIPT_TTL
="$(timeUntilNextDay)"; then
663 if [[ "$SCRIPT_TTL" -eq 0 ]]; then
664 ((SCRIPT_TTL
++)); # Add 1 because 0 would cause 'timeout' to never timeout.
666 yell
"ERROR: timeUntilNextDay exit code $?"; exit 1;
669 elif [[ "$argTimeElement" = "hour" ]]; then
670 # Set script lifespan to end at start of next hour
671 if ! SCRIPT_TTL
="$(timeUntilNextHour)"; then
672 if [[ "$SCRIPT_TTL" -eq 0 ]]; then
673 ((SCRIPT_TTL
++)); # Add 1 because 0 would cause 'timeout' to never timeout.
675 yell
"ERROR: timeUntilNextHour exit code $?"; exit 1;
679 yell
"ERROR:Invalid argument for setScriptTTL function:$argTimeElement"; exit 1;
681 } # Seconds until next (day|hour).
683 # Desc: Checks that a valid tar archive exists, creates one otherwise
684 # Usage: checkMakeTar [ path ]
686 # Input: arg1: path of tar archive
687 # Output: exit code 0 : tar readable
688 # exit code 1 : tar missing; created
689 # exit code 2 : tar not readable; moved; replaced
690 # Depends: try, tar, date
691 local PATH_TAR returnFlag0 returnFlag1 returnFlag2
694 # Check if file is a valid tar archive
695 if tar --list --file="$PATH_TAR" 1>/dev
/null
2>&1; then
696 ## T1: return success
697 returnFlag0
="tar valid";
699 ## F1: Check if file exists
700 if [[ -f "$PATH_TAR" ]]; then
702 try
mv "$PATH_TAR" "$PATH_TAR""--broken--""$(date +%Y%m%dT%H%M%S)" && \
703 returnFlag1
="tar moved";
708 ## F2: Create tar archive, return 0
709 try
tar --create --file="$PATH_TAR" --files-from=/dev
/null
&& \
710 returnFlag2
="tar created";
713 # Determine function return code
714 if [[ "$returnFlag0" = "tar valid" ]]; then
716 elif [[ "$returnFlag2" = "tar created" ]] && ! [[ "$returnFlag1" = "tar moved" ]]; then
717 return 1; # tar missing so created
718 elif [[ "$returnFlag2" = "tar created" ]] && [[ "$returnFlag1" = "tar moved" ]]; then
719 return 2; # tar not readable so moved; replaced
721 } # checks if arg1 is tar; creates one otherwise
723 # Desc: Writes first argument to temporary file with arguments as options, then appends file to tar
724 # Usage: appendArgTar "$(echo "Data to be written.")" [name of file to be inserted] [tar path] [temp dir] ([cmd1] [cmd2] [cmd3] [cmd4]...)
726 # Input: arg1: data to be written
727 # arg2: file name of file to be inserted into tar
728 # arg3: tar archive path (must exist first)
729 # arg4: temporary working dir
730 # arg5+: command strings (ex: "gpsbabel -i nmea -f - -o kml -F - ")
731 # Output: file written to disk
732 # Example: decrypt multiple large files in parallel
733 # appendArgTar "$(cat /tmp/largefile1.gpg)" "largefile1" $HOME/archive.tar /tmp "gpg --decrypt" &
734 # appendArgTar "$(cat /tmp/largefile2.gpg)" "largefile2" $HOME/archive.tar /tmp "gpg --decrypt" &
735 # appendArgTar "$(cat /tmp/largefile3.gpg)" "largefile3" $HOME/archive.tar /tmp "gpg --decrypt" &
737 # Ref/Attrib: Using 'eval' to construct command strings https://askubuntu.com/a/476533
740 local FN
="${FUNCNAME[0]}";
741 #yell "DEBUG:STATUS:$FN:Finished appendArgTar()."
744 if ! [ -z "$2" ]; then FILENAME
="$2"; else yell
"ERROR:$FN:Not enough arguments."; exit 1; fi
746 # Check tar path is a file
747 if [ -f "$3" ]; then TAR_PATH
="$3"; else yell
"ERROR:$FN:Tar archive arg not a file."; exit 1; fi
750 if ! [ -z "$4" ]; then TMP_DIR
="$4"; else yell
"ERROR:$FN:No temporary working dir set."; exit 1; fi
752 # Set command strings
753 if ! [ -z "$5" ]; then CMD1
="$5"; else CMD1
="tee /dev/null "; fi # command string 1
754 if ! [ -z "$6" ]; then CMD2
="$6"; else CMD2
="tee /dev/null "; fi # command string 2
755 if ! [ -z "$7" ]; then CMD3
="$7"; else CMD3
="tee /dev/null "; fi # command string 3
756 if ! [ -z "$8" ]; then CMD4
="$8"; else CMD4
="tee /dev/null "; fi # command string 4
762 # yell "DEBUG:STATUS:$FN:CMD0:$CMD0"
763 # yell "DEBUG:STATUS:$FN:CMD1:$CMD1"
764 # yell "DEBUG:STATUS:$FN:CMD2:$CMD2"
765 # yell "DEBUG:STATUS:$FN:CMD3:$CMD3"
766 # yell "DEBUG:STATUS:$FN:CMD4:$CMD4"
767 # yell "DEBUG:STATUS:$FN:FILENAME:$FILENAME"
768 # yell "DEBUG:STATUS:$FN:TAR_PATH:$TAR_PATH"
769 # yell "DEBUG:STATUS:$FN:TMP_DIR:$TMP_DIR"
771 # Write to temporary working dir
772 eval "$CMD0"" | ""$CMD1"" | ""$CMD2"" | ""$CMD3"" | ""$CMD4" > "$TMP_DIR"/"$FILENAME";
775 try
tar --append --directory="$TMP_DIR" --file="$TAR_PATH" "$FILENAME";
776 #yell "DEBUG:STATUS:$FN:Finished appendArgTar()."
777 } # Append Bash var to file appended to Tar archive
779 # Desc: Processes first file and then appends to tar
780 # Usage: appendFileTar [file path] [name of file to be inserted] [tar path] [temp dir] ([cmd1] [cmd2] [cmd3] [cmd4]...)
782 # Input: arg1: path of file to be (processed and) written
783 # arg2: name to use for file inserted into tar
784 # arg3: tar archive path (must exist first)
785 # arg4: temporary working dir
786 # arg5+: command strings (ex: "gpsbabel -i nmea -f - -o kml -F - ")
787 # Output: file written to disk
788 # Example: decrypt multiple large files in parallel
789 # appendFileTar /tmp/largefile1.gpg "largefile1" $HOME/archive.tar /tmp "gpg --decrypt" &
790 # appendFileTar /tmp/largefile2.gpg "largefile2" $HOME/archive.tar /tmp "gpg --decrypt" &
791 # appendFileTar /tmp/largefile3.gpg "largefile3" $HOME/archive.tar /tmp "gpg --decrypt" &
795 local FN
="${FUNCNAME[0]}";
796 #yell "DEBUG:STATUS:$FN:Finished appendFileTar()."
799 if ! [ -z "$2" ]; then FILENAME
="$2"; else yell
"ERROR:$FN:Not enough arguments."; exit 1; fi
800 # Check tar path is a file
801 if [ -f "$3" ]; then TAR_PATH
="$3"; else yell
"ERROR:$FN:Tar archive arg not a file."; exit 1; fi
803 if ! [ -z "$4" ]; then TMP_DIR
="$4"; else yell
"ERROR:$FN:No temporary working dir set."; exit 1; fi
804 # Set command strings
805 if ! [ -z "$5" ]; then CMD1
="$5"; else CMD1
="tee /dev/null "; fi # command string 1
806 if ! [ -z "$6" ]; then CMD2
="$6"; else CMD2
="tee /dev/null "; fi # command string 2
807 if ! [ -z "$7" ]; then CMD3
="$7"; else CMD3
="tee /dev/null "; fi # command string 3
808 if ! [ -z "$8" ]; then CMD4
="$8"; else CMD4
="tee /dev/null "; fi # command string 4
810 # Input command string
814 # yell "DEBUG:STATUS:$FN:CMD0:$CMD0"
815 # yell "DEBUG:STATUS:$FN:CMD1:$CMD1"
816 # yell "DEBUG:STATUS:$FN:CMD2:$CMD2"
817 # yell "DEBUG:STATUS:$FN:CMD3:$CMD3"
818 # yell "DEBUG:STATUS:$FN:CMD4:$CMD4"
819 # yell "DEBUG:STATUS:$FN:FILENAME:$FILENAME"
820 # yell "DEBUG:STATUS:$FN:TAR_PATH:$TAR_PATH"
821 # yell "DEBUG:STATUS:$FN:TMP_DIR:$TMP_DIR"
823 # Write to temporary working dir
824 eval "$CMD0 | $CMD1 | $CMD2 | $CMD3 | $CMD4" > "$TMP_DIR"/"$FILENAME";
827 try
tar --append --directory="$TMP_DIR" --file="$TAR_PATH" "$FILENAME";
828 #yell "DEBUG:STATUS:$FN:Finished appendFileTar()."
829 } # Append file to Tar archive
831 # Desc: Checks if string is an age-compatible pubkey
832 # Usage: checkAgePubkey [str pubkey]
834 # Input: arg1: string
835 # Output: return code 0: string is age-compatible pubkey
836 # return code 1: string is NOT an age-compatible pubkey
837 # age stderr (ex: there is stderr if invalid string provided)
838 # Depends: age (v0.1.0-beta2; https://github.com/FiloSottile/age/releases/tag/v1.0.0-beta2 )
842 if echo "test" | age
-a -r "$argPubkey" 1>/dev
/null
; then
849 # Desc: Validates Input
850 # Usage: validateInput [str input] [str input type]
852 # Input: arg1: string to validate
853 # arg2: string specifying input type (ex:"ssh_pubkey")
854 # Output: return code 0: if input string matched specified string type
855 # Depends: bash 5, yell
858 local FN
="${FUNCNAME[0]}";
863 if [[ $# -gt 2 ]]; then yell
"ERROR:$0:$FN:Too many arguments."; exit 1; fi;
866 if [[ -z "$argInput" ]]; then return 1; fi
870 ### Check for alnum/dash base64 (ex: "ssh-rsa AAAAB3NzaC1yc2EAAA")
871 if [[ "$argType" = "ssh_pubkey" ]]; then
872 if [[ "$argInput" =~ ^
[[:alnum
:]-]*[\
]*[[:alnum
:]+/=]*$
]]; then
876 ### Check for age1[:bech32:]
877 if [[ "$argType" = "age_pubkey" ]]; then
878 if [[ "$argInput" =~ ^age1
[qpzry9x8gf2tvdw0s3jn54khce6mua7l
]*$
]]; then
882 if [[ "$argType" = "integer" ]]; then
883 if [[ "$argInput" =~ ^
[[:digit
:]]*$
]]; then
886 ## time element (year, month, week, day, hour, minute, second)
887 if [[ "$argType" = "time_element" ]]; then
888 if [[ "$argInput" = "year" ]] || \
889 [[ "$argInput" = "month" ]] || \
890 [[ "$argInput" = "week" ]] || \
891 [[ "$argInput" = "day" ]] || \
892 [[ "$argInput" = "hour" ]] || \
893 [[ "$argInput" = "minute" ]] || \
894 [[ "$argInput" = "second" ]]; then
897 # Return error if no condition matched.
899 } # Validates strings
901 # Desc: Get epoch nanoseconds
904 # Input: arg1: 'date'-parsable timestamp string (optional)
905 # Output: Nanoseconds since 1970-01-01
906 # Depends: date 8, yell()
907 # Ref/Attrib: Force base 10 Bash arith with '10#'. https://stackoverflow.com/a/24777667
908 local TIME_CURRENT TIME_INPUT TIME_EPOCH_FLOAT TIME_EPOCH_NSFRAC
914 TIME_CURRENT
="$(date --iso-8601=ns)"; # Produce `date`-parsable current timestamp with resolution of 1 nanosecond.
916 # Decide to parse current or supplied time
917 ## Check if time argument empty
918 if [[ -z "$argTime" ]]; then
919 ## T: Time argument empty, use current time
920 TIME_INPUT
="$TIME_CURRENT";
922 ## F: Time argument exists, validate time
923 if date --date="$argTime" 1>/dev
/null
2>&1; then
924 ### T: Time argument is valid; use it
925 TIME_INPUT
="$argTime";
927 ### F: Time argument not valid; exit
928 yell
"ERROR:Invalid time argument supplied. Exiting."; exit 1;
931 # Construct and deliver nanoseconds since 1970-01-01
932 TIME_EPOCH_FLOAT
="$(date --date="$TIME_INPUT" +%s.%N)"; # Save ssss.NNNNNNNNN
933 TIME_EPOCH_INT
="$(echo "$TIME_EPOCH_FLOAT" | cut -d. -f1)"; # Get ssss
934 TIME_EPOCH_NSFRAC
="$(echo "$TIME_EPOCH_FLOAT" | cut -d. -f2)"; # Get NNNNNNNNN
935 TIME_EPOCH_NS
="$(( (10#"$TIME_EPOCH_INT" * 10**9) + (10#"$TIME_EPOCH_NSFRAC") ))";
936 echo "$TIME_EPOCH_NS";
937 } # Nanoseconds since 1970-01-01
938 magicBufferSleepPID
() {
939 # Desc: Compensates for lag so buffer rounds start every BUFFER_TTL seconds
940 # Input: vars: BUFFER_TTL, errResetx10e3, K_P, T_I, T_D
941 # # Input: array: errorHistory
942 # Output: vars: BUFFER_TTL_ADJ_FLOAT
943 # Re/Attrib: https://en.wikipedia.org/wiki/PID_controller#Standard_versus_parallel_(ideal)_form
945 local timeBufferStartNS timeBufferStartNSExp errNS errNSx10e3
946 local errResetx10e3 errRatex10e3 ADJ BUFFER_TTL_ADJ_NS BUFFER_TTL_ADJ_INT
947 local BUFFER_TTL_ADJ_FLOATFRAC
948 # local errorHistorySize
950 # ## Define errorHistorySize
951 # errorHistorySize=100;
952 ## Define BUFFER_TTL in nanoseconds
953 BUFFER_TTL_NS
=$
((BUFFER_TTL
* 10**9)) && vbm
"BUFFER_TTL_NS:$BUFFER_TTL_NS";
955 # Calculate Error, errNS, in nanoseconds
957 timeBufferStartNS
="$(timeEpochNS)" && vbm
"timeBufferStartNS:$timeBufferStartNS";
958 ## Calculate expected time (from start time, current buffer round number, nominal BUFFER_TTL)
959 timeBufferStartNSExp
="$(( (timeBufferFirstNS) + (BUFFER_TTL_NS * bufferRound) ))" && vbm
"timeBufferStartNSExp:$timeBufferStartNSExp";
960 ## Calculate error (diff between timeBufferStartNSExp and timeBufferStartNS; usually negative)
961 errNS
="$(( timeBufferStartNSExp - timeBufferStartNS ))" && vbm
"errNS:$errNS";
962 errNSx10e3
="$((errNS*10**3))" && vbm
"errNSx10e3:$errNSx10e3";
963 # ## Append error to errorHistory
964 # errorHistory+=("errNS");
965 # ### Trim errorHistory array if over errorHistorySize
966 # while [[ "${#errorHistory[@]}" -gt "errorHistorySize" ]]; then do
967 # unset "errorHistory[0]"; # remove oldest entry, creating sparse array
968 # errorHistory=("${errorHistory[@]}"); # reindex sparse array
969 # vbm "STATUS:Trimmed errorHistory array. Entry count:${#errorHistory[@]}";
972 # Calculate errReset in nanoseconds^2
973 ## errReset = int(errHistory(t),wrt(delta_BUFFER_TTL))
974 ## Integrate errorHistory with respect to time
975 # for value in "${errorHistory[@]}"; do
976 # errReset=$(( errReset + ( value*BUFFER_TTL_NS ) ));
978 errResetx10e3
="$(( ( errResetx10e3 + ( errNSx10e3 * BUFFER_TTL_NS ) )*10**3 ))" && vbm
"errResetx10e3:$errResetx10e3";
980 # Calculate errRate in nanoseconds per 1000 nanoseconds
981 errRatex10e3
="$(( ( errNSx10e3 ) / BUFFER_TTL_NS ))" && vbm
"errRatex10e3:$errRatex10e3";
983 # Calculate PID control signal
984 ## ADJ = K_P * (errNS + errReset/T_I + errRate*T_D)
985 ADJ
="$((K_P*(errNSx10e3 + (errResetx10e3/T_I) + (errRatex10e3*T_D) )/(10**3)))" && vbm
"ADJ:$ADJ";
987 # Calculate BUFFER_TTL_ADJ_FLOAT from ADJ (ns)
988 ## Calculate BUFFER_TTL_ADJ in nanoseconds (BUFFER_TTL_ADJ_NS = BUFFER_TTL_NS + ADJ)
989 BUFFER_TTL_ADJ_NS
="$((BUFFER_TTL_NS + ADJ))" && vbm
"BUFFER_TTL_ADJ_NS:$BUFFER_TTL_ADJ_NS";
990 ## Calculate integer seconds
991 BUFFER_TTL_ADJ_INT
="$((BUFFER_TTL_ADJ_NS/(10**9)))" && vbm
"BUFFER_TTL_ADJ_INT:$BUFFER_TTL_ADJ_INT";
992 ### Catch negative integer seconds, set minimum of BUFFER_TTL/10 seconds
993 if [[ "$BUFFER_TTL_ADJ_INT" -le "$((BUFFER_TTL/10))" ]]; then
994 BUFFER_TTL_ADJ_INT
="$((BUFFER_TTL/10))";
995 yell
"WARNING:Buffer lag adjustment yielded negative seconds.";
997 ## Calculate nanosecond remainder
998 BUFFER_TTL_ADJ_FLOATFRAC
="$((BUFFER_TTL_NS - (BUFFER_TTL_ADJ_INT*(10**9)) ))" && vbm
"BUFFER_TTL_ADJ_FLOATFRAC:$BUFFER_TTL_ADJ_FLOATFRAC";
999 ## Form float BUFFER_TTL_ADJ_FLOAT
1000 BUFFER_TTL_ADJ_FLOAT
="$BUFFER_TTL_ADJ_INT".
"$BUFFER_TTL_ADJ_FLOATFRAC" && vbm
"BUFFER_TTL_ADJ_FLOAT:$BUFFER_TTL_ADJ_FLOAT";
1001 vbm
"STATUS:Calculated adjusted BUFFER_TTL (seconds):$BUFFER_TTL_ADJ_FLOAT";
1003 } # Calc BUFFER_TTL_ADJ_FLOAT so buffer starts every BUFFER_TTL seconds
1004 magicWriteVersion
() {
1005 # Desc: Appends time-stamped VERSION to PATHOUT_TAR
1006 # Usage: magicWriteVersion
1008 # Input: CONTENT_VERSION, FILEOUT_VERSION, PATHOUT_TAR, DIR_TMP
1009 # Input: SCRIPT_VERSION, SCRIPT_URL, AGE_VERSION, AGE_URL, SCRIPT_HOSTNAME
1010 # Output: appends tar PATHOUT_TAR
1011 # Depends: dateTimeShort, appendArgTar
1012 local CONTENT_VERSION pubKeyIndex
1014 # Set VERSION file name
1015 FILEOUT_VERSION
="$(dateTimeShort)..VERSION";
1017 # Gather VERSION data in CONTENT_VERSION
1018 CONTENT_VERSION
="SCRIPT_VERSION=$SCRIPT_VERSION";
1019 #CONTENT_VERSION="$CONTENT_VERSION""\\n";
1020 CONTENT_VERSION
="$CONTENT_VERSION""\\n""SCRIPT_NAME=$SCRIPT_NAME";
1021 CONTENT_VERSION
="$CONTENT_VERSION""\\n""SCRIPT_URL=$SCRIPT_URL";
1022 CONTENT_VERSION
="$CONTENT_VERSION""\\n""AGE_VERSION=$AGE_VERSION";
1023 CONTENT_VERSION
="$CONTENT_VERSION""\\n""AGE_URL=$AGE_URL";
1024 CONTENT_VERSION
="$CONTENT_VERSION""\\n""DATE=$(date --iso-8601=seconds)";
1025 CONTENT_VERSION
="$CONTENT_VERSION""\\n""HOSTNAME=$SCRIPT_HOSTNAME";
1026 ## Add list of recipient pubkeys
1027 for pubkey
in "${recPubKeysValid[@]}"; do
1029 CONTENT_VERSION
="$CONTENT_VERSION""\\n""PUBKEY_$pubKeyIndex=$pubkey";
1031 ## Process newline escapes
1032 CONTENT_VERSION
="$(echo -e "$CONTENT_VERSION")"
1034 # Write CONTENT_VERSION as file FILEOUT_VERSION and write-append to PATHOUT_TAR
1035 appendArgTar
"$CONTENT_VERSION" "$FILEOUT_VERSION" "$PATHOUT_TAR" "$DIR_TMP";
1037 } # bkgpslog: write version data to PATHOUT_TAR via appendArgTar()
1038 magicGatherWriteBuffer
() {
1039 # Desc: bkgpslog-specific meta function for writing data to DIR_TMP then appending each file to PATHOUT_TAR
1040 # Inputs: vars: PATHOUT_TAR FILEOUT_{NMEA,GPX,KML} CMD_CONV_{NMEA,GPX,KML} CMD_{COMPRESS,ENCRYPT} DIR_TMP,
1041 # Inputs: vars: BUFFER_TTL bufferTTL_STR SCRIPT_HOSTNAME CMD_COMPRESS_SUFFIX CMD_ENCRYPT_SUFFIX
1042 # Output: file: (PATHOUT_TAR)
1043 # Depends: yell(), try(), vbm(), appendArgTar(), tar 1, sleep 8, checkMakeTar()
1044 # Depends: magicWriteVersion(), appendFileTar()
1047 # Debug:Get function name
1048 FN
="${FUNCNAME[0]}";
1050 # Create buffer file with unique name
1051 PATHOUT_BUFFER
="$DIR_TMP/buffer$SECONDS";
1053 timeout
"$BUFFER_TTL"s gpspipe
-r -o "$PATHOUT_BUFFER" ;
1054 timeBufferStart
="$(dateTimeShort "$
(date --date="$BUFFER_TTL seconds ago")")"; # Note start time
1055 vbm
"DEBUG:STATUS:$FN:Started magicWriteBuffer().";
1056 # Determine file paths (time is start of buffer period)
1057 FILEOUT_BASENAME
="$timeBufferStart""--""$bufferTTL_STR""..""$SCRIPT_HOSTNAME""_location" && vbm
"STATUS:Set FILEOUT_BASENAME to:$FILEOUT_BASENAME";
1058 ## Files saved to DIR_TMP
1059 FILEOUT_NMEA
="$FILEOUT_BASENAME".nmea
"$CMD_COMPRESS_SUFFIX""$CMD_ENCRYPT_SUFFIX" && vbm
"STATUS:Set FILEOUT_NMEA to:$FILEOUT_NMEA";
1060 FILEOUT_GPX
="$FILEOUT_BASENAME".gpx
"$CMD_COMPRESS_SUFFIX""$CMD_ENCRYPT_SUFFIX" && vbm
"STATUS:Set FILEOUT_GPX to:$FILEOUT_GPX";
1061 FILEOUT_KML
="$FILEOUT_BASENAME".kml
"$CMD_COMPRESS_SUFFIX""$CMD_ENCRYPT_SUFFIX" && vbm
"STATUS:Set FILEOUT_KML to:$FILEOUT_KML";
1062 PATHOUT_NMEA
="$DIR_TMP"/"$FILEOUT_NMEA" && vbm
"STATUS:Set PATHOUT_NMEA to:$PATHOUT_NMEA";
1063 PATHOUT_GPX
="$DIR_TMP"/"$FILEOUT_GPX" && vbm
"STATUS:Set PATHOUT_GPX to:$PATHOUT_GPX";
1064 PATHOUT_KML
="$DIR_TMP"/"$FILEOUT_KML" && vbm
"STATUS:Set PATHOUT_KML to:$PATHOUT_KML";
1065 ## Files saved to disk (DIR_OUT)
1066 ### one file per day (Ex: "20200731..hostname_location.[.gpx.gz].tar")
1067 PATHOUT_TAR
="$DIR_OUT"/"$(dateShort "$
(date --date="$BUFFER_TTL seconds ago")")"..
"$SCRIPT_HOSTNAME""_location""$CMD_COMPRESS_SUFFIX""$CMD_ENCRYPT_SUFFIX".
tar && \
1068 vbm
"STATUS:Set PATHOUT_TAR to:$PATHOUT_TAR";
1070 vbm
"STATUS:DIR_TMP :$DIR_TMP";
1071 vbm
"STATUS:PATHOUT_TAR :$PATHOUT_TAR";
1072 vbm
"STATUS:PATHOUT_NMEA:$PATHOUT_NMEA";
1073 vbm
"STATUS:PATHOUT_GPX:$PATHOUT_GPX";
1074 vbm
"STATUS:PATHOUT_KML:$PATHOUT_KML";
1077 # Validate PATHOUT_TAR as tar.
1078 checkMakeTar
"$PATHOUT_TAR";
1079 ## Add VERSION file if checkMakeTar had to create a tar (exited 1) or replace one (exited 2)
1080 if [[ $?
-eq 1 ]] ||
[[ $?
-eq 2 ]]; then magicWriteVersion
; fi
1082 # Write bufferBash to PATHOUT_TAR
1083 wait; # Wait to avoid collision with older magicWriteBuffer() instances (see https://www.tldp.org/LDP/abs/html/x9644.html )
1084 appendFileTar
"$PATHOUT_BUFFER" "$FILEOUT_NMEA" "$PATHOUT_TAR" "$DIR_TMP" "$CMD_CONV_NMEA" "$CMD_COMPRESS" "$CMD_ENCRYPT"; # Write NMEA data
1085 appendFileTar
"$PATHOUT_BUFFER" "$FILEOUT_GPX" "$PATHOUT_TAR" "$DIR_TMP" "$CMD_CONV_GPX" "$CMD_COMPRESS" "$CMD_ENCRYPT"; # Write GPX file
1086 appendFileTar
"$PATHOUT_BUFFER" "$FILEOUT_KML" "$PATHOUT_TAR" "$DIR_TMP" "$CMD_CONV_KML" "$CMD_COMPRESS" "$CMD_ENCRYPT"; # Write KML file
1088 # Remove secured chunks from DIR_TMP
1089 rm "$PATHOUT_BUFFER" "$PATHOUT_NMEA" "$PATHOUT_GPX" "$PATHOUT_KML";
1090 vbm
"DEBUG:STATUS:$FN:Finished magicWriteBuffer().";
1091 } # write buffer to disk
1092 magicParseRecipientDir
() {
1093 # Desc: Updates recPubKeysValid with pubkeys in dir specified by '-R' option ("recipient directory")
1094 # Inputs: vars: OPTION_RECDIR, argRecDir, OPTION_ENCRYPT
1095 # arry: recPubKeysValid
1096 # Outputs: arry: recPubKeysValid
1097 # Depends: processArguments,
1098 local recFileLine updateRecipients recipientDir
1099 declare -a candRecPubKeysValid
1101 # Check that '-e' and '-R' set
1102 if [[ "$OPTION_ENCRYPT" = "true" ]] && [[ "$OPTION_RECDIR" = "true" ]]; then
1103 ### Check that argRecDir is a directory.
1104 if [[ -d "$argRecDir" ]]; then
1105 recipientDir
="$argRecDir" && vbm
"STATUS:Recipient watch directory detected:\"$recipientDir\"";
1106 #### Initialize variable indicating outcome of pubkey review
1107 unset updateRecipients
1108 #### Add existing recipients
1109 candRecPubKeysValid
=("${recPubKeysValidStatic[@]}");
1110 #### Parse files in recipientDir
1111 for file in "$recipientDir"/*; do
1112 ##### Read first line of each file
1113 recFileLine
="$(head -n1 "$file")" && vbm
"STATUS:Checking if pubkey:\"$recFileLine\"";
1114 ##### check if first line is a valid pubkey
1115 if checkAgePubkey
"$recFileLine" && \
1116 ( validateInput
"$recFileLine" "ssh_pubkey" || validateInput
"$recFileLine" "age_pubkey"); then
1117 ###### T: add candidate pubkey to candRecPubKeysValid
1118 candRecPubKeysValid
+=("$recFileLine") && vbm
"STATUS:RecDir pubkey is valid pubkey:\"$recFileLine\"";
1120 ###### F: throw warning;
1121 yell
"ERROR:Invalid recipient file detected. Not modifying recipient list."
1122 updateRecipients
="false";
1125 #### Write updated recPubKeysValid array to recPubKeysValid if no failure detected
1126 if ! [[ "$updateRecipients" = "false" ]]; then
1127 recPubKeysValid
=("${candRecPubKeysValid[@]}") && vbm
"STATUS:Wrote candRecPubkeysValid to recPubKeysValid:\"${recPubKeysValid[*]}\"";
1130 yell
"ERROR:$0:Recipient directory $argRecDir does not exist. Exiting."; exit 1;
1133 # Handle case if '-R' set but '-e' not set
1134 if [[ ! "$OPTION_ENCRYPT" = "true" ]] && [[ "$OPTION_RECDIR" = "true" ]]; then
1135 yell
"ERROR: \\'-R\\' is set but \\'-e\\' is not set."; fi;
1136 } # Update recPubKeysValid with argRecDir
1137 magicParseRecipientArgs
() {
1138 # Desc: Parses recipient arguments specified by '-r' option
1139 # Input: vars: OPTION_ENCRYPT from processArguments()
1140 # arry: argRecPubKeys from processArguments()
1141 # Output: vars: CMD_ENCRYPT, CMD_ENCRYPT_SUFFIX
1142 # arry: recPubKeysValid, recPubKeysValidStatic
1143 # Depends: checkapp(), checkAgePubkey(), validateInput(), processArguments()
1146 # Check if encryption option active.
1147 if [[ "$OPTION_ENCRYPT" = "true" ]] && [[ "$OPTION_RECIPIENTS" = "true" ]]; then
1148 if checkapp age
; then # Check that age is available.
1149 for pubkey
in "${argRecPubKeys[@]}"; do # Validate recipient pubkey strings by forming test message
1150 vbm
"DEBUG:Testing pubkey string:$pubkey";
1151 if checkAgePubkey
"$pubkey" && \
1152 ( validateInput
"$pubkey" "ssh_pubkey" || validateInput
"$pubkey" "age_pubkey"); then
1153 #### Form age recipient string
1154 recipients
="$recipients""-r '$pubkey' ";
1155 vbm
"STATUS:Added pubkey for forming age recipient string:""$pubkey";
1156 vbm
"DEBUG:recipients:""$recipients";
1157 #### Add validated pubkey to recPubKeysValid array
1158 recPubKeysValid
+=("$pubkey") && vbm
"DEBUG:recPubkeysValid:pubkey added:$pubkey";
1160 yell
"ERROR:Exit code ""$?"". Invalid recipient pubkey string. Exiting."; exit 1;
1163 vbm
"DEBUG:Finished processing argRecPubKeys array";
1164 vbm
"STATUS:Array of validated pubkeys:${recPubKeysValid[*]}";
1165 recPubKeysValidStatic
=("${recPubKeysValid[@]}"); # Save static image of pubkeys validated by this function
1167 ## Form age command string
1168 CMD_ENCRYPT
="age ""$recipients " && vbm
"CMD_ENCRYPT:$CMD_ENCRYPT";
1169 CMD_ENCRYPT_SUFFIX
=".age" && vbm
"CMD_ENCRYPT_SUFFIX:$CMD_ENCRYPT_SUFFIX";
1171 yell
"ERROR:Encryption enabled but \"age\" not found. Exiting."; exit 1;
1174 CMD_ENCRYPT
="tee /dev/null " && vbm
"CMD_ENCRYPT:$CMD_ENCRYPT";
1175 CMD_ENCRYPT_SUFFIX
="" && vbm
"CMD_ENCRYPT_SUFFIX:$CMD_ENCRYPT_SUFFIX";
1176 vbm
"DEBUG:Encryption not enabled."
1178 # Catch case if '-e' is set but '-r' or '-R' is not
1179 if [[ "$OPTION_ENCRYPT" = "true" ]] && [[ ! "$OPTION_RECIPIENTS" = "true" ]]; then
1180 yell
"ERROR:\\'-e\\' set but no \\'-r\\' or \\'-R\\' set."; exit 1; fi;
1181 # Catch case if '-r' or '-R' set but '-e' is not
1182 if [[ ! "$OPTION_ENCRYPT" = "true" ]] && [[ "$OPTION_RECIPIENTS" = "true" ]]; then
1183 yell
"ERROR:\\'-r\\' or \\'-R\\' set but \\'-e\\' is not set."; exit 1; fi;
1184 } # Populate recPubKeysValid with argRecPubKeys; form encryption cmd string and filename suffix
1185 magicParseCompressionArg
() {
1186 # Desc: Parses compression arguments specified by '-c' option
1187 # Input: vars: OPTION_COMPRESS
1188 # Output: CMD_COMPRESS, CMD_COMPRESS_SUFFIX
1189 # Depends: checkapp(), vbm(), gzip,
1190 if [[ "$OPTION_COMPRESS" = "true" ]]; then # Check if compression option active
1191 if checkapp
gzip; then # Check if gzip available
1192 CMD_COMPRESS
="gzip " && vbm
"CMD_COMPRESS:$CMD_COMPRESS";
1193 CMD_COMPRESS_SUFFIX
=".gz" && vbm
"CMD_COMPRESS_SUFFIX:$CMD_COMPRESS_SUFFIX";
1195 yell
"ERROR:Compression enabled but \"gzip\" not found. Exiting."; exit 1;
1198 CMD_COMPRESS
="tee /dev/null " && vbm
"CMD_COMPRESS:$CMD_COMPRESS";
1199 CMD_COMPRESS_SUFFIX
="" && vbm
"CMD_COMPRESS_SUFFIX:$CMD_COMPRESS_SUFFIX";
1200 vbm
"DEBUG:Compression not enabled.";
1202 } # Form compression cmd string and filename suffix
1203 magicInitWorkingDir
() {
1204 # Desc: Determine temporary working directory from defaults or user input
1205 # Usage: magicInitWorkignDir
1206 # Input: vars: OPTION_TEMPDIR, argTempDirPriority, DIR_TMP_DEFAULT
1207 # Input: vars: SCRIPT_TIME_START
1208 # Output: vars: DIR_TMP
1209 # Depends: processArguments(), vbm(), yell()
1210 # Parse '-t' option (user-specified temporary working dir)
1211 ## Set DIR_TMP_PARENT to user-specified value if specified
1212 local DIR_TMP_PARENT
1214 if [[ "$OPTION_TMPDIR" = "true" ]]; then
1215 if [[ -d "$argTempDirPriority" ]]; then
1216 DIR_TMP_PARENT
="$argTempDirPriority";
1218 yell
"WARNING:Specified temporary working directory not valid:$argTempDirPriority";
1219 exit 1; # Exit since user requires a specific temp dir and it is not available.
1222 ## Set DIR_TMP_PARENT to default or fallback otherwise
1223 if [[ -d "$DIR_TMP_DEFAULT" ]]; then
1224 DIR_TMP_PARENT
="$DIR_TMP_DEFAULT";
1225 elif [[ -d /tmp
]]; then
1226 yell
"WARNING:$DIR_TMP_DEFAULT not available. Falling back to /tmp .";
1227 DIR_TMP_PARENT
="/tmp";
1229 yell
"ERROR:No valid working directory available. Exiting.";
1233 ## Set DIR_TMP using DIR_TMP_PARENT and nonce (SCRIPT_TIME_START)
1234 DIR_TMP
="$DIR_TMP_PARENT"/"$SCRIPT_TIME_START""..bkgpslog" && vbm
"DEBUG:Set DIR_TMP to:$DIR_TMP"; # Note: removed at end of main().
1235 } # Sets working dir
1236 magicParseCustomTTL
() {
1237 # Desc: Set user-specified TTLs for buffer and script
1238 # Input: vars: argCustomBufferTTL (integer), argCustomScriptTTL_TE (string)
1239 # Input: vars: OPTION_CUSTOM_BUFFERTTL, OPTION_CUSTOM_SCRIPTTTL
1240 # Input: vars: BUFFER_TTL (integer), SCRIPT_TTL_TE (string)
1241 # Output: BUFFER_TTL (integer), SCRIPT_TTL_TE (string)
1242 # Depends validateInput(), showUsage(), yell
1244 # React to '-b, --buffer-ttl' option
1245 if [[ "$OPTION_CUSTOM_BUFFERTTL" = "true" ]]; then
1246 ## T: Check if argCustomBufferTTL is an integer
1247 if validateInput
"$argCustomBufferTTL" "integer"; then
1248 ### T: argCustomBufferTTL is an integer
1249 BUFFER_TTL
="$argCustomBufferTTL";
1251 ### F: argcustomBufferTTL is not an integer
1252 yell
"ERROR:Invalid integer argument for custom buffer time-to-live."; showUsage
; exit 1;
1254 ## F: do not change BUFFER_TTL
1257 # React to '-B, --script-ttl' option
1258 if [[ "$OPTION_CUSTOM_SCRIPTTTL_TE" = "true" ]]; then
1259 ## T: Check if argCustomScriptTTL is a time element (ex: "day", "hour")
1260 if validateInput
"$argCustomScriptTTL" "time_element"; then
1261 ### T: argCustomScriptTTL is a time element
1262 SCRIPT_TTL_TE
="$argCustomScriptTTL";
1264 ### F: argcustomScriptTTL is not a time element
1265 yell
"ERROR:Invalid time element argument for custom script time-to-live."; showUsage
; exit 1;
1267 ## F: do not change SCRIPT_TTL_TE
1269 } # Sets custom script or buffer TTL if specified
1274 processArguments
"$@";
1275 ## Act upon arguments
1276 ### Determine working directory
1277 magicInitWorkingDir
; # Sets DIR_TMP from argTempDirPriority
1278 ### Set output encryption and compression option strings
1279 #### React to "-r" ("encryption recipients") option
1280 magicParseRecipientArgs
; # Updates recPubKeysValid, CMD_ENCRYPT[_SUFFIX] from argRecPubKeys
1281 #### React to "-c" ("compression") option
1282 magicParseCompressionArg
; # Updates CMD_COMPRESS[_SUFFIX]
1283 #### React to "-R" ("recipient directory") option
1284 magicParseRecipientDir
; # Updates recPubKeysValid
1285 #### React to custom buffer and script TTL options ("-b", "-B")
1286 magicParseCustomTTL
; # Sets custom SCRIPT_TTL_TE and/or BUFFER_TTL if specified
1288 # Check that critical apps and dirs are available, display missing ones.
1289 if ! checkapp gpspipe
tar && ! checkdir
"$DIR_OUT" "DIR_TMP"; then
1290 yell
"ERROR:Critical components missing.";
1291 displayMissing
; yell
"Exiting."; exit 1; fi
1293 # Set script lifespan (SCRIPT_TTL from SCRIPT_TTL_TE)
1294 magicSetScriptTTL
"$SCRIPT_TTL_TE";
1295 ## Note: SCRIPT_TTL_TE is time element string (ex: "day") while SCRIPT_TTL is integer seconds
1297 # File name substring (ISO-8601 duration from BUFFER_TTL)
1298 bufferTTL_STR
="$(timeDuration "$BUFFER_TTL")";
1300 # Init temp working dir
1301 try mkdir
"$DIR_TMP" && vbm
"DEBUG:Working dir creatd at:$DIR_TMP";
1303 # Initialize 'tar' archive
1304 ## Define output tar path (note: each day gets *one* tar file (Ex: "20200731..hostname_location.[.gpx.gz].tar"))
1305 PATHOUT_TAR
="$DIR_OUT"/"$(dateShort)"..
"$SCRIPT_HOSTNAME""_location""$CMD_COMPRESS_SUFFIX""$CMD_ENCRYPT_SUFFIX".
tar && \
1306 vbm
"STATUS:Set PATHOUT_TAR to:$PATHOUT_TAR";
1307 ## Check that PATHOUT_TAR is a tar. Rename old and create empty one otherwise.
1308 checkMakeTar
"$PATHOUT_TAR" && vbm
"DEBUG:Confirmed or Created to be a tar:$PATHOUT_TAR";
1309 ## Append VERSION file to PATHOUT_TAR
1312 # Define GPS conversion commands
1313 CMD_CONV_NMEA
="tee /dev/null " && vbm
"STATUS:Set CMD_CONV_NMEA to:$CMD_CONV_NMEA"; # tee as passthrough
1314 CMD_CONV_GPX
="gpsbabel -i nmea -f - -o gpx -F - " && vbm
"STATUS:Set CMD_CONV_GPX to:$CMD_CONV_GPX"; # convert NMEA to GPX
1315 CMD_CONV_KML
="gpsbabel -i nmea -f - -o kml -F - " && vbm
"STATUS:Set CMD_CONV_KML to:$CMD_CONV_KML"; # convert NMEA to KML
1317 # MAIN LOOP:Record gps data until script lifespan ends
1318 timeBufferFirstNS
="$(timeEpochNS)"; bufferRound
=0; BUFFER_TTL_ADJ_FLOAT
="10.0";
1319 while [[ "$SECONDS" -lt "$SCRIPT_TTL" ]]; do
1320 magicParseRecipientDir
;
1321 magicGatherWriteBuffer
&
1322 sleep "$BUFFER_TTL_ADJ_FLOAT";
1324 magicBufferSleepPID
; # Calculates BUFFER_TTL_ADJ from BUFFER_TTL given buffer expected start time vs. actual
1329 try
rm -r "$DIR_TMP";
1331 vbm
"STATUS:Main function finished.";
1333 #===END Declare local script functions===
1334 #==END Define script parameters==
1337 #==BEGIN Perform work and exit==
1338 main
"$@" # Run main function.
1340 #==END Perform work and exit==