blob: 5641c00991e549dcf17f3c9e6d6815340fcafe19 [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
93 echo > $2; cat > $2 <<EOF
94${1}
95EOF
96 else
97 return $s_err
98 fi
99}
100
101# @1 : text to append
102# @2 : absolute path to a valid file
103# @return : success if @2 is valid
104function appvar
105{
106 [ -f "$2" ] && catvar "$1" >> "$2" || lets -l -e "invalid: $2"
107}
108
109# @1 : (optional) absolute path to a valid file
110# @desc : echo a formatted header, including the file name if passed in
111function get_header
112{
113 if is_in_tree_instance_d "$1"; then
114 local header="\
115# $head_tag $(basename $1)
116
117$license"
118 else
119 local header="\
120$license"
121 fi
122 echo "$header"
123}
124
125# @desc : echo a formatted footer
126function get_footer
127{
128 local __ver="$(get_script_ver --file -q)"
129 __ver="${__ver%@*}"
130
131 local footer=\
132"# $foot_tag $__ver@$script_now"
133 echo "$footer"
134}
135
136function __create_instance_d
137{
138 is_valid_new_dir $2 || return $s_err4
139 mkdir -p $2 || return $s_err5
140
141 # do not remove directory - it should never be so
142 rm -f $1 || return $s_err6
143 ln -sf $2 $1 || return $s_err7
144}
145
146# @1 : absolute path to $instance_d
147# @desc : validate dkrc_out_d acutally is what we expect it to be
148function __is_valid_instance_d_link
149{
150 local ver_id="$(readlink -f $1)"
151 local version_f="$1/$__version"
152 ver_id="${ver_id##*-}"
153 local run_time="${ver_id#*@}"
154
155 test -n "$ver_id" || return $s_err3
156 is_unix_path "$1" || return $s_err4
157 test -L "$1" || return $s_err5
158 test -f "$version_f" || return $s_err6
159 [ "$ver_id" = "$(< $version_f)" ] || return $s_err7
160
161 # This is the case calling doins with multiple modules
162 [ "$run_time" = "$script_now" ] && return $s_err8 || \
163 return $s_ok
164}
165
166# @1 : absolute path to $instance_d
167# @desc : validate dkrc_out_d acutally is what we expect it to be
168function is_valid_instance_d_link
169{
170 __is_valid_instance_d_link $1
171 case "$?" in
172 $s_err3 ) lets -l -e "instance version id" ;;
173 $s_err4 ) lets -l -e "instance path" ;;
174 $s_err5 ) lets -l -e "instance link" ;;
175 $s_err6 ) lets -l -e "instance version file" ;;
176 $s_err7 ) lets -l -e "signature mismatch" ;;
177 $s_err8 ) return $s_err2 ;;
178 $s_ok ) return $s_ok ;;
179 esac; return $s_err
180}
181
182function __is_valid_input
183{
184 test -L "$1" && {
185 is_valid_instance_d_link "$1"
186 case "$?" in
187 $s_err2 ) return $s_err2 ;; # different module, same run
188 $s_err ) return $s_err ;; # genuine error
189 $s_ok )
190 local old_instance="$(basename "$(readlink -f $1)")"
191
192 lets -l -i "'$(basename $1)' points to '$old_instance'"
193 lets -l -i "update this link to '$2'"
194 lets -a "would you like to continue?" && \
195 return $s_ok || return $s_err ;;
196 esac
197 }
198
199 ! test -e "$1" || return $s_err3
200}
201
202# @desc : create a new instance_d, either by updating an existing
203# : symlink or creating everything from scratch.
204function create_instance_d
205{
206 local ver="$(get_script_ver --git)"
207 local new_instance="$(basename $1)-$ver"
208 local new_instance_d="$(dirname $1)/$new_instance"
209
210 __is_valid_input "$1" "$new_instance"
211 case "$?" in
212 $s_err ) lets -l -i "goodbye"; return $s_err ;;
213 $s_err2 ) return $s_ok ;; # no error, no further actions
214 $s_err3 ) lets -l -e "$1 busy"; return $s_err ;;
215 esac
216
217 __create_instance_d "$1" "$new_instance_d"
218 case "$?" in
219 $s_err4 ) lets -l -e "instance path" ; return $s_err ;;
220 $s_err5 ) lets -l -e "instance mkdir" ; return $s_err ;;
221 $s_err6 ) lets -l -e "instance unlink" ; return $s_err ;;
222 $s_err7 ) lets -l -e "instance link" ; return $s_err ;;
223 esac
224
225 set_script_ver $ver $instance_d/$__version || return $s_err
226}
227
228# @ : a list of absolute paths to prospective folders
229function create_directory
230{
231 local d
232
233 for d in ${@}; do
234 is_unix_path $d || { lets -l -d "invalid path: $d"; return $s_err; }
235 [ -d "$d" ] || { mkdir -p $d && lets -l -d "mkdir $d"; }
236 done
237}
238
239# @1 : (optional) option --dry
240# @2 : file text content
241# @3 : absolute path to a valid target file
242# @return : Success if @3 is valid
243# @desc : Redirect a bash variable to stdout or file
244function __write_to_file
245{
246 [ "$1" = "--dry" ] && shift || local file_path=$2
247 local file_text="${!1}"
248
249 case "$1" in
250 *"_bang_"* )
251 local shebang="$(catvar "$file_text" | head --lines 1)"
252 file_text="$(catvar "$file_text" | tail --lines +2)"
253 local file_content="\
254${shebang}
255
256$(get_header $2)
257
258${file_text}
259
260$(get_footer)"
261 ;;
262 * )
263 local file_content="\
264$(get_header $2)
265
266${file_text}
267
268$(get_footer)"
269 ;;
270 esac
271
272 catvar "$file_content" $file_path
273}
274
275# @1 : diff flags
276# @2 : file text content
277# @3 : absolute path to an existing file
278# @desc : This function can diff an existing file against some new content
279function __diff_file
280{
281 local flags=$1; shift
282
283 if [ -f "$2" ]; then
284 # NOTE: "$(echo "$1")" makes sure $1 isn't broken down into multiple
285 # strings according to the bash IFS expasion.
286 # FIXME: check __write_to_file
287 diff $flags $2 <(__write_to_file --dry "$(echo "$1")" $2)
288 else
289 lets -l -d "invalid file: $2"
290 return $s_err2
291 fi
292}
293
294# @1 : diff flags
295# @2 : file text content
296# @3 : absolute path to an existing file
297# @desc : This function can diff an existing file against some new content
298function is_file_diff
299{
300 __diff_file "$1" "$2" "$3" &>/dev/null
301}
302
303# @1 : file text content
304# @2 : absolute path to a valid file
305function __splash_file
306{
307 local flags="--suppress-common-lines"
308 flags+=" --ignore-matching-lines=$rex_legal_file_header_tag"
309 flags+=" --ignore-matching-lines=$rex_legal_file_footer_tag"
310 flags+=" --report-identical-files"
311 flags+=" --color"
312# flags+=" --ignore-blank-lines"
313
314 __diff_file "$flags" "$1" "$2" | lets -l -x "diff"
315 case "${PIPESTATUS[0]}" in
316 $s_ok ) lets -l -i --phew "MATCH: $(basename "$2")" ;;
317 $s_err ) lets -l -w "DIFFER: $(basename "$2")" ;;
318 $s_err2 ) lets -l -w "NOT-FOUND: $(basename "$2")" ;;
319 esac
320
321 return $s_ok
322}
323
324# @1 : file text content
325# @2 : absolute path to a valid file
326# @desc : Update the content of an already generated file. This is useful when
327# : the user doesn't want/need to recreate a new rootfs from scratch.
328function __update_file
329{
330 local file_name="$(basename $2)"
331 local flags="--suppress-common-lines"
332 flags+=" --ignore-matching-lines=$rex_legal_file_header_tag"
333 flags+=" --ignore-matching-lines=$rex_legal_file_footer_tag"
334 #flags+=" --report-identical-files"
335 #flags+=" --ignore-blank-lines"
336
337 is_file_diff "$flags" "$1" $2
338 case "$?" in
339 $s_ok ) lets -l -i "no need to update: $file_name"; return $s_ok ;;
340 $s_err2 ) lets -l -w "add one on update: $file_name" ;;
341 esac
342
343 local old_foot_tags="$(grep -E "$rex_legal_file_footer_tag" -- $2 2>/dev/null)"
344
345 if ! __write_to_file "$1" $2; then
346 lets -l -e "failed to update $file_name"; return $s_err
347 fi
348
349 lets -l -i "changed on update: $file_name"
350
351 if [ -n "$old_foot_tags" ]; then
352 if ! appvar "$old_foot_tags" $2; then
353 lets -l -e "failed to append $old_foot_tags"
354 fi
355 fi
356}
357
358function __create_file
359{
360 [ "$1" = "--dry" ] && local dry=$1 && shift || local dry=""
361
362 if [ -f "$2" ]; then
363 lets -l -w "overriding: $(basename $2)"
364 fi
365
366 __write_to_file $dry "$1" "$2"
367}
368
369function __file_do_helper
370{
371 local __file="$2"
372 local __cmd="rm"
373 local __line="$__cmd $__file $script_now $module $1"
374
375 case "$1" in
376 --update )
377 [ -f "$3" ] && {
378 local flags="--follow-symlinks -i -E"
379
380 # Has $2 been actually updated?
381 grep -qE "$rex_legal_file_footer_tag$script_now" $2 && {
382
383 # $3 is not marked for deletion
384 grep -qE "^#$__cmd $__file .*" $3 && {
385
386 # Update line to report script_now, module and option
387 sed $flags "s%^#$__cmd $__file .*$%#$__line%g" -- $3
388 return $s_ok
389 }
390
391 # $3 is marked for deletion
392 grep -qE "^$__cmd $__file .*" $3 && {
393
394 # Update line to report script_now, module and option.
395 # Also, comment it out, as $3 is not to be removed.
396 sed $flags "s%^$__cmd $__file .*$%#$__line%g" -- $3
397 return $s_ok
398 }
399
400 [ "$(grep -E "$rex_legal_file_footer_tag" $2 | wc -l)" -eq 1 ] && {
401
402 # Update instance by adding a file. Equivalent to --create, but
403 # do not mark $2 for removal.
404 [ -f "$3" ] && {
405 ! grep -q "$__cmd $2" $3 && echo "#$__line" >> $3
406 return $s_ok
407 }
408
409 lets -l -w "update without $3. Advise to wipe it all"
410 echo "#$__line" > $3 && return $s_ok
411 }
412
413 lets -l -e "$(basename $3) not tracking $(basename $2)"
414 return $s_err
415 }
416
417 # Is $2 marked for deletion? If so, un-mark it
418 grep -qE "^$__cmd $__file .*" $3 && {
419 sed $flags "s%^$__cmd $__file%#$__cmd $__file%g" -- $3
420 return $s_ok
421 }
422 } ;;
423 --create )
424 [ -f "$3" ] && {
425 ! grep -q "$__cmd $2" $3 && echo "$__line" >> $3
426 return $s_ok
427 }; echo "$__line" > $3 ;;
428 esac
429}
430
431# @1 : file text content
432# @2 : absolute path to a valid file
433# @desc : Update the content of an already generated file. This is useful
434# : when the user doesn't want/need to recreate a new rootfs from
435# : scratch.
436function __file_do
437{
438 # Option
439 local option=$1; shift
440 local __un_f=$1; shift
441
442 case "$option" in
443 --update ) local -r func="__update_file" ;;
444 --create ) local -r func="__create_file" ;;
445 --splash ) local -r func="__splash_file" ;;
446 *) lets -l -e "invalid option $option"; return $s_err ;;
447 esac
448
449 local -i len="${#} / 2"
450 local -i i
451
452 local -a path=( "${@:1:$len}" ); shift $len
453 local -a cont=( "${@}" )
454
455 if [ ${#path[@]} -ne ${#cont[@]} ]; then
456 lets -l -e "mismatch: $len, ${#path[@]} vs ${#cont[@]}"
457 return $s_err
458 fi
459
460 # Remove comment before processing all files, for this module only
461 if [ -f "$__un_f" ]; then
462 cp "$__un_f" "$__un_f.bkp" # be safe
463 sed --follow-symlinks -i -E "s%^#(.+ $module .+)%\1%g" -- $__un_f
464 fi
465
466 for i in ${!path[@]}; do
467 if ! $func ${cont[$i]} ${path[$i]}; then
468 lets -l -d "failed to ${func%_file} ${path[$i]}"
469 cp "$__un_f.bkp" "$__un_f" && rm -f "$__un_f.bkp"
470 return $s_err
471 fi
472
473 # FIXME: handle file permissions in a proper way
474 case "${cont[$i]}" in *"_bang_"* ) chmod +x ${path[$i]} ;; esac
475
476 __file_do_helper $option ${path[$i]} $__un_f || return $s_err
477 lets -l -d "${func%_file} $(basename ${path[$i]})"
478 done
479
480 case "$option" in
481 --update )
482 [ ! -f "$__un_f" ] && {
483 cp "$__un_f.bkp" "$__un_f" && rm -f "$__un_f.bkp"; return $s_err
484 }
485
486 local -a __rmline
487 while read -a __rmline; do
488 local __cmd="${__rmline[0]}"; local __f="${__rmline[1]}"
489 case "${__rmline[*]}" in
490 '' | '\n' | '#'* )
491 lets -l -d "skip: $__cmd: $(basename "$__f")"
492 continue ;;
493 * ) [ ! -f "$__f" ] && {
494 lets -l -w "$__cmd *not* installed: $(basename "$__f")"
495 } || { lets -l -i "$__cmd installed: $(basename "$__f")"; }
496
497 eval "$__cmd $__f" && {
498 # Got to remove this line and it will be lost forever
499 sed --follow-symlinks -i -E "\:$__cmd $__f.*:d" -- "$__un_f"
500 } || { lets -l -e "failed to remove $__f"; } ;;
501 esac
502 done <<< $(grep -E "[[:space:]]$module[[:space:]]" -- $__un_f) ;;
503 esac
504
505 rm -f "$__un_f.bkp"
506 return $s_ok
507}