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
"""Create and edit manifest for session contents"""
16
class ManifestError(Exception):
17
"""Exception raised when problem with manifest"""
21
"""List of volumes and information about each one"""
23
"""Create blank Manifest"""
24
self.hostname = None; self.local_dirname = None
25
self.volume_info_dict = {} # dictionary vol numbers -> vol infos
27
def set_dirinfo(self):
28
"""Set information about directory from globals"""
29
self.hostname = globals.hostname
30
self.local_dirname = globals.local_path.name
33
def check_dirinfo(self):
34
"""Return None if dirinfo is the same, otherwise error message
36
Does not raise an error message if hostname or local_dirname
40
if globals.allow_source_mismatch: return
41
if self.hostname and self.hostname != globals.hostname:
42
errmsg = """Fatal Error: Backup source host has changed.
44
Previous hostname: %s""" % (globals.hostname, self.hostname)
45
elif (self.local_dirname and
46
self.local_dirname != globals.local_path.name):
47
errmsg = """Fatal Error: Backup source directory has changed.
49
Previous directory: %s""" % (self.local_dirname, globals.local_path.name)
52
log.FatalError(errmsg + """
54
Aborting because you may have accidentally tried to backup two
55
different data sets to the same remote location, or using the same
56
archive directory. If this is not a mistake, use the
57
--allow-source-mismatch switch to avoid seeing this message""")
59
def add_volume_info(self, vi):
60
"""Add volume info vi to manifest"""
61
vol_num = vi.volume_number
62
if self.volume_info_dict.has_key(vol_num):
63
raise ManifestError("Volume %d already present" % (vol_num,))
64
self.volume_info_dict[vol_num] = vi
67
"""Return string version of self (just concatenate vi strings)"""
69
if self.hostname: result += "Hostname %s\n" % self.hostname
70
if self.local_dirname:
71
result += "Localdir %s\n" % Quote(self.local_dirname)
73
vol_num_list = self.volume_info_dict.keys()
75
def vol_num_to_string(vol_num):
76
return self.volume_info_dict[vol_num].to_string()
77
result = "%s%s\n" % (result,
78
"\n".join(map(vol_num_to_string, vol_num_list)))
82
def from_string(self, s):
83
"""Initialize self from string s, return self"""
84
def get_field(fieldname):
85
"""Return the value of a field by parsing s, or None if no field"""
86
m = re.search("(^|\\n)%s\\s(.*?)\n" % fieldname, s, re.I)
88
else: return Unquote(m.group(2))
89
self.hostname = get_field("hostname")
90
self.local_dirname = get_field("localdir")
92
next_vi_string_regexp = re.compile("(^|\\n)(volume\\s.*?)"
93
"(\\nvolume\\s|$)", re.I | re.S)
96
match = next_vi_string_regexp.search(s[starting_s_index:])
98
self.add_volume_info(VolumeInfo().from_string(match.group(2)))
99
starting_s_index += match.end(2)
102
def __eq__(self, other):
103
"""Two manifests are equal if they contain the same volume infos"""
104
vi_list1 = self.volume_info_dict.keys()
106
vi_list2 = other.volume_info_dict.keys()
108
if vi_list1 != vi_list2:
109
log.Log("Manifests not equal because different volume numbers", 3)
111
for i in range(len(vi_list1)):
112
if not vi_list1[i] == vi_list2[i]: return None
114
if (self.hostname != other.hostname or
115
self.local_dirname != other.local_dirname): return None
118
def __ne__(self, other):
119
"""Defines !=. Not doing this always leads to annoying bugs..."""
120
return not self.__eq__(other)
122
def write_to_path(self, path):
123
"""Write string version of manifest to given path"""
124
assert not path.exists()
125
fout = path.open("w")
126
fout.write(self.to_string())
127
assert not fout.close()
130
def get_containing_volumes(self, index_prefix):
131
"""Return list of volume numbers that may contain index_prefix"""
132
return filter(lambda vol_num:
133
self.volume_info_dict[vol_num].contains(index_prefix),
134
self.volume_info_dict.keys())
137
class VolumeInfoError(Exception):
138
"""Raised when there is a problem initializing a VolumeInfo from string"""
142
"""Information about a single volume"""
144
"""VolumeInfo initializer"""
145
self.volume_number = None
146
self.start_index, self.end_index = None, None
149
def set_info(self, vol_number, start_index, end_index):
150
"""Set essential VolumeInfo information, return self
152
Call with starting and ending paths stored in the volume. If
153
a multivol diff gets split between volumes, count it as being
154
part of both volumes.
157
self.volume_number = vol_number
158
self.start_index, self.end_index = start_index, end_index
161
def set_hash(self, hash_name, data):
162
"""Set the value of hash hash_name (e.g. "MD5") to data"""
163
self.hashes[hash_name] = data
165
def get_best_hash(self):
166
"""Return pair (hash_type, hash_data)
168
SHA1 is the best hash, and MD5 is the second best hash. None
169
is returned if no hash is available.
172
if not self.hashes: return None
173
try: return ("SHA1", self.hashes['SHA1'])
174
except KeyError: pass
175
try: return ("MD5", self.hashes['MD5'])
176
except KeyError: pass
177
return self.hashes.items()[0]
180
"""Return nicely formatted string reporting all information"""
181
def index_to_string(index):
182
"""Return printable version of index without any whitespace"""
188
slist = ["Volume %d:" % self.volume_number]
190
slist.append("%sStartingPath %s" %
191
(whitespace, index_to_string(self.start_index)))
192
slist.append("%sEndingPath %s" %
193
(whitespace, index_to_string(self.end_index)))
194
for key in self.hashes:
195
slist.append("%sHash %s %s" %
196
(whitespace, key, self.hashes[key]))
197
return "\n".join(slist)
200
def from_string(self, s):
201
"""Initialize self from string s as created by to_string"""
202
def string_to_index(s):
203
"""Return tuple index from string"""
205
if s == ".": return ()
206
return tuple(s.split("/"))
208
linelist = s.strip().split("\n")
211
m = re.search("^Volume ([0-9]+):", linelist[0], re.I)
212
if not m: raise VolumeInfoError("Bad first line '%s'" % (linelist[0],))
213
self.volume_number = int(m.group(1))
216
for line in linelist[1:]:
217
if not line: continue
218
line_split = line.strip().split()
219
field_name = line_split[0].lower()
220
other_fields = line_split[1:]
221
if field_name == "Volume":
222
log.Log("Warning, found extra Volume identifier", 2)
224
elif field_name == "startingpath":
225
self.start_index = string_to_index(other_fields[0])
226
elif field_name == "endingpath":
227
self.end_index = string_to_index(other_fields[0])
228
elif field_name == "hash":
229
self.set_hash(other_fields[0], other_fields[1])
231
if self.start_index is None or self.end_index is None:
232
raise VolumeInfoError("Start or end index not set")
235
def __eq__(self, other):
236
"""Used in test suite"""
237
if not isinstance(other, VolumeInfo):
238
log.Log("Other is not VolumeInfo", 3)
240
if self.volume_number != other.volume_number:
241
log.Log("Volume numbers don't match", 3)
243
if self.start_index != other.start_index:
244
log.Log("start_indicies don't match", 3)
246
if self.end_index != other.end_index:
247
log.Log("end_index don't match", 3)
249
hash_list1 = self.hashes.items()
251
hash_list2 = other.hashes.items()
253
if hash_list1 != hash_list2:
254
log.Log("Hashes don't match", 3)
258
def __ne__(self, other):
260
return not self.__eq__(other)
262
def contains(self, index_prefix, recursive = 1):
263
"""Return true if volume might contain index
265
If recursive is true, then return true if any index starting
266
with index_prefix could be contained. Otherwise, just check
267
if index_prefix itself is between starting and ending
271
if recursive: return (self.start_index[:len(index_prefix)] <=
272
index_prefix <= self.end_index)
273
else: return self.start_index <= index_prefix <= self.end_index
276
nonnormal_char_re = re.compile("(\\s|[\\\\\"'])")
278
"""Return quoted version of s safe to put in a manifest or volume info"""
279
if not nonnormal_char_re.search(s): return s # no quoting necessary
282
if nonnormal_char_re.search(char):
283
slist.append("\\x%02x" % ord(char))
284
else: slist.append(char)
285
return '"%s"' % "".join(slist)
287
def Unquote(quoted_string):
288
"""Return original string from quoted_string produced by above"""
289
if not quoted_string[0] == '"' or quoted_string[0] == "'":
291
assert quoted_string[0] == quoted_string[-1]
293
i = 1 # skip initial char
294
while i < len(quoted_string)-1:
295
char = quoted_string[i]
296
if char == "\\": # quoted section
297
assert quoted_string[i+1] == "x"
298
return_list.append(chr(int(quoted_string[i+2:i+4], 16)))
301
return_list.append(char)
303
return "".join(return_list)