1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 """Create and edit manifest for session contents"""
23
24 import re
25
26 from duplicity import log
27 from duplicity import globals
28 from duplicity import util
29
31 """
32 Exception raised when problem with manifest
33 """
34 pass
35
36
38 """
39 List of volumes and information about each one
40 """
42 """
43 Create blank Manifest
44
45 @param fh: fileobj for manifest
46 @type fh: DupPath
47
48 @rtype: Manifest
49 @return: manifest
50 """
51 self.hostname = None
52 self.local_dirname = None
53 self.volume_info_dict = {}
54 self.fh = fh
55
57 """
58 Set information about directory from globals,
59 and write to manifest file.
60
61 @rtype: Manifest
62 @return: manifest
63 """
64 self.hostname = globals.hostname
65 self.local_dirname = globals.local_path.name
66 if self.fh:
67 if self.hostname:
68 self.fh.write("Hostname %s\n" % self.hostname)
69 if self.local_dirname:
70 self.fh.write("Localdir %s\n" % Quote(self.local_dirname))
71 return self
72
109
111 """
112 Add volume info vi to manifest and write to manifest
113
114 @param vi: volume info to add
115 @type vi: VolumeInfo
116
117 @return: void
118 """
119 vol_num = vi.volume_number
120 self.volume_info_dict[vol_num] = vi
121 if self.fh:
122 self.fh.write(vi.to_string() + "\n")
123
125 """
126 Remove volume vol_num from the manifest
127
128 @param vol_num: volume number to delete
129 @type vi: int
130
131 @return: void
132 """
133 try:
134 del self.volume_info_dict[vol_num]
135 except Exception:
136 raise ManifestError("Volume %d not present in manifest" % (vol_num,))
137
139 """
140 Return string version of self (just concatenate vi strings)
141
142 @rtype: string
143 @return: self in string form
144 """
145 result = ""
146 if self.hostname:
147 result += "Hostname %s\n" % self.hostname
148 if self.local_dirname:
149 result += "Localdir %s\n" % Quote(self.local_dirname)
150
151 vol_num_list = self.volume_info_dict.keys()
152 vol_num_list.sort()
153 def vol_num_to_string(vol_num):
154 return self.volume_info_dict[vol_num].to_string()
155 result = "%s%s\n" % (result,
156 "\n".join(map(vol_num_to_string, vol_num_list)))
157 return result
158
159 __str__ = to_string
160
162 """
163 Initialize self from string s, return self
164 """
165 def get_field(fieldname):
166 """
167 Return the value of a field by parsing s, or None if no field
168 """
169 m = re.search("(^|\\n)%s\\s(.*?)\n" % fieldname, s, re.I)
170 if not m:
171 return None
172 else:
173 return Unquote(m.group(2))
174 self.hostname = get_field("hostname")
175 self.local_dirname = get_field("localdir")
176
177 next_vi_string_regexp = re.compile("(^|\\n)(volume\\s.*?)"
178 "(\\nvolume\\s|$)", re.I | re.S)
179 starting_s_index = 0
180 while 1:
181 match = next_vi_string_regexp.search(s[starting_s_index:])
182 if not match:
183 break
184 self.add_volume_info(VolumeInfo().from_string(match.group(2)))
185 starting_s_index += match.end(2)
186 return self
187
189 """
190 Two manifests are equal if they contain the same volume infos
191 """
192 vi_list1 = self.volume_info_dict.keys()
193 vi_list1.sort()
194 vi_list2 = other.volume_info_dict.keys()
195 vi_list2.sort()
196
197 if vi_list1 != vi_list2:
198 log.Notice(_("Manifests not equal because different volume numbers"))
199 return False
200
201 for i in range(len(vi_list1)):
202 if not vi_list1[i] == vi_list2[i]:
203 log.Notice(_("Manifests not equal because volume lists differ"))
204 return False
205
206 if (self.hostname != other.hostname or
207 self.local_dirname != other.local_dirname):
208 log.Notice(_("Manifests not equal because hosts or directories differ"))
209 return False
210
211 return True
212
214 """
215 Defines !=. Not doing this always leads to annoying bugs...
216 """
217 return not self.__eq__(other)
218
228
230 """
231 Return list of volume numbers that may contain index_prefix
232 """
233 return filter(lambda vol_num:
234 self.volume_info_dict[vol_num].contains(index_prefix),
235 self.volume_info_dict.keys())
236
237
239 """
240 Raised when there is a problem initializing a VolumeInfo from string
241 """
242 pass
243
244
246 """
247 Information about a single volume
248 """
250 """VolumeInfo initializer"""
251 self.volume_number = None
252 self.start_index = None
253 self.start_block = None
254 self.end_index = None
255 self.end_block = None
256 self.hashes = {}
257
258 - def set_info(self, vol_number,
259 start_index, start_block,
260 end_index, end_block):
261 """
262 Set essential VolumeInfo information, return self
263
264 Call with starting and ending paths stored in the volume. If
265 a multivol diff gets split between volumes, count it as being
266 part of both volumes.
267 """
268 self.volume_number = vol_number
269 self.start_index = start_index
270 self.start_block = start_block
271 self.end_index = end_index
272 self.end_block = end_block
273
274 return self
275
277 """
278 Set the value of hash hash_name (e.g. "MD5") to data
279 """
280 self.hashes[hash_name] = data
281
283 """
284 Return pair (hash_type, hash_data)
285
286 SHA1 is the best hash, and MD5 is the second best hash. None
287 is returned if no hash is available.
288 """
289 if not self.hashes:
290 return None
291 try:
292 return ("SHA1", self.hashes['SHA1'])
293 except KeyError:
294 pass
295 try:
296 return ("MD5", self.hashes['MD5'])
297 except KeyError:
298 pass
299 return self.hashes.items()[0]
300
302 """
303 Return nicely formatted string reporting all information
304 """
305 def index_to_string(index):
306 """Return printable version of index without any whitespace"""
307 if index:
308 s = "/".join(index)
309 return Quote(s)
310 else:
311 return "."
312
313 slist = ["Volume %d:" % self.volume_number]
314 whitespace = " "
315 slist.append("%sStartingPath %s %s" %
316 (whitespace, index_to_string(self.start_index), (self.start_block or " ")))
317 slist.append("%sEndingPath %s %s" %
318 (whitespace, index_to_string(self.end_index), (self.end_block or " ")))
319 for key in self.hashes:
320 slist.append("%sHash %s %s" %
321 (whitespace, key, self.hashes[key]))
322 return "\n".join(slist)
323
324 __str__ = to_string
325
327 """
328 Initialize self from string s as created by to_string
329 """
330 def string_to_index(s):
331 """
332 Return tuple index from string
333 """
334 s = Unquote(s)
335 if s == ".":
336 return ()
337 return tuple(s.split("/"))
338
339 linelist = s.strip().split("\n")
340
341
342 m = re.search("^Volume ([0-9]+):", linelist[0], re.I)
343 if not m:
344 raise VolumeInfoError("Bad first line '%s'" % (linelist[0],))
345 self.volume_number = int(m.group(1))
346
347
348 for line in linelist[1:]:
349 if not line:
350 continue
351 line_split = line.strip().split()
352 field_name = line_split[0].lower()
353 other_fields = line_split[1:]
354 if field_name == "Volume":
355 log.Warn(_("Warning, found extra Volume identifier"))
356 break
357 elif field_name == "startingpath":
358 self.start_index = string_to_index(other_fields[0])
359 if len(other_fields) > 1:
360 self.start_block = int(other_fields[1])
361 else:
362 self.start_block = None
363 elif field_name == "endingpath":
364 self.end_index = string_to_index(other_fields[0])
365 if len(other_fields) > 1:
366 self.end_block = int(other_fields[1])
367 else:
368 self.end_block = None
369 elif field_name == "hash":
370 self.set_hash(other_fields[0], other_fields[1])
371
372 if self.start_index is None or self.end_index is None:
373 raise VolumeInfoError("Start or end index not set")
374 return self
375
377 """
378 Used in test suite
379 """
380 if not isinstance(other, VolumeInfo):
381 log.Notice(_("Other is not VolumeInfo"))
382 return None
383 if self.volume_number != other.volume_number:
384 log.Notice(_("Volume numbers don't match"))
385 return None
386 if self.start_index != other.start_index:
387 log.Notice(_("start_indicies don't match"))
388 return None
389 if self.end_index != other.end_index:
390 log.Notice(_("end_index don't match"))
391 return None
392 hash_list1 = self.hashes.items()
393 hash_list1.sort()
394 hash_list2 = other.hashes.items()
395 hash_list2.sort()
396 if hash_list1 != hash_list2:
397 log.Notice(_("Hashes don't match"))
398 return None
399 return 1
400
402 """
403 Defines !=
404 """
405 return not self.__eq__(other)
406
407 - def contains(self, index_prefix, recursive = 1):
408 """
409 Return true if volume might contain index
410
411 If recursive is true, then return true if any index starting
412 with index_prefix could be contained. Otherwise, just check
413 if index_prefix itself is between starting and ending
414 indicies.
415 """
416 if recursive:
417 return (self.start_index[:len(index_prefix)] <=
418 index_prefix <= self.end_index)
419 else:
420 return self.start_index <= index_prefix <= self.end_index
421
422
423 nonnormal_char_re = re.compile("(\\s|[\\\\\"'])")
425 """
426 Return quoted version of s safe to put in a manifest or volume info
427 """
428 if not nonnormal_char_re.search(s):
429 return s
430 slist = []
431 for char in s:
432 if nonnormal_char_re.search(char):
433 slist.append("\\x%02x" % ord(char))
434 else:
435 slist.append(char)
436 return '"%s"' % "".join(slist)
437
438
440 """
441 Return original string from quoted_string produced by above
442 """
443 if not quoted_string[0] == '"' or quoted_string[0] == "'":
444 return quoted_string
445 assert quoted_string[0] == quoted_string[-1]
446 return_list = []
447 i = 1
448 while i < len(quoted_string)-1:
449 char = quoted_string[i]
450 if char == "\\":
451
452 assert quoted_string[i+1] == "x"
453 return_list.append(chr(int(quoted_string[i+2:i+4], 16)))
454 i += 4
455 else:
456 return_list.append(char)
457 i += 1
458 return "".join(return_list)
459