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