#!/bin/sh
##
##  lsync -- Access Layer Synchronization Tool
##  Copyright (c) 2000-2003 Ralf S. Engelschall <rse@engelschall.com> 
##
##  Permission to use, copy, modify, and distribute this software for
##  any purpose with or without fee is hereby granted, provided that
##  the above copyright notice and this permission notice appear in all
##  copies.
##
##  THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
##  WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
##  MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
##  IN NO EVENT SHALL THE AUTHORS AND COPYRIGHT HOLDERS AND THEIR
##  CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
##  SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
##  LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
##  USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
##  ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
##  OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
##  OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
##  SUCH DAMAGE.
##

##
##  filesystem hierarchy configuration
##

#   program name and version/date
progname="lsync"
progvers="1.0.4"
progdate="04-Aug-2001"

#   root directory
#   (if empty, .lsyncrc files provide default) 
root=""

#   subdirectory where packages are physically installed
pkgdir="PKG"

#   subdirectories which are synchronized between physically
#   installed package areas and access layer
subdirs="bin,sbin,man,info,include,lib"

##
##  command line option parsing
##

#   default run-time modes
nop=0  
quiet=0
trace=0
help=''
init=0
uninstall=0
local=0

#   be aware of .lsyncrc files
cwd=`pwd`
while [ 1 ]; do
    if [ -f "$cwd/.lsyncrc" ]; then
        set -- `cat $cwd/.lsyncrc` "$@"
    fi
    [ ".$cwd" = ./ ] && break
    cwd=`echo $cwd | sed -e 's;/[^/]*$;;' -e 's;^$;/;'`
done
if [ ".$HOME" != . -a -f "$HOME/.lsyncrc" ]; then
    set -- `cat $HOME/.lsyncrc` "$@"
fi

#   iterate over argument line
for opt
do
    case $opt in
        -*=*) arg=`echo "$opt" | sed 's/^[-_a-zA-Z0-9]*=//'` ;;
           *) arg='' ;;
    esac
    case $opt in
        -n|--nop       ) nop=1         ;;
        -q|--quiet     ) quiet=1       ;;
        -t|--trace     ) trace=1       ;;
        -v|--version   ) version=1     ;;
        -h|--help      ) help="Usage"  ;;
        -i|--init      ) init=1        ;;
        -u|--uninstall ) uninstall=1   ;;
        -l|--local     ) local=1       ;;
        --root=*       ) root=$arg     ;;
        --pkgdir=*     ) pkgdir=$arg   ;;
        --subdirs=*    ) subdirs=$arg  ;;
        *              ) help="Invalid option \`$opt'"; break ;;
    esac
done

#   error or usage message
if [ ".$help" != . ]; then
    if [ ".$help" != ".Usage" ]; then
        echo "$progname:ERROR: $help" 1>&2
    fi
    cat 1>&2 <<EOT
Usage: $progname [options]

Global options:
  --version,   -v   display tool version information
  --help,      -h   display usage information
  --init,      -i   create an initial directory hierarchy

Run-time options:
  --nop,       -n   perform no filesystem operations
  --quiet,     -q   display no verbose messages
  --trace,     -t   display performed filesystem operations
  --local,     -l   process a local package area only
  --uninstall, -u   uninstall all files

Filesystem options:
  --root=DIR        override root directory
  --pkgdir=DIR      override package sub-directory
  --subdirs=DIR     override synchronized sub-directories

Current configuration:
  root directory:       $root
  package root subdir:  $pkgdir
  synchronized subdirs: $subdirs
EOT
    if [ ".$help" != ".Usage" ]; then
        exit 2
    else
        exit 0
    fi
fi

#   version information
if [ ".$version" = .1 ]; then
    echo "$progname $progvers ($progdate)"
    exit 0
fi

#   make sure a root directory was found or specified
if [ ".$root" = . ]; then
    echo "$progname:ERROR: no root directory specified!" 1>&2
    echo "$progname:HINT: use --root=DIR option explicitly on command line" 1>&2 
    echo "$progname:HINT: or implicitly inside an .lsyncrc file in your home" 1>&2
    echo "$progname:HINT: directory or in any parent directory." 1>&2
    exit 3
fi

##
##  helper functions
##

display_hd () {
    if [ ".$headline" != . ]; then
        if [ ".$quiet" = .0 ]; then
            echo "$headline"
        fi
        headline=''
    fi
}

display_op () {
    if [ ".$quiet" = .0 ]; then
        echo "  $@"
    fi
}

display_warning () {
    echo "$progname:WARNING: $*" 1>&2
}

display_error () {
    echo "$progname:ERROR: $*" 1>&2
}

perform_op () {
    if [ ".$trace" = .1 ]; then
        echo "  \$ $@"
    fi
    if [ ".$nop" = .0 ]; then
        eval "$@"
    fi
}

##
##  main processing
##

#   extend a "man" subdir to a complete list with subdirs
#   in order to avoid special cases in the loop processing
manex=''
if [ ".$init" = .1 ]; then
    manex='man'
fi
for i in 1 2 3 4 5 6 7 8; do
    manex="$manex,man/man$i"
done
manex=`echo $manex | sed -e 's;^,;;'`
subdirs=`echo $subdirs | sed -e "s;man;$manex;"`

#   special processing: create initial hierarchy
if [ ".$init" = .1 ]; then
    if [ ! -d $root ]; then
        echo "creating $root"
        perform_op "mkdir $root" || exit 1
    fi
    for subdir in $pkgdir `IFS=,; echo $subdirs`; do
        if [ ! -d "$root/$subdir" ]; then
            echo "creating $root/$subdir"
            perform_op "mkdir $root/$subdir" || exit 1
        fi
    done
    exit 0
fi

#   make sure the root directory actually exists
if [ ! -d "$root" ]; then
    display_warning "root directory \`$root' does not exist"
    exit 3
fi

#   if processing is restricted to a local package area, pre-determine its name
if [ ".$local" = .1 ]; then
   realroot=`cd $root; pwd`
   realthis=`pwd`
   pkgname=`expr "$realthis" : "^$realroot/$pkgdir/\\([^/]*\\).*"`
   if [ ".$pkgname" = . ]; then
       display_error "you are not staying under a local package sub-directory"
       exit 3
   fi
fi

#   now perform the synchronization for each sub-directory...
for subdir in `IFS=,; echo $subdirs`; do
    headline="$root/$subdir:"

    #   make sure the subdir actually exists in the access layer
    if [ ! -d "$root/$subdir" ]; then
        display_warning "access layer directory \`$root/$subdir' does not exist"
        continue
    fi

    #   
    #   PASS 1: remove dangling symbolic links in access layer
    #
    
    #   iterate over all symlinks in the access layer subdir
    for link in . `ls "$root/$subdir/" | sed -e "s;^$root/$subdir/*;;g"`; do
        test ".$link" = ".." && continue

        #   determine the target file of the symlink
        target=`ls -l "$root/$subdir/$link" 2>/dev/null | sed -e 's;.*-> *;;'`
        if [ ".$target" = . ]; then
            display_warning "$root/$subdir/$link seems to be not a symbolic link"
            continue
        fi

        #   (optionally) make sure that link target points into local package area
        if [ ".$local" = .1 -a .`expr $target : "../$pkgdir/$pkgname/.*"` = .0 ]; then
            continue
        fi

        #   check whether link is valid, i.e., points to
        #   an existing target file or directory
        if [ ".$uninstall" = .1 ] ||\
           [ ! -f "$root/$subdir/$target" -a \
             ! -d "$root/$subdir/$target"      ]; then
            #   target no longer exists, so remove dangling symlink
            display_hd
            display_op "remove: $link -> $target"
            perform_op "rm -f $root/$subdir/$link"
        fi
    done

    #   if we are uninstalling only, our work is now done
    if [ ".$uninstall" = ".1" ]; then
        continue
    fi

    #
    #   PASS 2: create new symbolic links in access layer
    #

    #   calculate the corresponding reverse directory for the current subdir
    revdir=`echo $subdir | sed -e 's;[^/][^/]*;..;g'`

    #   iterate over all package directories
    for dir in . `ls "$root/$pkgdir/" | sed -e "s;^$root/$pkgdir/*;;g"`; do
        test ".$dir" = ".." && continue

        #   (optionally) make sure that we operate only for the local package area
        if [ ".$local" = .1 -a ".$dir" != ".$pkgname" ]; then
            continue
        fi

        #   skip all directories with appended version numbers
        #   in order to support manual versioning of packages
        case $dir in
            *-[0-9]* ) continue ;;
        esac

        #   skip if package directory or package sub-directories has sticky bit set
        if [ ".`ls -l -d $root/$pkgdir/$dir 2>/dev/null | cut -c10`" = .t ] ||\
           [ ".`ls -l -d $root/$pkgdir/$dir/$subdir 2>/dev/null | cut -c10`" = .t ]; then
            continue
        fi

        #   check whether the processed subdir exists in package area
        if [ -d "$root/$pkgdir/$dir/$subdir" ]; then

            #   iterate over all files/directories in package's subdir
            for file in . `ls "$root/$pkgdir/$dir/$subdir/" |\
                           sed -e "s;^$root/$pkgdir/$dir/$subdir/*;;g"`; do
                test ".$file" = ".." && continue

                #   calculate the access layer symlink target
                target="$revdir/$pkgdir/$dir/$subdir/$file"

                #   check whether a possibly conflicting symlink exists
                exlink=`ls -l $root/$subdir/$file 2>/dev/null`
                if [ ".$exlink" != . ]; then
                    extarget=`echo $exlink | sed -e 's;.*-> *;;'`
                    if [ ".$extarget" = . ]; then
                        display_warning "$root/$subdir/$file exits, but seems to be not a symbolic link"
                    elif [ ".$extarget" != ".$target" ]; then
                        display_hd
                        display_op "conflict: $file -> $extarget [existing]"
                        display_op "          $file -> $target [alternative]"
                    fi
                    continue
                fi

                #   create new symlink in access layer
                display_hd
                display_op "create: $file -> $target"
                perform_op "cd $root/$subdir && ln -s $target $file"
            done
        fi
    done
done

