16
16
# along with duplicity; if not, write to the Free Software Foundation,
17
17
# Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
19
from future_builtins import filter
21
23
from duplicity import backend
22
from duplicity.errors import UnsupportedBackendScheme, BackendException
24
from duplicity.errors import BackendException
23
25
from duplicity import log
24
26
from duplicity import globals
26
class Par2WrapperBackend(backend.Backend):
28
class Par2Backend(backend.Backend):
27
29
"""This backend wrap around other backends and create Par2 recovery files
28
30
before the file and the Par2 files are transfered with the wrapped backend.
37
39
except AttributeError:
38
40
self.redundancy = 10
41
url_string = self.parsed_url.url_string.lstrip('par2+')
42
self.wrapped_backend = backend.get_backend(url_string)
44
raise UnsupportedBackendScheme(self.parsed_url.url_string)
46
def put(self, source_path, remote_filename = None):
42
self.wrapped_backend = backend.get_backend_object(parsed_url.url_string)
44
for attr in ['_get', '_put', '_list', '_delete', '_delete_list',
45
'_query', '_query_list', '_retry_cleanup', '_error_code',
47
if hasattr(self.wrapped_backend, attr):
48
setattr(self, attr, getattr(self, attr[1:]))
50
def transfer(self, method, source_path, remote_filename):
47
51
"""create Par2 files and transfer the given file and the Par2 files
48
52
with the wrapped backend.
52
56
the soure_path with remote_filename into this.
55
if remote_filename is None:
56
remote_filename = source_path.get_filename()
58
60
par2temp = source_path.get_temp_in_same_dir()
60
62
source_symlink = par2temp.append(remote_filename)
61
os.symlink(source_path.get_canonical(), source_symlink.get_canonical())
63
source_target = source_path.get_canonical()
64
if not os.path.isabs(source_target):
65
source_target = os.path.join(os.getcwd(), source_target)
66
os.symlink(source_target, source_symlink.get_canonical())
62
67
source_symlink.setdata()
64
69
log.Info("Create Par2 recovery files")
70
75
for file in par2temp.listdir():
71
76
files_to_transfer.append(par2temp.append(file))
73
ret = self.wrapped_backend.put(source_path, remote_filename)
78
method(source_path, remote_filename)
74
79
for file in files_to_transfer:
75
self.wrapped_backend.put(file, file.get_filename())
80
method(file, file.get_filename())
80
def move(self, source_path, remote_filename = None):
81
self.put(source_path, remote_filename)
84
def put(self, local, remote):
85
self.transfer(self.wrapped_backend._put, local, remote)
87
def move(self, local, remote):
88
self.transfer(self.wrapped_backend._move, local, remote)
84
90
def get(self, remote_filename, local_path):
85
91
"""transfer remote_filename and the related .par2 file into
95
101
local_path_temp = par2temp.append(remote_filename)
97
ret = self.wrapped_backend.get(remote_filename, local_path_temp)
103
self.wrapped_backend._get(remote_filename, local_path_temp)
100
106
par2file = par2temp.append(remote_filename + '.par2')
101
self.wrapped_backend.get(par2file.get_filename(), par2file)
107
self.wrapped_backend._get(par2file.get_filename(), par2file)
103
109
par2verify = 'par2 v -q -q %s %s' % (par2file.get_canonical(), local_path_temp.get_canonical())
104
110
out, returncode = pexpect.run(par2verify, -1, True)
107
113
log.Warn("File is corrupt. Try to repair %s" % remote_filename)
108
par2volumes = self.list(re.compile(r'%s\.vol[\d+]*\.par2' % remote_filename))
114
par2volumes = filter(re.compile((r'%s\.vol[\d+]*\.par2' % remote_filename).match,
115
self.wrapped_backend._list()))
110
117
for filename in par2volumes:
111
118
file = par2temp.append(filename)
112
self.wrapped_backend.get(filename, file)
119
self.wrapped_backend._get(filename, file)
114
121
par2repair = 'par2 r -q -q %s %s' % (par2file.get_canonical(), local_path_temp.get_canonical())
115
122
out, returncode = pexpect.run(par2repair, -1, True)
125
132
local_path_temp.rename(local_path)
126
133
par2temp.deltree()
129
def list(self, filter = re.compile(r'(?!.*\.par2$)')):
130
"""default filter all files that ends with ".par"
131
filter can be a re.compile instance or False for all remote files
135
def delete(self, filename):
136
"""delete given filename and its .par2 files
133
list = self.wrapped_backend.list()
138
if filter.match(item):
139
filtered_list.append(item)
142
def delete(self, filename_list):
138
self.wrapped_backend._delete(filename)
140
remote_list = self.list()
141
filename_list = [filename]
142
c = re.compile(r'%s(?:\.vol[\d+]*)?\.par2' % filename)
143
for remote_filename in remote_list:
144
if c.match(remote_filename):
145
self.wrapped_backend._delete(remote_filename)
147
def delete_list(self, filename_list):
143
148
"""delete given filename_list and all .par2 files that belong to them
145
remote_list = self.list(False)
150
remote_list = self.list()
147
152
for filename in filename_list[:]:
148
153
c = re.compile(r'%s(?:\.vol[\d+]*)?\.par2' % filename)
150
155
if c.match(remote_filename):
151
156
filename_list.append(remote_filename)
153
return self.wrapped_backend.delete(filename_list)
155
"""just return the output of coresponding wrapped backend
156
for all other functions
158
def query_info(self, filename_list, raise_errors=True):
159
return self.wrapped_backend.query_info(filename_list, raise_errors)
161
def get_password(self):
162
return self.wrapped_backend.get_password()
164
def munge_password(self, commandline):
165
return self.wrapped_backend.munge_password(commandline)
167
def run_command(self, commandline):
168
return self.wrapped_backend.run_command(commandline)
169
def run_command_persist(self, commandline):
170
return self.wrapped_backend.run_command_persist(commandline)
172
def popen(self, commandline):
173
return self.wrapped_backend.popen(commandline)
174
def popen_persist(self, commandline):
175
return self.wrapped_backend.popen_persist(commandline)
177
def _subprocess_popen(self, commandline):
178
return self.wrapped_backend._subprocess_popen(commandline)
180
def subprocess_popen(self, commandline):
181
return self.wrapped_backend.subprocess_popen(commandline)
183
def subprocess_popen_persist(self, commandline):
184
return self.wrapped_backend.subprocess_popen_persist(commandline)
158
return self.wrapped_backend._delete_list(filename_list)
162
return self.wrapped_backend._list()
164
def retry_cleanup(self):
165
self.wrapped_backend._retry_cleanup()
167
def error_code(self, operation, e):
168
return self.wrapped_backend._error_code(operation, e)
170
def query(self, filename):
171
return self.wrapped_backend._query(filename)
173
def query_list(self, filename_list):
174
return self.wrapped_backend._query(filename_list)
187
return self.wrapped_backend.close()
189
"""register this backend with leading "par2+" for all already known backends
191
files must be sorted in duplicity.backend.import_backends to catch
192
all supported backends
194
for item in backend._backends.keys():
195
backend.register_backend('par2+' + item, Par2WrapperBackend)
177
self.wrapped_backend._close()
179
backend.register_backend_prefix('par2', Par2Backend)