1# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4; encoding:utf8 -*- 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 22u"""Wrapper class around a file like "/usr/bin/env" 23 24This class makes certain file operations more convenient and 25associates stat information with filenames 26 27""" 28 29from __future__ import print_function 30from future import standard_library 31standard_library.install_aliases() 32from builtins import str 33from builtins import object 34 35import errno 36import gzip 37import os 38import re 39import shutil 40import socket 41import stat 42import time 43 44from duplicity import cached_ops 45from duplicity import config 46from duplicity import dup_time 47from duplicity import file_naming 48from duplicity import gpg 49from duplicity import librsync 50from duplicity import log 51from duplicity import tarfile 52from duplicity import util 53from duplicity.lazy import * # pylint: disable=unused-wildcard-import,redefined-builtin 54 55_copy_blocksize = 64 * 1024 56_tmp_path_counter = 1 57 58 59class StatResult(object): 60 u"""Used to emulate the output of os.stat() and related""" 61 # st_mode is required by the TarInfo class, but it's unclear how 62 # to generate it from file permissions. 63 st_mode = 0 64 65 66class PathException(Exception): 67 pass 68 69 70class ROPath(object): 71 u"""Read only Path 72 73 Objects of this class doesn't represent real files, so they don't 74 have a name. They are required to be indexed though. 75 76 """ 77 def __init__(self, index, stat=None): # pylint: disable=unused-argument 78 u"""ROPath initializer""" 79 self.opened, self.fileobj = None, None 80 self.index = index 81 self.stat, self.type = None, None 82 self.mode, self.devnums = None, None 83 84 def set_from_stat(self): 85 u"""Set the value of self.type, self.mode from self.stat""" 86 if not self.stat: 87 self.type = None 88 89 st_mode = self.stat.st_mode 90 if stat.S_ISREG(st_mode): 91 self.type = u"reg" 92 elif stat.S_ISDIR(st_mode): 93 self.type = u"dir" 94 elif stat.S_ISLNK(st_mode): 95 self.type = u"sym" 96 elif stat.S_ISFIFO(st_mode): 97 self.type = u"fifo" 98 elif stat.S_ISSOCK(st_mode): 99 raise PathException(util.fsdecode(self.get_relative_path()) + 100 u"is a socket, unsupported by tar") 101 self.type = u"sock" # pylint: disable=unreachable 102 elif stat.S_ISCHR(st_mode): 103 self.type = u"chr" 104 elif stat.S_ISBLK(st_mode): 105 self.type = u"blk" 106 else: 107 raise PathException(u"Unknown type") 108 109 self.mode = stat.S_IMODE(st_mode) 110 if self.type in (u"chr", u"blk"): 111 try: 112 self.devnums = (os.major(self.stat.st_rdev), 113 os.minor(self.stat.st_rdev)) 114 except: 115 log.Warn(_(u"Warning: %s invalid devnums (0x%X), treating as (0, 0).") 116 % (util.fsdecode(self.get_relative_path()), self.stat.st_rdev)) 117 self.devnums = (0, 0) 118 119 def blank(self): 120 u"""Black out self - set type and stat to None""" 121 self.type, self.stat = None, None 122 123 def exists(self): 124 u"""True if corresponding file exists""" 125 return self.type 126 127 def isreg(self): 128 u"""True if self corresponds to regular file""" 129 return self.type == u"reg" 130 131 def isdir(self): 132 u"""True if self is dir""" 133 return self.type == u"dir" 134 135 def issym(self): 136 u"""True if self is sym""" 137 return self.type == u"sym" 138 139 def isfifo(self): 140 u"""True if self is fifo""" 141 return self.type == u"fifo" 142 143 def issock(self): 144 u"""True is self is socket""" 145 return self.type == u"sock" 146 147 def isdev(self): 148 u"""True is self is a device file""" 149 return self.type == u"chr" or self.type == u"blk" 150 151 def getdevloc(self): 152 u"""Return device number path resides on""" 153 return self.stat.st_dev 154 155 def getsize(self): 156 u"""Return length in bytes from stat object""" 157 return self.stat.st_size 158 159 def getmtime(self): 160 u"""Return mod time of path in seconds""" 161 return int(self.stat.st_mtime) 162 163 def get_relative_path(self): 164 u"""Return relative path, created from index""" 165 if self.index: 166 return b"/".join(self.index) 167 else: 168 return b"." 169 170 def getperms(self): 171 u"""Return permissions mode, owner and group""" 172 s1 = self.stat 173 return u'%s:%s %o' % (s1.st_uid, s1.st_gid, self.mode) 174 175 def open(self, mode): 176 u"""Return fileobj associated with self""" 177 assert mode == u"rb" and self.fileobj and not self.opened, \ 178 u"%s %s %s" % (mode, self.fileobj, self.opened) 179 self.opened = 1 180 return self.fileobj 181 182 def get_data(self): 183 u"""Return contents of associated fileobj in string""" 184 fin = self.open(u"rb") 185 buf = fin.read() 186 assert not fin.close() 187 return buf 188 189 def setfileobj(self, fileobj): 190 u"""Set file object returned by open()""" 191 assert not self.fileobj 192 self.fileobj = fileobj 193 self.opened = None 194 195 def init_from_tarinfo(self, tarinfo): 196 u"""Set data from tarinfo object (part of tarfile module)""" 197 # Set the typepp 198 type = tarinfo.type # pylint: disable=redefined-builtin 199 if type == tarfile.REGTYPE or type == tarfile.AREGTYPE: 200 self.type = u"reg" 201 elif type == tarfile.LNKTYPE: 202 raise PathException(u"Hard links not supported yet") 203 elif type == tarfile.SYMTYPE: 204 self.type = u"sym" 205 self.symtext = tarinfo.linkname 206 if isinstance(self.symtext, u"".__class__): 207 self.symtext = util.fsencode(self.symtext) 208 elif type == tarfile.CHRTYPE: 209 self.type = u"chr" 210 self.devnums = (tarinfo.devmajor, tarinfo.devminor) 211 elif type == tarfile.BLKTYPE: 212 self.type = u"blk" 213 self.devnums = (tarinfo.devmajor, tarinfo.devminor) 214 elif type == tarfile.DIRTYPE: 215 self.type = u"dir" 216 elif type == tarfile.FIFOTYPE: 217 self.type = u"fifo" 218 else: 219 raise PathException(u"Unknown tarinfo type %s" % (type,)) 220 221 self.mode = tarinfo.mode 222 self.stat = StatResult() 223 224 u""" If do_not_restore_owner is False, 225 set user and group id 226 use numeric id if name lookup fails 227 OR 228 --numeric-owner is set 229 """ 230 try: 231 if config.numeric_owner: 232 raise KeyError 233 self.stat.st_uid = cached_ops.getpwnam(tarinfo.uname)[2] 234 except KeyError: 235 self.stat.st_uid = tarinfo.uid 236 try: 237 if config.numeric_owner: 238 raise KeyError 239 self.stat.st_gid = cached_ops.getgrnam(tarinfo.gname)[2] 240 except KeyError: 241 self.stat.st_gid = tarinfo.gid 242 243 self.stat.st_mtime = int(tarinfo.mtime) 244 if self.stat.st_mtime < 0: 245 log.Warn(_(u"Warning: %s has negative mtime, treating as 0.") 246 % (tarinfo.uc_name)) 247 self.stat.st_mtime = 0 248 self.stat.st_size = tarinfo.size 249 250 def get_ropath(self): 251 u"""Return ropath copy of self""" 252 new_ropath = ROPath(self.index, self.stat) 253 new_ropath.type, new_ropath.mode = self.type, self.mode 254 if self.issym(): 255 new_ropath.symtext = self.symtext 256 elif self.isdev(): 257 new_ropath.devnums = self.devnums 258 if self.exists(): 259 new_ropath.stat = self.stat 260 return new_ropath 261 262 def get_tarinfo(self): 263 u"""Generate a tarfile.TarInfo object based on self 264 265 Doesn't set size based on stat, because we may want to replace 266 data wiht other stream. Size should be set separately by 267 calling function. 268 269 """ 270 ti = tarfile.TarInfo() 271 if self.index: 272 ti.name = util.fsdecode(b"/".join(self.index)) 273 else: 274 ti.name = u"." 275 if self.isdir(): 276 ti.name += u"/" # tar dir naming convention 277 278 ti.size = 0 279 if self.type: 280 # Lots of this is specific to tarfile.py, hope it doesn't 281 # change much... 282 if self.isreg(): 283 ti.type = tarfile.REGTYPE 284 ti.size = self.stat.st_size 285 elif self.isdir(): 286 ti.type = tarfile.DIRTYPE 287 elif self.isfifo(): 288 ti.type = tarfile.FIFOTYPE 289 elif self.issym(): 290 ti.type = tarfile.SYMTYPE 291 ti.linkname = self.symtext 292 if isinstance(ti.linkname, bytes): 293 ti.linkname = util.fsdecode(ti.linkname) 294 elif self.isdev(): 295 if self.type == u"chr": 296 ti.type = tarfile.CHRTYPE 297 else: 298 ti.type = tarfile.BLKTYPE 299 ti.devmajor, ti.devminor = self.devnums 300 else: 301 raise PathException(u"Unrecognized type " + str(self.type)) 302 303 ti.mode = self.mode 304 ti.uid, ti.gid = self.stat.st_uid, self.stat.st_gid 305 if self.stat.st_mtime < 0: 306 log.Warn(_(u"Warning: %s has negative mtime, treating as 0.") 307 % (util.fsdecode(self.get_relative_path()))) 308 ti.mtime = 0 309 else: 310 ti.mtime = int(self.stat.st_mtime) 311 312 try: 313 ti.uname = cached_ops.getpwuid(ti.uid)[0] 314 except KeyError: 315 ti.uname = u'' 316 try: 317 ti.gname = cached_ops.getgrgid(ti.gid)[0] 318 except KeyError: 319 ti.gname = u'' 320 321 if ti.type in (tarfile.CHRTYPE, tarfile.BLKTYPE): 322 if hasattr(os, u"major") and hasattr(os, u"minor"): 323 ti.devmajor, ti.devminor = self.devnums 324 else: 325 # Currently we depend on an uninitiliazed tarinfo file to 326 # already have appropriate headers. Still, might as well 327 # make sure mode and size set. 328 ti.mode, ti.size = 0, 0 329 return ti 330 331 def __eq__(self, other): 332 u"""Used to compare two ROPaths. Doesn't look at fileobjs""" 333 if not self.type and not other.type: 334 return 1 # neither exists 335 if not self.stat and other.stat or not other.stat and self.stat: 336 return 0 337 if self.type != other.type: 338 return 0 339 340 if self.isreg() or self.isdir() or self.isfifo(): 341 # Don't compare sizes, because we might be comparing 342 # signature size to size of file. 343 if not self.perms_equal(other): 344 return 0 345 if int(self.stat.st_mtime) == int(other.stat.st_mtime): 346 return 1 347 # Below, treat negative mtimes as equal to 0 348 return self.stat.st_mtime <= 0 and other.stat.st_mtime <= 0 349 elif self.issym(): 350 # here only symtext matters 351 return self.symtext == other.symtext 352 elif self.isdev(): 353 return self.perms_equal(other) and self.devnums == other.devnums 354 assert 0 355 356 def __ne__(self, other): 357 return not self.__eq__(other) 358 359 def compare_verbose(self, other, include_data=0): 360 u"""Compare ROPaths like __eq__, but log reason if different 361 362 This is placed in a separate function from __eq__ because 363 __eq__ should be very time sensitive, and logging statements 364 would slow it down. Used when verifying. 365 366 Only run if include_data is true. 367 368 """ 369 def log_diff(log_string): 370 log_str = _(u"Difference found:") + u" " + log_string 371 log.Notice(log_str % (util.fsdecode(self.get_relative_path()))) 372 373 if include_data is False: 374 return True 375 376 if not self.type and not other.type: 377 return 1 378 if not self.stat and other.stat: 379 log_diff(_(u"New file %s")) 380 return 0 381 if not other.stat and self.stat: 382 log_diff(_(u"File %s is missing")) 383 return 0 384 if self.type != other.type: 385 log_diff(_(u"File %%s has type %s, expected %s") % 386 (other.type, self.type)) 387 return 0 388 389 if self.isreg() or self.isdir() or self.isfifo(): 390 if not self.perms_equal(other): 391 log_diff(_(u"File %%s has permissions %s, expected %s") % 392 (other.getperms(), self.getperms())) 393 return 0 394 if ((int(self.stat.st_mtime) != int(other.stat.st_mtime)) and 395 (self.stat.st_mtime > 0 or other.stat.st_mtime > 0)): 396 log_diff(_(u"File %%s has mtime %s, expected %s") % 397 (dup_time.timetopretty(int(other.stat.st_mtime)), 398 dup_time.timetopretty(int(self.stat.st_mtime)))) 399 return 0 400 if self.isreg(): 401 if self.compare_data(other): 402 return 1 403 else: 404 log_diff(_(u"Data for file %s is different")) 405 return 0 406 else: 407 return 1 408 elif self.issym(): 409 if self.symtext == other.symtext or self.symtext + util.fsencode(os.sep) == other.symtext: 410 return 1 411 else: 412 log_diff(_(u"Symlink %%s points to %s, expected %s") % 413 (other.symtext, self.symtext)) 414 return 0 415 elif self.isdev(): 416 if not self.perms_equal(other): 417 log_diff(_(u"File %%s has permissions %s, expected %s") % 418 (other.getperms(), self.getperms())) 419 return 0 420 if self.devnums != other.devnums: 421 log_diff(_(u"Device file %%s has numbers %s, expected %s") 422 % (other.devnums, self.devnums)) 423 return 0 424 return 1 425 assert 0 426 427 def compare_data(self, other): 428 u"""Compare data from two regular files, return true if same""" 429 f1 = self.open(u"rb") 430 f2 = other.open(u"rb") 431 432 def close(): 433 assert not f1.close() 434 assert not f2.close() 435 436 while 1: 437 buf1 = f1.read(_copy_blocksize) 438 buf2 = f2.read(_copy_blocksize) 439 if buf1 != buf2: 440 close() 441 return 0 442 if not buf1: 443 close() 444 return 1 445 446 def perms_equal(self, other): 447 u"""True if self and other have same permissions and ownership""" 448 s1, s2 = self.stat, other.stat 449 return (self.mode == other.mode and 450 s1.st_gid == s2.st_gid and s1.st_uid == s2.st_uid) 451 452 def copy(self, other): 453 u"""Copy self to other. Also copies data. Other must be Path""" 454 if self.isreg(): 455 other.writefileobj(self.open(u"rb")) 456 elif self.isdir(): 457 os.mkdir(other.name) 458 elif self.issym(): 459 os.symlink(self.symtext, other.name) 460 if not config.do_not_restore_ownership: 461 os.lchown(other.name, self.stat.st_uid, self.stat.st_gid) 462 other.setdata() 463 return # no need to copy symlink attributes 464 elif self.isfifo(): 465 os.mkfifo(other.name) 466 elif self.issock(): 467 socket.socket(socket.AF_UNIX).bind(other.name) 468 elif self.isdev(): 469 if self.type == u"chr": 470 devtype = u"c" 471 else: 472 devtype = u"b" 473 other.makedev(devtype, *self.devnums) 474 self.copy_attribs(other) 475 476 def copy_attribs(self, other): 477 u"""Only copy attributes from self to other""" 478 if isinstance(other, Path): 479 if self.stat and not config.do_not_restore_ownership: 480 util.maybe_ignore_errors(lambda: os.chown(other.name, self.stat.st_uid, self.stat.st_gid)) 481 util.maybe_ignore_errors(lambda: os.chmod(other.name, self.mode)) 482 util.maybe_ignore_errors(lambda: os.utime(other.name, (time.time(), self.stat.st_mtime))) 483 other.setdata() 484 else: 485 # write results to fake stat object 486 assert isinstance(other, ROPath) 487 stat = StatResult() 488 stat.st_uid, stat.st_gid = self.stat.st_uid, self.stat.st_gid 489 stat.st_mtime = int(self.stat.st_mtime) 490 other.stat = stat 491 other.mode = self.mode 492 493 def __str__(self): 494 u"""Return string representation""" 495 return u"(%s %s)" % (util.uindex(self.index), self.type) 496 497 498class Path(ROPath): 499 u""" 500 Path class - wrapper around ordinary local files 501 502 Besides caching stat() results, this class organizes various file 503 code. 504 """ 505 regex_chars_to_quote = re.compile(u"[\\\\\\\"\\$`]") 506 507 def rename_index(self, index): 508 if not config.rename or not index: 509 return index # early exit 510 path = os.path.normcase(os.path.join(*index)) 511 tail = [] 512 while path and path not in config.rename: 513 path, extra = os.path.split(path) 514 tail.insert(0, extra) 515 if path: 516 return config.rename[path].split(util.fsencode(os.sep)) + tail 517 else: 518 return index # no rename found 519 520 def __init__(self, base, index=()): 521 u"""Path initializer""" 522 # self.opened should be true if the file has been opened, and 523 # self.fileobj can override returned fileobj 524 self.opened, self.fileobj = None, None 525 if isinstance(base, str): 526 # For now (Python 2), it is helpful to know that all paths 527 # are starting with bytes -- see note above util.fsencode definition 528 base = util.fsencode(base) 529 self.base = base 530 531 # Create self.index, which is the path as a tuple 532 self.index = self.rename_index(index) 533 534 self.name = os.path.join(base, *self.index) 535 536 # We converted any unicode base to filesystem encoding, so self.name should 537 # be in filesystem encoding already and does not need to change 538 self.uc_name = util.fsdecode(self.name) 539 540 self.setdata() 541 542 def setdata(self): 543 u"""Refresh stat cache""" 544 try: 545 # We may be asked to look at the target of symlinks rather than 546 # the link itself. 547 if config.copy_links: 548 self.stat = os.stat(self.name) 549 else: 550 self.stat = os.lstat(self.name) 551 except OSError as e: 552 err_string = errno.errorcode[e.errno] 553 if err_string in [u"ENOENT", u"ENOTDIR", u"ELOOP", u"ENOTCONN", u"ENODEV"]: 554 self.stat, self.type = None, None # file doesn't exist 555 self.mode = None 556 else: 557 raise 558 else: 559 self.set_from_stat() 560 if self.issym(): 561 self.symtext = os.readlink(self.name) 562 563 def append(self, ext): 564 u"""Return new Path with ext added to index""" 565 if isinstance(ext, u"".__class__): 566 ext = util.fsencode(ext) 567 return self.__class__(self.base, self.index + (ext,)) 568 569 def new_index(self, index): 570 u"""Return new Path with index index""" 571 return self.__class__(self.base, index) 572 573 def listdir(self): 574 u"""Return list generated by os.listdir""" 575 return os.listdir(self.name) 576 577 def isemptydir(self): 578 u"""Return true if path is a directory and is empty""" 579 return self.isdir() and not self.listdir() 580 581 def contains(self, child): 582 u"""Return true if path is a directory and contains child""" 583 if isinstance(child, u"".__class__): 584 child = util.fsencode(child) 585 # We don't use append(child).exists() here because that requires exec 586 # permissions as well as read. listdir() just needs read permissions. 587 return self.isdir() and child in self.listdir() 588 589 def open(self, mode=u"rb"): 590 u""" 591 Return fileobj associated with self 592 593 Usually this is just the file data on disk, but can be 594 replaced with arbitrary data using the setfileobj method. 595 """ 596 assert not self.opened 597 if self.fileobj: 598 result = self.fileobj 599 else: 600 result = open(self.name, mode) 601 return result 602 603 def makedev(self, type, major, minor): # pylint: disable=redefined-builtin 604 u"""Make a device file with specified type, major/minor nums""" 605 cmdlist = [u'mknod', self.name, type, str(major), str(minor)] 606 if os.spawnvp(os.P_WAIT, u'mknod', cmdlist) != 0: 607 raise PathException(u"Error running %s" % cmdlist) 608 self.setdata() 609 610 def mkdir(self): 611 u"""Make directory(s) at specified path""" 612 log.Info(_(u"Making directory %s") % self.uc_name) 613 try: 614 os.makedirs(self.name) 615 except OSError: 616 if (not config.force): 617 raise PathException(u"Error creating directory %s" % self.uc_name, 7) 618 self.setdata() 619 620 def delete(self): 621 u"""Remove this file""" 622 log.Info(_(u"Deleting %s") % self.uc_name) 623 if self.isdir(): 624 util.ignore_missing(os.rmdir, self.name) 625 else: 626 util.ignore_missing(os.unlink, self.name) 627 self.setdata() 628 629 def touch(self): 630 u"""Open the file, write 0 bytes, close""" 631 log.Info(_(u"Touching %s") % self.uc_name) 632 fp = self.open(u"wb") 633 fp.close() 634 635 def deltree(self): 636 u"""Remove self by recursively deleting files under it""" 637 from duplicity import selection # todo: avoid circ. dep. issue 638 log.Info(_(u"Deleting tree %s") % self.uc_name) 639 itr = IterTreeReducer(PathDeleter, []) 640 for path in selection.Select(self).set_iter(): 641 itr(path.index, path) 642 itr.Finish() 643 self.setdata() 644 645 def get_parent_dir(self): 646 u"""Return directory that self is in""" 647 if self.index: 648 return Path(self.base, self.index[:-1]) 649 else: 650 components = self.base.split(b"/") 651 if len(components) == 2 and not components[0]: 652 return Path(b"/") # already in root directory 653 else: 654 return Path(b"/".join(components[:-1])) 655 656 def writefileobj(self, fin): 657 u"""Copy file object fin to self. Close both when done.""" 658 fout = self.open(u"wb") 659 while 1: 660 buf = fin.read(_copy_blocksize) 661 if not buf: 662 break 663 fout.write(buf) 664 if fin.close() or fout.close(): 665 raise PathException(u"Error closing file object") 666 self.setdata() 667 668 def rename(self, new_path): 669 u"""Rename file at current path to new_path.""" 670 shutil.move(self.name, new_path.name) 671 self.setdata() 672 new_path.setdata() 673 674 def move(self, new_path): 675 u"""Like rename but destination may be on different file system""" 676 self.copy(new_path) 677 self.delete() 678 679 def chmod(self, mode): 680 u"""Change permissions of the path""" 681 os.chmod(self.name, mode) 682 self.setdata() 683 684 def patch_with_attribs(self, diff_ropath): 685 u"""Patch self with diff and then copy attributes over""" 686 assert self.isreg() and diff_ropath.isreg() 687 temp_path = self.get_temp_in_same_dir() 688 fbase = self.open(u"rb") 689 fdiff = diff_ropath.open(u"rb") 690 patch_fileobj = librsync.PatchedFile(fbase, fdiff) 691 temp_path.writefileobj(patch_fileobj) 692 assert not fbase.close() 693 assert not fdiff.close() 694 diff_ropath.copy_attribs(temp_path) 695 temp_path.rename(self) 696 697 def get_temp_in_same_dir(self): 698 u"""Return temp non existent path in same directory as self""" 699 global _tmp_path_counter 700 parent_dir = self.get_parent_dir() 701 while 1: 702 temp_path = parent_dir.append(u"duplicity_temp." + 703 str(_tmp_path_counter)) 704 if not temp_path.type: 705 return temp_path 706 _tmp_path_counter += 1 707 assert _tmp_path_counter < 10000, \ 708 u"Warning too many temp files created for " + self.uc_name 709 710 def compare_recursive(self, other, verbose=None): 711 u"""Compare self to other Path, descending down directories""" 712 from duplicity import selection # todo: avoid circ. dep. issue 713 selfsel = selection.Select(self).set_iter() 714 othersel = selection.Select(other).set_iter() 715 return Iter.equal(selfsel, othersel, verbose) 716 717 def __repr__(self): 718 u"""Return string representation""" 719 return u"(%s %s %s)" % (self.index, self.name, self.type) 720 721 def quote(self, s=None): 722 u""" 723 Return quoted version of s (defaults to self.name) 724 725 The output is meant to be interpreted with shells, so can be 726 used with os.system. 727 """ 728 if not s: 729 s = self.uc_name 730 return u'"%s"' % self.regex_chars_to_quote.sub(lambda m: u"\\" + m.group(0), s) 731 732 def unquote(self, s): 733 u"""Return unquoted version of string s, as quoted by above quote()""" 734 assert s[0] == s[-1] == u"\"" # string must be quoted by above 735 result = u"" 736 i = 1 737 while i < len(s) - 1: 738 if s[i] == u"\\": 739 result += s[i + 1] 740 i += 2 741 else: 742 result += s[i] 743 i += 1 744 return result 745 746 def get_filename(self): 747 u"""Return filename of last component""" 748 components = self.name.split(b"/") 749 assert components and components[-1] 750 return components[-1] 751 752 def get_canonical(self): 753 u""" 754 Return string of canonical version of path 755 756 Remove ".", and trailing slashes where possible. Note that 757 it's harder to remove "..", as "foo/bar/.." is not necessarily 758 "foo", so we can't use path.normpath() 759 """ 760 newpath = b"/".join([x for x in self.name.split(b"/") if x and x != b"."]) 761 if self.uc_name[0] == u"/": 762 return b"/" + newpath 763 elif newpath: 764 return newpath 765 else: 766 return b"." 767 768 769class DupPath(Path): 770 u""" 771 Represent duplicity data files 772 773 Based on the file name, files that are compressed or encrypted 774 will have different open() methods. 775 """ 776 def __init__(self, base, index=(), parseresults=None): 777 u""" 778 DupPath initializer 779 780 The actual filename (no directory) must be the single element 781 of the index, unless parseresults is given. 782 783 """ 784 if parseresults: 785 self.pr = parseresults 786 else: 787 assert len(index) == 1 788 self.pr = file_naming.parse(index[0]) 789 assert self.pr, u"must be a recognizable duplicity file" 790 791 Path.__init__(self, base, index) 792 793 def filtered_open(self, mode=u"rb", gpg_profile=None): 794 u""" 795 Return fileobj with appropriate encryption/compression 796 797 If encryption is specified but no gpg_profile, use 798 config.default_profile. 799 """ 800 assert not self.opened and not self.fileobj 801 assert not (self.pr.encrypted and self.pr.compressed) 802 if gpg_profile: 803 assert self.pr.encrypted 804 805 if self.pr.compressed: 806 return gzip.GzipFile(self.name, mode) 807 elif self.pr.encrypted: 808 if not gpg_profile: 809 gpg_profile = config.gpg_profile 810 if mode == u"rb": 811 return gpg.GPGFile(False, self, gpg_profile) 812 elif mode == u"wb": 813 return gpg.GPGFile(True, self, gpg_profile) 814 else: 815 return self.open(mode) 816 817 818class PathDeleter(ITRBranch): 819 u"""Delete a directory. Called by Path.deltree""" 820 def start_process(self, index, path): # pylint: disable=unused-argument 821 self.path = path 822 823 def end_process(self): 824 self.path.delete() 825 826 def can_fast_process(self, index, path): # pylint: disable=unused-argument 827 return not path.isdir() 828 829 def fast_process(self, index, path): # pylint: disable=unused-argument 830 path.delete() 831