# Part of the A-A-P recipe executive: copy and move files (remotely)

# Copyright (C) 2002 Stichting NLnet Labs
# Permission to copy and use this file is specified in the file COPYING.
# If this file is missing you can find it here: http://www.a-a-p.org/COPYING

#
# These are functions to copy and move files.  Not only on the local system,
# also to remote systems, using http://, ftp://, etc.
#

import os
import os.path
import string

from Dictlist import str2dictlist
from Error import *
from Process import recipe_error, option_error
from Util import *
from Work import getrpstack
from Message import *


def copy_move(line_nr, recdict, arg, copy):
    """Implementation of ":copy -x from to" and ":move -x from to".
       When "copy" is non-zero copying is done, otherwise moving.
       "arg" is the whole argument.
       "line_nr" is used for error messages."""

    # Skip when not actually building.
    cmdname = (copy and ':copy') or ':move'
    if skip_commands():
	msg_info(recdict, _('skip %s %s') % (cmdname, arg))
	return
    rpstack = getrpstack(recdict, line_nr)

    # Get the arguments and check the options.
    # TODO: check if all options are handled properly
    from Commands import get_args
    opt = {"f": "force", "force": "force",
	   "i": "interactive", "interactive": "interactive",
	   "e": "exist", "exist": "exist", "exists": "exist",
	   "q": "quiet", "quiet": "quiet"}
    if copy:
	opt["u"] = "unlink"
	opt["unlink"] = "unlink"
	opt["p"] = "preserve"
	opt["preserve"] = "preserve"
        opt["r"] = "recursive"
	opt["recursive"] = "recursive"

    optiondict, attrdict, argdictlist = get_args(line_nr, recdict, arg, opt)
    if attrdict:
	option_error(rpstack, attrdict, cmdname)
    if len(argdictlist) < 2:
	recipe_error(rpstack, _("%s command requires at least two arguments")
								     % cmdname)

    remote_copy_move(rpstack, recdict, copy, argdictlist[:-1],
				    argdictlist[-1], optiondict, 1, errmsg = 1)


def scp_copy(recdict, fromfile, tmach, destpath, fcount):
    """Use scp to copy "fromfile" (with "fcount" file names in quotes) to
       tmach:destpath.
       Return non-zero for success."""
    cmd = "scp -C %s '%s:%s'" % (fromfile, tmach, destpath)
    ok, text = redir_system_int(recdict, cmd)
    if text:
	msg_log(text, msgt_result)
	# Check for an error connecting, scp doesn't exit with an error value
	# then.  Usual messages are:
	#    "Secure connection to vim.sf.net refused."
	#    "lost connection"
	#    "Read-only file system"
	i = string.find(text, "connection to")
	if i > 0:
	    e = string.find(text, "\n", i)
	    if e > 0:
		i = string.find(text, "refused", i, e)
	    else:
		i = -1
	if i > 0 or string.find(text, "lost connection") >= 0:
	    ok = 0
	elif string.find(text, "Permission denied") >= 0:
	    # Happens when a file exists and permissions don't allow
	    # overwriting: scp: vim/new/con_cvs.php: Permission denied
	    # TODO: check the file name and only mark this one as failed.
	    ok = 0
	elif string.find(text, "Read-only file system") >= 0:
	    # Happens when the file system was mounted read-only.
	    # TODO: check the file name and only mark this one as failed.
	    ok = 0
	elif string.find(text, "Not a directory") >= 0:
	    # Happens when copying multiple files into a file.
	    ok = 0
	elif string.find(text, "No such file or directory") >= 0:
	    ok = 0
	    msg_info(recdict,
		     _('looks like the directory does not exist, creating it'))
	    # TODO: This is just guessing; how do we know if the destination
	    # is a directory or a file?
	    if fcount > 1:
		dirname = destpath
	    else:
		dirname = os.path.dirname(destpath)
	    mkdircmd = ('ssh %s mkdir -p %s' % (tmach, dirname))
	    if logged_system(recdict, mkdircmd) == 0:
		# Try copying again.
		ok = (logged_system(recdict, cmd) == 0)
    return ok


def remote_copy_move(rpstack, recdict, copy, from_items, to_item,
					    optiondict, argexpand, errmsg = 0):
    """Copy or move command.  Copy when "copy" is non-zero.
       "from_items" is a dictlist of the files to copy or move from.
       "to_item" is the dictionary of the destination.
       "optiondict" is a dictionary with options.
       "argexpand" is non-zero when wildcards are to be expanded (called from
       ":copy" or ":move").
       When "errmsg" is non-zero, call recipe_error() for an error.
       Returns a list of filenames that failed."""
    from Remote import url_split3
    import glob
    failed = []

    # -l: change symlinks to real files
    #if optiondict.get("unlink"):
    #    keepsymlinks = 0
    #else:
    #	keepsymlinks = 1

    # Expand and check the "from" arguments.
    fromlist = []
    for a in from_items:
	fname = a["name"]
	fscheme, fmach, fpath = url_split3(fname)
	if fscheme == '':
	    # It's a local file, may expand ~user and wildcards.
	    if argexpand:
		fl = glob.glob(os.path.expanduser(fname))
		if len(fl) == 0:
		    recipe_error(rpstack, _('No match for "%s"') % fname)
	    else:
		fl = [ os.path.expanduser(fname) ]
	    # For copy without -r sources can't be a directory.
	    if copy and not optiondict.get("recursive"):
		for l in fl:
		    if os.path.isdir(l):
			recipe_error(rpstack,
			     _('Copying a directory requires -r flag: %s') % l)
	    fromlist.extend(fl)

	else:
	    # It's a URL, append without expanding.
	    if not copy:
		recipe_error(rpstack, _('Cannot move from a URL yet: "%s"')
								       % fname)
	    fromlist.append(fname)

    # Expand and check the "to" argument.
    tname = to_item["name"]
    tscheme, tmach, tpath = url_split3(tname)
    if tscheme == '':
	# For a local file expand "~/" and "~user/".  Don't use "tname", it may
	# start with "file:".
	tpath = os.path.expanduser(tpath)
    if tscheme == '' and argexpand:
	# For a local destination file expand ~user and wildcards.
	l = glob.glob(tpath)
	if len(l) > 1:
	    recipe_error(rpstack, _('More than one match for "%s"') % tname)
	if len(l) == 1:
	    tpath = l[0]

    # If there is more than one source, target must be a directory.
    if tscheme == '' and len(fromlist) > 1 and not os.path.isdir(tpath):
	recipe_error(rpstack, _('Destination must be a directory: "%s"')
								       % tname)

    msg = ''
    if tscheme == "ftp":
	#
	# Prepare for uploading through ftp.
	#
	import ftplib

	# For "user:passwd@ftp.com" split at the @
	passwd = ''
	acct = ''
	i = string.find(tmach, '@')
	if i > 0:
	    user = tmach[:i]
	    machname = tmach[i+1:]
	    i = string.find(user, ':')
	    if i > 0:
		passwd = user[i+1:]
		user = user[:i]
	    else:
		prompt = (_('Enter password for user %s at %s: ')
							    % (user, machname))
		try:
		    import getpass
		    passwd = getpass.getpass(prompt)
		except:
		    # TODO: should display stars for typed chars
		    passwd = raw_input(prompt)
	else:
	    user = ''
	    machname = tmach

	    import aapnetrc
	    # obtain the login name and password from the netrc file
	    try:
		n = aapnetrc.netrc()
		res = n.authenticators(machname)
		if res is None:
		    user = ''
		else:
		    user, acct, passwd = res
	    except aapnetrc.NetrcParseError, e:
		pass

	# Try to open the connection to the ftp server.
	try:
	    ftp = ftplib.FTP(machname)
	    if user != '':
		if passwd != '':
		    if acct != '':
			ftp.login(user, passwd, acct)
		    else:
			ftp.login(user, passwd)
		else:
		    ftp.login(user)
	    else:
		ftp.login()
	except ftplib.all_errors, e:
	    msg = (_('Cannot open connection for "%s"') % tname) + str(e)

    elif tscheme == "scp":
	#
	# TODO: Prepare for uploading through scp.
	#
	pass

    elif tscheme != '':
	msg = _('Can only upload to scp:// and ftp://')

    # If copy/move isn't possible either give an error message or return the
    # whole list of sources.
    if msg:
	if argexpand:
	    msg_error(recdict, msg)
	    return map(lambda x : x["name"], from_items)
	recipe_error(rpstack, msg)

    # files to be copied with scp later.
    scp_files = ''
    scp_filelist = []

    #
    # Loop over all "from" files.
    #
    for fname in fromlist:
	fscheme, fmach, fpath = url_split3(fname)

	# If the destination is a directory, append the source file name to the
	# destination directory.
	# "dest" includes the scheme (may be "file:" when "tscheme" is empty).
	if ((tscheme != '' and len(fromlist) > 1)
				  or (tscheme == '' and os.path.isdir(tpath))):
	    dest = os.path.join(tname, os.path.basename(fname))
	    destpath = os.path.join(tpath, os.path.basename(fname))
	else:
	    dest = tname
	    destpath = tpath

	# If destination is a local file and exists:
	# - When {exist} option used, silently ignore.
	# - When {interactive} option used, ask for overwriting.  Use a special
	#   string to allow translating the response characters.
	if tscheme == '' and os.path.exists(destpath):
	    if optiondict.get("exist"):
		msg = _('file already exists: "%s"') % dest
		msg_warning(recdict, msg)
		failed.append(fname)
		continue
	    if optiondict.get("interactive"):
		reply = raw_input(_('"%s" exists, overwrite? (y/n) ') % dest)
		if (len(reply) == 0 or not reply[0]
			      in _("yY   up to four chars that mean yes")[:4]):
		    if copy:
			msg_warning(recdict, _("file not copied"))
		    else:
			msg_warning(recdict, _("file not moved"))
		    failed.append(fname)
		    continue

	if fscheme == '' and tscheme == '':
	    #
	    # local file copy or move
	    #
	    if not copy:
		try:
		    os.rename(fname, destpath)
		    done = 1
		except:
		    done = 0	    # renaming failed, try copying

	    if copy or not done:
		try:
		    import shutil

		    # TODO: handle symlinks and "keepsymlinks".
		    if os.path.isdir(fname):
			# Cannot use shutil.copytree here, it fails when the
			# destination already exists.
			if not os.path.exists(destpath):
			    os.mkdir(destpath)
			# Call ourselves recursively
			from Commands import dir_contents
			flist = map(lambda x: {"name": x}, dir_contents(fname))
			f = remote_copy_move(rpstack, recdict, copy, flist,
				     {"name": destpath}, optiondict, 0, errmsg)
			if f:
			    failed.extend(f)
		    elif not copy or optiondict.get("preserve"):
			shutil.copy2(fname, destpath)
		    else:
			shutil.copy(fname, destpath)
		except (IOError, OSError), e:
		    msg = (_('Cannot copy "%s" to "%s"')
						      % (fname, dest)) + str(e)
		    if not argexpand and not errmsg:
			msg_warning(recdict, msg)
			failed.append(fname)
		    else:
			recipe_error(rpstack, msg)
		    continue
		if not copy:
		    if os.path.isdir(fname):
			deltree(fname)
		    else:
			os.remove(fname)

	    if not optiondict.get("quiet"):
		f = shorten_name(fname)
		if copy:
		    msg_info(recdict, _('Copied "%s" to "%s"') % (f, dest))
		else:
		    msg_info(recdict, _('Moved "%s" to "%s"') % (f, dest))

	else:
	    if fscheme != '':
		# download to local file
		from Remote import url_download

		try:
		    if tscheme != '':
			tmpfile, rtime = url_download(recdict, fname, '')
		    else:
			tmpfile, rtime = url_download(recdict, fname, destpath)
		except IOError, e:
		    msg = (_('Cannot download "%s" to "%s": ')
						      % (fname, dest)) + str(e)
		    if not argexpand and not errmsg:
			msg_warning(recdict, msg)
			failed.append(fname)
		    else:
			recipe_error(rpstack, msg)
		    continue

	    if tscheme != '':
		if fscheme != '':
		    # use temporary file
		    fromfile = tmpfile
		else:
		    fromfile = fpath

		postponed = 0
		if tscheme == 'ftp':
		    # No system command, give a message about what we're doing,
		    # it may take a while.
		    if fscheme != '':
			msg_info(recdict, _('Uploading to "%s"' % dest))
		    else:
			if not copy:
			    msg_info(recdict, _('Moving "%s" to "%s"'
							      % (fname, dest)))
			else:
			    msg_info(recdict, _('Uploading "%s" to "%s"'
							      % (fname, dest)))

		    try:
			f = open(fromfile, "r")
			ftp.storbinary("STOR " + destpath, f, 8192)
			f.close()
		    except ftplib.all_errors, e:
			msg = (_('Cannot upload "%s" to "%s"')
						      % (fname, dest)) + str(e)
			if not argexpand and not errmsg:
			    msg_warning(recdict, msg)
			    failed.append(fname)
			else:
			    recipe_error(rpstack, msg)
			if fscheme != '':
			    os.remove(tmpfile)	    # delete temporary file
			continue

		elif tscheme == 'scp':
		    # TODO: check if scp command is available
		    if fscheme == '' and len(fromlist) > 1:
			# do all local files at once below
			scp_files = scp_files + "'" + fromfile + "' "
			scp_filelist.append(fromfile)
			postponed = 1
		    elif not scp_copy(recdict,
				     "'" + fromfile + "'", tmach, destpath, 1):
			msg = _('Cannot upload "%s" to "%s"') % (fname, dest)
			if not argexpand and not errmsg:
			    msg_warning(recdict, msg)
			    failed.append(fname)
			else:
			    recipe_error(rpstack, msg)
			if fscheme != '':
			    os.remove(tmpfile)	    # delete temporary file
			continue

		if fscheme != '':
		    os.remove(tmpfile)	    # delete temporary file
		    msg_info(recdict, _('Uploaded to "%s"' % dest))
		else:
		    if not copy:
			os.remove(fname)
			msg_info(recdict, _('Moved "%s" to "%s"'
						% (shorten_name(fname), dest)))
		    elif not postponed:
			msg_info(recdict, _('Uploaded "%s" to "%s"'
						% (shorten_name(fname), dest)))



    # End of loop over all "from" files.

    if tscheme == 'ftp':
	ftp.quit()	# close connection to ftp server
    if fscheme != '':
	from Remote import url_cleanup
	url_cleanup(fscheme)

    # Copy collected files with scp.
    if scp_files:
	# If there is only one file to copy, add its basename to the
	# destination path to avoid creating a file by the name of the
	# directory of that file.
	if len(scp_filelist) == 1:
	    tpath = os.path.join(tpath, os.path.basename(scp_filelist[0]))

	if scp_copy(recdict, scp_files, tmach, tpath, len(scp_filelist)):
	    dl = shorten_dictlist(str2dictlist(rpstack, scp_files))
	    flist = map(lambda x: x["name"], dl)
	    msg_info(recdict, _('Uploaded "%s" to "%s"' % (flist, tname)))
	else:
	    msg = _('Cannot upload "%s" to "%s"') % (scp_files, tname)
	    if not argexpand and not errmsg:
		msg_warning(recdict, msg)
		failed.extend(scp_filelist)
	    else:
		recipe_error(rpstack, msg)

    return failed


# vim: set sw=4 sts=4 tw=79 fo+=l:
