blob: 663413bdff470f9f320efa08b29523180bf686e9 [file] [log] [blame]
#!/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
}