blob: 663413bdff470f9f320efa08b29523180bf686e9 [file] [log] [blame]
Luigi Santivetti0fdd4702020-06-22 19:00:32 +01001#!/bin/bash
2#
3# io.sh
4#
5# Copyright 2019 Luigi Santivetti <luigi.santivetti@gmail.com>
6
7# Permission is hereby granted, free of charge, to any person obtaining a
8# copy of this software and associated documentation files (the "Software"),
9# to deal in the Software without restriction, including without limitation
10# the rights to use, copy, modify, merge, publish, distribute, sublicense,
11# and/or sell copies of the Software, and to permit persons to whom the
12# Software is furnished to do so, subject to the following conditions:
13
14# The above copyright notice and this permission notice (including the next
15# paragraph) shall be included in all copies or substantial portions of the
16# Software.
17
18# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
21# ITS SUPPLIERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
22# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
23# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
24
25# @1 : version file containg git sha
26# @return : stdout, instance version: '${git_sha}@${script_now}'
27function __get_script_ver_file
28{
29 [ "$1" = "-q" ] && local q=1 && shift
30
31 git diff-index --quiet HEAD 2>/dev/null || {
32 [ -n "$q" ] || lets -l -w "git unstaged changes"
33 }
34
35 [ -f "$instance_d/$__version" ] && {
36 local ver="$(< "$instance_d/$__version")"
37 local now="${ver##*@}"; local sha="${ver%%@*}"
38 [ ${#now} -eq ${#sha} ] && [ ${#now} -eq ${#script_now} ] || {
39 lets -l -e "invalid version"; return $s_err
40 }
41 echo "$ver" && return $s_ok
42 }
43 lets -l -w "invalid version file" && return $s_err
44}
45
46function __get_script_ver_git
47{
48 [ "$1" = "-q" ] && local q=1 && shift
49
50 git diff-index --quiet HEAD 2>/dev/null || {
51 [ -n "$q" ] || lets -l -w "git unstaged changes"
52 }
53
54 local -i len="${#script_now}"
55 local sha="$(git log --pretty=format:'%H' -n 1)"
56 [ -n "$sha" ] || { lets -l -e "git log failed"; return $s_err; }
57
58 echo "${sha:0:$len}@$script_now"
59}
60
61function get_script_ver
62{
63 case "$1" in
64 --file | -f ) shift; __get_script_ver_file "${@}" ;;
65 --git | -g ) shift; __get_script_ver_git "${@}" ;;
66 esac
67}
68
69# @1 : absolute path to prospective file
70# @return : file, instance version: '${git_sha}@${script_now}'
71function set_script_ver
72{
73 local -r vfile="$2"
74
75 is_unix_path $vfile && touch $vfile || {
76 lets -l -e "invalid version file"; return 1
77 }
78
79 echo "$1" > "$vfile"
80}
81
82# @1 : text to be sent to stdout via cat
83# @2 : (optional) absolute path to a prospective file in DOCKER_ROOT
84# @return : success if @2 isn't given or if @2 is valid
85# @desc : do not write to file if it doesn't live inside $instance_d
86function catvar
87{
88 if [ -z "$2" ]; then
89 cat <<EOF
90${1}
91EOF
92 elif is_in_tree_instance_d $2; then
Luigi Santivetticbb3d542020-11-07 22:31:27 +000093 local -r _uid="$(stat -c '%u' $2)"
94 local -r _gid="$(stat -c '%g' $2)"
95 cat <<EOF | sudo -E -u "#$_uid" -g "#$_gid" tee $2
Luigi Santivetti0fdd4702020-06-22 19:00:32 +010096${1}
97EOF
98 else
99 return $s_err
100 fi
101}
102
103# @1 : text to append
104# @2 : absolute path to a valid file
105# @return : success if @2 is valid
106function appvar
107{
Luigi Santivetticbb3d542020-11-07 22:31:27 +0000108 if [ -f "$2" ] && is_in_tree_instance_d $2; then
109 local -r _uid="$(stat -c '%u' $2)"
110 local -r _gid="$(stat -c '%g' $2)"
111 cat <<EOF | sudo -E -u "#$_uid" -g "#$_gid" tee -a $2
112${1}
113EOF
114 else
115 lets -l -e "invalid: $2"
116 fi
Luigi Santivetti0fdd4702020-06-22 19:00:32 +0100117}
118
119# @1 : (optional) absolute path to a valid file
120# @desc : echo a formatted header, including the file name if passed in
121function get_header
122{
123 if is_in_tree_instance_d "$1"; then
124 local header="\
125# $head_tag $(basename $1)
126
127$license"
128 else
129 local header="\
130$license"
131 fi
132 echo "$header"
133}
134
135# @desc : echo a formatted footer
136function get_footer
137{
138 local __ver="$(get_script_ver --file -q)"
139 __ver="${__ver%@*}"
140
141 local footer=\
142"# $foot_tag $__ver@$script_now"
143 echo "$footer"
144}
145
146function __create_instance_d
147{
148 is_valid_new_dir $2 || return $s_err4
149 mkdir -p $2 || return $s_err5
150
151 # do not remove directory - it should never be so
152 rm -f $1 || return $s_err6
153 ln -sf $2 $1 || return $s_err7
154}
155
156# @1 : absolute path to $instance_d
157# @desc : validate dkrc_out_d acutally is what we expect it to be
158function __is_valid_instance_d_link
159{
160 local ver_id="$(readlink -f $1)"
161 local version_f="$1/$__version"
162 ver_id="${ver_id##*-}"
163 local run_time="${ver_id#*@}"
164
165 test -n "$ver_id" || return $s_err3
166 is_unix_path "$1" || return $s_err4
167 test -L "$1" || return $s_err5
168 test -f "$version_f" || return $s_err6
169 [ "$ver_id" = "$(< $version_f)" ] || return $s_err7
170
171 # This is the case calling doins with multiple modules
172 [ "$run_time" = "$script_now" ] && return $s_err8 || \
173 return $s_ok
174}
175
176# @1 : absolute path to $instance_d
177# @desc : validate dkrc_out_d acutally is what we expect it to be
178function is_valid_instance_d_link
179{
180 __is_valid_instance_d_link $1
181 case "$?" in
182 $s_err3 ) lets -l -e "instance version id" ;;
183 $s_err4 ) lets -l -e "instance path" ;;
184 $s_err5 ) lets -l -e "instance link" ;;
185 $s_err6 ) lets -l -e "instance version file" ;;
186 $s_err7 ) lets -l -e "signature mismatch" ;;
187 $s_err8 ) return $s_err2 ;;
188 $s_ok ) return $s_ok ;;
189 esac; return $s_err
190}
191
192function __is_valid_input
193{
194 test -L "$1" && {
195 is_valid_instance_d_link "$1"
196 case "$?" in
197 $s_err2 ) return $s_err2 ;; # different module, same run
198 $s_err ) return $s_err ;; # genuine error
199 $s_ok )
200 local old_instance="$(basename "$(readlink -f $1)")"
201
202 lets -l -i "'$(basename $1)' points to '$old_instance'"
203 lets -l -i "update this link to '$2'"
204 lets -a "would you like to continue?" && \
205 return $s_ok || return $s_err ;;
206 esac
207 }
208
209 ! test -e "$1" || return $s_err3
210}
211
212# @desc : create a new instance_d, either by updating an existing
213# : symlink or creating everything from scratch.
214function create_instance_d
215{
216 local ver="$(get_script_ver --git)"
217 local new_instance="$(basename $1)-$ver"
218 local new_instance_d="$(dirname $1)/$new_instance"
219
220 __is_valid_input "$1" "$new_instance"
221 case "$?" in
222 $s_err ) lets -l -i "goodbye"; return $s_err ;;
223 $s_err2 ) return $s_ok ;; # no error, no further actions
224 $s_err3 ) lets -l -e "$1 busy"; return $s_err ;;
225 esac
226
227 __create_instance_d "$1" "$new_instance_d"
228 case "$?" in
229 $s_err4 ) lets -l -e "instance path" ; return $s_err ;;
230 $s_err5 ) lets -l -e "instance mkdir" ; return $s_err ;;
231 $s_err6 ) lets -l -e "instance unlink" ; return $s_err ;;
232 $s_err7 ) lets -l -e "instance link" ; return $s_err ;;
233 esac
234
235 set_script_ver $ver $instance_d/$__version || return $s_err
236}
237
238# @ : a list of absolute paths to prospective folders
239function create_directory
240{
241 local d
242
243 for d in ${@}; do
244 is_unix_path $d || { lets -l -d "invalid path: $d"; return $s_err; }
245 [ -d "$d" ] || { mkdir -p $d && lets -l -d "mkdir $d"; }
246 done
247}
248
Luigi Santivetticbb3d542020-11-07 22:31:27 +0000249# @1 : template name
250# @2 : absolute path to file
251function __file_mode
252{
253 local _mode="_${module^^}_${1^^}_FMODE_"
254 local _uid="_${module^^}_${1^^}_UID_"
255 local _gid="_${module^^}_${1^^}_GID_"
256
257 _uid="${!_uid}"; _gid="${!_gid}"; _mode="${!_mode}"
258
259 if [ -n "${_uid:-}" ] && [ -n "${_gid:-}" ]; then
260 lets -l -i "set uid:gid: ${_uid}:${_gid}"
261 sudo chown ${_uid}:${_gid} $2 || return $s_err
262 elif [ -n "${_uid:-}" ]; then
263 lets -l -i "set uid: ${_uid}"
264 sudo chown ${_uid} $2 || return $s_err
265 elif [ -n "${_gid:-}" ]; then
266 lets -l -i "set gid: ${_gid}"
267 sudo chgrp ${_gid} $2 || return $s_err
268 fi
269
270 if [ -n "${_mode:-}" ]; then
271 lets -l -i "set mode: ${_mode}"
272 sudo chmod ${_mode} $2 || return $s_err
273 fi
274}
275
Luigi Santivetti0fdd4702020-06-22 19:00:32 +0100276# @1 : (optional) option --dry
Luigi Santivetticbb3d542020-11-07 22:31:27 +0000277# @2 : file template name
Luigi Santivetti0fdd4702020-06-22 19:00:32 +0100278# @3 : absolute path to a valid target file
279# @return : Success if @3 is valid
280# @desc : Redirect a bash variable to stdout or file
281function __write_to_file
282{
283 [ "$1" = "--dry" ] && shift || local file_path=$2
284 local file_text="${!1}"
285
286 case "$1" in
287 *"_bang_"* )
288 local shebang="$(catvar "$file_text" | head --lines 1)"
289 file_text="$(catvar "$file_text" | tail --lines +2)"
290 local file_content="\
291${shebang}
292
293$(get_header $2)
294
295${file_text}
296
297$(get_footer)"
298 ;;
299 * )
300 local file_content="\
301$(get_header $2)
302
303${file_text}
304
305$(get_footer)"
306 ;;
307 esac
308
309 catvar "$file_content" $file_path
310}
311
312# @1 : diff flags
Luigi Santivetticbb3d542020-11-07 22:31:27 +0000313# @2 : file template name
Luigi Santivetti0fdd4702020-06-22 19:00:32 +0100314# @3 : absolute path to an existing file
315# @desc : This function can diff an existing file against some new content
316function __diff_file
317{
318 local flags=$1; shift
319
320 if [ -f "$2" ]; then
321 # NOTE: "$(echo "$1")" makes sure $1 isn't broken down into multiple
322 # strings according to the bash IFS expasion.
323 # FIXME: check __write_to_file
Luigi Santivetticbb3d542020-11-07 22:31:27 +0000324 diff $flags $2 \
325 <(__write_to_file --dry "$(echo "$1")" $2) || return $s_err
326
327 # Check file access mode
328 local _mode="_${module^^}_${1^^}_FMODE_"
329 local _uid="_${module^^}_${1^^}_UID_"
330 local _gid="_${module^^}_${1^^}_GID_"
331
332 if [ -n "${!_uid}" ]; then
333 [ "$(stat -c '%u' $2)" -eq "${!_uid}" ] || {
334 echo >&2 "uid: $(stat -c '%u' $2) VS ${!_uid}"
335 return $s_err3
336 }
337 fi
338
339 if [ -n "${!_gid}" ]; then
340 [ "$(stat -c '%g' $2)" -eq "${!_gid}" ] || {
341 echo >&2 "gid: $(stat -c '%g' $2) VS ${!_gid}"
342 return $s_err3
343 }
344 fi
345
346 if [ -n "${!_mode}" ]; then
347 [ "$(stat -c '%04a' $2)" -eq "${!_mode}" ] || {
348 echo >&2 "mode: $(stat -c '%04a' $2) VS ${!_mode}"
349 return $s_err3
350 }
351 fi
Luigi Santivetti0fdd4702020-06-22 19:00:32 +0100352 else
Luigi Santivetticbb3d542020-11-07 22:31:27 +0000353 lets -l -d "no such file: $2"
Luigi Santivetti0fdd4702020-06-22 19:00:32 +0100354 return $s_err2
355 fi
356}
357
358# @1 : diff flags
Luigi Santivetticbb3d542020-11-07 22:31:27 +0000359# @2 : file template name
Luigi Santivetti0fdd4702020-06-22 19:00:32 +0100360# @3 : absolute path to an existing file
361# @desc : This function can diff an existing file against some new content
362function is_file_diff
363{
364 __diff_file "$1" "$2" "$3" &>/dev/null
365}
366
Luigi Santivetticbb3d542020-11-07 22:31:27 +0000367# @1 : file template name
Luigi Santivetti0fdd4702020-06-22 19:00:32 +0100368# @2 : absolute path to a valid file
369function __splash_file
370{
371 local flags="--suppress-common-lines"
372 flags+=" --ignore-matching-lines=$rex_legal_file_header_tag"
373 flags+=" --ignore-matching-lines=$rex_legal_file_footer_tag"
374 flags+=" --report-identical-files"
375 flags+=" --color"
376# flags+=" --ignore-blank-lines"
377
Luigi Santivetticbb3d542020-11-07 22:31:27 +0000378 __diff_file "$flags" "$1" "$2" 2>&1 | lets -l -x "diff"
Luigi Santivetti0fdd4702020-06-22 19:00:32 +0100379 case "${PIPESTATUS[0]}" in
380 $s_ok ) lets -l -i --phew "MATCH: $(basename "$2")" ;;
381 $s_err ) lets -l -w "DIFFER: $(basename "$2")" ;;
382 $s_err2 ) lets -l -w "NOT-FOUND: $(basename "$2")" ;;
Luigi Santivetticbb3d542020-11-07 22:31:27 +0000383 $s_err3 ) lets -l -w "FILE-MODE: $(basename "$2")" ;;
Luigi Santivetti0fdd4702020-06-22 19:00:32 +0100384 esac
385
386 return $s_ok
387}
388
Luigi Santivetticbb3d542020-11-07 22:31:27 +0000389# @1 : file template name
Luigi Santivetti0fdd4702020-06-22 19:00:32 +0100390# @2 : absolute path to a valid file
391# @desc : Update the content of an already generated file. This is useful when
392# : the user doesn't want/need to recreate a new rootfs from scratch.
393function __update_file
394{
395 local file_name="$(basename $2)"
396 local flags="--suppress-common-lines"
397 flags+=" --ignore-matching-lines=$rex_legal_file_header_tag"
398 flags+=" --ignore-matching-lines=$rex_legal_file_footer_tag"
399 #flags+=" --report-identical-files"
400 #flags+=" --ignore-blank-lines"
401
402 is_file_diff "$flags" "$1" $2
403 case "$?" in
404 $s_ok ) lets -l -i "no need to update: $file_name"; return $s_ok ;;
405 $s_err2 ) lets -l -w "add one on update: $file_name" ;;
406 esac
407
408 local old_foot_tags="$(grep -E "$rex_legal_file_footer_tag" -- $2 2>/dev/null)"
409
410 if ! __write_to_file "$1" $2; then
411 lets -l -e "failed to update $file_name"; return $s_err
412 fi
413
414 lets -l -i "changed on update: $file_name"
415
416 if [ -n "$old_foot_tags" ]; then
417 if ! appvar "$old_foot_tags" $2; then
418 lets -l -e "failed to append $old_foot_tags"
419 fi
420 fi
Luigi Santivetticbb3d542020-11-07 22:31:27 +0000421
422 [ -f "$2" ] && __file_mode $1 $2
Luigi Santivetti0fdd4702020-06-22 19:00:32 +0100423}
424
425function __create_file
426{
427 [ "$1" = "--dry" ] && local dry=$1 && shift || local dry=""
428
429 if [ -f "$2" ]; then
430 lets -l -w "overriding: $(basename $2)"
431 fi
432
Luigi Santivetticbb3d542020-11-07 22:31:27 +0000433 __write_to_file $dry "$1" "$2" || return $?
434
435 [ ! -f "$2" ] || __file_mode $1 $2
Luigi Santivetti0fdd4702020-06-22 19:00:32 +0100436}
437
438function __file_do_helper
439{
440 local __file="$2"
441 local __cmd="rm"
442 local __line="$__cmd $__file $script_now $module $1"
443
444 case "$1" in
445 --update )
446 [ -f "$3" ] && {
447 local flags="--follow-symlinks -i -E"
448
449 # Has $2 been actually updated?
450 grep -qE "$rex_legal_file_footer_tag$script_now" $2 && {
451
452 # $3 is not marked for deletion
453 grep -qE "^#$__cmd $__file .*" $3 && {
454
455 # Update line to report script_now, module and option
456 sed $flags "s%^#$__cmd $__file .*$%#$__line%g" -- $3
457 return $s_ok
458 }
459
460 # $3 is marked for deletion
461 grep -qE "^$__cmd $__file .*" $3 && {
462
463 # Update line to report script_now, module and option.
464 # Also, comment it out, as $3 is not to be removed.
465 sed $flags "s%^$__cmd $__file .*$%#$__line%g" -- $3
466 return $s_ok
467 }
468
469 [ "$(grep -E "$rex_legal_file_footer_tag" $2 | wc -l)" -eq 1 ] && {
470
471 # Update instance by adding a file. Equivalent to --create, but
472 # do not mark $2 for removal.
473 [ -f "$3" ] && {
474 ! grep -q "$__cmd $2" $3 && echo "#$__line" >> $3
475 return $s_ok
476 }
477
478 lets -l -w "update without $3. Advise to wipe it all"
479 echo "#$__line" > $3 && return $s_ok
480 }
481
482 lets -l -e "$(basename $3) not tracking $(basename $2)"
483 return $s_err
484 }
485
486 # Is $2 marked for deletion? If so, un-mark it
487 grep -qE "^$__cmd $__file .*" $3 && {
488 sed $flags "s%^$__cmd $__file%#$__cmd $__file%g" -- $3
489 return $s_ok
490 }
491 } ;;
492 --create )
493 [ -f "$3" ] && {
494 ! grep -q "$__cmd $2" $3 && echo "$__line" >> $3
495 return $s_ok
496 }; echo "$__line" > $3 ;;
497 esac
498}
499
500# @1 : file text content
501# @2 : absolute path to a valid file
502# @desc : Update the content of an already generated file. This is useful
503# : when the user doesn't want/need to recreate a new rootfs from
504# : scratch.
505function __file_do
506{
507 # Option
508 local option=$1; shift
509 local __un_f=$1; shift
510
511 case "$option" in
512 --update ) local -r func="__update_file" ;;
513 --create ) local -r func="__create_file" ;;
514 --splash ) local -r func="__splash_file" ;;
515 *) lets -l -e "invalid option $option"; return $s_err ;;
516 esac
517
518 local -i len="${#} / 2"
519 local -i i
520
521 local -a path=( "${@:1:$len}" ); shift $len
522 local -a cont=( "${@}" )
523
524 if [ ${#path[@]} -ne ${#cont[@]} ]; then
525 lets -l -e "mismatch: $len, ${#path[@]} vs ${#cont[@]}"
526 return $s_err
527 fi
528
529 # Remove comment before processing all files, for this module only
530 if [ -f "$__un_f" ]; then
531 cp "$__un_f" "$__un_f.bkp" # be safe
532 sed --follow-symlinks -i -E "s%^#(.+ $module .+)%\1%g" -- $__un_f
533 fi
534
535 for i in ${!path[@]}; do
536 if ! $func ${cont[$i]} ${path[$i]}; then
537 lets -l -d "failed to ${func%_file} ${path[$i]}"
538 cp "$__un_f.bkp" "$__un_f" && rm -f "$__un_f.bkp"
539 return $s_err
540 fi
541
542 # FIXME: handle file permissions in a proper way
543 case "${cont[$i]}" in *"_bang_"* ) chmod +x ${path[$i]} ;; esac
544
545 __file_do_helper $option ${path[$i]} $__un_f || return $s_err
546 lets -l -d "${func%_file} $(basename ${path[$i]})"
547 done
548
549 case "$option" in
550 --update )
551 [ ! -f "$__un_f" ] && {
552 cp "$__un_f.bkp" "$__un_f" && rm -f "$__un_f.bkp"; return $s_err
553 }
554
555 local -a __rmline
556 while read -a __rmline; do
557 local __cmd="${__rmline[0]}"; local __f="${__rmline[1]}"
558 case "${__rmline[*]}" in
559 '' | '\n' | '#'* )
560 lets -l -d "skip: $__cmd: $(basename "$__f")"
561 continue ;;
562 * ) [ ! -f "$__f" ] && {
563 lets -l -w "$__cmd *not* installed: $(basename "$__f")"
564 } || { lets -l -i "$__cmd installed: $(basename "$__f")"; }
565
566 eval "$__cmd $__f" && {
567 # Got to remove this line and it will be lost forever
568 sed --follow-symlinks -i -E "\:$__cmd $__f.*:d" -- "$__un_f"
569 } || { lets -l -e "failed to remove $__f"; } ;;
570 esac
571 done <<< $(grep -E "[[:space:]]$module[[:space:]]" -- $__un_f) ;;
572 esac
573
574 rm -f "$__un_f.bkp"
575 return $s_ok
576}