Package duplicity :: Module manifest
[hide private]
[frames] | no frames]

Source Code for Module duplicity.manifest

  1  # -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- 
  2  # 
  3  # Copyright 2002 Ben Escoto <ben@emerose.org> 
  4  # Copyright 2007 Kenneth Loafman <kenneth@loafman.com> 
  5  # 
  6  # This file is part of duplicity. 
  7  # 
  8  # Duplicity is free software; you can redistribute it and/or modify it 
  9  # under the terms of the GNU General Public License as published by the 
 10  # Free Software Foundation; either version 2 of the License, or (at your 
 11  # option) any later version. 
 12  # 
 13  # Duplicity is distributed in the hope that it will be useful, but 
 14  # WITHOUT ANY WARRANTY; without even the implied warranty of 
 15  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU 
 16  # General Public License for more details. 
 17  # 
 18  # You should have received a copy of the GNU General Public License 
 19  # along with duplicity; if not, write to the Free Software Foundation, 
 20  # Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 
 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   
30 -class ManifestError(Exception):
31 """ 32 Exception raised when problem with manifest 33 """ 34 pass
35 36
37 -class Manifest:
38 """ 39 List of volumes and information about each one 40 """
41 - def __init__(self, fh = None):
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 = {} # dictionary vol numbers -> vol infos 54 self.fh = fh
55
56 - def set_dirinfo(self):
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 #@UndefinedVariable 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
73 - def check_dirinfo(self):
74 """ 75 Return None if dirinfo is the same, otherwise error message 76 77 Does not raise an error message if hostname or local_dirname 78 are not available. 79 80 @rtype: string 81 @return: None or error message 82 """ 83 if globals.allow_source_mismatch: 84 return 85 86 if self.hostname and self.hostname != globals.hostname: 87 errmsg = _("Fatal Error: Backup source host has changed.\n" 88 "Current hostname: %s\n" 89 "Previous hostname: %s") % (globals.hostname, self.hostname) 90 code = log.ErrorCode.hostname_mismatch 91 code_extra = "%s %s" % (util.escape(globals.hostname), util.escape(self.hostname)) 92 93 elif (self.local_dirname and self.local_dirname != globals.local_path.name): #@UndefinedVariable 94 errmsg = _("Fatal Error: Backup source directory has changed.\n" 95 "Current directory: %s\n" 96 "Previous directory: %s") % (globals.local_path.name, self.local_dirname) #@UndefinedVariable 97 code = log.ErrorCode.source_dir_mismatch 98 code_extra = "%s %s" % (util.escape(globals.local_path.name), util.escape(self.local_dirname)) #@UndefinedVariable 99 else: 100 return 101 102 log.FatalError(errmsg + "\n\n" + 103 _("Aborting because you may have accidentally tried to " 104 "backup two different data sets to the same remote " 105 "location, or using the same archive directory. If " 106 "this is not a mistake, use the " 107 "--allow-source-mismatch switch to avoid seeing this " 108 "message"), code, code_extra)
109
110 - def add_volume_info(self, vi):
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
124 - def del_volume_info(self, vol_num):
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
138 - def to_string(self):
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
161 - def from_string(self, s):
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
188 - def __eq__(self, other):
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
213 - def __ne__(self, other):
214 """ 215 Defines !=. Not doing this always leads to annoying bugs... 216 """ 217 return not self.__eq__(other)
218
219 - def write_to_path(self, path):
220 """ 221 Write string version of manifest to given path 222 """ 223 assert not path.exists() 224 fout = path.open("w") 225 fout.write(self.to_string()) 226 assert not fout.close() 227 path.setdata()
228
229 - def get_containing_volumes(self, index_prefix):
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
238 -class VolumeInfoError(Exception):
239 """ 240 Raised when there is a problem initializing a VolumeInfo from string 241 """ 242 pass
243 244
245 -class VolumeInfo:
246 """ 247 Information about a single volume 248 """
249 - def __init__(self):
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
276 - def set_hash(self, hash_name, data):
277 """ 278 Set the value of hash hash_name (e.g. "MD5") to data 279 """ 280 self.hashes[hash_name] = data
281
282 - def get_best_hash(self):
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
301 - def to_string(self):
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
326 - def from_string(self, s):
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 # Set volume number 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 # Set other fields 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
376 - def __eq__(self, other):
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
401 - def __ne__(self, other):
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|[\\\\\"'])")
424 -def Quote(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 # no quoting necessary 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
439 -def Unquote(quoted_string):
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 # skip initial char 448 while i < len(quoted_string)-1: 449 char = quoted_string[i] 450 if char == "\\": 451 # quoted section 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