diff options
Diffstat (limited to 'Misc/zftp-functions')
-rw-r--r-- | Misc/zftp-functions | 1281 |
1 files changed, 1281 insertions, 0 deletions
diff --git a/Misc/zftp-functions b/Misc/zftp-functions new file mode 100644 index 000000000..a07e46d72 --- /dev/null +++ b/Misc/zftp-functions @@ -0,0 +1,1281 @@ +# zftp is a loadable module implementing an FTP client as a builtin +# command so that you can use the shell command language and line +# editing to make life easier. If your system has dynamically +# load libraries and zsh was compiled to use them, it is probably +# somewhere where it can be loaded at run time. Otherwise, it depends +# whether the shell was compiled with zftp already built into it. +# +# Here is a suite of functions, plus assorted other code, to make +# zftp work smoothly. +# +# Completion is implemented in a fairly natural way, except that +# very little support has been provided for non-UNIX remote hosts. +# On such machines, the safest thing to do is only try to complete +# files in the current directory; this should be OK. +# +# Remote globbing for commands which retrieve files is also +# implemented. This can be done in two different ways. The default +# is for zsh to do the globbing locally. The advantage is that full +# zsh pattern matching (respecting the setting of extendedglob) is +# possible, and no assumption (apart from the restrictions on +# directory handling noted above) is made about the behaviour of the +# server. The disadvantage is that the entire filename list for the +# current directory must be retrieved, and then zsh must laboriously +# do pattern matching against every file, so it is potentially slow +# for large directories. Only the non-directory part of file names is +# globbed. +# +# The alternative will be used if $zfrglob has non-zero length. +# Zsh then sends the pattern to the server for globbing. Best of +# luck. +# +# To support remote globbing, some functions have been aliased +# with 'noglob' in front. Currently, this has a dire effect on +# completion unless the completeinaliases option is set, so +# it is set below. This can conceivably cause you problems +# if you expect completion for aliases automatically to give you +# completion for the base command. I suspect that most people +# don't even know that happens. +# +# The following functions are provided. +# +# General status changing and displaying functions: +# zfparams +# Simple front end to `zftp params', except it will automatically +# query host, user and password. These are then stored to be +# used with a `zfopen' with no arguments. +# zfopen [ host [ user ... ] ] +# Open a connection and login. Unless the option -1 (once) +# is given, will store the parameters for the open (including +# a password which is prompted for and not echoed) so that +# if you call zfopen subsequently without arguments it will +# reopen the same connection. +# zfanon anonftphost +# Open a connection for anonymous FTP. Tries to guess an +# email address to use as the password, unless $EMAIL_ADDR is +# already set. The first time, will tell you what it has guessed. +# It's rude to set EMAIL_ADDR=mozilla. +# zfcd [ dir | old new ] +# Change directory on the server. This tries to mimic the behaviour +# of the shell's cd. In particular, +# zfcd change to '~' on server, if it interprets it +# zfcd - change to previous directory of current connection +# zfcd OLD NEW change directory from fooOLDbar to fooNEWbar +# One piece of magic is builtin: an initial part of the directory +# matching $HOME is translated back to `~'. Most UNIX servers +# recognise the usual shell convention. So things like `zfcd $PWD' +# is useful provide you are under your home directory and the +# structure on the remote machine mirrors that on the local. +# zfhere +# Synonym for `zfcd $PWD', see above. +# zfdir [args] +# Show a long diretory list of the remote connection. Any +# arguments are passed on to the server, apart from options. +# Currently this always uses a pager to show the directory +# list. Caching is implemented: zfdir on its own always shows +# the current diretory, which is cached; zfdir with some other +# directory arguments shows that, which is cached separately +# and can be reviewed with `zfdir -r'. Other options: +# -f force reget, overriding the cache, in case something's changed +# -d delete the cache, but don't show anything. +# To pass options to the server, use e.g. `zfdir -- -C'. +# This also has the zfcd ~ hack. +# zfls [args] +# Short list of the long directory, depending on what [args] +# do to the server. No options, no caching, no pager. +# zftype [ a[scii] | i[mage] | b[inary] ] +# Set or display the transfer type; currently only ASCII +# and image (same as binary) types are supported. +# zfclose +# Close the connection. +# zfstat +# Print the zftp status from local variables; doesn't do any network +# operations unless -v is supplied, in which case the server is +# asked for its views on the status, too. +# +# Functions for retrieving data: +# All accept the following options: +# -G Don't do remote globbing (see above); the default is to do it. +# -t Try to set local files to the same time as the remote ones. +# Unfortunately we only know the remote time in GMT, so it's +# a little tricky and you need perl 5 (installed as `perl') +# for this to work. Suggestions welcome. +# zfget file1 file2 ... +# Retrieve each file from the server. The remote file is the +# full name given, the local file is the non-directory part of that +# (assuming UNIX file paths). +# zfuget file1 file2 .. +# Get with update. Check remote and local sizes and times and +# retrieve files which are newer on the server. Will query +# hard cases, which are where the remote file is newer but a +# different size, or is older but the same size. With option -s +# (silent) assumes it's best to retrieve the files in both those +# cases. With -v (may be combined with -s), print the information +# about the files being considered. +# zfcget file1 ... +# Assuming file1 was incompletely retrieved, try to get the rest of +# it. This relies on a normal UNIX server behaviour which is not +# as specified in the FTP standard and hence is not universal. +# zfgcp file1 file2 +# zfgcp file1 file2 ... dir +# Get with the behaviour of cp, i.e. copy remote file1 to local +# file2, or get remote fileN into local diretory dir. +# +# Function for sending data: +# zfput file1 file2 ... +# Put the local files onto the server under the same name. The +# local files are exactly as given; the remote files are the +# non-diretory parts of that. +# zfuput file1 file2 .. +# Put the local files onto the server, with update. Works +# similarly to zfuget. +# +# Utility functions: +# zftp_chpwd +# Show the new directory when it changes; try to put it into +# an xterm on shelltool header. Works best alongside chpwd. +# zftp_progress +# Show the percentage of a file retrieved as it is coming; if the +# size is not available show the size transferred so far. The +# percentage may be wrong if sending data from a local pipe. +# If you transfer files in the background, you should undefine +# this before the transfer. It is smart enough not to print +# anything when stderr is not a terminal. +# zfcd_match +# Function for remote directory completion. +# zfget_match +# Function for remote filename completion. +# zfrglob varname +# This is used for the remote globbing. The pattern resides +# in $varname (note extra level of indirection), and on return +# $varname will contain the list of matching files. +# zfrtime locfile remfile [ time ] +# This sad thing does the setting of local file times to those +# of the remote, see horror story above. + +zmodload -ia zftp + +alias zfcd='noglob zfcd' +alias zfget='noglob zfget' +alias zfls='noglob zfls' +alias zfdir='noglob zfdir' +alias zfuget='noglob zfuget' +# only way of getting that noglob out of the way at the moment +setopt completealiases + +# +# zftp completions +# +compctl -f -x 'p[1]' \ + -k '(open params user login type ascii binary mode put putat + get getat append appendat ls dir local remote mkdir rmdir delete + close quit)' - \ + 'w[1,cd][1,ls][1,dir][1,rmdir]' -K zfcd_match -S/ -q - \ + 'W[1,get*]' -K zfget_match - 'w[1,delete][1,remote]' -K zfget_match - \ + 'w[1,open][1,params]' -k hosts -- zftp +compctl -K zfcd_match -S/ -q zfcd zfdir zfls +compctl -K zfget_match zfget zfgcp zfuget zfcget +compctl -k hosts zfopen zfparams + +function zfanon { + local opt optlist once + + while [[ $1 = -* ]]; do + if [[ $1 = - || $1 = -- ]]; then + shift; + break; + fi + optlist=${1#-} + for (( i = 1; i <= $#optlist; i++)); do + opt=$optlist[$i] + case $optlist[$i] in + 1) once=1 + ;; + *) print option $opt not recognised >&2 + ;; + esac + done + shift + done + + if [[ -z $EMAIL_ADDR ]]; then + # Exercise in futility. There's a poem by Wallace Stevens + # called something like `N ways of looking at a blackbird', + # where N is somewhere around 0x14 to 0x18. Now zftp is + # ashamed to prsent `N ways of looking at a hostname'. + local domain host + # First, maybe we've already got it. Zen-like. + if [[ $HOST = *.* ]]; then + # assume this is the full host name + host=$HOST + elif [[ -f /etc/resolv.conf ]]; then + # Next, maybe we've got resolv.conf. + domain=$(awk '/domain/ { print $2 }' /etc/resolv.conf) + [[ -n $domain ]] && host=$HOST.$domain + fi + # Next, maybe we've got nlsookup. May not work on LINUX. + [[ -z $host ]] && host=$(nslookup $HOST | awk '/Name:/ { print $2 }') + if [[ -z $host ]]; then + # we're running out of ideas, but this should work. + # after all, i wrote it... + # don't want user to know about this, too embarrassed. + local oldvb=$ZFTP_VERBOSE oldtm=$ZFTP_TMOUT + ZFTP_VERBOSE= + ZFTP_TMOUT=5 + if zftp open $host >& /dev/null; then + host=$ZFTP_HOST + zftp close $host + fi + ZFTP_VERBOSE=$oldvb + ZFTP_TMOUT=$oldtm + fi + if [[ -z $host ]]; then + print "Can't get your hostname. Define \$EMAIL_ADDR by hand." + return 1; + fi + EMAIL_ADDR="$USER@$host" + print "Using $EMAIL_ADDR as anonymous FTP password." + fi + + if [[ $once = 1 ]]; then + zftp open $1 anonymous $EMAIL_ADDR + else + zftp params $1 anonymous $EMAIL_ADDR + zftp open + fi +} + +function zfcd { + # zfcd: change directory on the remote server. + # + # Currently has the following features: + # --- an initial string matching $HOME in the directory is turned back into ~ + # to be re-interpreted by the remote server. + # --- zfcd with no arguments changes directory to '~' + # --- `zfcd old new' and `zfcd -' work analagously to cd + # --- if the connection is not currently open, it will try to + # re-open it with the stored parameters as set by zfopen. + # If the connection timed out, however, it won't know until + # too late. In that case, just try the same zfcd command again + # (but now `zfcd -' and `zfcd old new' won't work). + + # hack: if directory begins with $HOME, turn it back into ~ + # there are two reasons for this: + # first, a ~ on the command line gets expanded even with noglob. + # (I suppose this is correct, but I wouldn't like to swear to it.) + # second, we can no do 'zfcd $PWD' and the like, and that will + # work just as long as the directory structures under the home match. + + # Autoopen: if not already open, hope there are parameters set up to + # do so. If not, we get the right error message, so no harm done. + [[ -z $ZFTP_HOST ]] && { zfopen || return 1; } + + if [[ $1 = $HOME || $1 = $HOME/* ]]; then + 1="~${1#$HOME}" + fi + + if (( $# == 0 )); then + # Emulate `cd' behaviour + set -- '~' + elif [[ $# -eq 1 && $1 = - ]]; then + # Emulate `cd -' behaviour. + set -- $zflastdir + elif [[ $# -eq 2 ]]; then + # Emulate `cd old new' behaviour. + # We have to find a character not in $1 or $2; ! is a good bet. + eval set -- "\${ZFTP_PWD:s!$1!$2!}" + fi + + # We have to remember the current directory before changing it + # if we want to keep it. + local lastdir=$ZFTP_PWD + + zftp cd "$@" && zflastdir=$lastdir +} + +function zfcd_match { + # see zfcd for details of this hack + if [[ $1 = $HOME || $1 = $HOME/* ]]; then + 1="~${1#$HOME}" + fi + + # error messages only + local ZFTP_VERBOSE=45 + # should we redirect 2>/dev/null or let the user see it? + + if [[ $ZFTP_SYSTEM = UNIX* ]]; then + # hoo, aren't we lucky: this makes things so much easier + setopt localoptions rcexpandparam + local dir + if [[ $1 = ?*/* ]]; then + dir=${1%/*} + elif [[ $1 = /* ]]; then + dir=/ + fi + # If we're using -F, we get away with using a directory + # to list, but not a glob. Don't ask me why. + # I hate having to rely on awk here. + reply=($(zftp ls -F $dir | + awk '/\/$/ { print substr($1, 0, length($1)-1) }')) + if [[ $dir = / ]]; then + reply=(${dir}$reply) + elif [[ -n $dir ]]; then + reply=($dir/$reply) + fi + else + # I simply don't know what to do here. + # Just use the list of files for the current directory. + zfget_match $* + fi + +} + +function zfcget { + # Continuation get of files from remote server. + # For each file, if it's shorter here, try to get the remainder from + # over there. This requires the server to support the REST command + # in the way many do but RFC959 doesn't specify. + # Options: + # -G don't to remote globbing, else do + # -t update the local file times to the same time as the remote. + # Currently this only works if you have the `perl' command, + # and that perl is version 5 with the standard library. + # See the function zfrtime for more gory details. + + setopt localoptions + unsetopt ksharrays shwordsplit + + local loc rem stat=0 optlist opt nglob remlist locst remst + local tmpfile=${TMPPREFIX}zfcget$$ rstat tsize time + + while [[ $1 = -* ]]; do + if [[ $1 = - || $1 = -- ]]; then + shift; + break; + fi + optlist=${1#-} + for (( i = 1; i <= $#optlist; i++)); do + opt=$optlist[$i] + case $optlist[$i] in + G) nglob=1 + ;; + t) time=1 + ;; + *) print option $opt not recognised >&2 + ;; + esac + done + shift + done + + for remlist in $*; do + # zfcd directory hack to put the front back to ~ + if [[ $remlist = $HOME || $remlist = $HOME/* ]]; then + remlist="~${remlist#$HOME}" + fi + if [[ $nglob != 1 ]]; then + zfrglob remlist + fi + if (( $#remlist )); then + for rem in $remlist; do + loc=${rem:t} + if [[ ! -f $loc ]]; then + # File does not yet exist + zftp get $rem >$loc || stat=$? + else + # Compare the sizes. + locst=($(zftp local $loc)) + zftp remote $rem >$tmpfile + rstat=$? + remst=($(<$tmpfile)) + rm -f $tmpfile + if [[ $rstat = 2 ]]; then + print "Server does not support SIZE command.\n" \ + "Assuming you know what you're doing..." 2>&1 + zftp getat $rem $locst[1] >>$loc || stat=$? + continue + elif [[ $rstat = 1 ]]; then + print "Remote file not found: $rem" 2>&1 + continue + fi + if [[ $locst[1] -gt $remst[1] ]]; then + print "Local file is larger!" 2>&1 + continue; + elif [[ $locst[1] == $remst[1] ]]; then + print "Files are already the same size." 2>&1 + continue + else + if zftp getat $rem $locst[1] >>$loc; then + [[ $time = 1 ]] && zfrtime $loc $rem $remst[2] + else + stat=1 + fi + fi + fi + done + fi + done + + return $stat +} + +function zfclose { + zftp close +} + +function zfdir { + # Long directory of remote server. + # The remote directory is cached. In fact, two caches are kept: + # one of the standard listing of the current directory, i.e. zfdir + # with no arguments, and another for everything else. + # To access the appropriate cache, just use zfdir with the same + # arguments as previously. zfdir -r will also re-use the `everything + # else' cache; you can always reuse the current directory cache just + # with zfdir on its own. + # + # The current directory cache is emptied when the directory changes; + # the other is kept until a new zfdir with a non-empty argument list. + # Both are removed when the connection is closed. + # + # zfdir -f will force the existing cache to be ignored, e.g. if you know + # or suspect the directory has changed. + # zfdir -d will remove both caches without listing anything. + # If you need to pass -r, -f or -d to the dir itself, use zfdir -- -d etc. + + setopt localoptions unset + unsetopt shwordsplit ksharrays + + local file opt optlist redir i newargs force + + while [[ $1 = -* ]]; do + if [[ $1 = - || $1 = -- ]]; then + shift; + break; + fi + optlist=${1#-} + for (( i = 1; i <= $#optlist; i++)); do + opt=$optlist[$i] + case $optlist[$i] in + r) redir=1 + ;; + f) force=1 + ;; + d) [[ -n $zfcurdir && -f $zfcurdir ]] && rm -f $zfcurdir + [[ -n $zfotherdir && -f $zfotherdir ]] && rm -f $zfotherdir + zftp_fcache=() + return 0 + ;; + *) print option $opt not recognised >&2 + ;; + esac + done + shift + done + + # directory hack, see zfcd + for (( i = 1; i <= $#argv; i++ )); do + if [[ $argv[$i] = $HOME || $argv[$i] = $HOME/* ]]; then + argv[$i]="~${argv[$i]#$HOME}" + fi + done + + if [[ $# -eq 0 ]]; then + # Cache it in the current directory file. This means that repeated + # calls to zfdir with no arguments always use a cached file. + [[ -z $zfcurdir ]] && zfcurdir=${TMPPREFIX}zfcurdir$$ + file=$zfcurdir + else + # Last directly looked at was not the current one, or at least + # had non-standard arguments. + [[ -z $zfotherdir ]] && zfotherdir=${TMPPREFIX}zfotherdir$$ + file=$zfotherdir + newargs="$*" + if [[ -f $file && $redir != 1 && $force -ne 1 ]]; then + # Don't use the cached file if the arguments changed. + [[ $newargs = $zfotherargs ]] || rm -f $file + fi + zfotherargs=$newargs + fi + + if [[ $force -eq 1 ]]; then + rm -f $file + # if it looks like current directory has changed, better invalidate + # the filename cache, too. + (( $# == 0 )) && zftp_fcache=() + fi + + if [[ -n $file && -f $file ]]; then + eval ${PAGER:-more} \$file + else + zftp dir $* | tee $file | eval ${PAGER-:more} + fi +} + +function zfgcp { + # ZFTP get as copy: i.e. first arguments are remote, last is local. + # Supposed to work exactly like a normal copy otherwise, i.e. + # zfcp rfile lfile + # or + # zfcp rfile1 rfile2 rfile3 ... ldir + # Options: + # -G don't to remote globbing, else do + # -t update the local file times to the same time as the remote. + # Currently this only works if you have the `perl' command, + # and that perl is version 5 with the standard library. + # See the function zfrtime for more gory details. + + setopt localoptions + unsetopt shwordsplit + + local opt optlist nglob remlist rem loc stat=0 time + + while [[ $1 == -* ]]; do + if [[ $1 == - || $1 == -- ]]; then + shift; + break; + fi + optlist=${1#-} + for (( i = 1; i <= $#optlist; i++)); do + opt=$optlist[$i] + case $opt in + G) nglob=1 + ;; + t) time=1 + ;; + *) print option $opt not recognised >&2 + ;; + esac + done + shift + done + + # hmm, we should really check this after expanding the glob, + # but we shouldn't expand the last argument remotely anyway. + if [[ $# -gt 2 && ! -d $argv[-1] ]]; then + print "zfgcp: last argument must be a directory." 2>&1 + return 1 + elif [[ $# == 1 ]]; then + print "zfgcp: not enough arguments." 2>&1 + return 1 + fi + + if [[ -d $argv[-1] ]]; then + local dir=$argv[-1] + argv[-1]= + for remlist in $*; do + # zfcd directory hack to put the front back to ~ + if [[ $remlist = $HOME || $remlist = $HOME/* ]]; then + remlist="~${remlist#$HOME}" + fi + if [[ $nglob != 1 ]]; then + zfrglob remlist + fi + if (( $#remlist )); then + for rem in $remlist; do + loc=$dir/${rem:t} + if zftp get $rem >$loc; then + [[ $time = 1 ]] && zfrtime $rem $loc + else + stat=1 + fi + done + fi + done + else + zftp get $1 >$2 || stat=$? + fi + return $stat +} + +function zfget { + # Get files from remote server. Options: + # -G don't to remote globbing, else do + # -t update the local file times to the same time as the remote. + # Currently this only works if you have the `perl' command, + # and that perl is version 5 with the standard library. + # See the function zfrtime for more gory details. + + local loc rem stat=0 optlist opt nglob remlist time + + while [[ $1 == -* ]]; do + if [[ $1 == - || $1 == -- ]]; then + shift; + break; + fi + optlist=${1#-} + for (( i = 1; i <= $#optlist; i++)); do + opt=$optlist[$i] + case $opt in + G) nglob=1 + ;; + t) time=1 + ;; + *) print option $opt not recognised >&2 + ;; + esac + done + shift + done + + for remlist in $*; do + # zfcd directory hack to put the front back to ~ + if [[ $remlist == $HOME || $remlist == $HOME/* ]]; then + remlist="~${remlist#$HOME}" + fi + if [[ $nglob != 1 ]]; then + zfrglob remlist + fi + if (( $#remlist )); then + for rem in $remlist; do + loc=${rem:t} + if zftp get $rem >$loc; then + [[ $time = 1 ]] && zfrtime $rem $loc + else + stat=1 + fi + done + fi + done + + return $stat +} + +function zfget_match { + # the zfcd hack: this may not be necessary here + if [[ $1 == $HOME || $1 == $HOME/* ]]; then + 1="~${1#$HOME}" + fi + + + if [[ $ZFTP_SYSTEM == UNIX* && $1 == */* ]]; then + # On the first argument to ls, we usually get away with a glob. + reply=($(zftp ls "$1*$2")) + else + if (( $#zftp_fcache == 0 )); then + # Always cache the current directory and use it + # even if the system is UNIX. + zftp_fcache=($(zftp ls)) + fi + reply=($zftp_fcache); + fi +} + +function zfhere { + # Change to the directory corresponding to $PWD on the server. + # See zfcd for how this works. + zfcd $PWD +} + +function zfls { + # directory hack, see zfcd + if [[ $1 = $HOME || $1 = $HOME/* ]]; then + 1="~${1#$HOME}" + fi + zftp ls $* +} + +function zfopen { + # Use zftp params to set parameters for open, rather than sending + # them straight to open. That way they are stored for a future open + # command. + # + # With option -1 (just this 1ce), don't do that. + + local optlist opt once + + while [[ $1 = -* ]]; do + if [[ $1 = - || $1 = -- ]]; then + shift; + break; + fi + optlist=${1#-} + for (( i = 1; i <= $#optlist; i++)); do + opt=$optlist[$i] + case $optlist[$i] in + 1) once=1 + ;; + *) print option $opt not recognised >&2 + ;; + esac + done + shift + done + + # This is where we should try and do same name-lookupage in + # both .netrc and .ncftp/bookmarks . We could even try saving + # the info in their for new hosts, like ncftp does. + + if [[ $once = 1 ]]; then + zftp open $* + else + # set parameters, but only if there was at least a host + (( $# > 0 )) && zfparams $* + # now call with no parameters + zftp open + fi +} + +function zfparams { + # Set to prompt for any user or password if not given. + # Don't worry about accounts here. + if (( $# > 0 )); then + (( $# < 2 )) && 2='?' + (( $# < 3 )) && 3='?' + fi + zftp params $* +} + +function zfput { + # Simple put: dump every file under the same name, but stripping + # off any directory parts. + local loc rem stat=0 + for loc in $*; do + rem=${loc:t} + zftp put $rem <$loc + [[ $? == 0 ]] || stat=$? + done + return $stat +} + +function zfrglob { + # Do the remote globbing for zfput, etc. + # We have two choices: + # (1) Get the entire file list and match it one by one + # locally against the pattern. + # Causes problems if we are globbing directories (rare, presumably). + # But: we can cache the current directory, which + # we need for completion anyway. Works on any OS if you + # stick with a single directory. This is the default. + # (2) Use remote globbing, i.e. pass it to ls at the site. + # Faster, but only works with UNIX, and only basic globbing. + # We do this if $zfrglob is non-null. + + # There is only one argument, the variable containing the + # pattern to be globbed. We set this back to an array containing + # all the matches. + setopt localoptions unset + unsetopt ksharrays + + local pat dir nondir files i + + eval pat=\$$1 + + # Check if we really need to do anything. Look for standard + # globbing characters, and if extendedglob is set and we are + # using zsh for the actual pattern matching also look for + # extendedglob characters. + if [[ $remlist != *[][*?]* && + ( -n $zfrglob || ! -o extendedglob || $remlist != *[(|)~#^]* ) ]]; then + return 0 + fi + + if [[ $zfrglob != '' ]]; then + eval "$1=(\$(zftp ls \"$pat\" 2>/dev/null))" + else + if [[ $ZFTP_SYSTEM = UNIX* && $pat = */* ]]; then + # not the current directory and we know how to handle paths + if [[ $pat = ?*/* ]]; then + # careful not to remove too many slashes + dir=${pat%/*} + else + dir=/ + fi + nondir=${pat##*/} + files=($(zftp ls "$dir" 2>/dev/null)) + else + # we just have to do an ls and hope that's right + nondir=$pat + if (( $#zftp_fcache == 0 )); then + zftp_fcache=($(zftp ls)) + fi + files=($zftp_fcache) + fi + # now we want to see which of the $files match $nondir + for (( i = 1; i <= $#files; i++)); do + # empty words are elided in array assignment + [[ $files[$i] = ${~nondir} ]] || files[$i]='' + done + eval "$1=(\$files)" + fi +} + +function zfrtime { + # Set the modification time of file LOCAL to that of REMOTE. + # If the optional TIME is passed, it should be in the FTP format + # CCYYMMDDhhmmSS, i.e. no dot before the seconds, and in GMT. + # This is what both `zftp remote' and `zftp local' return. + # + # Unfortunately, since the time returned from FTP is GMT and + # your file needs to be set in local time, we need to do some + # hacking around with time. At the moment this requires perl 5 + # with the standard library. + + setopt localoptions unset + unsetopt ksharrays + + local time gmtime loctime + + if [[ -n $3 ]]; then + time=$3 + else + time=($(zftp remote $2 2>/dev/null)) + [[ -n $time ]] && time=$time[2] + fi + [[ -z $time ]] && return 1 + + # Now's the real *!@**!?!. We have the date in GMT and want to turn + # it into local time for touch to handle. It's just too nasty + # to handle in zsh; do it in perl. + if perl -mTime::Local -e '($file, $t) = @ARGV; + $yr = substr($t, 0, 4) - 1900; + $mon = substr($t, 4, 2) - 1; + $mday = substr($t, 6, 2) + 0; + $hr = substr($t, 8, 2) + 0; + $min = substr($t, 10, 2) + 0; + $sec = substr($t, 12, 2) + 0; + $time = Time::Local::timegm($sec, $min, $hr, $mday, $mon, $yr); + utime $time, $time, $file and return 0;' $1 $time 2>/dev/null; then + print "Setting time for $1 failed. Need perl 5." 2>1 + fi + + # If it wasn't for the GMT/local time thing, it would be this simple. + # + # time="${time[1,12]}.${time[13,14]}" + # + # touch -t $time $1 + +} + +function zfstat { + # Give a zftp status report using local variables. + # With option -v, connect the remote host and ask it what it + # thinks the status is. + + setopt localoptions unset + unsetopt ksharrays + + local i stat=0 opt optlist verbose + + while [[ $1 = -* ]]; do + if [[ $1 = - || $1 = -- ]]; then + shift; + break; + fi + optlist=${1#-} + for (( i = 1; i <= $#optlist; i++)); do + opt=$optlist[$i] + case $opt in + v) verbose=1 + ;; + *) print option $opt not recognised >&2 + ;; + esac + done + shift + done + + # hack: in case the status from a subshell process hasn't been + # fixed yet + zftp type >&/dev/null + + if [[ -n $ZFTP_HOST ]]; then + print "Host:\t\t$ZFTP_HOST" + print "IP:\t\t$ZFTP_IP" + [[ -n $ZFTP_SYSTEM ]] && print "System type:\t$ZFTP_SYSTEM" + if [[ -n $ZFTP_USER ]]; then + print "User:\t\t$ZFTP_USER " + [[ -n $ZFTP_ACCOUNT ]] && print "Account:\t$AFTP_ACCOUNT" + print "Directory:\t$ZFTP_PWD" + print -n "Transfer type:\t" + if [[ $ZFTP_TYPE = "I" ]]; then + print Image + elif [[ $ZFTP_TYPE = "A" ]]; then + print Ascii + else + print Unknown + fi + print -n "Transfer mode:\t" + if [[ $ZFTP_MODE = "S" ]]; then + print Stream + elif [[ $ZFTP_MODE = "B" ]]; then + print Block + else + print Unknown + fi + else + print "No user logged in." + fi + else + print "Not connected." + stat=1 + fi + + # things which may be set even if not connected: + [[ -n $ZFTP_REPLY ]] && print "Last reply:\t$ZFTP_REPLY" + print "Verbosity:\t$ZFTP_VERBOSE" + print -n "Preferences:\t" + for (( i = 1; i <= ${#ZFTP_PREFS}; i++ )); do + case $ZFTP_PREFS[$i] in + [pP]) print -n "Passive " + ;; + [sS]) print -n "Sendport " + ;; + [dD]) print -n "Dumb " + ;; + *) print -n "$ZFTP_PREFS[$i]???" + esac + done + print + + if [[ -n $ZFTP_HOST && $verbose = 1 ]]; then + print "Status of remote server:" + # make sure we print the reply + local ZFTP_VERBOSE=045 + zftp quote STAT + fi + + return $stat +} + +function zftp_chpwd { + # You may want to alter chpwd to call this when $ZFTP_USER is set. + # If so, call it with a non-zero first argument so it doesn't + # print the new FTP directory. + + # Cancel the filename cache for the current directory. + zftp_fcache=() + # ...and also empty the stored directory listing cache. + # As this function is called when we close the connection, this + # is the only place we need to do these two things. + [[ -n $zfcurdir && -f $zfcurdir ]] && rm -f $zfcurdir + + if [[ -z $ZFTP_USER ]]; then + # last call, after an FTP logout + + # delete the non-current cached directory + [[ -n $zfotherdir && -f $zfotherdir ]] && rm -f $zfotherdir + zfotherargs= + + # don't keep zflastdir between opens + zflastdir= + + # return the display to standard + # uncomment the following line if you have a chpwd which shows directories + # chpwd + else + [[ -z $zflastdir ]] && zflastdir=$ZFTP_PWD + local args + if [[ -t 1 && -t 2 ]]; then + local str="$ZFTP_HOST:$ZFTP_PWD" + [[ -z $1 ]] && print $str + [[ ${#str} -lt 70 ]] && str="%m: %~ $str" + case $TERM in + sun-cmd) print -n -P "\033]l$str\033\\" + ;; + xterm) print -n -P "\033]2;$str\a" + ;; + esac + fi + fi +} + +function zftp_progress { + # Basic progress metre, showing the percent of the file transferred. + # You want growing bars? You gotta write growing bars. + + # Don't show progress unless stderr is a terminal + [[ ! -t 2 ]] && return 0 + + if [[ $ZFTP_TRANSFER = *F ]]; then + print 1>&2 + elif [[ -n $ZFTP_TRANSFER ]]; then + if [[ -n $ZFTP_SIZE ]]; then + local frac="$(( ZFTP_COUNT * 100 / ZFTP_SIZE ))%" + print -n "\r$ZFTP_FILE ($ZFTP_SIZE bytes): $ZFTP_TRANSFER $frac" 1>&2 + else + print -n "\r$ZFTP_FILE: $ZFTP_TRANSFER $ZFTP_COUNT" 1>&2 + fi + fi +} + +function zftype { + local type + + if (( $# == 0 )); then + type=$(zftp type) + if [[ $type = I ]]; then + print "Current type is image (binary)" + return 0 + elif [[ $type = A ]]; then + print "Current type is ASCII" + return 0 + else + return 1 + fi + else + if [[ $1 == [aA]([sS][cC]([iI][iI]|)|) ]]; then + type=A + elif [[ $1 == [iI]([mM]([aA][gG][eE]|)|) || + $1 == [bB]([iI][nN]([aA][rR][yY]|)|) ]]; then + type=I + else + print "Type not recognised: $1" 2>&1 + return 1 + fi + zftp type $type + fi +} + +function zfuget { + # Get a list of files from the server with update. + # In other words, only retrieve files which are newer than local + # ones. This depends on the clocks being adjusted correctly + # (i.e. if one is fifteen minutes out, for the next fifteen minutes + # updates may not be correctly calculated). However, difficult + # cases --- where the files are the same size, but the remote is newer, + # or have different sizes, but the local is newer -- are prompted for. + # + # Files are globbed on the remote host --- assuming, of course, they + # haven't already been globbed local, so use 'noglob' e.g. as + # `alias zfuget="noglob zfuget"'. + # + # Options: + # -G Glob: turn off globbing + # -v verbose: print more about the files listed. + # -s silent: don't ask, just guess. The guesses are: + # - if the files have different sizes but remote is older ) grab + # - if they have the same size but remote is newer ) + # which is safe if the remote files are always the right ones. + # -t time: update the local file times to the same time as the remote. + # Currently this only works if you have the `perl' command, + # and that perl is version 5 with the standard library. + # See the function zfrtime for more gory details. + + setopt localoptions + unsetopt ksharrays shwordsplit + + local loc rem stat=0 locstats remstats doit tmpfile=${TMPPREFIX}zfuget$$ + local rstat remlist verbose optlist opt bad i silent nglob time + + zfuget_print_time() { + local tim=$1 + print -n "$tim[1,4]/$tim[5,6]/$tim[7,8] $tim[9,10]:$tim[11,12].$tim[13,14]" + print -n GMT + } + + zfuget_print () { + print -n "\nremote $rem (" + zfuget_print_time $remstats[2] + print -n ", $remstats[1] bytes)\nlocal $loc (" + zfuget_print_time $locstats[2] + print ", $locstats[1] bytes)" + } + + while [[ $1 = -* ]]; do + if [[ $1 = - || $1 = -- ]]; then + shift; + break; + fi + optlist=${1#-} + for (( i = 1; i <= $#optlist; i++)); do + opt=$optlist[$i] + case $optlist[$i] in + v) verbose=1 + ;; + s) silent=1 + ;; + G) nglob=1 + ;; + t) time=1 + ;; + *) print option $opt not recognised >&2 + ;; + esac + done + shift + done + + [[ -n $bad ]] && return 1 + + for remlist in $*; do + # zfcd directory hack to put the front back to ~ + if [[ $remlist == $HOME || $remlist == $HOME/* ]]; then + remlist="~${remlist#$HOME}" + fi + if [[ $nglob != 1 ]]; then + zfrglob remlist + fi + if (( $#remlist )); then + for rem in $remlist; do + loc=${rem:t} + doit=y + remstats=() + if [[ -f $loc ]]; then + zftp local $loc >$tmpfile + locstats=($(<$tmpfile)) + zftp remote $rem >$tmpfile + rstat=$? + remstats=($(<$tmpfile)) + rm -f $tmpfile + if [[ $rstat = 2 ]]; then + print "Server does not implement full command set required." 1>&2 + return 1 + elif [[ $rstat = 1 ]]; then + print "File not found on server: $rem" 1>&2 + stat=1 + continue + fi + [[ $verbose = 1 ]] && zfuget_print + if (( $locstats[1] != $remstats[1] )); then + # Files have different sizes + if [[ $locstats[2] > $remstats[2] && $silent != 1 ]]; then + [[ $verbose != 1 ]] && zfuget_print + print "Local file $loc more recent than remote," 1>&2 + print -n "but sizes are different. Transfer anyway [y/n]? " 1>&2 + read -q doit + fi + else + # Files have same size + if [[ $locstats[2] < $remstats[2] ]]; then + if [[ $silent != 1 ]]; then + [[ $verbose != 1 ]] && zfuget_print + print "Local file $loc has same size as remote," 1>&2 + print -n "but local file is older. Transfer anyway [y/n]? " 1>&2 + read -q doit + fi + else + # presumably same file, so don't get it. + [[ $verbose = 1 ]] && print Not transferring + doit=n + fi + fi + else + [[ $verbose = 1 ]] && print New file $loc + fi + if [[ $doit = y ]]; then + if zftp get $rem >$loc; then + if [[ $time = 1 ]]; then + # if $remstats is set, it's second element is the remote time + zfrtime $loc $rem $remstats[2] + fi + else + stat=$? + fi + + fi + done + fi + done + return $stat +} + +function zfuput { + # Put a list of files from the server with update. + # See zfuget for details. + # + # Options: + # -v verbose: print more about the files listed. + # -s silent: don't ask, just guess. The guesses are: + # - if the files have different sizes but remote is older ) grab + # - if they have the same size but remote is newer ) + # which is safe if the remote files are always the right ones. + + setopt localoptions + unsetopt ksharrays shwordsplit + + local loc rem stat=0 locstats remstats doit tmpfile=${TMPPREFIX}zfuput$$ + local rstat verbose optlist opt bad i silent + + zfuput_print_time() { + local tim=$1 + print -n "$tim[1,4]/$tim[5,6]/$tim[7,8] $tim[9,10]:$tim[11,12].$tim[13,14]" + print -n GMT + } + + zfuput_print () { + print -n "\nremote $rem (" + zfuput_print_time $remstats[2] + print -n ", $remstats[1] bytes)\nlocal $loc (" + zfuput_print_time $locstats[2] + print ", $locstats[1] bytes)" + } + + while [[ $1 = -* ]]; do + if [[ $1 = - || $1 = -- ]]; then + shift; + break; + fi + optlist=${1#-} + for (( i = 1; i <= $#optlist; i++)); do + opt=$optlist[$i] + case $optlist[$i] in + v) verbose=1 + ;; + s) silent=1 + ;; + *) print option $opt not recognised >&2 + ;; + esac + done + shift + done + + [[ -n $bad ]] && return 1 + + if [[ $ZFTP_VERBOSE = *5* ]]; then + # should we turn it off locally? + print "Messages with code 550 are harmless." >&2 + fi + + for loc in $*; do + rem=${loc:t} + doit=y + remstats=() + if [[ ! -f $loc ]]; then + print "$loc: file not found" >&2 + stat=1 + continue + fi + zftp local $loc >$tmpfile + locstats=($(<$tmpfile)) + zftp remote $rem >$tmpfile + rstat=$? + remstats=($(<$tmpfile)) + rm -f $tmpfile + if [[ $rstat = 2 ]]; then + print "Server does not implement full command set required." 1>&2 + return 1 + elif [[ $rstat = 1 ]]; then + [[ $verbose = 1 ]] && print New file $loc + else + [[ $verbose = 1 ]] && zfuput_print + if (( $locstats[1] != $remstats[1] )); then + # Files have different sizes + if [[ $locstats[2] < $remstats[2] && $silent != 1 ]]; then + [[ $verbose != 1 ]] && zfuput_print + print "Remote file $rem more recent than local," 1>&2 + print -n "but sizes are different. Transfer anyway [y/n]? " 1>&2 + read -q doit + fi + else + # Files have same size + if [[ $locstats[2] > $remstats[2] ]]; then + if [[ $silent != 1 ]]; then + [[ $verbose != 1 ]] && zfuput_print + print "Remote file $rem has same size as local," 1>&2 + print -n "but remote file is older. Transfer anyway [y/n]? " 1>&2 + read -q doit + fi + else + # presumably same file, so don't get it. + [[ $verbose = 1 ]] && print Not transferring + doit=n + fi + fi + fi + if [[ $doit = y ]]; then + zftp put $rem <$loc || stat=$? + fi + done + return $stat +} |