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
"""Wrapper class around a file like "/usr/bin/env"
13
This class makes certain file operations more convenient and
14
associates stat information with filenames
18
import stat, os, errno, pwd, grp, socket, time, re, gzip
22
_copy_blocksize = 64 * 1024
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.
32
class PathException(Exception): pass
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.
41
def __init__(self, index, stat = None):
42
"""ROPath initializer"""
43
self.opened, self.fileobj = None, None
45
self.stat, self.type = None, None
46
self.mode, self.devnums = None, None
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
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")
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")
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)
71
"""Black out self - set type and stat to None"""
72
self.type, self.stat = None, None
75
"""True if corresponding file exists"""
79
"""True if self corresponds to regular file"""
80
return self.type == "reg"
83
"""True if self is dir"""
84
return self.type == "dir"
87
"""True if self is sym"""
88
return self.type == "sym"
91
"""True if self is fifo"""
92
return self.type == "fifo"
95
"""True is self is socket"""
96
return self.type == "sock"
99
"""True is self is a device file"""
100
return self.type == "chr" or self.type == "blk"
103
"""Return device number path resides on"""
104
return self.stat.st_dev
106
def get_relative_path(self):
107
"""Return relative path, created from index"""
108
if self.index: return "/".join(self.index)
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)
119
"""Return contents of associated fileobj in string"""
120
fin = self.open("rb")
122
assert not fin.close()
125
def setfileobj(self, fileobj):
126
"""Set file object returned by open()"""
127
assert not self.fileobj
128
self.fileobj = fileobj
131
def init_from_tarinfo(self, tarinfo):
132
"""Set data from tarinfo object (part of tarfile module)"""
135
if type == tarfile.REGTYPE or type == tarfile.AREGTYPE:
137
elif type == tarfile.LNKTYPE:
138
raise PathException("Hard links not supported yet")
139
elif type == tarfile.SYMTYPE:
141
self.symtext = tarinfo.linkname
142
elif type == tarfile.CHRTYPE:
144
self.devnums = (tarinfo.devmajor, tarinfo.devminor)
145
elif type == tarfile.BLKTYPE:
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,))
152
self.mode = tarinfo.mode
153
self.stat = StatResult()
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
161
self.stat.st_mtime = tarinfo.mtime
162
self.stat.st_size = tarinfo.size
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
172
def get_tarinfo(self):
173
"""Generate a tarfile.TarInfo object based on self
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
180
ti = tarfile.TarInfo()
181
if self.index: ti.name = "/".join(self.index)
183
if self.isdir(): ti.name += "/" # tar dir naming convention
187
# Lots of this is specific to tarfile.py, hope it doesn't
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
195
ti.type = tarfile.SYMTYPE
196
ti.linkname = self.symtext
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))
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(),))
209
else: ti.mtime = self.stat.st_mtime
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
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)
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
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:
232
if self.type != other.type: return 0
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
244
return self.perms_equal(other) and self.devnums == other.devnums
247
def __ne__(self, other): return not self.__eq__(other)
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)
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)
260
os.symlink(self.symtext, other.name)
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)
266
if self.type == "chr": devtype = "c"
268
other.makedev(devtype, *self.devnums)
269
self.copy_attribs(other)
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))
278
else: # write results to fake stat object
279
assert isinstance(other, ROPath)
281
stat.st_uid, stat.st_gid = self.stat.st_uid, self.stat.st_gid
282
stat.st_mtime = self.stat.st_mtime
284
other.mode = self.mode
287
"""Return string representation"""
288
return "(%s %s)" % (self.index, self.type)
292
"""Path class - wrapper around ordinary local files
294
Besides caching stat() results, this class organizes various file
298
regex_chars_to_quote = re.compile("[\\\\\\\"\\$`]")
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
307
self.name = os.path.join(base, *index)
311
"""Refresh stat cache"""
312
try: self.stat = os.lstat(self.name)
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
321
if self.issym(): self.symtext = os.readlink(self.name)
323
def append(self, ext):
324
"""Return new Path with ext added to index"""
325
return self.__class__(self.base, self.index + (ext,))
327
def new_index(self, index):
328
"""Return new Path with index index"""
329
return self.__class__(self.base, index)
332
"""Return list generated by os.listdir"""
333
return os.listdir(self.name)
335
def isemptydir(self):
336
"""Return true if path is a directory and is empty"""
337
return self.isdir() and not self.listdir()
339
def open(self, mode = "rb"):
340
"""Return fileobj associated with self
342
Usually this is just the file data on disk, but can be
343
replaced with arbitrary data using the setfileobj method.
346
assert not self.opened
347
if self.fileobj: result = self.fileobj
348
else: result = open(self.name, mode)
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)
359
"""Make a directory at specified path"""
360
log.Log("Making directory %s" % (self.name,), 7)
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)
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)
379
def get_parent_dir(self):
380
"""Return directory that self is in"""
381
if self.index: return Path(self.base, self.index[:-1])
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]))
388
def writefileobj(self, fin):
389
"""Copy file object fin to self. Close both when done."""
390
fout = self.open("wb")
392
buf = fin.read(_copy_blocksize)
395
if fin.close() or fout.close():
396
raise PathException("Error closing file object")
399
def rename(self, new_path):
400
"""Rename file at current path to new_path."""
401
os.rename(self.name, new_path.name)
405
def move(self, new_path):
406
"""Like rename but destination may be on different file system"""
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)
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()
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
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)
439
"""Return string representation"""
440
return "(%s %s %s)" % (self.index, self.name, self.type)
442
def quote(self, s = None):
443
"""Return quoted version of s (defaults to self.name)
445
The output is meant to be interpreted with shells, so can be
449
if not s: s = self.name
450
return '"%s"' % self.regex_chars_to_quote.sub(
451
lambda m: "\\"+m.group(0), s)
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
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]
472
def get_canonical(self):
473
"""Return string of canonical version of path
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()
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
488
"""Represent duplicity data files
490
Based on the file name, files that are compressed or encrypted
491
will have different open() methods.
494
def __init__(self, base, index = (), parseresults = None):
495
"""DupPath initializer
497
The actual filename (no directory) must be the single element
498
of the index, unless parseresults is given.
501
if parseresults: self.pr = parseresults
503
assert len(index) == 1
504
self.pr = file_naming.parse(index[0])
505
assert self.pr, "must be a recognizable duplicity file"
507
Path.__init__(self, base, index)
509
def filtered_open(self, mode = "rb", gpg_profile = None):
510
"""Return fileobj with appropriate encryption/compression
512
If encryption is specified but no gpg_profile, use
513
globals.default_profile.
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
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)
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()
537
# Wait until end to avoid circular module trouble
538
import robust, tarfile, log, selection, globals, gpg, file_naming