1
# Copyright 2002 Ben Escoto
3
# This file is part of duplicity.
5
# duplicity is free software; you can redistribute it and/or modify it
6
# under the terms of the GNU General Public License as published by
7
# the Free Software Foundation, Inc., 675 Mass Ave, Cambridge MA
8
# 02139, USA; either version 2 of the License, or (at your option) any
9
# later version; incorporated herein by reference.
11
"""Provides functions and classes for getting/sending files to destination"""
14
import log, path, dup_temp, file_naming
16
class BackendException(Exception): pass
18
def get_backend(url_string):
19
"""Return Backend object from url string, or None if not a url string
22
scp://foobar:password@hostname.net:124/usr/local. If a protocol
23
is unsupported a fatal error will be raised.
26
global protocol_class_dict
27
def bad_url(message = None):
29
err_string = "Bad URL string '%s': %s" % (url_string, message)
30
else: err_string = "Bad URL string '%s'" % url_string
31
log.FatalError(err_string)
33
colon_position = url_string.find(":")
34
if colon_position < 1: return None
35
protocol = url_string[:colon_position]
36
if url_string[colon_position+1:colon_position+3] != '//': return None
37
remaining_string = url_string[colon_position+3:]
39
try: backend, separate_host = protocol_class_dict[protocol]
40
except KeyError: bad_url("Unknown protocol '%s'" % protocol)
41
assert not separate_host, "This part isn't done yet"
43
return backend(remaining_string)
47
"""Represent a connection to the destination device/computer
49
Classes that subclass this should implement the put, get, list,
53
def init(self, some_arguments): pass
55
def put(self, source_path, remote_filename = None):
56
"""Transfer source_path (Path object) to remote_filename (string)
58
If remote_filename is None, get the filename from the last
59
path component of pathname.
62
if not remote_filename: remote_filename = source_path.get_filename()
65
def get(self, remote_filename, local_path):
66
"""Retrieve remote_filename and place in local_path"""
70
"""Return list of filenames (strings) present in backend"""
73
def delete(self, filename_list):
74
"""Delete each filename in filename_list, in order if possible"""
77
def run_command(self, commandline):
78
"""Run given commandline with logging and error detection"""
79
log.Log("Running '%s'" % commandline, 4)
80
if os.system(commandline):
81
raise BackendException("Error running '%s'" % commandline)
83
def popen(self, commandline):
84
"""Run command and return stdout results"""
85
log.Log("Reading results of '%s'" % commandline, 4)
86
fout = os.popen(commandline)
89
raise BackendException("Error running '%s'" % commandline)
92
def get_fileobj_read(self, filename, parseresults = None):
93
"""Return fileobject opened for reading of filename on backend
95
The file will be downloaded first into a temp file. When the
96
returned fileobj is closed, the temp file will be deleted.
100
parseresults = file_naming.parse(filename)
101
assert parseresults, "Filename not correctly parsed"
102
tdp = dup_temp.new_tempduppath(parseresults)
103
self.get(filename, tdp)
105
return tdp.filtered_open_with_delete("rb")
107
def get_fileobj_write(self, filename, parseresults = None):
108
"""Return fileobj opened for writing, write to backend on close
110
The file will be encoded as specified in parseresults (or as
111
read from the filename), and stored in a temp file until it
112
can be copied over and deleted.
116
parseresults = file_naming.parse(filename)
117
assert parseresults, "Filename not correctly parsed"
118
tdp = dup_temp.new_tempduppath(parseresults)
120
def close_file_hook():
121
"""This is called when returned fileobj is closed"""
122
self.put(tdp, filename)
125
fh = dup_temp.FileobjHooked(tdp.filtered_open("wb"))
126
fh.addhook(close_file_hook)
129
def get_data(self, filename, parseresults = None):
130
"""Retrieve a file from backend, process it, return contents"""
131
fin = self.get_fileobj_read(filename, parseresults)
133
assert not fin.close()
136
def put_data(self, buffer, filename, parseresults = None):
137
"""Put buffer into filename on backend after processing"""
138
fout = self.get_fileobj_write(filename, parseresults)
140
assert not fout.close()
143
class LocalBackend(Backend):
144
"""Use this backend when saving to local disk
146
Urls look like file://testfiles/output. Relative to root can be
147
gotten with extra slash (file:///usr/local).
150
def __init__(self, directory_name):
151
self.remote_pathdir = path.Path(directory_name)
153
def put(self, source_path, remote_filename = None, rename = None):
154
"""If rename is set, try that first, copying if doesn't work"""
155
if not remote_filename: remote_filename = source_path.get_filename()
156
target_path = self.remote_pathdir.append(remote_filename)
157
log.Log("Writing %s" % target_path.name, 6)
159
try: source_path.rename(target_path)
162
target_path.writefileobj(source_path.open("rb"))
164
def get(self, filename, local_path):
165
"""Get file and put in local_path (Path object)"""
166
source_path = self.remote_pathdir.append(filename)
167
local_path.writefileobj(source_path.open("rb"))
170
"""List files in that directory"""
171
return self.remote_pathdir.listdir()
173
def delete(self, filename_list):
174
"""Delete all files in filename list"""
176
for filename in filename_list:
177
self.remote_pathdir.append(filename).delete()
178
except OSError, e: raise BackendException(str(e))
181
class scpBackend(Backend):
182
"""This backend copies files using scp. List not supported"""
183
def __init__(self, url_string):
184
"""scpBackend initializer
186
Here url_string is something like
187
username@host.net/file/whatever, which is produced after the
188
'scp://' of a url is stripped.
191
comps = url_string.split("/")
192
self.host_string = comps[0] # of form user@hostname
193
self.remote_dir = "/".join(comps[1:]) # can be empty string
194
if self.remote_dir: self.remote_prefix = self.remote_dir + "/"
195
else: self.remote_prefix = ""
197
def put(self, source_path, remote_filename = None):
198
"""Use scp to copy source_dir/filename to remote computer"""
199
if not remote_filename: remote_filename = source_path.get_filename()
200
commandline = "scp %s %s:%s%s" % \
201
(source_path.name, self.host_string,
202
self.remote_prefix, remote_filename)
203
self.run_command(commandline)
205
def get(self, remote_filename, local_path):
206
"""Use scp to get a remote file"""
207
commandline = "scp %s:%s%s %s" % \
208
(self.host_string, self.remote_prefix,
209
remote_filename, local_path.name)
210
self.run_command(commandline)
212
if not local_path.exists():
213
raise BackendException("File %s not found" % local_path.name)
216
"""List files available for scp
218
Note that this command can get confused when dealing with
219
files with newlines in them, as the embedded newlines cannot
220
be distinguished from the file boundaries.
223
commandline = "ssh %s ls %s" % (self.host_string, self.remote_dir)
224
return filter(lambda x: x, self.popen(commandline).split("\n"))
226
def delete(self, filename_list):
227
"""Runs ssh rm to delete files. Files must not require quoting"""
228
pathlist = map(lambda fn: self.remote_prefix + fn, filename_list)
229
commandline = "ssh %s rm %s" % \
230
(self.host_string, " ".join(pathlist))
231
self.run_command(commandline)
234
class sftpBackend(Backend):
235
"""This backend uses sftp to perform file operations"""
239
# Dictionary relating protocol strings to tuples (backend_object,
240
# separate_host). If separate_host is true, get_backend() above will
241
# parse the url further to try to extract a hostname, protocol, etc.
242
protocol_class_dict = {"scp": (scpBackend, 0),
243
"ssh": (scpBackend, 0),
244
"file": (LocalBackend, 0)}