~mterry/duplicity/gdrive

« back to all changes in this revision

Viewing changes to duplicity/manifest.py

  • Committer: bescoto
  • Date: 2002-10-29 01:49:46 UTC
  • Revision ID: vcs-imports@canonical.com-20021029014946-3m4rmm5plom7pl6q
Initial checkin

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright 2002 Ben Escoto
 
2
#
 
3
# This file is part of duplicity.
 
4
#
 
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.
 
10
 
 
11
"""Create and edit manifest for session contents"""
 
12
 
 
13
import re
 
14
import log, globals
 
15
 
 
16
class ManifestError(Exception):
 
17
        """Exception raised when problem with manifest"""
 
18
        pass
 
19
 
 
20
class Manifest:
 
21
        """List of volumes and information about each one"""
 
22
        def __init__(self):
 
23
                """Create blank Manifest"""
 
24
                self.hostname = None; self.local_dirname = None
 
25
                self.volume_info_dict = {} # dictionary vol numbers -> vol infos
 
26
 
 
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
 
31
                return self
 
32
 
 
33
        def check_dirinfo(self):
 
34
                """Return None if dirinfo is the same, otherwise error message
 
35
 
 
36
                Does not raise an error message if hostname or local_dirname
 
37
                are not available.
 
38
 
 
39
                """
 
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.
 
43
Current hostname: %s
 
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.
 
48
Current directory: %s
 
49
Previous directory: %s""" % (self.local_dirname, globals.local_path.name)
 
50
                else: return
 
51
 
 
52
                log.FatalError(errmsg + """
 
53
 
 
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""")
 
58
 
 
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
 
65
 
 
66
        def to_string(self):
 
67
                """Return string version of self (just concatenate vi strings)"""
 
68
                result = ""
 
69
                if self.hostname: result += "Hostname %s\n" % self.hostname
 
70
                if self.local_dirname:
 
71
                        result += "Localdir %s\n" % Quote(self.local_dirname)
 
72
 
 
73
                vol_num_list = self.volume_info_dict.keys()
 
74
                vol_num_list.sort()
 
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)))
 
79
                return result
 
80
        __str__ = to_string
 
81
 
 
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)
 
87
                        if not m: return None
 
88
                        else: return Unquote(m.group(2))
 
89
                self.hostname = get_field("hostname")
 
90
                self.local_dirname = get_field("localdir")
 
91
 
 
92
                next_vi_string_regexp = re.compile("(^|\\n)(volume\\s.*?)"
 
93
                                                                                   "(\\nvolume\\s|$)", re.I | re.S)
 
94
                starting_s_index = 0
 
95
                while 1:
 
96
                        match = next_vi_string_regexp.search(s[starting_s_index:])
 
97
                        if not match: break
 
98
                        self.add_volume_info(VolumeInfo().from_string(match.group(2)))
 
99
                        starting_s_index += match.end(2)
 
100
                return self
 
101
 
 
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()
 
105
                vi_list1.sort()
 
106
                vi_list2 = other.volume_info_dict.keys()
 
107
                vi_list2.sort()
 
108
                if vi_list1 != vi_list2:
 
109
                        log.Log("Manifests not equal because different volume numbers", 3)
 
110
                        return None
 
111
                for i in range(len(vi_list1)):
 
112
                        if not vi_list1[i] == vi_list2[i]: return None
 
113
 
 
114
                if (self.hostname != other.hostname or
 
115
                        self.local_dirname != other.local_dirname): return None
 
116
                return 1
 
117
 
 
118
        def __ne__(self, other):
 
119
                """Defines !=.  Not doing this always leads to annoying bugs..."""
 
120
                return not self.__eq__(other)
 
121
 
 
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()
 
128
                path.setdata()
 
129
 
 
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())
 
135
 
 
136
 
 
137
class VolumeInfoError(Exception):
 
138
        """Raised when there is a problem initializing a VolumeInfo from string"""
 
139
        pass
 
140
 
 
141
class VolumeInfo:
 
142
        """Information about a single volume"""
 
143
        def __init__(self):
 
144
                """VolumeInfo initializer"""
 
145
                self.volume_number = None
 
146
                self.start_index, self.end_index = None, None
 
147
                self.hashes = {}
 
148
 
 
149
        def set_info(self, vol_number, start_index, end_index):
 
150
                """Set essential VolumeInfo information, return self
 
151
 
 
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.
 
155
 
 
156
                """
 
157
                self.volume_number = vol_number
 
158
                self.start_index, self.end_index = start_index, end_index
 
159
                return self
 
160
 
 
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
 
164
 
 
165
        def get_best_hash(self):
 
166
                """Return pair (hash_type, hash_data)
 
167
 
 
168
                SHA1 is the best hash, and MD5 is the second best hash.  None
 
169
                is returned if no hash is available.
 
170
 
 
171
                """
 
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]
 
178
 
 
179
        def to_string(self):
 
180
                """Return nicely formatted string reporting all information"""
 
181
                def index_to_string(index):
 
182
                        """Return printable version of index without any whitespace"""
 
183
                        if index:
 
184
                                s = "/".join(index)
 
185
                                return Quote(s)
 
186
                        else: return "."
 
187
 
 
188
                slist = ["Volume %d:" % self.volume_number]
 
189
                whitespace = "    "
 
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)
 
198
        __str__ = to_string
 
199
 
 
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"""
 
204
                        s = Unquote(s)
 
205
                        if s == ".": return ()
 
206
                        return tuple(s.split("/"))
 
207
 
 
208
                linelist = s.strip().split("\n")
 
209
 
 
210
                # Set volume number
 
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))
 
214
 
 
215
                # Set other fields
 
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)
 
223
                                break
 
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])
 
230
 
 
231
                if self.start_index is None or self.end_index is None:
 
232
                        raise VolumeInfoError("Start or end index not set")
 
233
                return self
 
234
 
 
235
        def __eq__(self, other):
 
236
                """Used in test suite"""
 
237
                if not isinstance(other, VolumeInfo):
 
238
                        log.Log("Other is not VolumeInfo", 3)
 
239
                        return None
 
240
                if self.volume_number != other.volume_number:
 
241
                        log.Log("Volume numbers don't match", 3)
 
242
                        return None
 
243
                if self.start_index != other.start_index:
 
244
                        log.Log("start_indicies don't match", 3)
 
245
                        return None
 
246
                if self.end_index != other.end_index:
 
247
                        log.Log("end_index don't match", 3)
 
248
                        return None
 
249
                hash_list1 = self.hashes.items()
 
250
                hash_list1.sort()
 
251
                hash_list2 = other.hashes.items()
 
252
                hash_list2.sort()
 
253
                if hash_list1 != hash_list2:
 
254
                        log.Log("Hashes don't match", 3)
 
255
                        return None
 
256
                return 1
 
257
 
 
258
        def __ne__(self, other):
 
259
                """Defines !="""
 
260
                return not self.__eq__(other)
 
261
 
 
262
        def contains(self, index_prefix, recursive = 1):
 
263
                """Return true if volume might contain index
 
264
 
 
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
 
268
                indicies.
 
269
 
 
270
                """
 
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
 
274
 
 
275
 
 
276
nonnormal_char_re = re.compile("(\\s|[\\\\\"'])")
 
277
def Quote(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
 
280
        slist = []
 
281
        for char in s:
 
282
                if nonnormal_char_re.search(char):
 
283
                        slist.append("\\x%02x" % ord(char))
 
284
                else: slist.append(char)
 
285
        return '"%s"' % "".join(slist)
 
286
 
 
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] == "'":
 
290
                return quoted_string
 
291
        assert quoted_string[0] == quoted_string[-1]
 
292
        return_list = []
 
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)))
 
299
                        i += 4
 
300
                else:
 
301
                        return_list.append(char)
 
302
                        i += 1
 
303
        return "".join(return_list)