#!/bin/bash
# A way of copying files using dsh
# Copyright (C) Frank Lee 24-10-2010
# 
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
# 
# 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., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
# 

# VERSION=0.1

# TODO: Command line processing ought to be handled better
#       Specifically: -q for quiet / -v for verbose
#                     -d for arguments to DSH
#                     -c for compare/verify, perhaps?

### How to use ###
function usage() {
 cat <<EOF
 Use: $0 source [source [source ...]] target

target may be a filename [in which case using more than one
source is probably not what you intend] or a directory name.

EOF
}

function fullusage() {
 usage
cat <<EOF
Examples:
 $0 file1 /etc/file2 /etc/copy/
  $0 creates /etc/copy and places file1 and /etc/file2 into it.

 $0 file1 file2 /etc/file3
  If /etc/file3 is a directory on all hosts, file1 and file2 
  will be copied into it.
  If /etc/file3 is not a directory on any host, file1 and file2
  will be copied sequentially to /etc/file3. (Thus /etc/file3
  will be a copy of file2.)
  If /etc/file3 is a directory on some, but not all, hosts,
  $0 will exit with error code 2 and a complaint.

 When copying files, therefore, specifying the trailing slash 
 on target makes things clearer. (And incidentally saves 
 checking whether target is a file or directory on all hosts
 which speeds things up.)

Arguments to DSH:
 By default dsh is called with the '-a' option. Options
 may be specified in the DSHARGS environment variable.

DSHARGS='-a' dcp ...
 Explicitly re-states the default behaviour

DSHARGS='-g 64bitnodes' dcp ...
 Runs the copy against nodes listed in the /etc/dsh/group/64bitnodes file.

Known bugs:
 * The base64-encoded temporary file on each node has to have the same
   name, which is chosen on the machine dcp is running from.
EOF
}

## Method of copying files ##
# base64-encode the source file and split into chunks of 1700 lines
# (Chosen by trial-and-error.) Use dsh to loop over each host sending 
# one chunk at a time, then use dsh to base64-decode the file into
# the destination.

### A function to copy a file using dsh ###
function copyfile() {
 SRC="$1"
 DST="$2"
 # Source must exist and be readable
 if ( ! ([ -f "$SRC" ] && [ -r "$SRC" ])) ; then
  echo Cannot read "$SRC". Stopping
  exit 1
 fi
 # base64-encode the file 
 ENCSRC=`tempfile`
 SPLITD=`mktemp -d -t dcp.XXXXXXXX`
 PREFIX=`tempfile --directory $SPLITD`
 PREFIX=`basename $PREFIX`
 echo -n Encoding \"$SRC\" ...
 base64 $SRC | (cd $SPLITD ; split -l 1700 - $PREFIX)
 echo done.
 # Loop over all chunks
 CLIENTTMP=`tempfile`
 echo Copying $SRC to $DST
 $REMOTE "rm -f $CLIENTTMP ; touch $CLIENTTMP"
 for F in $SPLITD/$PREFIX?? ; do
  $REMOTE "cat <<EOF >>$CLIENTTMP
`cat $F`
EOF"
 done
 # If target is directory, append filename
 if [ ${DST:(-1)} = / ] ; then
  DST="$DST"/`basename "$SRC"`
  DST=${DST/\/\//\/}
 fi
 # join chunks, decode, tidy up
 TDIR=`dirname "$DST"`
 echo Decoding $DST
 $REMOTE "mkdir -p \"$TDIR\"
          cat $CLIENTTMP | base64 -d >\"$DST\"; 
          rm $CLIENTTMP
          touch    -d \"`stat -c %y $SRC`\" \"$DST\";
          chgrp `stat -c %G $SRC` \"$DST\"; 
          chown `stat -c %U $SRC` \"$DST\"; 
          chmod `stat -c %a $SRC` \"$DST\""
 rm -Rf $SPLITD $ENCSRC $CLIENTTMP
}

DSHARGS=${DSHARGS:--a}
REMOTE="dsh $DSHARGS"
if [ "$1" = '-h' ] ; then
 fullusage
 exit 0
fi

if [ $# -lt 2 ] ; then
 usage
 exit 1
fi

# Last argument is target
MDST=${@:$#}

echo Analysing target \"$MDST\" on all nodes
# If DST ends with trailing /, create directory on target machines
if [ ${MDST:(-1)} = / ] ; then
 $REMOTE mkdir -p \"$MDST\"
fi
# If DST is a directory on all machines, adjust DST to be target file
if $REMOTE [ -d "$MDST" ] ; then
 # Don't adjust DST here, just leave as explicit directory
 MDST="$MDST"/
 MDST=${MDST/\/\//\/}
else 
 # Not a directory on all machines. Is it a directory on any machine?
 if ! $REMOTE [ ! -d "$MDST" ] ; then
  echo $MDST is a directory on some machines but not on others. Not sure what to do.
  exit 2
 fi
fi

I=1
while [ $I -lt $# ] ; do
 copyfile "${@:$I:1}" "$MDST"
 I=$((I+1))
done
