# Part of the A-A-P recipe executive: Access files which may be remote

# 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

#
# Access files by their URL.
# If they are remote, may download or upload the file.
# Uses the Cache to avoid up/downloading too often.
#

import os.path
import shutil
import time
from urlparse import urlparse
from urllib import urlretrieve, urlcleanup, urlopen

from Util import *
from Message import *

def is_url(name):
    """Return non-zero when "name" is a URL, zero when it's a local file."""
    # On MS-Windows c:/name is a file, not a URL.
    if len(name) > 2 and name[1] == ':':
	return 0
    scheme, mach, path, parm, query, frag = urlparse(name, '', 0)
    return scheme != ''


def url_split3(name):
    """Split a URL into scheme, machine and path."""
    # On MS-Windows c:/name is a file, not a URL.
    if len(name) > 2 and name[1] == ':':
	return '', '', name
    scheme, mach, path, parm, query, frag = urlparse(name, '', 0)
    if scheme != '' and mach == '' and path[:2] == '//':
	# urlparse doesn't handle scp://machine/path correctly
	mach = path[2:]
	path = ''
	i = string.find(mach, '/')
	if i > 0:
	    path = mach[i + 1:]
	    mach = mach[:i]
    if scheme == "file":
	scheme = ''	# A file is the same as no scheme.
    # For "ftp://mach/foobar" the file is "foobar", not "/foobar".
    if scheme == "ftp" and len(path) > 1 and path[0] == '/':
	path = path[1:]
    return scheme, mach, path + parm + query + frag


def url_time(recdict, name):
    """Obtain the timestamp in seconds (in GMT if possible) for the URL "name".
       Returns zero (very old) if the timestamp can't be obtained."""
    if is_url(name):
	from Cache import cache_lookup
	c = cache_lookup(recdict, name)
	if c:
	    # use timestamp for cached file.
	    t = c.timestamp()
	else:
	    # obtain timestamp for remote files.
	    t = remote_time(recdict, name)

    else:
	try:
	    t = os.path.getmtime(name)
	except (IOError, OSError):
	    t = 0
    return t


def remote_time(recdict, name):
    """Get the timestamp of a remote file."""
    try:
	msg_info(recdict, _('getting timestamp for "%s"') % name)
	up = urlopen(name)
	t = get_header_date(up.info())
	up.close()
    except:
	t = 0
    return t


def get_header_date(headers):
    """Get the date from a MIME header.  Returns zero when not available."""
    from rfc822 import parsedate

    if headers.has_key("Last-Modified"):
	return time.mktime(parsedate(headers["Last-Modified"]))
    if headers.has_key("Date"):
	return time.mktime(parsedate(headers["Date"]))
    return 0


def url_download(recdict, url, fname):
    """Attempt downloading file "url" to file "fname".
       Overwrite "fname" if it already exists.
       When "fname" is empty, use a temporary file.  The caller has to use
       "url_cleanup()" when done with it.
       Returns a tuple of the filename and the timestamp of the remote file
       when possible.
       Throws an IOError if downloading failed."""
    msg_info(recdict, _('Attempting download of "%s"' % url))
    rtime = 0

    fscheme, fmach, fpath = url_split3(url)

    if fscheme == 'scp':
	if fname == '':
	    resfile = tempfname()
	else:
	    resfile = fname
	logged_system(recdict, 'scp -C %s:%s %s' % (fmach, fpath, resfile))
    else:
	if fname == '':
	    # read to temporary file
	    resfile, h = urlretrieve(url)
	else:
	    resfile, h = urlretrieve(url, fname)
	    if resfile != fname:
		# Using a cached file, need to make a copy.
		shutil.copy2(resfile, fname)
		resfile = fname
	    urlcleanup()
	if h:
	    rtime = get_header_date(h)

    if fname == '':
	msg_info(recdict, _('Downloaded "%s"' % url))
    else:
	msg_info(recdict, _('Downloaded "%s" to "%s"' % (url, fname)))

    return resfile, rtime


def url_cleanup(scheme):
    """Cleanup after using url_download with scheme "scheme"."""
    if scheme != 'scp':
	urlcleanup()	# remove any cached file from urlretrieve()


def download_file(recdict, url_dl, node, use_cache):
    """Download a file according to "url_dl" and copy it over "node.name".
       Use the cache when "use_cache" is non-zero, otherwise obtain a fresh
       copy.
       Can also be used for a local file, it is copied.
       Return non-zero for success."""
    from Cache import local_name

    if not use_cache:
	cu = "0 sec"
    elif url_dl.has_key("cache_update"):
	cu = url_dl["cache_update"]
    else:
	cu = None
    # This downloads the file when it's not in the cache already.
    # TODO: handle attributes (e.g., login and password)
    fname, use_cache = local_name(recdict, url_dl["name"], cu)

    if fname and os.path.exists(fname):
	if skip_commands():
	    msg_info(recdict, _('Skip copy file: "%s"') % node.short_name())
	else:
	    # copy the downloaded file over the original one.
	    try:
		import shutil
		shutil.copyfile(fname, node.absname)
	    except IOError, e:
		raise UserError, (_('Cannot copy "%s" to "%s": ')
						 % (fname, node.name) + str(e))
	    if use_cache:
		msg_info(recdict, _('Copied file from cache: "%s"')
							   % node.short_name())
	    else:
		msg_info(recdict, _('Copied file "%s" to "%s"')
						  % (fname, node.short_name()))
	return 1
    return 0


def upload_file(recdict, url_dl, nodelist):
    """Upload nodes in "nodelist" according to "url_dl".
       Return list of nodes that failed."""
    # TODO: use other attributes in url_dl, e.g. a login name.
    from CopyMove import remote_copy_move
    from VersCont import repl_file_name
    failed = []

    # Make a copy of the nodelist.  Remove items that have been done until none
    # are left.
    todolist = nodelist[:]
    while todolist:
	# Collect nodes that are in the same directory and where the source and
	# destination file names are identical.
	to_item = ''
	fromlist = []
	for node in todolist[:]:
	    to_name = repl_file_name(url_dl["name"], node.name)
	    if os.path.basename(node.absname) == os.path.basename(to_name):
		d = os.path.dirname(to_name)
		if not d:
		    d = "."
		if not to_item or d == to_item:
		    fromlist.append({"name" : node.absname})
		    todolist.remove(node)
		    to_item = d
	    elif not to_item:
		# Source and target name are different, must copy this one by
		# itself.
		fromlist = [ {"name" : node.absname } ]
		todolist.remove(node)
		to_item = to_name
		break

	# When there is only one item include the file name in the to_item,
	# this avoids using the name of a directory for a file.
	if len(fromlist) == 1 and to_item != to_name:
	    to_item = os.path.join(to_item,
					 os.path.basename(fromlist[0]["name"]))

	msg_info(recdict, 'Uploading %s to %s' % (str(map(lambda x:
			     x["name"], shorten_dictlist(fromlist))), to_item))

	flist = remote_copy_move([], recdict, 1, fromlist,
				       { "name" : to_item }, {}, 0, errmsg = 0)

	# Find the nodes for the failed file names.
	for f in flist:
	    for node in nodelist:
		if node.absname == f:
		    failed.append(node)

    return failed


def remote_remove(recdict, url_dl, node):
    """Delete remote file for node "node" according to "url_dl"."""
    msg_info(recdict, 'Removing "%s" NOT IMPLEMENTED YET' % url_dl["name"])
    node = node
    return 1


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