#!/bin/sh # # Copyright (c) 2013-2014 Codethink Ltd. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License Version 2 as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. set -eu : ${mounting_script="$(dirname "$0")/../libexec/mount-system-versions-dir"} : ${unmount="umount"} usage() { echo "Usage: $(basename $0) merge OLD_VERSION_LABEL NEW_VERSION_LABEL" >&2 echo " $(basename $0) sync CANONICAL_VERSION_LABEL" >&2 exit 1 } die () { echo $@ >&2 exit 1 } file_type() { mode=$(printf '%o\n' 0x$(stat -c %f "$1")) case "$mode" in 140???) echo "socket" ;; 120???) echo "symlink" ;; 100???) echo "regular" ;; 60???) echo "block" ;; 40???) echo "directory" ;; 20???) echo "char" ;; 10???) echo "fifo" ;; *) echo "Unknown file type $1: $mode" 1>&2; exit 1 ;; esac } check_same_type() { type1="$(file_type $1)" type2="$(file_type $2)" if [ "$type1" != "$type2" ]; then if [ "$type1" = symlink ] || [ "$type2" = symlink ]; then # We allow moving content and leaving a compatibility symlink, as # long as the content of the file didn't change. if ! cmp -s "$1" "$2"; then die "ERROR: $3: replacing a file with a symlink is only " \ "supported if there are no changes to the content." fi else die "ERROR: found two different types for '$3':" \ "$type1 and $type2" fi fi } merge() { local vp_dir="$1" # version being processed local v1_dir="$2" # factory version local vu_dir="$3" # user modified version local v2_dir="$4" # unmodified new deployed version local vt_dir="$5" # target version where the result of the # merge should be placed # use `find "$vp_dir/"*` instead of `find "$vp_dir"` because # the last one also gives $vp_dir in the list of directories find "$vp_dir/"* | while read -r f; do # echo "Processing $f" # strip first component from file name local stripped_filename=${f#$vp_dir/} local vp="$vp_dir/$stripped_filename" local v1="$v1_dir/$stripped_filename" local vu="$vu_dir/$stripped_filename" local v2="$v2_dir/$stripped_filename" local vt="$vt_dir/$stripped_filename" if [ ! -e "$vt" ]; then if [ -e "$v1" -a -e "$vu" ]; then check_same_type "$v1" "$vu" "$stripped_filename" fi if [ -e "$vu" -a -e "$v2" ]; then check_same_type "$vu" "$v2" "$stripped_filename" fi if [ -e "$v1" -a -e "$v2" ]; then check_same_type "$v1" "$v2" "$stripped_filename" fi if [ -d "$vp" -a ! -h "$vp" ]; then mkdir "$vt" elif [ -h "$vp" ]; then # Symbolic links. if [ -h "$vu" -a -h "$v1" ]; then if [ "$(readlink $v1)" == "$(readlink $vu)" ]; then # User hasn't overriden the old v1 value: switch # to v2. cp -a "$v2" "$vt" else # User overwrote the v1 value; keep what they did cp -a "$vu" "$vt" fi elif [ -h "$vu" ]; then # Old behaviour that may not make sense: preserve user # created symlink, if original was not a link cp -a "$vu" "$vt" elif [ -h "$v2" ]; then # Old behaviour that may not make sense: use newly # created link, if original was not a link cp -a "$v2" "$vt" else cp -a "$v1" "$vt" fi elif [ -f "$vp" ]; then merge_regular_file "$v1" "$vu" "$v2" "$vt" else die "ERROR: unexpected error" fi fi done } merge_regular_file() { local v1="$1" local vu="$2" local v2="$3" local vt="$4" # Reference table for merging a regular file # # V1 Vuser V2 action # ------------------------------------------ # none none none inconceivable! # exists none none do nothing # none exists none use Vuser # none none exists use V2 # exists none exists use V2 # exists exists none use Vuser # none exists exists diff V2 Vuser applied to V2 # exists exists exists diff V1 Vuser applied to V2 v1_exists=$(test -f "$v1" && echo exists || echo none) vu_exists=$(test -f "$vu" && echo exists || echo none) v2_exists=$(test -f "$v2" && echo exists || echo none) case "$v1_exists $vu_exists $v2_exists" in 'exists none none') # Do nothing, if the file was removed in vu and v2 doesn't have it, # then the file is not longer needed echo "File $v1 was removed by the user, no longer exists" ;; 'none exists none') cp -a "$vu" "$vt" echo "File $vu created by the user, copied to $vt" ;; 'none none exists') cp -a "$v2" "$vt" echo "File $v2 only present in the new version, copied to $vt" ;; 'exists none exists') cp -a "$v2" "$vt" echo "File $v2, removed by the user, copied to $vt" ;; 'exists exists none') cp -a "$vu" "$vt" echo "File $vu, not present in the new version, copied to $vt" ;; 'none exists exists') cp -a "$v2" "$vt" # If v2 == vu, then use v2 if ! cmp -s "$v2" "$vu"; then if ! (diff -u "$v2" --label="$v2" "$vu" --label="$vu" | patch "$vt" -f); then cp -a "$v2" "$vt" # merge failed, use v2 # 'patch' creates a file '.rej' with the diff that did not apply echo "File $vt left as it was in $v2 as merging failed" else echo "File $vt was pached with diff between $v2 and $vu" fi else echo "Files $vu and $v2 are the same, not patching" fi ;; 'exists exists exists') cp -a "$v2" "$vt" # If v2 == vu, then use v2 if ! cmp -s "$v2" "$vu"; then if ! (diff -u "$v1" --label="$v1" "$vu" --label="$vu" | patch "$vt" -f); then cp -a "$v2" "$vt" # merge failed, use v2 # 'patch' creates a file '.rej' with the diff that did not apply echo "File $vt left as it was in $v2 as merging failed" else echo "File $vt was pached with diff between $v1 and $vu" fi else echo "Files $vu and $v2 are the same, not patching" fi ;; *) die "ERROR: unexpected error" ;; esac } if [ "$#" = 0 ]; then usage "$0" fi if [ "$1" = "merge" ]; then if [ "$#" != 3 ]; then usage "$0" fi old_version="$2" new_version="$3" mounting_point=$(mktemp -d) "$mounting_script" "$mounting_point" if [ ! -d "$mounting_point/systems/$new_version" ]; then "$unmount" "$mounting_point" die "Error: version not found - '$new_version'" fi v1_dir="$mounting_point/systems/$old_version/orig/etc" vu_dir="$mounting_point/systems/$old_version/run/etc" v2_dir="$mounting_point/systems/$new_version/run/etc" vt_dir="$mounting_point/systems/$new_version/run/etc.new" mkdir "$vt_dir" trap 'rm -rvf "$vt_dir"; "$unmount" "$mounting_point"' EXIT # For every pathname in V1, Vuser, or V2 merge "$v1_dir" "$v1_dir" "$vu_dir" "$v2_dir" "$vt_dir" merge "$vu_dir" "$v1_dir" "$vu_dir" "$v2_dir" "$vt_dir" merge "$v2_dir" "$v1_dir" "$vu_dir" "$v2_dir" "$vt_dir" if [ -f "$vu_dir/passwd" ]; then cp "$vu_dir/passwd" "$vt_dir/passwd" fi if [ -f "$vu_dir/group" ]; then cp "$vu_dir/group" "$vt_dir/group" fi rm -rf "$v2_dir" mv "$vt_dir" "$v2_dir" elif [ "$1" = "sync" ]; then canonical_version="$2" mounting_point=$(mktemp -d) "$mounting_script" "$mounting_point" trap '"$unmount" "$mounting_point"' EXIT if [ ! -d "$mounting_point/systems/$canonical_version" ]; then die "Error: version not found - '$canonical_version'" fi for version_dir in "$mounting_point/systems/"*; do version="$(basename "$version_dir")" if [ -d "$version_dir" -a ! -h "$version_dir" \ -a $version != $canonical_version ]; then cp -a "$mounting_point/systems/$canonical_version/run/etc" \ "$version_dir/run/etc.new" mv "$version_dir/run/etc" "$version_dir/run/etc.old" mv "$version_dir/run/etc.new" "$version_dir/run/etc" rm -rf "$version_dir/run/etc.old" fi done else usage "$0" fi