# Copyright 2002 Ben Escoto
#
# This file is part of duplicity.
#
# Duplicity 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 3 of the License, or (at your
# option) any later version.
#
# Duplicity 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 duplicity; if not, write to the Free Software Foundation,
# Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
"""Provides functions and classes for getting/sending files to destination"""
import os, socket, types, tempfile, time, sys
import log, path, dup_temp, file_naming, atexit
import base64, getpass, xml.dom.minidom, httplib, urllib
import socket, globals, re, string
from duplicity import tempdir
import inspect
import urlparse
if inspect.getmembers(urlparse, inspect.isclass):
# Use the system urlparse library.
import urlparse as urlparser
else:
# Use the bundled urlparse library.
import urlparse_2_5 as urlparser
socket.setdefaulttimeout(globals.timeout)
class BackendException(Exception): pass
class ParsingException(Exception): pass
def straight_url(parsed_url):
"""Return a URL from a urlparse object without a username or password."""
# Get a copy of the network location without the username or password.
straight_netloc = parsed_url.netloc.split('@')[-1]
# Replace the full network location with the stripped copy.
return parsed_url.geturl().replace(parsed_url.netloc, straight_netloc, 1)
def ParsedUrl(url_string):
# These URL schemes have a backend with a notion of an RFC "network location".
# The 'file' and 's3+http' schemes should not be in this list.
urlparser.uses_netloc = [ 'ftp', 'hsi', 'rsync', 's3', 'scp', 'ssh', 'webdav', 'webdavs' ]
# Do not transform or otherwise parse the URL path component.
urlparser.uses_query = []
urlparser.uses_fragment = []
pu = urlparser.urlparse(url_string)
# This happens for implicit local paths.
if not pu.scheme:
return pu
# Our backends do not handle implicit hosts.
if pu.scheme in urlparser.uses_netloc and not pu.hostname:
log.FatalError('Bad %s:// URL syntax: %s' % (pu.scheme, url_string))
# Our backends do not handle implicit relative paths.
if not pu.scheme in urlparser.uses_netloc and not pu.path.startswith('//'):
log.FatalError('Bad %s:// URL syntax: %s' % (pu.scheme, url_string))
return pu
def get_backend(url_string):
"""Return Backend object from url string, or None if not a url string"""
"""If a protocol is unsupported a fatal error will be raised."""
pu = ParsedUrl(url_string)
# This happens for implicit local paths.
if not pu.scheme:
return None
global protocol_class_dict
try:
backend_class = protocol_class_dict[pu.scheme]
except KeyError:
log.FatalError("Unknown scheme '%s'" % (pu.scheme,))
return backend_class(pu)
class Backend:
"""Represent a connection to the destination device/computer
Classes that subclass this should implement the put, get, list,
and delete methods.
"""
def __init__(self, parsed_url):
self.parsed_url = parsed_url
def put(self, source_path, remote_filename = None):
"""Transfer source_path (Path object) to remote_filename (string)
If remote_filename is None, get the filename from the last
path component of pathname.
"""
if not remote_filename: remote_filename = source_path.get_filename()
pass
def get(self, remote_filename, local_path):
"""Retrieve remote_filename and place in local_path"""
local_path.setdata()
pass
def list(self):
"""Return list of filenames (strings) present in backend"""
pass
def delete(self, filename_list):
"""Delete each filename in filename_list, in order if possible"""
pass
def get_password(self):
"""Get a password from the target url or from the environment."""
if self.parsed_url.password:
return self.parsed_url.password
try:
password = os.environ['FTP_PASSWORD']
except KeyError:
password = getpass.getpass("Password for '%s': " % self.parsed_url.hostname)
os.environ['FTP_PASSWORD'] = password
return password
def munge_password(self, commandline):
"""Remove password from commandline"""
if self.parsed_url.password:
return re.sub(self.parsed_url.password, '<passwd>', commandline)
else:
return commandline
def run_command(self, commandline):
"""Run given commandline with logging and error detection"""
private = self.munge_password(commandline)
log.Log("Running '%s'" % private, 5)
if os.system(commandline):
raise BackendException("Error running '%s'" % private)
def run_command_persist(self, commandline):
"""Run given commandline with logging and error detection
repeating it several times if it fails"""
private = self.munge_password(commandline)
for n in range(1, globals.num_retries+1):
log.Log("Running '%s' (attempt #%d)" % (private, n), 5)
if not os.system(commandline):
return
log.Log("Running '%s' failed (attempt #%d)" % (private, n), 1)
time.sleep(30)
log.Log("Giving up trying to execute '%s' after %d attempts" % (private, globals.num_retries), 1)
raise BackendException("Error running '%s'" % private)
def popen(self, commandline):
"""Run command and return stdout results"""
private = self.munge_password(commandline)
log.Log("Reading results of '%s'" % private, 5)
fout = os.popen(commandline)
results = fout.read()
if fout.close():
raise BackendException("Error running '%s'" % private)
return results
def popen_persist(self, commandline):
"""Run command and return stdout results, repeating on failure"""
private = self.munge_password(commandline)
for n in range(1, globals.num_retries+1):
log.Log("Reading results of '%s'" % private, 5)
fout = os.popen(commandline)
results = fout.read()
result_status = fout.close()
if not result_status:
return results
elif result_status == 1280 and self.parsed_url.scheme == 'ftp':
# This squelches the "file not found" result fromm ncftpls when
# the ftp backend looks for a collection that does not exist.
return ''
log.Log("Running '%s' failed (attempt #%d)" % (private, n), 1)
time.sleep(30)
log.Log("Giving up trying to execute '%s' after %d attempts" % (private, globals.num_retries), 1)
raise BackendException("Error running '%s'" % private)
def get_fileobj_read(self, filename, parseresults = None):
"""Return fileobject opened for reading of filename on backend
The file will be downloaded first into a temp file. When the
returned fileobj is closed, the temp file will be deleted.
"""
if not parseresults:
parseresults = file_naming.parse(filename)
assert parseresults, "Filename not correctly parsed"
tdp = dup_temp.new_tempduppath(parseresults)
self.get(filename, tdp)
tdp.setdata()
return tdp.filtered_open_with_delete("rb")
def get_fileobj_write(self, filename, parseresults = None,
sizelist = None):
"""Return fileobj opened for writing, write to backend on close
The file will be encoded as specified in parseresults (or as
read from the filename), and stored in a temp file until it
can be copied over and deleted.
If sizelist is not None, it should be set to an empty list.
The number of bytes will be inserted into the list.
"""
if not parseresults:
parseresults = file_naming.parse(filename)
assert parseresults, "Filename %s not correctly parsed" % filename
tdp = dup_temp.new_tempduppath(parseresults)
def close_file_hook():
"""This is called when returned fileobj is closed"""
self.put(tdp, filename)
if sizelist is not None:
tdp.setdata()
sizelist.append(tdp.getsize())
tdp.delete()
fh = dup_temp.FileobjHooked(tdp.filtered_open("wb"))
fh.addhook(close_file_hook)
return fh
def get_data(self, filename, parseresults = None):
"""Retrieve a file from backend, process it, return contents"""
fin = self.get_fileobj_read(filename, parseresults)
buf = fin.read()
assert not fin.close()
return buf
def put_data(self, buffer, filename, parseresults = None):
"""Put buffer into filename on backend after processing"""
fout = self.get_fileobj_write(filename, parseresults)
fout.write(buffer)
assert not fout.close()
def close(self):
"""This is called when a connection is no longer needed"""
pass
class LocalBackend(Backend):
"""Use this backend when saving to local disk
Urls look like file://testfiles/output. Relative to root can be
gotten with extra slash (file:///usr/local).
"""
def __init__(self, parsed_url):
Backend.__init__(self, parsed_url)
# The URL form "file:MyFile" is not a valid duplicity target.
if not parsed_url.path.startswith( '//' ):
raise BackendException( "Bad file:// path syntax." )
self.remote_pathdir = path.Path(parsed_url.path[2:])
def put(self, source_path, remote_filename = None, rename = None):
"""If rename is set, try that first, copying if doesn't work"""
if not remote_filename: remote_filename = source_path.get_filename()
target_path = self.remote_pathdir.append(remote_filename)
log.Log("Writing %s" % target_path.name, 6)
if rename:
try: source_path.rename(target_path)
except OSError: pass
else: return
target_path.writefileobj(source_path.open("rb"))
def get(self, filename, local_path):
"""Get file and put in local_path (Path object)"""
source_path = self.remote_pathdir.append(filename)
local_path.writefileobj(source_path.open("rb"))
def list(self):
"""List files in that directory"""
try:
os.makedirs(self.remote_pathdir.base)
except:
pass
return self.remote_pathdir.listdir()
def delete(self, filename_list):
"""Delete all files in filename list"""
assert type(filename_list) is not types.StringType
try:
for filename in filename_list:
self.remote_pathdir.append(filename).delete()
except OSError, e: raise BackendException(str(e))
# The following can be redefined to use different shell commands from
# ssh or scp or to add more arguments. However, the replacements must
# have the same syntax. Also these strings will be executed by the
# shell, so shouldn't have strange characters in them.
scp_command = "scp"
sftp_command = "sftp"
# default to batch mode using public-key encryption
ssh_askpass = False
# user added ssh options
ssh_options = ""
class sshBackend(Backend):
"""This backend copies files using scp. List not supported"""
def __init__(self, parsed_url):
"""scpBackend initializer"""
Backend.__init__(self, parsed_url)
try:
import pexpect
self.pexpect = pexpect
except ImportError:
self.pexpect = None
if not (self.pexpect and
hasattr(self.pexpect, '__version__') and
self.pexpect.__version__ >= '2.1'):
log.FatalError("This backend requires the pexpect module version 2.1 or later."
"You can get pexpect from http://pexpect.sourceforge.net or "
"python-pexpect from your distro's repository.")
# host string of form user@hostname:port
self.host_string = parsed_url.netloc
# make sure remote_dir is always valid
if parsed_url.path:
# remove leading '/'
self.remote_dir = re.sub(r'^/', r'', parsed_url.path, 1)
else:
self.remote_dir = '.'
self.remote_prefix = self.remote_dir + '/'
# maybe use different ssh port
if parsed_url.port:
self.ssh_options = ssh_options + " -oPort=%s" % parsed_url.port
else:
self.ssh_options = ssh_options
# set up password
if ssh_askpass:
self.password = self.get_password()
else:
self.password = ''
def run_scp_command(self, commandline):
""" Run an scp command, responding to password prompts """
for n in range(1, globals.num_retries+1):
log.Log("Running '%s' (attempt #%d)" % (commandline, n), 5)
child = self.pexpect.spawn(commandline, timeout = globals.timeout)
cmdloc = 0
if ssh_askpass:
state = "authorizing"
else:
state = "copying"
while 1:
if state == "authorizing":
match = child.expect([self.pexpect.EOF,
self.pexpect.TIMEOUT,
"(?i)password:",
"(?i)permission denied",
"authenticity"],
timeout = globals.timeout)
log.Log("State = %s, Before = '%s'" % (state, child.before.strip()), 9)
if match == 0:
log.Log("Failed to authenticate", 5)
break
elif match == 1:
log.Log("Timeout waiting to authenticate", 5)
break
elif match == 2:
child.sendline(self.password)
state = "copying"
elif match == 3:
log.Log("Invalid SSH password", 1)
break
elif match == 4:
log.Log("Remote host authentication failed (missing known_hosts entry?)", 1)
break
elif state == "copying":
match = child.expect([self.pexpect.EOF,
self.pexpect.TIMEOUT,
"stalled",
"authenticity",
"ETA"],
timeout = globals.timeout)
log.Log("State = %s, Before = '%s'" % (state, child.before.strip()), 9)
if match == 0:
break
elif match == 1:
log.Log("Timeout waiting for response", 5)
break
elif match == 2:
state = "stalled"
elif match == 3:
log.Log("Remote host authentication failed (missing known_hosts entry?)", 1)
break
elif state == "stalled":
match = child.expect([self.pexpect.EOF,
self.pexpect.TIMEOUT,
"ETA"],
timeout = globals.timeout)
log.Log("State = %s, Before = '%s'" % (state, child.before.strip()), 9)
if match == 0:
break
elif match == 1:
log.Log("Stalled for too long, aborted copy", 5)
break
elif match == 2:
state = "copying"
child.close(force = True)
if child.exitstatus == 0:
return
log.Log("Running '%s' failed (attempt #%d)" % (commandline, n), 1)
time.sleep(30)
log.Log("Giving up trying to execute '%s' after %d attempts" % (commandline, globals.num_retries), 1)
raise BackendException("Error running '%s'" % commandline)
def run_sftp_command(self, commandline, commands):
""" Run an sftp command, responding to password prompts, passing commands from list """
for n in range(1, globals.num_retries+1):
log.Log("Running '%s' (attempt #%d)" % (commandline, n), 5)
child = self.pexpect.spawn(commandline, timeout = globals.timeout)
cmdloc = 0
while 1:
match = child.expect([self.pexpect.EOF,
self.pexpect.TIMEOUT,
"sftp>",
"(?i)password:",
"(?i)permission denied",
"authenticity",
"(?i)no such file or directory"])
log.Log("State = sftp, Before = '%s'" % (child.before.strip()), 9)
if match == 0:
break
elif match == 1:
log.Log("Timeout waiting for response", 5)
break
if match == 2:
if cmdloc < len(commands):
command = commands[cmdloc]
log.Log("sftp command: '%s'" % (command,), 5)
child.sendline(command)
cmdloc += 1
else:
command = 'quit'
child.sendline(command)
res = child.before
elif match == 3:
child.sendline(self.password)
elif match == 4:
log.Log("Invalid SSH password", 1)
break
elif match == 5:
log.Log("Host key authenticity could not be verified (missing known_hosts entry?)", 1)
break
elif match == 6:
log.Log("Remote file or directory '%s' does not exist" % self.remote_dir, 1)
break
child.close(force = True)
if child.exitstatus == 0:
return res
log.Log("Running '%s' failed (attempt #%d)" % (commandline, n), 1)
time.sleep(30)
log.Log("Giving up trying to execute '%s' after %d attempts" % (commandline, globals.num_retries), 1)
raise BackendException("Error running '%s'" % commandline)
def put(self, source_path, remote_filename = None):
"""Use scp to copy source_dir/filename to remote computer"""
if not remote_filename: remote_filename = source_path.get_filename()
commandline = "%s %s %s %s:%s%s" % \
(scp_command, self.ssh_options, source_path.name, self.host_string,
self.remote_prefix, remote_filename)
self.run_scp_command(commandline)
def get(self, remote_filename, local_path):
"""Use scp to get a remote file"""
commandline = "%s %s %s:%s%s %s" % \
(scp_command, self.ssh_options, self.host_string, self.remote_prefix,
remote_filename, local_path.name)
self.run_scp_command(commandline)
local_path.setdata()
if not local_path.exists():
raise BackendException("File %s not found" % local_path.name)
def list(self):
"""List files available for scp
Note that this command can get confused when dealing with
files with newlines in them, as the embedded newlines cannot
be distinguished from the file boundaries.
"""
commands = ["mkdir %s" % (self.remote_dir,),
"cd %s" % (self.remote_dir,),
"ls -1"]
commandline = ("%s %s %s" % (sftp_command, self.ssh_options, self.host_string))
l = self.run_sftp_command(commandline, commands).split('\n')[1:]
return filter(lambda x: x, map(string.strip, l))
def delete(self, filename_list):
"""Runs sftp rm to delete files. Files must not require quoting"""
commands = ["cd %s" % (self.remote_dir,)]
for fn in filename_list:
commands.append("rm %s" % fn)
commandline = ("%s %s %s" % (sftp_command, self.ssh_options, self.host_string))
self.run_sftp_command(commandline, commands)
class ftpBackend(Backend):
"""Connect to remote store using File Transfer Protocol"""
def __init__(self, parsed_url):
Backend.__init__(self, parsed_url)
# we expect an error return, so go low-level and ignore it
try:
p = os.popen("ncftpls -v")
fout = p.read()
ret = p.close()
except:
pass
# the expected error is 8 in the high-byte and some output
if ret != 0x0800 or not fout:
log.FatalError("NcFTP not found: Please install NcFTP version 3.1.9 or later")
# version is the second word of the first line
version = fout.split('\n')[0].split()[1]
if version < "3.1.9":
log.FatalError("NcFTP too old: Duplicity requires NcFTP version 3.1.9 or later")
log.Log("NcFTP version is %s" % version, 4)
self.parsed_url = parsed_url
self.url_string = straight_url(self.parsed_url)
# Use an explicit directory name.
if self.url_string[-1] != '/':
self.url_string += '/'
self.password = self.get_password()
if globals.ftp_connection == 'regular':
self.conn_opt = '-E'
else:
self.conn_opt = '-F'
self.tempfile, self.tempname = tempdir.default().mkstemp()
os.write(self.tempfile, "host %s\n" % self.parsed_url.hostname)
os.write(self.tempfile, "user %s\n" % self.parsed_url.username)
os.write(self.tempfile, "pass %s\n" % self.password)
os.close(self.tempfile)
self.flags = "-f %s %s -t %s" % \
(self.tempname, self.conn_opt, globals.timeout)
if parsed_url.port != None and parsed_url.port != 21:
self.flags += " -P '%s'" % (parsed_url.port)
def put(self, source_path, remote_filename = None):
"""Transfer source_path to remote_filename"""
remote_path = os.path.join(urllib.unquote(self.parsed_url.path), remote_filename).rstrip()
commandline = "ncftpput %s -m -V -C '%s' '%s'" % \
(self.flags, source_path.name, remote_path)
self.run_command_persist(commandline)
def get(self, remote_filename, local_path):
"""Get remote filename, saving it to local_path"""
remote_path = os.path.join(urllib.unquote(self.parsed_url.path), remote_filename).rstrip()
commandline = "ncftpget %s -V -C '%s' '%s' '%s'" % \
(self.flags, self.parsed_url.hostname, remote_path, local_path.name)
self.run_command_persist(commandline)
local_path.setdata()
def list(self):
"""List files in directory"""
# we create the directory first so we have target dir
# try for a long listing to avoid connection reset
commandline = "ncftpls %s -l '%s'" % \
(self.flags, self.url_string)
l = self.popen_persist(commandline).split('\n')
l = filter(lambda x: x, l)
if not l:
return l
# if long list is not empty, get short list of names only
commandline = "ncftpls %s -1 '%s'" % \
(self.flags, self.url_string)
l = self.popen_persist(commandline).split('\n')
return filter(lambda x: x, l)
def delete(self, filename_list):
"""Delete files in filename_list"""
for filename in filename_list:
commandline = "ncftpls %s -X 'DELE %s' '%s/%s'" % \
(self.flags, filename, self.url_string, filename)
self.popen_persist(commandline)
class rsyncBackend(Backend):
"""Connect to remote store using rsync
rsync backend contributed by Sebastian Wilhelmi <seppi@seppi.de>
"""
def __init__(self, parsed_url):
"""rsyncBackend initializer"""
Backend.__init__(self, parsed_url)
user, host = parsed_url.netloc.split('@')
if parsed_url.password:
user = user.split(':')[0]
mynetloc = '%s@%s' % (user, host)
self.url_string = "%s%s" % (mynetloc, parsed_url.path.lstrip('/'))
if self.url_string[-1] != '/':
self.url_string += '/'
def put(self, source_path, remote_filename = None):
"""Use rsync to copy source_dir/filename to remote computer"""
if not remote_filename: remote_filename = source_path.get_filename()
remote_path = os.path.join(self.url_string, remote_filename)
commandline = "rsync %s %s" % (source_path.name, remote_path)
self.run_command(commandline)
def get(self, remote_filename, local_path):
"""Use rsync to get a remote file"""
remote_path = os.path.join (self.url_string, remote_filename)
commandline = "rsync %s %s" % (remote_path, local_path.name)
self.run_command(commandline)
local_path.setdata()
if not local_path.exists():
raise BackendException("File %s not found" % local_path.name)
def list(self):
"""List files"""
def split (str):
line = str.split ()
if len (line) > 4 and line[4] != '.':
return line[4]
else:
return None
commandline = "rsync %s" % self.url_string
return filter (lambda x: x, map (split, self.popen(commandline).split('\n')))
def delete(self, filename_list):
"""Delete files."""
delete_list = filename_list
dont_delete_list = []
for file in self.list ():
if file in delete_list:
delete_list.remove (file)
else:
dont_delete_list.append (file)
if len (delete_list) > 0:
raise BackendException("Files %s not found" % str (delete_list))
dir = tempfile.mkdtemp()
exclude, exclude_name = tempdir.default().mkstemp_file()
to_delete = [exclude_name]
for file in dont_delete_list:
path = os.path.join (dir, file)
to_delete.append (path)
f = open (path, 'w')
print >>f, file
f.close()
exclude.close()
commandline = ("rsync --recursive --delete --exclude-from=%s %s/ %s" %
(exclude_name, dir, self.url_string))
self.run_command(commandline)
for file in to_delete:
os.unlink (file)
os.rmdir (dir)
class BotoBackend(Backend):
"""
Backend for Amazon's Simple Storage System, (aka Amazon S3), though
the use of the boto module, (http://code.google.com/p/boto/).
To make use of this backend you must export the environment variables
AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY with your Amazon Web
Services key id and secret respectively.
"""
def __init__(self, parsed_url):
Backend.__init__(self, parsed_url)
try:
from boto.s3.connection import S3Connection
from boto.s3.key import Key
assert hasattr(S3Connection, 'lookup')
except ImportError:
log.FatalError("This backend requires boto library, version 0.9d or later, "
"(http://code.google.com/p/boto/).")
if not os.environ.has_key('AWS_ACCESS_KEY_ID'):
raise BackendException("The AWS_ACCESS_KEY_ID environment variable is not set.")
if not os.environ.has_key('AWS_SECRET_ACCESS_KEY'):
raise BackendException("The AWS_SECRET_ACCESS_KEY environment variable is not set.")
if parsed_url.scheme == 's3+http':
# Use the default Amazon S3 host.
self.conn = S3Connection()
else:
assert parsed_url.scheme == 's3'
self.conn = S3Connection(host=parsed_url.hostname)
# This folds the null prefix and all null parts, which means that:
# //MyBucket/ and //MyBucket are equivalent.
# //MyBucket//My///My/Prefix/ and //MyBucket/My/Prefix are equivalent.
self.url_parts = filter(lambda x: x != '', parsed_url.path.split('/'))
if self.url_parts:
self.bucket_name = self.url_parts.pop(0)
else:
# Duplicity hangs if boto gets a null bucket name.
# HC: Caught a socket error, trying to recover
raise BackendException('Boto requires a bucket name.')
self.bucket = self.conn.lookup(self.bucket_name)
self.key_class = Key
if self.url_parts:
self.key_prefix = '%s/' % '/'.join(self.url_parts)
else:
self.key_prefix = ''
self.straight_url = straight_url(parsed_url)
def put(self, source_path, remote_filename=None):
if not self.bucket:
self.bucket = self.conn.create_bucket(self.bucket_name)
if not remote_filename:
remote_filename = source_path.get_filename()
key = self.key_class(self.bucket)
key.key = self.key_prefix + remote_filename
for n in range(1, globals.num_retries+1):
log.Log("Uploading %s/%s" % (self.straight_url, remote_filename), 5)
try:
key.set_contents_from_filename(source_path.name, {'Content-Type': 'application/octet-stream'})
return
except:
pass
log.Log("Upload '%s/%s' failed (attempt #%d)" % (self.straight_url, remote_filename, n), 1)
time.sleep(30)
log.Log("Giving up trying to upload %s/%s after %d attempts" % (self.straight_url, remote_filename, globals.num_retries), 1)
raise BackendException("Error uploading %s/%s" % (self.straight_url, remote_filename))
def get(self, remote_filename, local_path):
key = self.key_class(self.bucket)
key.key = self.key_prefix + remote_filename
for n in range(1, globals.num_retries+1):
log.Log("Downloading %s/%s" % (self.straight_url, remote_filename), 5)
try:
key.get_contents_to_filename(local_path.name)
local_path.setdata()
return
except:
pass
log.Log("Download %s/%s failed (attempt #%d)" % (self.straight_url, remote_filename, n), 1)
time.sleep(30)
log.Log("Giving up trying to download %s/%s after %d attempts" % (self.straight_url, remote_filename, globals.num_retries), 1)
raise BackendException("Error downloading %s/%s" % (self.staight_url, remote_filename))
def list(self):
filename_list = []
if self.bucket:
for k in self.bucket.list(prefix = self.key_prefix + 'd', delimiter = '/'):
try:
filename = k.key.replace(self.key_prefix, '', 1)
filename_list.append(filename)
except AttributeError:
pass
log.Log("Listed %s/%s" % (self.straight_url, filename), 9)
return filename_list
def delete(self, filename_list):
for filename in filename_list:
self.bucket.delete_key(self.key_prefix + filename)
log.Log("Deleted %s/%s" % (self.straight_url, filename), 9)
class webdavBackend(Backend):
"""Backend for accessing a WebDAV repository.
webdav backend contributed in 2006 by Jesper Zedlitz <jesper@zedlitz.de>
"""
listbody = """\
<?xml version="1.0" encoding="utf-8" ?>
<D:propfind xmlns:D="DAV:">
<D:allprop/>
</D:propfind>
"""
"""Connect to remote store using WebDAV Protocol"""
def __init__(self, parsed_url):
Backend.__init__(self, parsed_url)
self.headers = {}
self.parsed_url = parsed_url
if parsed_url.path:
foldpath = re.compile('/+')
self.directory = foldpath.sub('/', parsed_url.path + '/' )
else:
self.directory = '/'
log.Log("Using WebDAV host %s" % (parsed_url.hostname,), 5)
log.Log("Using WebDAV directory %s" % (self.directory,), 5)
log.Log("Using WebDAV protocol %s" % (globals.webdav_proto,), 5)
password = self.get_password()
if parsed_url.scheme == 'webdav':
self.conn = httplib.HTTPConnection(parsed_url.hostname)
elif parsed_url.scheme == 'webdavs':
self.conn = httplib.HTTPSConnection(parsed_url.hostname)
else:
raise BackendException("Unknown URI scheme: %s" % (parsed_url.scheme))
self.headers['Authorization'] = 'Basic ' + base64.encodestring(parsed_url.username+':'+ password).strip()
# check password by connection to the server
self.conn.request("OPTIONS", self.directory, None, self.headers)
response = self.conn.getresponse()
response.read()
if response.status != 200:
raise BackendException((response.status, response.reason))
def _getText(self,nodelist):
rc = ""
for node in nodelist:
if node.nodeType == node.TEXT_NODE:
rc = rc + node.data
return rc
def close(self):
self.conn.close()
def list(self):
"""List files in directory"""
for n in range(1, globals.num_retries+1):
log.Log("Listing directory %s on WebDAV server" % (self.directory,), 5)
self.headers['Depth'] = "1"
self.conn.request("PROPFIND", self.directory, self.listbody, self.headers)
del self.headers['Depth']
response = self.conn.getresponse()
if response.status == 207:
document = response.read()
break
log.Log("WebDAV PROPFIND attempt #%d failed: %s %s" % (n, response.status, response.reason), 5)
if n == globals.num_retries +1:
log.Log("WebDAV backend giving up after %d attempts to PROPFIND %s" % (globals.num_retries, self.directory), 1)
raise BackendException((response.status, response.reason))
log.Log("%s" % (document,), 6)
dom = xml.dom.minidom.parseString(document)
result = []
for href in dom.getElementsByTagName('D:href'):
filename = urllib.unquote(self._getText(href.childNodes).strip())
if filename.startswith(self.directory):
filename = filename.replace(self.directory,'',1)
result.append(filename)
return result
def get(self, remote_filename, local_path):
"""Get remote filename, saving it to local_path"""
url = self.directory + remote_filename
target_file = local_path.open("wb")
for n in range(1, globals.num_retries+1):
log.Log("Retrieving %s from WebDAV server" % (url ,), 5)
self.conn.request("GET", url, None, self.headers)
response = self.conn.getresponse()
if response.status == 200:
target_file.write(response.read())
assert not target_file.close()
local_path.setdata()
return
log.Log("WebDAV GET attempt #%d failed: %s %s" % (n, response.status, response.reason), 5)
log.Log("WebDAV backend giving up after %d attempts to GET %s" % (globals.num_retries, url), 1)
raise BackendException((response.status, response.reason))
def put(self, source_path, remote_filename = None):
"""Transfer source_path to remote_filename"""
if not remote_filename:
remote_filename = source_path.get_filename()
url = self.directory + remote_filename
source_file = source_path.open("rb")
for n in range(1, globals.num_retries+1):
log.Log("Saving %s on WebDAV server" % (url ,), 5)
self.conn.request("PUT", url, source_file.read(), self.headers)
response = self.conn.getresponse()
if response.status == 201:
response.read()
assert not source_file.close()
return
log.Log("WebDAV PUT attempt #%d failed: %s %s" % (n, response.status, response.reason), 5)
log.Log("WebDAV backend giving up after %d attempts to PUT %s" % (globals.num_retries, url), 1)
raise BackendException((response.status, response.reason))
def delete(self, filename_list):
"""Delete files in filename_list"""
for filename in filename_list:
url = self.directory + filename
for n in range(1, globals.num_retries+1):
log.Log("Deleting %s from WebDAV server" % (url ,), 5)
self.conn.request("DELETE", url, None, self.headers)
response = self.conn.getresponse()
if response.status == 204:
response.read()
break
log.Log("WebDAV DELETE attempt #%d failed: %s %s" % (n, response.status, response.reason), 5)
if n == globals.num_retries +1:
log.Log("WebDAV backend giving up after %d attempts to DELETE %s" % (globals.num_retries, url), 1)
raise BackendException((response.status, response.reason))
hsi_command = "hsi"
class hsiBackend(Backend):
def __init__(self, parsed_url):
Backend.__init__(self, parsed_url)
self.host_string = parsed_url.hostname
self.remote_dir = parsed_url.path
if self.remote_dir: self.remote_prefix = self.remote_dir + "/"
else: self.remote_prefix = ""
def put(self, source_path, remote_filename = None):
if not remote_filename: remote_filename = source_path.get_filename()
commandline = '%s "put %s : %s%s"' % (hsi_command,source_path.name,self.remote_prefix,remote_filename)
try:
self.run_command(commandline)
except:
print commandline
def get(self, remote_filename, local_path):
commandline = '%s "get %s : %s%s"' % (hsi_command, local_path.name, self.remote_prefix, remote_filename)
self.run_command(commandline)
local_path.setdata()
if not local_path.exists():
raise BackendException("File %s not found" % local_path.name)
def list(self):
commandline = '%s "ls -l %s"' % (hsi_command, self.remote_dir)
l = os.popen3(commandline)[2].readlines()[3:]
for i in range(0,len(l)):
l[i] = l[i].split()[-1]
print filter(lambda x: x, l)
return filter(lambda x: x, l)
def delete(self, filename_list):
assert len(filename_ist) > 0
pathlist = map(lambda fn: self.remote_prefix + fn, filename_list)
for fn in filename_list:
commandline = '%s "rm %s%s"' % (hsi_command, self.remote_prefix, fn)
self.run_command(commandline)
# Dictionary relating protocol strings to backend_object classes.
protocol_class_dict = {"file": LocalBackend,
"ftp": ftpBackend,
"hsi": hsiBackend,
"rsync": rsyncBackend,
"scp": sshBackend,
"ssh": sshBackend,
"s3": BotoBackend,
"s3+http": BotoBackend,
"webdav": webdavBackend,
"webdavs": webdavBackend,
}
syntax highlighted by Code2HTML, v. 0.9.1