~mterry/duplicity/gdrive

« back to all changes in this revision

Viewing changes to duplicity/path.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
"""Wrapper class around a file like "/usr/bin/env"
 
12
 
 
13
This class makes certain file operations more convenient and
 
14
associates stat information with filenames
 
15
 
 
16
"""
 
17
 
 
18
import stat, os, errno, pwd, grp, socket, time, re, gzip
 
19
import librsync
 
20
from lazy import *
 
21
 
 
22
_copy_blocksize = 64 * 1024
 
23
_tmp_path_counter = 1
 
24
 
 
25
class StatResult:
 
26
        """Used to emulate the output of os.stat() and related"""
 
27
        # st_mode is required by the TarInfo class, but it's unclear how
 
28
        # to generate it from file permissions.
 
29
        st_mode = 0
 
30
 
 
31
 
 
32
class PathException(Exception): pass
 
33
 
 
34
class ROPath:
 
35
        """Read only Path
 
36
 
 
37
        Objects of this class doesn't represent real files, so they don't
 
38
        have a name.  They are required to be indexed though.
 
39
 
 
40
        """
 
41
        def __init__(self, index, stat = None):
 
42
                """ROPath initializer"""
 
43
                self.opened, self.fileobj = None, None
 
44
                self.index = index
 
45
                self.stat, self.type = None, None
 
46
                self.mode, self.devnums = None, None
 
47
 
 
48
        def set_from_stat(self):
 
49
                """Set the value of self.type, self.mode from self.stat"""
 
50
                if not self.stat: self.type = None
 
51
 
 
52
                st_mode = self.stat.st_mode
 
53
                if stat.S_ISREG(st_mode): self.type = "reg"
 
54
                elif stat.S_ISDIR(st_mode): self.type = "dir"
 
55
                elif stat.S_ISLNK(st_mode): self.type = "sym"
 
56
                elif stat.S_ISFIFO(st_mode): self.type = "fifo"
 
57
                elif stat.S_ISSOCK(st_mode):
 
58
                        raise PathException(self.get_relative_path() +
 
59
                                                                "is a socket, unsupported by tar")
 
60
                        self.type = "sock"
 
61
                elif stat.S_ISCHR(st_mode): self.type = "chr"
 
62
                elif stat.S_ISBLK(st_mode): self.type = "blk"
 
63
                else: raise PathException("Unknown type")
 
64
 
 
65
                self.mode = stat.S_IMODE(st_mode)
 
66
                # The following can be replaced with major(), minor() macros
 
67
                # in later versions of python (>= 2.3 I think)
 
68
                self.devnums = (self.stat.st_rdev >> 8, self.stat.st_rdev & 0xff)
 
69
                
 
70
        def blank(self):
 
71
                """Black out self - set type and stat to None"""
 
72
                self.type, self.stat = None, None
 
73
 
 
74
        def exists(self):
 
75
                """True if corresponding file exists"""
 
76
                return self.type
 
77
 
 
78
        def isreg(self):
 
79
                """True if self corresponds to regular file"""
 
80
                return self.type == "reg"
 
81
 
 
82
        def isdir(self):
 
83
                """True if self is dir"""
 
84
                return self.type == "dir"
 
85
 
 
86
        def issym(self):
 
87
                """True if self is sym"""
 
88
                return self.type == "sym"
 
89
 
 
90
        def isfifo(self):
 
91
                """True if self is fifo"""
 
92
                return self.type == "fifo"
 
93
 
 
94
        def issock(self):
 
95
                """True is self is socket"""
 
96
                return self.type == "sock"
 
97
 
 
98
        def isdev(self):
 
99
                """True is self is a device file"""
 
100
                return self.type == "chr" or self.type == "blk"
 
101
 
 
102
        def getdevloc(self):
 
103
                """Return device number path resides on"""
 
104
                return self.stat.st_dev
 
105
 
 
106
        def get_relative_path(self):
 
107
                """Return relative path, created from index"""
 
108
                if self.index: return "/".join(self.index)
 
109
                else: return "."
 
110
 
 
111
        def open(self, mode):
 
112
                """Return fileobj associated with self"""
 
113
                assert mode == "rb" and self.fileobj and not self.opened, \
 
114
                           "%s %s %s" % (mode, self.fileobj, self.opened)
 
115
                self.opened = 1
 
116
                return self.fileobj
 
117
 
 
118
        def get_data(self):
 
119
                """Return contents of associated fileobj in string"""
 
120
                fin = self.open("rb")
 
121
                buf = fin.read()
 
122
                assert not fin.close()
 
123
                return buf
 
124
 
 
125
        def setfileobj(self, fileobj):
 
126
                """Set file object returned by open()"""
 
127
                assert not self.fileobj
 
128
                self.fileobj = fileobj
 
129
                self.opened = None
 
130
 
 
131
        def init_from_tarinfo(self, tarinfo):
 
132
                """Set data from tarinfo object (part of tarfile module)"""
 
133
                # Set the typepp
 
134
                type = tarinfo.type
 
135
                if type == tarfile.REGTYPE or type == tarfile.AREGTYPE:
 
136
                        self.type = "reg"
 
137
                elif type == tarfile.LNKTYPE:
 
138
                        raise PathException("Hard links not supported yet")
 
139
                elif type == tarfile.SYMTYPE:
 
140
                        self.type = "sym"
 
141
                        self.symtext = tarinfo.linkname
 
142
                elif type == tarfile.CHRTYPE:
 
143
                        self.type = "chr"
 
144
                        self.devnums = (tarinfo.devmajor, tarinfo.devminor)
 
145
                elif type == tarfile.BLKTYPE:
 
146
                        self.type = "blk"
 
147
                        self.devnums = (tarinfo.devmajor, tarinfo.devminor)
 
148
                elif type == tarfile.DIRTYPE: self.type = "dir"
 
149
                elif type == tarfile.FIFOTYPE: self.type = "fifo"
 
150
                else: raise PathException("Unknown tarinfo type %s" % (type,))
 
151
 
 
152
                self.mode = tarinfo.mode
 
153
                self.stat = StatResult()
 
154
 
 
155
                # Set user and group id
 
156
                try: self.stat.st_uid = pwd.getpwnam(tarinfo.uname)[2]
 
157
                except KeyError: self.stat.st_uid = tarinfo.uid
 
158
                try: self.stat.st_gid = grp.getgrnam(tarinfo.gname)[2]
 
159
                except KeyError: self.stat.st_gid = tarinfo.gid
 
160
 
 
161
                self.stat.st_mtime = tarinfo.mtime
 
162
                self.stat.st_size = tarinfo.size
 
163
 
 
164
        def get_rorpath(self):
 
165
                """Return rorpath copy of self"""
 
166
                new_rorpath = ROPath(self.index, self.stat)
 
167
                new_rorpath.type, new_rorpath.mode = self.type, self.mode
 
168
                if self.issym(): new_rorpath.symtext = self.symtext
 
169
                elif self.isdev(): new_rorpath.devnums = self.devnums
 
170
                return new_rorpath
 
171
 
 
172
        def get_tarinfo(self):
 
173
                """Generate a tarfile.TarInfo object based on self
 
174
 
 
175
                Doesn't set size based on stat, because we may want to replace
 
176
                data wiht other stream.  Size should be set separately by
 
177
                calling function.
 
178
 
 
179
                """
 
180
                ti = tarfile.TarInfo()
 
181
                if self.index: ti.name = "/".join(self.index)
 
182
                else: ti.name = "."
 
183
                if self.isdir(): ti.name += "/" # tar dir naming convention
 
184
 
 
185
                ti.size = 0
 
186
                if self.type:
 
187
                        # Lots of this is specific to tarfile.py, hope it doesn't
 
188
                        # change much...
 
189
                        if self.isreg():
 
190
                                ti.type = tarfile.REGTYPE
 
191
                                ti.size = self.stat.st_size
 
192
                        elif self.isdir(): ti.type = tarfile.DIRTYPE
 
193
                        elif self.isfifo(): ti.type = tarfile.FIFOTYPE
 
194
                        elif self.issym():
 
195
                                ti.type = tarfile.SYMTYPE
 
196
                                ti.linkname = self.symtext
 
197
                        elif self.isdev():
 
198
                                if self.type == "chr": ti.type = tarfile.CHRTYPE
 
199
                                else: ti.type = tarfile.BLKTYPE
 
200
                                ti.devmajor, ti.devminor = self.devnums
 
201
                        else: raise PathError("Unrecognized type " + str(self.type))
 
202
 
 
203
                        ti.mode = self.mode
 
204
                        ti.uid, ti.gid = self.stat.st_uid, self.stat.st_gid
 
205
                        if self.stat.st_mtime < 0:
 
206
                                log.Warn("Warning: %s has negative mtime, treating as 0."
 
207
                                                 % (self.get_relative_path(),))
 
208
                                ti.mtime = 0
 
209
                        else: ti.mtime = self.stat.st_mtime
 
210
 
 
211
                        try: ti.uname = pwd.getpwuid(ti.uid)[0]
 
212
                        except KeyError: pass
 
213
                        try: ti.gname = grp.getgrgid(ti.gid)[0]
 
214
                        except KeyError: pass                   
 
215
 
 
216
                        if ti.type in (tarfile.CHRTYPE, tarfile.BLKTYPE):
 
217
                                if hasattr(os, "major") and hasattr(os, "minor"):
 
218
                                        ti.devmajor = os.major(self.stat.st_rdev)
 
219
                                        ti.devminor = os.minor(self.stat.st_rdev)
 
220
                else:
 
221
                        # Currently we depend on an uninitiliazed tarinfo file to
 
222
                        # already have appropriate headers.  Still, might as well
 
223
                        # make sure mode and size set.
 
224
                        ti.mode, ti.size = 0, 0
 
225
                return ti
 
226
 
 
227
        def __eq__(self, other):
 
228
                """Used to compare two ROPaths.  Doesn't look at fileobjs"""
 
229
                if not self.type and not other.type: return 1 # neither exists
 
230
                if not self.stat and other.stat or not other.stat and self.stat:
 
231
                        return 0
 
232
                if self.type != other.type: return 0
 
233
 
 
234
                if self.isreg() or self.isdir() or self.isfifo():
 
235
                        # Don't compare sizes, because we would be comparing
 
236
                        # signature size to size of file.
 
237
                        if not self.perms_equal(other): return 0
 
238
                        if self.stat.st_mtime == other.stat.st_mtime: return 1
 
239
                        # Below, treat negative mtimes as equal to 0
 
240
                        return self.stat.st_mtime <= 0 and other.stat.st_time <= 0
 
241
                elif self.issym(): # here only symtext matters
 
242
                        return self.symtext == other.symtext
 
243
                elif self.isdev():
 
244
                        return self.perms_equal(other) and self.devnums == other.devnums
 
245
                assert 0
 
246
 
 
247
        def __ne__(self, other): return not self.__eq__(other)
 
248
 
 
249
        def perms_equal(self, other):
 
250
                """True if self and other have same permissions and ownership"""
 
251
                s1, s2 = self.stat, other.stat
 
252
                return (self.mode == other.mode and
 
253
                                s1.st_gid == s2.st_gid and s1.st_uid == s2.st_uid)
 
254
 
 
255
        def copy(self, other):
 
256
                """Copy self to other.  Also copies data.  Other must be Path"""
 
257
                if self.isreg(): other.writefileobj(self.open("rb"))
 
258
                elif self.isdir(): os.mkdir(other.name)
 
259
                elif self.issym():
 
260
                        os.symlink(self.symtext, other.name)
 
261
                        other.setdata()
 
262
                        return # no need to copy symlink attributes
 
263
                elif self.isfifo(): os.mkfifo(other.name)
 
264
                elif self.issock(): socket.socket(socket.AF_UNIX).bind(other.name)
 
265
                elif self.isdev():
 
266
                        if self.type == "chr": devtype = "c"
 
267
                        else: devtype = "b"
 
268
                        other.makedev(devtype, *self.devnums)
 
269
                self.copy_attribs(other)
 
270
 
 
271
        def copy_attribs(self, other):
 
272
                """Only copy attributes from self to other"""
 
273
                if isinstance(other, Path):
 
274
                        os.chown(other.name, self.stat.st_uid, self.stat.st_gid)
 
275
                        os.chmod(other.name, self.mode)
 
276
                        os.utime(other.name, (time.time(), self.stat.st_mtime))
 
277
                        other.setdata()
 
278
                else: # write results to fake stat object
 
279
                        assert isinstance(other, ROPath)
 
280
                        stat = StatResult()
 
281
                        stat.st_uid, stat.st_gid = self.stat.st_uid, self.stat.st_gid
 
282
                        stat.st_mtime = self.stat.st_mtime
 
283
                        other.stat = stat
 
284
                        other.mode = self.mode
 
285
 
 
286
        def __repr__(self):
 
287
                """Return string representation"""
 
288
                return "(%s %s)" % (self.index, self.type)
 
289
 
 
290
 
 
291
class Path(ROPath):
 
292
        """Path class - wrapper around ordinary local files
 
293
 
 
294
        Besides caching stat() results, this class organizes various file
 
295
        code.
 
296
 
 
297
        """
 
298
        regex_chars_to_quote = re.compile("[\\\\\\\"\\$`]")
 
299
 
 
300
        def __init__(self, base, index = ()):
 
301
                """Path initializer"""
 
302
                # self.opened should be true if the file has been opened, and
 
303
                # self.fileobj can override returned fileobj 
 
304
                self.opened, self.fileobj = None, None
 
305
                self.base = base
 
306
                self.index = index
 
307
                self.name = os.path.join(base, *index)
 
308
                self.setdata()
 
309
 
 
310
        def setdata(self):
 
311
                """Refresh stat cache"""
 
312
                try: self.stat = os.lstat(self.name)
 
313
                except OSError, e:
 
314
                        err_string = errno.errorcode[e[0]]
 
315
                        if err_string == "ENOENT" or err_string == "ENOTDIR":
 
316
                                self.stat, self.type = None, None # file doesn't exist
 
317
                                self.mode = None
 
318
                        else: raise
 
319
                else:
 
320
                        self.set_from_stat()
 
321
                        if self.issym(): self.symtext = os.readlink(self.name)
 
322
 
 
323
        def append(self, ext):
 
324
                """Return new Path with ext added to index"""
 
325
                return self.__class__(self.base, self.index + (ext,))
 
326
 
 
327
        def new_index(self, index):
 
328
                """Return new Path with index index"""
 
329
                return self.__class__(self.base, index)
 
330
 
 
331
        def listdir(self):
 
332
                """Return list generated by os.listdir"""
 
333
                return os.listdir(self.name)
 
334
 
 
335
        def isemptydir(self):
 
336
                """Return true if path is a directory and is empty"""
 
337
                return self.isdir() and not self.listdir()
 
338
 
 
339
        def open(self, mode = "rb"):
 
340
                """Return fileobj associated with self
 
341
 
 
342
                Usually this is just the file data on disk, but can be
 
343
                replaced with arbitrary data using the setfileobj method.
 
344
 
 
345
                """
 
346
                assert not self.opened
 
347
                if self.fileobj: result = self.fileobj
 
348
                else: result = open(self.name, mode)
 
349
                return result
 
350
 
 
351
        def makedev(self, type, major, minor):
 
352
                """Make a device file with specified type, major/minor nums"""
 
353
                cmdlist = ['mknod', self.name, type, str(major), str(minor)]
 
354
                if os.spawnvp(os.P_WAIT, 'mknod', cmdlist) != 0:
 
355
                        raise PathException("Error running %s" % cmdlist)
 
356
                self.setdata()
 
357
 
 
358
        def mkdir(self):
 
359
                """Make a directory at specified path"""
 
360
                log.Log("Making directory %s" % (self.name,), 7)
 
361
                os.mkdir(self.name)
 
362
                self.setdata()
 
363
 
 
364
        def delete(self):
 
365
                """Remove this file"""
 
366
                log.Log("Deleting %s" % (self.name,), 7)
 
367
                if self.isdir(): os.rmdir(self.name)
 
368
                else: os.unlink(self.name)
 
369
                self.setdata()
 
370
 
 
371
        def deltree(self):
 
372
                """Remove self by recursively deleting files under it"""
 
373
                log.Log("Deleting tree %s" % (self.name,), 7)
 
374
                itr = IterTreeReducer(PathDeleter, [])
 
375
                for path in selection.Select(self).set_iter(): itr(path.index, path)
 
376
                itr.Finish()
 
377
                self.setdata()
 
378
 
 
379
        def get_parent_dir(self):
 
380
                """Return directory that self is in"""
 
381
                if self.index: return Path(self.base, self.index[:-1])
 
382
                else:
 
383
                        components = self.base.split("/")
 
384
                        if len(components) == 2 and not components[0]:
 
385
                                return Path("/") # already in root directory
 
386
                        else: return Path("/".join(components[:-1]))
 
387
 
 
388
        def writefileobj(self, fin):
 
389
                """Copy file object fin to self.  Close both when done."""
 
390
                fout = self.open("wb")
 
391
                while 1:
 
392
                        buf = fin.read(_copy_blocksize)
 
393
                        if not buf: break
 
394
                        fout.write(buf)
 
395
                if fin.close() or fout.close():
 
396
                        raise PathException("Error closing file object")
 
397
                self.setdata()
 
398
 
 
399
        def rename(self, new_path):
 
400
                """Rename file at current path to new_path."""
 
401
                os.rename(self.name, new_path.name)
 
402
                self.setdata()
 
403
                new_path.setdata()
 
404
 
 
405
        def move(self, new_path):
 
406
                """Like rename but destination may be on different file system"""
 
407
                self.copy(new_path)
 
408
                self.delete()
 
409
 
 
410
        def patch_with_attribs(self, diff_ropath):
 
411
                """Patch self with diff and then copy attributes over"""
 
412
                assert self.isreg() and diff_ropath.isreg()
 
413
                temp_path = self.get_temp_in_same_dir()
 
414
                patch_fileobj = librsync.PatchedFile(self.open("rb"),
 
415
                                                                                         diff_ropath.open("rb"))
 
416
                temp_path.writefileobj(patch_fileobj)
 
417
                diff_ropath.copy_attribs(temp_path)
 
418
                temp_path.rename(self)
 
419
 
 
420
        def get_temp_in_same_dir(self):
 
421
                """Return temp non existent path in same directory as self"""
 
422
                global _tmp_path_counter
 
423
                parent_dir = self.get_parent_dir()
 
424
                while 1:
 
425
                        temp_path = parent_dir.append("duplicity_temp." +
 
426
                                                                                  str(_tmp_path_counter))
 
427
                        if not temp_path.type: return temp_path
 
428
                        _tmp_path_counter += 1
 
429
                        assert _tmp_path_counter < 10000, \
 
430
                                   "Warning too many temp files created for " + self.name
 
431
 
 
432
        def compare_recursive(self, other, verbose = None):
 
433
                """Compare self to other Path, descending down directories"""
 
434
                selfsel = selection.Select(self).set_iter()
 
435
                othersel = selection.Select(other).set_iter()
 
436
                return Iter.equal(selfsel, othersel, verbose)
 
437
 
 
438
        def __repr__(self):
 
439
                """Return string representation"""
 
440
                return "(%s %s %s)" % (self.index, self.name, self.type)
 
441
 
 
442
        def quote(self, s = None):
 
443
                """Return quoted version of s (defaults to self.name)
 
444
 
 
445
                The output is meant to be interpreted with shells, so can be
 
446
                used with os.system.
 
447
 
 
448
                """
 
449
                if not s: s = self.name
 
450
                return '"%s"' % self.regex_chars_to_quote.sub(
 
451
                        lambda m: "\\"+m.group(0), s)
 
452
 
 
453
        def unquote(self, s):
 
454
                """Return unquoted version of string s, as quoted by above quote()"""
 
455
                assert s[0] == s[-1] == "\"" # string must be quoted by above
 
456
                result = ""; i = 1
 
457
                while i < len(s)-1:
 
458
                        if s[i] == "\\":
 
459
                                result += s[i+1]
 
460
                                i += 2
 
461
                        else:
 
462
                                result += s[i]
 
463
                                i += 1
 
464
                return result
 
465
 
 
466
        def get_filename(self):
 
467
                """Return filename of last component"""
 
468
                components = self.name.split("/")
 
469
                assert components and components[-1]
 
470
                return components[-1]
 
471
 
 
472
        def get_canonical(self):
 
473
                """Return string of canonical version of path
 
474
 
 
475
                Remove ".", and trailing slashes where possible.  Note that
 
476
                it's harder to remove "..", as "foo/bar/.." is not necessarily
 
477
                "foo", so we can't use path.normpath()
 
478
 
 
479
                """
 
480
                newpath = "/".join(filter(lambda x: x and x != ".",
 
481
                                                                  self.name.split("/")))
 
482
                if self.name[0] == "/": return "/" + newpath
 
483
                elif newpath: return newpath
 
484
                else: return "."
 
485
 
 
486
 
 
487
class DupPath(Path):
 
488
        """Represent duplicity data files
 
489
 
 
490
        Based on the file name, files that are compressed or encrypted
 
491
        will have different open() methods.
 
492
 
 
493
        """
 
494
        def __init__(self, base, index = (), parseresults = None):
 
495
                """DupPath initializer
 
496
 
 
497
                The actual filename (no directory) must be the single element
 
498
                of the index, unless parseresults is given.
 
499
 
 
500
                """
 
501
                if parseresults: self.pr = parseresults
 
502
                else:
 
503
                        assert len(index) == 1
 
504
                        self.pr = file_naming.parse(index[0])
 
505
                        assert self.pr, "must be a recognizable duplicity file"
 
506
 
 
507
                Path.__init__(self, base, index)
 
508
 
 
509
        def filtered_open(self, mode = "rb", gpg_profile = None):
 
510
                """Return fileobj with appropriate encryption/compression
 
511
 
 
512
                If encryption is specified but no gpg_profile, use
 
513
                globals.default_profile.
 
514
 
 
515
                """
 
516
                assert not self.opened and not self.fileobj
 
517
                assert mode == "rb" or mode == "wb" # demand binary mode, no appends
 
518
                assert not (self.pr.encrypted and self.pr.compressed)
 
519
                if gpg_profile: assert self.pr.encrypted
 
520
 
 
521
                if self.pr.compressed: return gzip.GzipFile(self.name, mode)
 
522
                elif self.pr.encrypted: 
 
523
                        if not gpg_profile: gpg_profile = globals.gpg_profile
 
524
                        if mode == "rb": return gpg.GPGFile(None, self, gpg_profile)
 
525
                        elif mode == "wb": return gpg.GPGFile(1, self, gpg_profile)
 
526
                else: return self.open(mode)
 
527
 
 
528
 
 
529
class PathDeleter(ITRBranch):
 
530
        """Delete a directory.  Called by Path.deltree"""
 
531
        def start_process(self, index, path): self.path = path
 
532
        def end_process(self): self.path.delete()
 
533
        def can_fast_process(self, index, path): return not path.isdir()
 
534
        def fast_process(self, index, path): path.delete()
 
535
 
 
536
        
 
537
# Wait until end to avoid circular module trouble
 
538
import robust, tarfile, log, selection, globals, gpg, file_naming