#!/bin/zsh # http://Yost.com/computers/yostupload # See notice of copyright and license at end. # To do: # * Provide a way for the root file to specify # files and folders that are not to be uploaded # in other words, an easy front end to rsync --exclude # * specify permissions for dirs and files # instead of always complaining if not world-accessible #------------------------------------------------------------------------------- # Constants commandName=${0##*/} #commandDir=${0%/*} #if [[ "$commandDir" == "$commandName" ]] ; then commandDir=`pwd` ; fi #if [[ ${commandDir##/} == "$commandDir" ]] ; then commandDir=`pwd`/$commandDir ; fi rootFile=.YostUploadRoot rsyncStdArgsUp=-rRhHLtp rsyncStdArgsDown=-rRhHtp #------------------------------------------------------------------------------- summary() { echo 1>&2 " Usage: $commandName [ -d ] [ -v ] [ -n ] [ --rsync-args args ] [ -r rootFile ] [ path ... ] $commandName -l [ -r rootFile ] [ path ] $commandName -f [ -r rootFile ] [ path ] $commandName -u [ -r rootFile ] [ path ]" } optionError() { echo 1>&2 " $commandName: $1 Try --help " exit 2 } usage() { if [[ $1 != '' ]] ; then echo 1>&2 "\n$1" ; fi summary echo 1>&2 " path defaults to the current directory. Uploads files unless -d to download instead. Or: use -l to ssh to the server. The remote shell's current directory will correspond to path. Or: use -f to output path to rootFile. Or: use -u to output the URL for path. All path arguments (files and/or folders) must be within a folder hierarchy that serves as a local staging area for a folder on the server. yostupload uploads (or downloads) everything between their corresponding locations in the local hierarchy and the server hierarchy. The root of the local hierarchy is identified by the fact that it contains a file called \"$rootFile\"; $commandName executes this file and expects it to output key-value pairs, as in this example: protocol rsync command /usr/local/bin/rsync host upload.yourhost.com url https://somedomain.com/ port 12345 user neddie dir public_html rsync-args-up --bwlimit=3 finishUp echo Upload succeeded \$(date) * protocol can be rsync, ftp, sftp, or omitted (default is rsync). * command is optional and defaults to the protocol name. * host is a domain name or IP address with no port appended. * url is optional, defaults to http://\$host; used only by the -u option. * port is optional. * dir is the path on the server; default is your home directory. * rsync-args, rsync-args-up, and rsync-args-down are optional; if present, these arguments are passed to rsync for up/download after the standard upload args $rsyncStdArgsUp or download args $rsyncStdArgsDown. Don't use --rsh here. * finishUp is a command to be run via ssh on host after a successful rsync upload. For example, after uploading crontab, this installs that crontab file on host and reports back the result: finishUp crontab crontab ; crontab -l Use rsync if possible; ftp and sftp copy with world-readable permissions. The rsync command sets permissions on the server to those in the local hierarchy, so when the destination is a public web site, local dirs should be searchable by all and local files should be readable by all; $commandName warns if some permissions are too restrictive. The -v option shows the rsync, ftp, or sftp command actually used and puts the command itself into verbose mode. The -n option sets -v and refrains from actually running the command. The -r rootFile option specifies that the root filename is foo instead of \"$rootFile\". Unfortunately, older versions of rsync can't download more than one thing at a time, so the script as is won't try, but the script provides for this if you want to modify the script. http://Yost.com/computers/yostupload " exit 2 } #------------------------------------------------------------------------------- # Gobals server= rootDir= file= sshPortOption= trouble= sourceDirs=() sourceFiles=() # rootFile options protocol= host= port= user= remoteDir= rsyncArgs= rsyncArgsUp= rsyncArgsDown= finishUp= #--------------------- argsRsync=() argRoot=() argVerbose=() argDryRun=() argDownload=() argLogin=() zparseopts -D -K - -help=argHelp \ -rsync-args:=argsRsync \ r:=argRoot \ v=argVerbose \ n=argDryRun \ d=argDownload \ l=argLogin \ f=argRootPath \ u=argURL # Max one of these: -d -f -u # Max one path arg for -f and -u # Bug: must fix the code below to enforce these. # BUG This doesn't work, and what if --rsync-args and no arg after it? if [[ $#argLogin != 0 && $#argsRsync > 1 ]] ; then optionError "-l and --rsync-args options are incompatible" fi if [[ $#argRootPath != 0 && $#argsRsync > 1 ]] ; then optionError "-f and --rsync-args options are incompatible" fi if [[ $#argLogin != 0 && $#argDownload != 0 ]] ; then optionError "-l and -d options are incompatible" fi if [[ $#argRootPath != 0 && $#argDownload != 0 ]] ; then optionError "-f and -d options are incompatible" fi if [[ $#argRootPath != 0 && $#argLogin != 0 ]] ; then optionError "-f and -l options are incompatible" fi case $1 in -*) optionError "Unknown option: $1" ;; esac if [[ $#argHelp != 0 ]] ; then usage fi if [[ $#argDryRun != 0 ]] ; then argVerbose=(-v) fi argsRsync=${argsRsync:#--rsync-args} if [[ $#argRoot != 0 ]] ; then rootFile=${argRoot[2]} fi #------------------------------------------------------------------------------- echo-quoted() { local noNewlineArg case "$1" in -n) noNewlineArg=-n ; shift ;; *) noNewlineArg= ;; esac local first=YES local output='' while [[ $# != 0 ]] do case "$first" in YES) first=NO ;; *) output=$output' ' ;; esac case "$1" in "") output=$output"''" ;; # warning: what you see on the following line is really *' \t '*) *['" []()&$*|;<>?`'"'"]*) case "$1" in *'"'*"'"* | *"'"*'"'*) # both kinds of quote - output in '' output=$output"'" output=$output`echo "$1" | sed "s/'/\\\\\\'/g"` output=$output"'" ;; *"'"*) # single quote only - output in "" output=$output'"' output=$output`echo "$1" | sed 's/\([\`$"]\)/\\\\\1/g'` output=$output'"' ;; *) # double quote or no quotes - output in '' output=$output"'$1'" esac ;; *) output=$output$1 esac shift done echo $noNewlineArg "$output" } echo-command() { echo -n 1>&2 "[ " echo-quoted -n 1>&2 $@ echo 1>&2 " ]" } echodo() { if [[ $#argVerbose != 0 ]] ; then eval "echo-command $@" fi eval "$@" } checkOK() { local rootTmp="$1" local name="$2" local value="$3" if [[ ! -z "$value" \ && "${value##* }" != "$value" \ ]] then echo 1>&2 "$commandName: $rootTmp/$rootFile $name is bad: \"$value\"" trouble=true fi } # sets $server setServer() { local node=$1 local params if [[ ! -z "$server" \ && "$server" != $node \ ]] \ then echo echo 1>&2 "$commandName: can't upload files from two different roots: $server and $node" trouble=true return 1 fi server=$node if ! params="$($node/$rootFile)" ; then echo echo 1>&2 "$commandName: Trouble executing: $node/$rootFile" trouble=true return 1 fi protocol="$(echo "$params" | /usr/bin/sed -n 's,^[ ]*protocol[ ][ ]*,,p' )" host="$(echo "$params" | /usr/bin/sed -n 's,^[ ]*host[ ][ ]*,,p' )" url="$(echo "$params" | /usr/bin/sed -n 's,^[ ]*url[ ][ ]*,,p' )" port="$(echo "$params" | /usr/bin/sed -n 's,^[ ]*port[ ][ ]*,,p' )" user="$(echo "$params" | /usr/bin/sed -n 's,^[ ]*user[ ][ ]*,,p' )" remoteDir="$(echo "$params" | /usr/bin/sed -n 's,^[ ]*dir[ ][ ]*,,p' )" rsyncArgs="$(echo "$params" | /usr/bin/sed -n 's,^[ ]*rsync-args[ ][ ]*,,p' )" rsyncArgsUp="$(echo "$params" | /usr/bin/sed -n 's,^[ ]*rsync-args-up[ ][ ]*,,p' )" rsyncArgsDown="$(echo "$params" | /usr/bin/sed -n 's,^[ ]*rsync-args-down[ ][ ]*,,p')" finishUp="$(echo "$params" | /usr/bin/sed -n 's,^[ ]*finishUp[ ][ ]*,,p' )" if [[ -z "$user" ]] ; then echo 1>&2 "$commandName: user must be specified in $node/$rootFile" trouble=true else checkOK $node user "$user" fi if [[ -z $url ]] ; then url="http://$host/" fi if [[ -n $port ]] ; then sshPortOption=" -p $port" fi case "$protocol" in rsync | "") protocol=rsync command="rsync --rsh 'ssh -l $user$sshPortOption'" ;; ftp|sftp) if [[ -z "$host" ]] ; then echo 1>&2 "$commandName: host must be specified in $node/$rootFile" trouble=true return 1 else checkOK $node host "$host" fi local portOption if [[ -n $port ]] ; then portOption=":$port" fi command="$protocol $user@${host}$portOption" ;; *) echo 1>&2 "$commandName: protocol in $node/$rootFile must rsync, ftp, or sftp." trouble=true esac if [[ -z "$command" ]] ; then command="$(echo "$params" | /usr/bin/sed -n 's,^[ ]*command[ ]*,,p'))" fi rootDir=$node/ #echo host=$host #echo user=$user #echo remoteDir=${(qq)remoteDir} #echo rootDir=$rootDir return 0 } mkdirp() { local node=$1 while [[ $node != . ]] do echo mkdir '"'"$node"'"' node=$node:h done } # Find rootDir for the given path. # Sets $rootDir to the dir containing $rootFile. # Sets $file to the part of the path that starts at $rootDir. findRoot() { local arg="$1" local node="$arg" file= if [[ $#argDownload != 0 ]] ; then # Download # Deal with possibility that whole path doesn't exist locally. while [[ "$node" != / ]] do if [[ -e "$node" ]] ; then break fi if [[ -z "$file" ]] ; then file=$node:t else file=${node:t}/$file fi node=$node:h done fi if [[ ! -d "$node" ]] ; then file=${node##*/} node=${node%/*} if [[ "$node" == "$file" ]] ; then node=. fi fi #echo \===1 node=$node file=$file if [[ "$node" == *..* ]] ; then node="$(realpath $node)" fi #echo \===2 node=$node file=$file if [[ "$node" == "${node#/}" ]] ; then if [[ "$node" == . ]] ; then node="$(pwd -P)" else node="$(pwd -P)/$node" fi fi #echo \===3 node=$node file=$file host= user= remoteDir= rootDir= local foundRoot= while [[ $node != / ]] do if [[ -x $node/$rootFile ]] ; then if ! setServer "$node" ; then // $trouble is set return 1 fi foundRoot=true if [[ -z "$file" ]] ; then file= fi break fi if [[ -z "$file" ]] ; then file=$node:t else file=${node:t}/$file fi node=$node:h done if [[ ! -z "$trouble" ]] ; then return 1 fi if [[ -z "$foundRoot" ]] ; then echo echo 1>&2 "$commandName: Can't find $rootFile file for $arg" return 1 fi return 0 } # Add an argument to sourceDirs or sourceFiles as appropriate # after finding its root directory. examine() { local arg="$1" if [[ $#argDownload == 0 ]] ; then # Upload if [[ ! -e $arg ]] ; then echo echo 1>&2 "$commandName: Does not exist: $arg" trouble=true return 1 fi fi if ! findRoot "$arg" ; then trouble=true return fi #echo file=$file if [[ -d "$rootDir$file" ]] ; then sourceDirs+="${file:-.}" else sourceFiles+="$file" fi } finish() { if [[ ! -z $finishUp ]] then if [[ $#argVerbose != 0 && ! -z $finishUp ]] ; then echo "[ Execute on $host: $finishUp ]" fi sshCommand "$finishUp" fi } sshCommand() { local baseDir if [[ -n $sourceDirs && -n $sourceDirs[0] ]] then baseDir=$sourceDirs[0] else baseDir=$sourceFiles[0]:h fi # Unfortunately, there is no way to make the following code work on all remote shells. # The code as I provide it works when zsh is the remote shell. If you're using bash # as the remote shell, move the -l before the $0. if [[ $#argVerbose != 0 ]] then echo 1>&2 "[ ssh -t -l $user $host$sshPortOption "'"'"cd ${(qq)remoteDir}/${(qq)baseDir} && $@"'"'" ]" fi if [[ $#argDryRun != 0 ]] then return 0 fi if ssh -t -l $user $host$sshPortOption "cd ${(qq)remoteDir}/${(qq)baseDir} && $@" ; then return 0 else echo 1>&2 "$commandName: trouble executing ssh" return 2 fi } #------------------------------------------------------------------------------- # Examine the args to find $rootFile, etc. if [[ $# < 1 ]] ; then # No arguments; do current directory. if [[ $#argVerbose != 0 ]] ; then echo -n $commandName: examining current directory' ' fi examine . else # With arguments if [[ $#argVerbose != 0 ]] ; then echo -n $commandName: examining arguments' ' fi for arg in "$@" do if [[ $#argVerbose != 0 ]] ; then echo -n . fi examine "$arg" done fi if [[ ! -z "$trouble" ]] ; then exit 2 fi if [[ $#argVerbose != 0 ]] ; then echo fi #echo rootDir=$rootDir #echo sourceDirs="${sourceDirs[@]}" #echo sourceFiles="${sourceFiles[@]}" if [[ $#argLogin != 0 ]] ; then # ssh to the remote dir exec sshCommand 'exec $0 -l' exit 2 fi if [[ $#argRootPath != 0 ]] ; then echo ${rootDir}$rootFile exit 0 fi if [[ $#argURL != 0 ]] ; then eval echo ${url}${(qq)file} exit 0 fi set -eu if [[ $#argVerbose != 0 ]] ; then echo "[ cd ${(qq)rootDir} ]" fi cd $rootDir case $protocol in rsync) ( if [[ $#argDownload == 0 ]] ; then # Upload if [[ "${(u)#sourceDirs[@]}" != 0 ]] ; then local count count=$(find "${(u)sourceDirs[@]}" -type d ! -perm -111 | wc -l) if [[ $(($count)) != 0 ]] ; then echo 1>&2 "$commandName: Warning: permissions are too restrictive on some folders" fi count=$(find "${(u)sourceDirs[@]}" -type f ! -perm -444 | wc -l) if [[ $(($count)) != 0 ]] ; then echo 1>&2 "$commandName: Warning: permissions are too restrictive on some files" fi fi if [[ "${(u)#sourceFiles[@]}" != 0 ]] ; then if ls -l "${(u)sourceFiles[@]}" | grep -v '^.r..r..r' > /dev/null ; then echo 1>&2 "$commandName: Warning: permissions are too restrictive on some files" fi fi echodo \ $command \ $rsyncStdArgsUp \ ${rsyncArgs[@]} \ ${rsyncArgsUp[@]} \ ${argsRsync[@]} \ ${argVerbose:+--progress} \ $argDryRun \ ${(qq)sourceDirs[@]} \ ${(qq)sourceFiles[@]} \ ${host}:${(qq)remoteDir} \ && finish else # Download local downloadArgs if true ; then # Support for --files-from is not yet universal local items items=($sourceDirs[@] $sourceFiles[@]) for ((i = 1 ; i <= $#items ; ++i)) do if [[ "${items[i]##* }" != "$items[i]" ]] ; then items[i]="${items[i]// /\ }" fi done downloadArgs="--rsync-path='cd ${(qq)remoteDir};rsync' ${host}:'$items[@]'" else # This is another way in more recent versions of rsync. local items= for x in "$sourceDirs[@]" "$sourceFiles[@]" do items="$items$x " done echo "$items" > $rootDir/.rsyncFileList chmod 600 $rootDir/.rsyncFileList echodo rsync -p \ $argVerbose \ $rootDir/.rsyncFileList \ $host${(qq)remoteDir} downloadArgs="--files-from=:${(qq)remoteDir}/.rsyncFileList $items" fi echodo \ $command \ $rsyncStdArgsDown \ ${rsyncArgs[@]} \ ${rsyncArgsDown[@]} \ ${argsRsync[@]} \ ${argVerbose:+--progress} \ $argDryRun \ $downloadArgs \ . fi ) ;; ftp|sftp) if [[ $#argDownload != 0 ]] ; then echo 1>&2 "$commandName: Only upload supported for ftp in this release." exit 2 fi if [[ $#argVerbose != 0 ]] ; then echo $protocol $user@$host fi if [[ $#argDownload != 0 ]] ; then echo 1>&2 "$commandName: Only upload supported for ftp in this release." exit 2 fi ( echo "binary on" if [[ $#argVerbose != 0 ]] ; then echo "verbose on" else echo "verbose off" fi if [[ ! -z "${(qq)remoteDir}" ]] ; then echo "cd ${(qq)remoteDir}" fi ( local file for file in "${(ou)sourceDirs[@]}" do mkdirp "$file" find "$file" -type d | sed 's,.*,mkdir "&",' done ) | sort -u ( local file for file in "${(ou)sourceDirs[@]}" do find "$file" -type f | sed 's,.*,put "&" "&",' done ) | sort -u ) \ | tee /tmp/$commandName-$(date '+%Y-%m-%dT%H:%M:%S') \ | \ if [[ $#argVerbose != 0 ]] ; then tee /dev/tty else cat fi \ | \ if [[ $#argDryRun == 0 ]] ; then eval ${command[@]} fi ;; esac # Copyright 2005-2008 Dave Yost # All rights reserved. # This version is # yostupload 3.8 2008-06-29 # which at time of this publication can be found at: # http://Yost.com/computers/yostupload # Redistribution and use in the form of source code or derivative data built # from the source code, with or without modification, are permitted provided # that the following conditions are met: # 1. THE USER AGREES THAT THERE IS NO WARRANTY. # 2. If and only if appropriate, the above phrase "This version is" must be # followed by the phrase "a modified form of" or "extracted from" or # "extracted and modified from". # 3. Redistributions of source code must retain this notice intact. # 4. Redistributions in the form of derivative data built from the source # code must reproduce this notice intact in the documentation and/or other # materials provided with the distribution, and each file in the derivative # data must reproduce any Yost.com URI included in the original distribution. # 5. Neither the name of Dave Yost nor the names of its contributors may be # used to endorse or promote products derived from this software without # specific prior written permission. # 6. Written permission by the author is required for redistribution as part # of a commercial product. # This notice comprises all text from "Copyright" above through here.