| #!/bin/bash |
| # |
| # io.sh |
| # |
| # Copyright 2019 Luigi Santivetti <luigi.santivetti@gmail.com> |
| |
| # Permission is hereby granted, free of charge, to any person obtaining a |
| # copy of this software and associated documentation files (the "Software"), |
| # to deal in the Software without restriction, including without limitation |
| # the rights to use, copy, modify, merge, publish, distribute, sublicense, |
| # and/or sell copies of the Software, and to permit persons to whom the |
| # Software is furnished to do so, subject to the following conditions: |
| |
| # The above copyright notice and this permission notice (including the next |
| # paragraph) shall be included in all copies or substantial portions of the |
| # Software. |
| |
| # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
| # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
| # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL |
| # ITS SUPPLIERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER |
| # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN |
| # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
| |
| # @1 : version file containg git sha |
| # @return : stdout, instance version: '${git_sha}@${script_now}' |
| function __get_script_ver_file |
| { |
| [ "$1" = "-q" ] && local q=1 && shift |
| |
| git diff-index --quiet HEAD 2>/dev/null || { |
| [ -n "$q" ] || lets -l -w "git unstaged changes" |
| } |
| |
| [ -f "$instance_d/$__version" ] && { |
| local ver="$(< "$instance_d/$__version")" |
| local now="${ver##*@}"; local sha="${ver%%@*}" |
| [ ${#now} -eq ${#sha} ] && [ ${#now} -eq ${#script_now} ] || { |
| lets -l -e "invalid version"; return $s_err |
| } |
| echo "$ver" && return $s_ok |
| } |
| lets -l -w "invalid version file" && return $s_err |
| } |
| |
| function __get_script_ver_git |
| { |
| [ "$1" = "-q" ] && local q=1 && shift |
| |
| git diff-index --quiet HEAD 2>/dev/null || { |
| [ -n "$q" ] || lets -l -w "git unstaged changes" |
| } |
| |
| local -i len="${#script_now}" |
| local sha="$(git log --pretty=format:'%H' -n 1)" |
| [ -n "$sha" ] || { lets -l -e "git log failed"; return $s_err; } |
| |
| echo "${sha:0:$len}@$script_now" |
| } |
| |
| function get_script_ver |
| { |
| case "$1" in |
| --file | -f ) shift; __get_script_ver_file "${@}" ;; |
| --git | -g ) shift; __get_script_ver_git "${@}" ;; |
| esac |
| } |
| |
| # @1 : absolute path to prospective file |
| # @return : file, instance version: '${git_sha}@${script_now}' |
| function set_script_ver |
| { |
| local -r vfile="$2" |
| |
| is_unix_path $vfile && touch $vfile || { |
| lets -l -e "invalid version file"; return 1 |
| } |
| |
| echo "$1" > "$vfile" |
| } |
| |
| # @1 : text to be sent to stdout via cat |
| # @2 : (optional) absolute path to a prospective file in DOCKER_ROOT |
| # @return : success if @2 isn't given or if @2 is valid |
| # @desc : do not write to file if it doesn't live inside $instance_d |
| function catvar |
| { |
| if [ -z "$2" ]; then |
| cat <<EOF |
| ${1} |
| EOF |
| elif is_in_tree_instance_d $2; then |
| local -r _uid="$(stat -c '%u' $2)" |
| local -r _gid="$(stat -c '%g' $2)" |
| cat <<EOF | sudo -E -u "#$_uid" -g "#$_gid" tee $2 |
| ${1} |
| EOF |
| else |
| return $s_err |
| fi |
| } |
| |
| # @1 : text to append |
| # @2 : absolute path to a valid file |
| # @return : success if @2 is valid |
| function appvar |
| { |
| if [ -f "$2" ] && is_in_tree_instance_d $2; then |
| local -r _uid="$(stat -c '%u' $2)" |
| local -r _gid="$(stat -c '%g' $2)" |
| cat <<EOF | sudo -E -u "#$_uid" -g "#$_gid" tee -a $2 |
| ${1} |
| EOF |
| else |
| lets -l -e "invalid: $2" |
| fi |
| } |
| |
| # @1 : (optional) absolute path to a valid file |
| # @desc : echo a formatted header, including the file name if passed in |
| function get_header |
| { |
| if is_in_tree_instance_d "$1"; then |
| local header="\ |
| # $head_tag $(basename $1) |
| |
| $license" |
| else |
| local header="\ |
| $license" |
| fi |
| echo "$header" |
| } |
| |
| # @desc : echo a formatted footer |
| function get_footer |
| { |
| local __ver="$(get_script_ver --file -q)" |
| __ver="${__ver%@*}" |
| |
| local footer=\ |
| "# $foot_tag $__ver@$script_now" |
| echo "$footer" |
| } |
| |
| function __create_instance_d |
| { |
| is_valid_new_dir $2 || return $s_err4 |
| mkdir -p $2 || return $s_err5 |
| |
| # do not remove directory - it should never be so |
| rm -f $1 || return $s_err6 |
| ln -sf $2 $1 || return $s_err7 |
| } |
| |
| # @1 : absolute path to $instance_d |
| # @desc : validate dkrc_out_d acutally is what we expect it to be |
| function __is_valid_instance_d_link |
| { |
| local ver_id="$(readlink -f $1)" |
| local version_f="$1/$__version" |
| ver_id="${ver_id##*-}" |
| local run_time="${ver_id#*@}" |
| |
| test -n "$ver_id" || return $s_err3 |
| is_unix_path "$1" || return $s_err4 |
| test -L "$1" || return $s_err5 |
| test -f "$version_f" || return $s_err6 |
| [ "$ver_id" = "$(< $version_f)" ] || return $s_err7 |
| |
| # This is the case calling doins with multiple modules |
| [ "$run_time" = "$script_now" ] && return $s_err8 || \ |
| return $s_ok |
| } |
| |
| # @1 : absolute path to $instance_d |
| # @desc : validate dkrc_out_d acutally is what we expect it to be |
| function is_valid_instance_d_link |
| { |
| __is_valid_instance_d_link $1 |
| case "$?" in |
| $s_err3 ) lets -l -e "instance version id" ;; |
| $s_err4 ) lets -l -e "instance path" ;; |
| $s_err5 ) lets -l -e "instance link" ;; |
| $s_err6 ) lets -l -e "instance version file" ;; |
| $s_err7 ) lets -l -e "signature mismatch" ;; |
| $s_err8 ) return $s_err2 ;; |
| $s_ok ) return $s_ok ;; |
| esac; return $s_err |
| } |
| |
| function __is_valid_input |
| { |
| test -L "$1" && { |
| is_valid_instance_d_link "$1" |
| case "$?" in |
| $s_err2 ) return $s_err2 ;; # different module, same run |
| $s_err ) return $s_err ;; # genuine error |
| $s_ok ) |
| local old_instance="$(basename "$(readlink -f $1)")" |
| |
| lets -l -i "'$(basename $1)' points to '$old_instance'" |
| lets -l -i "update this link to '$2'" |
| lets -a "would you like to continue?" && \ |
| return $s_ok || return $s_err ;; |
| esac |
| } |
| |
| ! test -e "$1" || return $s_err3 |
| } |
| |
| # @desc : create a new instance_d, either by updating an existing |
| # : symlink or creating everything from scratch. |
| function create_instance_d |
| { |
| local ver="$(get_script_ver --git)" |
| local new_instance="$(basename $1)-$ver" |
| local new_instance_d="$(dirname $1)/$new_instance" |
| |
| __is_valid_input "$1" "$new_instance" |
| case "$?" in |
| $s_err ) lets -l -i "goodbye"; return $s_err ;; |
| $s_err2 ) return $s_ok ;; # no error, no further actions |
| $s_err3 ) lets -l -e "$1 busy"; return $s_err ;; |
| esac |
| |
| __create_instance_d "$1" "$new_instance_d" |
| case "$?" in |
| $s_err4 ) lets -l -e "instance path" ; return $s_err ;; |
| $s_err5 ) lets -l -e "instance mkdir" ; return $s_err ;; |
| $s_err6 ) lets -l -e "instance unlink" ; return $s_err ;; |
| $s_err7 ) lets -l -e "instance link" ; return $s_err ;; |
| esac |
| |
| set_script_ver $ver $instance_d/$__version || return $s_err |
| } |
| |
| # @ : a list of absolute paths to prospective folders |
| function create_directory |
| { |
| local d |
| |
| for d in ${@}; do |
| is_unix_path $d || { lets -l -d "invalid path: $d"; return $s_err; } |
| [ -d "$d" ] || { mkdir -p $d && lets -l -d "mkdir $d"; } |
| done |
| } |
| |
| # @1 : template name |
| # @2 : absolute path to file |
| function __file_mode |
| { |
| local _mode="_${module^^}_${1^^}_FMODE_" |
| local _uid="_${module^^}_${1^^}_UID_" |
| local _gid="_${module^^}_${1^^}_GID_" |
| |
| _uid="${!_uid}"; _gid="${!_gid}"; _mode="${!_mode}" |
| |
| if [ -n "${_uid:-}" ] && [ -n "${_gid:-}" ]; then |
| lets -l -i "set uid:gid: ${_uid}:${_gid}" |
| sudo chown ${_uid}:${_gid} $2 || return $s_err |
| elif [ -n "${_uid:-}" ]; then |
| lets -l -i "set uid: ${_uid}" |
| sudo chown ${_uid} $2 || return $s_err |
| elif [ -n "${_gid:-}" ]; then |
| lets -l -i "set gid: ${_gid}" |
| sudo chgrp ${_gid} $2 || return $s_err |
| fi |
| |
| if [ -n "${_mode:-}" ]; then |
| lets -l -i "set mode: ${_mode}" |
| sudo chmod ${_mode} $2 || return $s_err |
| fi |
| } |
| |
| # @1 : (optional) option --dry |
| # @2 : file template name |
| # @3 : absolute path to a valid target file |
| # @return : Success if @3 is valid |
| # @desc : Redirect a bash variable to stdout or file |
| function __write_to_file |
| { |
| [ "$1" = "--dry" ] && shift || local file_path=$2 |
| local file_text="${!1}" |
| |
| case "$1" in |
| *"_bang_"* ) |
| local shebang="$(catvar "$file_text" | head --lines 1)" |
| file_text="$(catvar "$file_text" | tail --lines +2)" |
| local file_content="\ |
| ${shebang} |
| |
| $(get_header $2) |
| |
| ${file_text} |
| |
| $(get_footer)" |
| ;; |
| * ) |
| local file_content="\ |
| $(get_header $2) |
| |
| ${file_text} |
| |
| $(get_footer)" |
| ;; |
| esac |
| |
| catvar "$file_content" $file_path |
| } |
| |
| # @1 : diff flags |
| # @2 : file template name |
| # @3 : absolute path to an existing file |
| # @desc : This function can diff an existing file against some new content |
| function __diff_file |
| { |
| local flags=$1; shift |
| |
| if [ -f "$2" ]; then |
| # NOTE: "$(echo "$1")" makes sure $1 isn't broken down into multiple |
| # strings according to the bash IFS expasion. |
| # FIXME: check __write_to_file |
| diff $flags $2 \ |
| <(__write_to_file --dry "$(echo "$1")" $2) || return $s_err |
| |
| # Check file access mode |
| local _mode="_${module^^}_${1^^}_FMODE_" |
| local _uid="_${module^^}_${1^^}_UID_" |
| local _gid="_${module^^}_${1^^}_GID_" |
| |
| if [ -n "${!_uid}" ]; then |
| [ "$(stat -c '%u' $2)" -eq "${!_uid}" ] || { |
| echo >&2 "uid: $(stat -c '%u' $2) VS ${!_uid}" |
| return $s_err3 |
| } |
| fi |
| |
| if [ -n "${!_gid}" ]; then |
| [ "$(stat -c '%g' $2)" -eq "${!_gid}" ] || { |
| echo >&2 "gid: $(stat -c '%g' $2) VS ${!_gid}" |
| return $s_err3 |
| } |
| fi |
| |
| if [ -n "${!_mode}" ]; then |
| [ "$(stat -c '%04a' $2)" -eq "${!_mode}" ] || { |
| echo >&2 "mode: $(stat -c '%04a' $2) VS ${!_mode}" |
| return $s_err3 |
| } |
| fi |
| else |
| lets -l -d "no such file: $2" |
| return $s_err2 |
| fi |
| } |
| |
| # @1 : diff flags |
| # @2 : file template name |
| # @3 : absolute path to an existing file |
| # @desc : This function can diff an existing file against some new content |
| function is_file_diff |
| { |
| __diff_file "$1" "$2" "$3" &>/dev/null |
| } |
| |
| # @1 : file template name |
| # @2 : absolute path to a valid file |
| function __splash_file |
| { |
| local flags="--suppress-common-lines" |
| flags+=" --ignore-matching-lines=$rex_legal_file_header_tag" |
| flags+=" --ignore-matching-lines=$rex_legal_file_footer_tag" |
| flags+=" --report-identical-files" |
| flags+=" --color" |
| # flags+=" --ignore-blank-lines" |
| |
| __diff_file "$flags" "$1" "$2" 2>&1 | lets -l -x "diff" |
| case "${PIPESTATUS[0]}" in |
| $s_ok ) lets -l -i --phew "MATCH: $(basename "$2")" ;; |
| $s_err ) lets -l -w "DIFFER: $(basename "$2")" ;; |
| $s_err2 ) lets -l -w "NOT-FOUND: $(basename "$2")" ;; |
| $s_err3 ) lets -l -w "FILE-MODE: $(basename "$2")" ;; |
| esac |
| |
| return $s_ok |
| } |
| |
| # @1 : file template name |
| # @2 : absolute path to a valid file |
| # @desc : Update the content of an already generated file. This is useful when |
| # : the user doesn't want/need to recreate a new rootfs from scratch. |
| function __update_file |
| { |
| local file_name="$(basename $2)" |
| local flags="--suppress-common-lines" |
| flags+=" --ignore-matching-lines=$rex_legal_file_header_tag" |
| flags+=" --ignore-matching-lines=$rex_legal_file_footer_tag" |
| #flags+=" --report-identical-files" |
| #flags+=" --ignore-blank-lines" |
| |
| is_file_diff "$flags" "$1" $2 |
| case "$?" in |
| $s_ok ) lets -l -i "no need to update: $file_name"; return $s_ok ;; |
| $s_err2 ) lets -l -w "add one on update: $file_name" ;; |
| esac |
| |
| local old_foot_tags="$(grep -E "$rex_legal_file_footer_tag" -- $2 2>/dev/null)" |
| |
| if ! __write_to_file "$1" $2; then |
| lets -l -e "failed to update $file_name"; return $s_err |
| fi |
| |
| lets -l -i "changed on update: $file_name" |
| |
| if [ -n "$old_foot_tags" ]; then |
| if ! appvar "$old_foot_tags" $2; then |
| lets -l -e "failed to append $old_foot_tags" |
| fi |
| fi |
| |
| [ -f "$2" ] && __file_mode $1 $2 |
| } |
| |
| function __create_file |
| { |
| [ "$1" = "--dry" ] && local dry=$1 && shift || local dry="" |
| |
| if [ -f "$2" ]; then |
| lets -l -w "overriding: $(basename $2)" |
| fi |
| |
| __write_to_file $dry "$1" "$2" || return $? |
| |
| [ ! -f "$2" ] || __file_mode $1 $2 |
| } |
| |
| function __file_do_helper |
| { |
| local __file="$2" |
| local __cmd="rm" |
| local __line="$__cmd $__file $script_now $module $1" |
| |
| case "$1" in |
| --update ) |
| [ -f "$3" ] && { |
| local flags="--follow-symlinks -i -E" |
| |
| # Has $2 been actually updated? |
| grep -qE "$rex_legal_file_footer_tag$script_now" $2 && { |
| |
| # $3 is not marked for deletion |
| grep -qE "^#$__cmd $__file .*" $3 && { |
| |
| # Update line to report script_now, module and option |
| sed $flags "s%^#$__cmd $__file .*$%#$__line%g" -- $3 |
| return $s_ok |
| } |
| |
| # $3 is marked for deletion |
| grep -qE "^$__cmd $__file .*" $3 && { |
| |
| # Update line to report script_now, module and option. |
| # Also, comment it out, as $3 is not to be removed. |
| sed $flags "s%^$__cmd $__file .*$%#$__line%g" -- $3 |
| return $s_ok |
| } |
| |
| [ "$(grep -E "$rex_legal_file_footer_tag" $2 | wc -l)" -eq 1 ] && { |
| |
| # Update instance by adding a file. Equivalent to --create, but |
| # do not mark $2 for removal. |
| [ -f "$3" ] && { |
| ! grep -q "$__cmd $2" $3 && echo "#$__line" >> $3 |
| return $s_ok |
| } |
| |
| lets -l -w "update without $3. Advise to wipe it all" |
| echo "#$__line" > $3 && return $s_ok |
| } |
| |
| lets -l -e "$(basename $3) not tracking $(basename $2)" |
| return $s_err |
| } |
| |
| # Is $2 marked for deletion? If so, un-mark it |
| grep -qE "^$__cmd $__file .*" $3 && { |
| sed $flags "s%^$__cmd $__file%#$__cmd $__file%g" -- $3 |
| return $s_ok |
| } |
| } ;; |
| --create ) |
| [ -f "$3" ] && { |
| ! grep -q "$__cmd $2" $3 && echo "$__line" >> $3 |
| return $s_ok |
| }; echo "$__line" > $3 ;; |
| esac |
| } |
| |
| # @1 : file text content |
| # @2 : absolute path to a valid file |
| # @desc : Update the content of an already generated file. This is useful |
| # : when the user doesn't want/need to recreate a new rootfs from |
| # : scratch. |
| function __file_do |
| { |
| # Option |
| local option=$1; shift |
| local __un_f=$1; shift |
| |
| case "$option" in |
| --update ) local -r func="__update_file" ;; |
| --create ) local -r func="__create_file" ;; |
| --splash ) local -r func="__splash_file" ;; |
| *) lets -l -e "invalid option $option"; return $s_err ;; |
| esac |
| |
| local -i len="${#} / 2" |
| local -i i |
| |
| local -a path=( "${@:1:$len}" ); shift $len |
| local -a cont=( "${@}" ) |
| |
| if [ ${#path[@]} -ne ${#cont[@]} ]; then |
| lets -l -e "mismatch: $len, ${#path[@]} vs ${#cont[@]}" |
| return $s_err |
| fi |
| |
| # Remove comment before processing all files, for this module only |
| if [ -f "$__un_f" ]; then |
| cp "$__un_f" "$__un_f.bkp" # be safe |
| sed --follow-symlinks -i -E "s%^#(.+ $module .+)%\1%g" -- $__un_f |
| fi |
| |
| for i in ${!path[@]}; do |
| if ! $func ${cont[$i]} ${path[$i]}; then |
| lets -l -d "failed to ${func%_file} ${path[$i]}" |
| cp "$__un_f.bkp" "$__un_f" && rm -f "$__un_f.bkp" |
| return $s_err |
| fi |
| |
| # FIXME: handle file permissions in a proper way |
| case "${cont[$i]}" in *"_bang_"* ) chmod +x ${path[$i]} ;; esac |
| |
| __file_do_helper $option ${path[$i]} $__un_f || return $s_err |
| lets -l -d "${func%_file} $(basename ${path[$i]})" |
| done |
| |
| case "$option" in |
| --update ) |
| [ ! -f "$__un_f" ] && { |
| cp "$__un_f.bkp" "$__un_f" && rm -f "$__un_f.bkp"; return $s_err |
| } |
| |
| local -a __rmline |
| while read -a __rmline; do |
| local __cmd="${__rmline[0]}"; local __f="${__rmline[1]}" |
| case "${__rmline[*]}" in |
| '' | '\n' | '#'* ) |
| lets -l -d "skip: $__cmd: $(basename "$__f")" |
| continue ;; |
| * ) [ ! -f "$__f" ] && { |
| lets -l -w "$__cmd *not* installed: $(basename "$__f")" |
| } || { lets -l -i "$__cmd installed: $(basename "$__f")"; } |
| |
| eval "$__cmd $__f" && { |
| # Got to remove this line and it will be lost forever |
| sed --follow-symlinks -i -E "\:$__cmd $__f.*:d" -- "$__un_f" |
| } || { lets -l -e "failed to remove $__f"; } ;; |
| esac |
| done <<< $(grep -E "[[:space:]]$module[[:space:]]" -- $__un_f) ;; |
| esac |
| |
| rm -f "$__un_f.bkp" |
| return $s_ok |
| } |