root/trunk/twisted/python/filepath.py

Revision 29803, 32.7 KB (checked in by exarkun, 7 weeks ago)

Merge child-args-3169-2

Author: exarkun
Reviewer: glyph, zseil, therve
Fixes: #3169

Add FilePath.descendant to replace repeated child calls.

Line 
1# -*- test-case-name: twisted.test.test_paths -*-
2# Copyright (c) 2001-2010 Twisted Matrix Laboratories.
3# See LICENSE for details.
4
5"""
6Object-oriented filesystem path representation.
7"""
8
9import os
10import errno
11import random
12import base64
13
14from os.path import isabs, exists, normpath, abspath, splitext
15from os.path import basename, dirname
16from os.path import join as joinpath
17from os import sep as slash
18from os import listdir, utime, stat
19
20from stat import S_ISREG, S_ISDIR
21
22# Please keep this as light as possible on other Twisted imports; many, many
23# things import this module, and it would be good if it could easily be
24# modified for inclusion in the standard library.  --glyph
25
26from twisted.python.runtime import platform
27from twisted.python.hashlib import sha1
28
29from twisted.python.win32 import ERROR_FILE_NOT_FOUND, ERROR_PATH_NOT_FOUND
30from twisted.python.win32 import ERROR_INVALID_NAME, ERROR_DIRECTORY, O_BINARY
31from twisted.python.win32 import WindowsError
32
33_CREATE_FLAGS = (os.O_EXCL |
34                 os.O_CREAT |
35                 os.O_RDWR |
36                 O_BINARY)
37
38
39def _stub_islink(path):
40    """
41    Always return 'false' if the operating system does not support symlinks.
42
43    @param path: a path string.
44    @type path: L{str}
45    @return: false
46    """
47    return False
48
49
50def _stub_urandom(n):
51    """
52    Provide random data in versions of Python prior to 2.4.  This is an
53    effectively compatible replacement for 'os.urandom'.
54
55    @type n: L{int}
56    @param n: the number of bytes of data to return
57    @return: C{n} bytes of random data.
58    @rtype: str
59    """
60    randomData = [random.randrange(256) for n in xrange(n)]
61    return ''.join(map(chr, randomData))
62
63
64def _stub_armor(s):
65    """
66    ASCII-armor for random data.  This uses a hex encoding, although we will
67    prefer url-safe base64 encoding for features in this module if it is
68    available.
69    """
70    return s.encode('hex')
71
72islink = getattr(os.path, 'islink', _stub_islink)
73randomBytes = getattr(os, 'urandom', _stub_urandom)
74armor = getattr(base64, 'urlsafe_b64encode', _stub_armor)
75
76class InsecurePath(Exception):
77    """
78    Error that is raised when the path provided to FilePath is invalid.
79    """
80
81
82
83class LinkError(Exception):
84    """
85    An error with symlinks - either that there are cyclical symlinks or that
86    symlink are not supported on this platform.
87    """
88
89
90
91class UnlistableError(OSError):
92    """
93    An exception which is used to distinguish between errors which mean 'this
94    is not a directory you can list' and other, more catastrophic errors.
95
96    This error will try to look as much like the original error as possible,
97    while still being catchable as an independent type.
98
99    @ivar originalException: the actual original exception instance, either an
100    L{OSError} or a L{WindowsError}.
101    """
102    def __init__(self, originalException):
103        """
104        Create an UnlistableError exception.
105
106        @param originalException: an instance of OSError.
107        """
108        self.__dict__.update(originalException.__dict__)
109        self.originalException = originalException
110
111
112
113class _WindowsUnlistableError(UnlistableError, WindowsError):
114    """
115    This exception is raised on Windows, for compatibility with previous
116    releases of FilePath where unportable programs may have done "except
117    WindowsError:" around a call to children().
118
119    It is private because all application code may portably catch
120    L{UnlistableError} instead.
121    """
122
123
124
125def _secureEnoughString():
126    """
127    Create a pseudorandom, 16-character string for use in secure filenames.
128    """
129    return armor(sha1(randomBytes(64)).digest())[:16]
130
131
132
133class _PathHelper:
134    """
135    Abstract helper class also used by ZipPath; implements certain utility
136    methods.
137    """
138
139    def getContent(self):
140        fp = self.open()
141        try:
142            return fp.read()
143        finally:
144            fp.close()
145
146
147    def parents(self):
148        """
149        @return: an iterator of all the ancestors of this path, from the most
150        recent (its immediate parent) to the root of its filesystem.
151        """
152        path = self
153        parent = path.parent()
154        # root.parent() == root, so this means "are we the root"
155        while path != parent:
156            yield parent
157            path = parent
158            parent = parent.parent()
159
160
161    def children(self):
162        """
163        List the children of this path object.
164
165        @raise OSError: If an error occurs while listing the directory.  If the
166        error is 'serious', meaning that the operation failed due to an access
167        violation, exhaustion of some kind of resource (file descriptors or
168        memory), OSError or a platform-specific variant will be raised.
169
170        @raise UnlistableError: If the inability to list the directory is due
171        to this path not existing or not being a directory, the more specific
172        OSError subclass L{UnlistableError} is raised instead.
173
174        @return: an iterable of all currently-existing children of this object
175        accessible with L{_PathHelper.child}.
176        """
177        try:
178            subnames = self.listdir()
179        except WindowsError, winErrObj:
180            # WindowsError is an OSError subclass, so if not for this clause
181            # the OSError clause below would be handling these.  Windows error
182            # codes aren't the same as POSIX error codes, so we need to handle
183            # them differently.
184
185            # Under Python 2.5 on Windows, WindowsError has a winerror
186            # attribute and an errno attribute.  The winerror attribute is
187            # bound to the Windows error code while the errno attribute is
188            # bound to a translation of that code to a perhaps equivalent POSIX
189            # error number.
190
191            # Under Python 2.4 on Windows, WindowsError only has an errno
192            # attribute.  It is bound to the Windows error code.
193
194            # For simplicity of code and to keep the number of paths through
195            # this suite minimal, we grab the Windows error code under either
196            # version.
197
198            # Furthermore, attempting to use os.listdir on a non-existent path
199            # in Python 2.4 will result in a Windows error code of
200            # ERROR_PATH_NOT_FOUND.  However, in Python 2.5,
201            # ERROR_FILE_NOT_FOUND results instead. -exarkun
202            winerror = getattr(winErrObj, 'winerror', winErrObj.errno)
203            if winerror not in (ERROR_PATH_NOT_FOUND,
204                                ERROR_FILE_NOT_FOUND,
205                                ERROR_INVALID_NAME,
206                                ERROR_DIRECTORY):
207                raise
208            raise _WindowsUnlistableError(winErrObj)
209        except OSError, ose:
210            if ose.errno not in (errno.ENOENT, errno.ENOTDIR):
211                # Other possible errors here, according to linux manpages:
212                # EACCES, EMIFLE, ENFILE, ENOMEM.  None of these seem like the
213                # sort of thing which should be handled normally. -glyph
214                raise
215            raise UnlistableError(ose)
216        return map(self.child, subnames)
217
218    def walk(self, descend=None):
219        """
220        Yield myself, then each of my children, and each of those children's
221        children in turn.  The optional argument C{descend} is a predicate that
222        takes a FilePath, and determines whether or not that FilePath is
223        traversed/descended into.  It will be called with each path for which
224        C{isdir} returns C{True}.  If C{descend} is not specified, all
225        directories will be traversed (including symbolic links which refer to
226        directories).
227
228        @param descend: A one-argument callable that will return True for
229            FilePaths that should be traversed, False otherwise.
230
231        @return: a generator yielding FilePath-like objects.
232        """
233        yield self
234        if self.isdir():
235            for c in self.children():
236                # we should first see if it's what we want, then we
237                # can walk through the directory
238                if (descend is None or descend(c)):
239                    for subc in c.walk(descend):
240                        if os.path.realpath(self.path).startswith(
241                            os.path.realpath(subc.path)):
242                            raise LinkError("Cycle in file graph.")
243                        yield subc
244                else:
245                    yield c
246
247
248    def sibling(self, path):
249        return self.parent().child(path)
250
251
252    def descendant(self, segments):
253        """
254        Retrieve a child or child's child of this path.
255
256        @param segments: A sequence of path segments as C{str} instances.
257
258        @return: A L{FilePath} constructed by looking up the C{segments[0]}
259            child of this path, the C{segments[1]} child of that path, and so
260            on.
261
262        @since: 10.2
263        """
264        path = self
265        for name in segments:
266            path = path.child(name)
267        return path
268
269
270    def segmentsFrom(self, ancestor):
271        """
272        Return a list of segments between a child and its ancestor.
273
274        For example, in the case of a path X representing /a/b/c/d and a path Y
275        representing /a/b, C{Y.segmentsFrom(X)} will return C{['c',
276        'd']}.
277
278        @param ancestor: an instance of the same class as self, ostensibly an
279        ancestor of self.
280
281        @raise: ValueError if the 'ancestor' parameter is not actually an
282        ancestor, i.e. a path for /x/y/z is passed as an ancestor for /a/b/c/d.
283
284        @return: a list of strs
285        """
286        # this might be an unnecessarily inefficient implementation but it will
287        # work on win32 and for zipfiles; later I will deterimine if the
288        # obvious fast implemenation does the right thing too
289        f = self
290        p = f.parent()
291        segments = []
292        while f != ancestor and p != f:
293            segments[0:0] = [f.basename()]
294            f = p
295            p = p.parent()
296        if f == ancestor and segments:
297            return segments
298        raise ValueError("%r not parent of %r" % (ancestor, self))
299
300
301    # new in 8.0
302    def __hash__(self):
303        """
304        Hash the same as another FilePath with the same path as mine.
305        """
306        return hash((self.__class__, self.path))
307
308
309    # pending deprecation in 8.0
310    def getmtime(self):
311        """
312        Deprecated.  Use getModificationTime instead.
313        """
314        return int(self.getModificationTime())
315
316
317    def getatime(self):
318        """
319        Deprecated.  Use getAccessTime instead.
320        """
321        return int(self.getAccessTime())
322
323
324    def getctime(self):
325        """
326        Deprecated.  Use getStatusChangeTime instead.
327        """
328        return int(self.getStatusChangeTime())
329
330
331
332class FilePath(_PathHelper):
333    """
334    I am a path on the filesystem that only permits 'downwards' access.
335
336    Instantiate me with a pathname (for example,
337    FilePath('/home/myuser/public_html')) and I will attempt to only provide
338    access to files which reside inside that path.  I may be a path to a file,
339    a directory, or a file which does not exist.
340
341    The correct way to use me is to instantiate me, and then do ALL filesystem
342    access through me.  In other words, do not import the 'os' module; if you
343    need to open a file, call my 'open' method.  If you need to list a
344    directory, call my 'path' method.
345
346    Even if you pass me a relative path, I will convert that to an absolute
347    path internally.
348
349    Note: although time-related methods do return floating-point results, they
350    may still be only second resolution depending on the platform and the last
351    value passed to L{os.stat_float_times}.  If you want greater-than-second
352    precision, call C{os.stat_float_times(True)}, or use Python 2.5.
353    Greater-than-second precision is only available in Windows on Python2.5 and
354    later.
355
356    @type alwaysCreate: C{bool}
357    @ivar alwaysCreate: When opening this file, only succeed if the file does
358        not already exist.
359
360    @type path: C{str}
361    @ivar path: The path from which 'downward' traversal is permitted.
362
363    @ivar statinfo: The currently cached status information about the file on
364        the filesystem that this L{FilePath} points to.  This attribute is
365        C{None} if the file is in an indeterminate state (either this
366        L{FilePath} has not yet had cause to call C{stat()} yet or
367        L{FilePath.changed} indicated that new information is required), 0 if
368        C{stat()} was called and returned an error (i.e. the path did not exist
369        when C{stat()} was called), or a C{stat_result} object that describes
370        the last known status of the underlying file (or directory, as the case
371        may be).  Trust me when I tell you that you do not want to use this
372        attribute.  Instead, use the methods on L{FilePath} which give you
373        information about it, like C{getsize()}, C{isdir()},
374        C{getModificationTime()}, and so on.
375    @type statinfo: C{int} or L{types.NoneType} or L{os.stat_result}
376    """
377
378    statinfo = None
379    path = None
380
381    def __init__(self, path, alwaysCreate=False):
382        self.path = abspath(path)
383        self.alwaysCreate = alwaysCreate
384
385    def __getstate__(self):
386        d = self.__dict__.copy()
387        if d.has_key('statinfo'):
388            del d['statinfo']
389        return d
390
391    def child(self, path):
392        if platform.isWindows() and path.count(":"):
393            # Catch paths like C:blah that don't have a slash
394            raise InsecurePath("%r contains a colon." % (path,))
395        norm = normpath(path)
396        if slash in norm:
397            raise InsecurePath("%r contains one or more directory separators" % (path,))
398        newpath = abspath(joinpath(self.path, norm))
399        if not newpath.startswith(self.path):
400            raise InsecurePath("%r is not a child of %s" % (newpath, self.path))
401        return self.clonePath(newpath)
402
403
404    def preauthChild(self, path):
405        """
406        Use me if `path' might have slashes in it, but you know they're safe.
407
408        (NOT slashes at the beginning. It still needs to be a _child_).
409        """
410        newpath = abspath(joinpath(self.path, normpath(path)))
411        if not newpath.startswith(self.path):
412            raise InsecurePath("%s is not a child of %s" % (newpath, self.path))
413        return self.clonePath(newpath)
414
415    def childSearchPreauth(self, *paths):
416        """Return my first existing child with a name in 'paths'.
417
418        paths is expected to be a list of *pre-secured* path fragments; in most
419        cases this will be specified by a system administrator and not an
420        arbitrary user.
421
422        If no appropriately-named children exist, this will return None.
423        """
424        p = self.path
425        for child in paths:
426            jp = joinpath(p, child)
427            if exists(jp):
428                return self.clonePath(jp)
429
430    def siblingExtensionSearch(self, *exts):
431        """Attempt to return a path with my name, given multiple possible
432        extensions.
433
434        Each extension in exts will be tested and the first path which exists
435        will be returned.  If no path exists, None will be returned.  If '' is
436        in exts, then if the file referred to by this path exists, 'self' will
437        be returned.
438
439        The extension '*' has a magic meaning, which means "any path that
440        begins with self.path+'.' is acceptable".
441        """
442        p = self.path
443        for ext in exts:
444            if not ext and self.exists():
445                return self
446            if ext == '*':
447                basedot = basename(p)+'.'
448                for fn in listdir(dirname(p)):
449                    if fn.startswith(basedot):
450                        return self.clonePath(joinpath(dirname(p), fn))
451            p2 = p + ext
452            if exists(p2):
453                return self.clonePath(p2)
454
455
456    def realpath(self):
457        """
458        Returns the absolute target as a FilePath if self is a link, self
459        otherwise.  The absolute link is the ultimate file or directory the
460        link refers to (for instance, if the link refers to another link, and
461        another...).  If the filesystem does not support symlinks, or
462        if the link is cyclical, raises a LinkError.
463
464        Behaves like L{os.path.realpath} in that it does not resolve link
465        names in the middle (ex. /x/y/z, y is a link to w - realpath on z
466        will return /x/y/z, not /x/w/z).
467
468        @return: FilePath of the target path
469        @raises LinkError: if links are not supported or links are cyclical.
470        """
471        if self.islink():
472            result = os.path.realpath(self.path)
473            if result == self.path:
474                raise LinkError("Cyclical link - will loop forever")
475            return self.clonePath(result)
476        return self
477
478
479    def siblingExtension(self, ext):
480        return self.clonePath(self.path+ext)
481
482
483    def linkTo(self, linkFilePath):
484        """
485        Creates a symlink to self to at the path in the L{FilePath}
486        C{linkFilePath}.  Only works on posix systems due to its dependence on
487        C{os.symlink}.  Propagates C{OSError}s up from C{os.symlink} if
488        C{linkFilePath.parent()} does not exist, or C{linkFilePath} already
489        exists.
490
491        @param linkFilePath: a FilePath representing the link to be created
492        @type linkFilePath: L{FilePath}
493        """
494        os.symlink(self.path, linkFilePath.path)
495
496
497    def open(self, mode='r'):
498        """
499        Open this file using C{mode} or for writing if C{alwaysCreate} is
500        C{True}.
501
502        In all cases the file is opened in binary mode, so it is not necessary
503        to include C{b} in C{mode}.
504
505        @param mode: The mode to open the file in.  Default is C{r}.
506        @type mode: C{str}
507        @raises AssertionError: If C{a} is included in the mode and
508            C{alwaysCreate} is C{True}.
509        @rtype: C{file}
510        @return: An open C{file} object.
511        """
512        if self.alwaysCreate:
513            assert 'a' not in mode, ("Appending not supported when "
514                                     "alwaysCreate == True")
515            return self.create()
516        # This hack is necessary because of a bug in Python 2.7 on Windows:
517        # http://bugs.python.org/issue7686
518        mode = mode.replace('b', '')
519        return open(self.path, mode + 'b')
520
521    # stat methods below
522
523    def restat(self, reraise=True):
524        """
525        Re-calculate cached effects of 'stat'.  To refresh information on this path
526        after you know the filesystem may have changed, call this method.
527
528        @param reraise: a boolean.  If true, re-raise exceptions from
529        L{os.stat}; otherwise, mark this path as not existing, and remove any
530        cached stat information.
531        """
532        try:
533            self.statinfo = stat(self.path)
534        except OSError:
535            self.statinfo = 0
536            if reraise:
537                raise
538
539
540    def changed(self):
541        """
542        Clear any cached information about the state of this path on disk.
543
544        @since: 10.1.0
545        """
546        self.statinfo = None
547
548
549    def chmod(self, mode):
550        """
551        Changes the permissions on self, if possible.  Propagates errors from
552        C{os.chmod} up.
553
554        @param mode: integer representing the new permissions desired (same as
555            the command line chmod)
556        @type mode: C{int}
557        """
558        os.chmod(self.path, mode)
559
560
561    def getsize(self):
562        st = self.statinfo
563        if not st:
564            self.restat()
565            st = self.statinfo
566        return st.st_size
567
568
569    def getModificationTime(self):
570        """
571        Retrieve the time of last access from this file.
572
573        @return: a number of seconds from the epoch.
574        @rtype: float
575        """
576        st = self.statinfo
577        if not st:
578            self.restat()
579            st = self.statinfo
580        return float(st.st_mtime)
581
582
583    def getStatusChangeTime(self):
584        """
585        Retrieve the time of the last status change for this file.
586
587        @return: a number of seconds from the epoch.
588        @rtype: float
589        """
590        st = self.statinfo
591        if not st:
592            self.restat()
593            st = self.statinfo
594        return float(st.st_ctime)
595
596
597    def getAccessTime(self):
598        """
599        Retrieve the time that this file was last accessed.
600
601        @return: a number of seconds from the epoch.
602        @rtype: float
603        """
604        st = self.statinfo
605        if not st:
606            self.restat()
607            st = self.statinfo
608        return float(st.st_atime)
609
610
611    def exists(self):
612        """
613        Check if the C{path} exists.
614
615        @return: C{True} if the stats of C{path} can be retrieved successfully,
616            C{False} in the other cases.
617        @rtype: C{bool}
618        """
619        if self.statinfo:
620            return True
621        else:
622            self.restat(False)
623            if self.statinfo:
624                return True
625            else:
626                return False
627
628
629    def isdir(self):
630        st = self.statinfo
631        if not st:
632            self.restat(False)
633            st = self.statinfo
634            if not st:
635                return False
636        return S_ISDIR(st.st_mode)
637
638    def isfile(self):
639        st = self.statinfo
640        if not st:
641            self.restat(False)
642            st = self.statinfo
643            if not st:
644                return False
645        return S_ISREG(st.st_mode)
646
647    def islink(self):
648        # We can't use cached stat results here, because that is the stat of
649        # the destination - (see #1773) which in *every case* but this one is
650        # the right thing to use.  We could call lstat here and use that, but
651        # it seems unlikely we'd actually save any work that way.  -glyph
652        return islink(self.path)
653
654    def isabs(self):
655        return isabs(self.path)
656
657    def listdir(self):
658        return listdir(self.path)
659
660    def splitext(self):
661        return splitext(self.path)
662
663    def __repr__(self):
664        return 'FilePath(%r)' % (self.path,)
665
666    def touch(self):
667        try:
668            self.open('a').close()
669        except IOError:
670            pass
671        utime(self.path, None)
672
673    def remove(self):
674        """
675        Removes the file or directory that is represented by self.  If
676        C{self.path} is a directory, recursively remove all its children
677        before removing the directory.  If it's a file or link, just delete
678        it.
679        """
680        if self.isdir() and not self.islink():
681            for child in self.children():
682                child.remove()
683            os.rmdir(self.path)
684        else:
685            os.remove(self.path)
686        self.changed()
687
688
689    def makedirs(self):
690        """
691        Create all directories not yet existing in C{path} segments, using
692        C{os.makedirs}.
693        """
694        return os.makedirs(self.path)
695
696
697    def globChildren(self, pattern):
698        """
699        Assuming I am representing a directory, return a list of
700        FilePaths representing my children that match the given
701        pattern.
702        """
703        import glob
704        path = self.path[-1] == '/' and self.path + pattern or slash.join([self.path, pattern])
705        return map(self.clonePath, glob.glob(path))
706
707    def basename(self):
708        return basename(self.path)
709
710    def dirname(self):
711        return dirname(self.path)
712
713    def parent(self):
714        return self.clonePath(self.dirname())
715
716
717    def setContent(self, content, ext='.new'):
718        """
719        Replace the file at this path with a new file that contains the given
720        bytes, trying to avoid data-loss in the meanwhile.
721
722        On UNIX-like platforms, this method does its best to ensure that by the
723        time this method returns, either the old contents I{or} the new contents
724        of the file will be present at this path for subsequent readers
725        regardless of premature device removal, program crash, or power loss,
726        making the following assumptions:
727
728            - your filesystem is journaled (i.e. your filesystem will not
729              I{itself} lose data due to power loss)
730
731            - your filesystem's C{rename()} is atomic
732
733            - your filesystem will not discard new data while preserving new
734              metadata (see U{http://mjg59.livejournal.com/108257.html} for more
735              detail)
736
737        On most versions of Windows there is no atomic C{rename()} (see
738        U{http://bit.ly/win32-overwrite} for more information), so this method
739        is slightly less helpful.  There is a small window where the file at
740        this path may be deleted before the new file is moved to replace it:
741        however, the new file will be fully written and flushed beforehand so in
742        the unlikely event that there is a crash at that point, it should be
743        possible for the user to manually recover the new version of their data.
744        In the future, Twisted will support atomic file moves on those versions
745        of Windows which I{do} support them: see U{Twisted ticket
746        3004<http://twistedmatrix.com/trac/ticket/3004>}.
747
748        This method should be safe for use by multiple concurrent processes, but
749        note that it is not easy to predict which process's contents will
750        ultimately end up on disk if they invoke this method at close to the
751        same time.
752
753        @param content: The desired contents of the file at this path.
754
755        @type content: L{str}
756
757        @param ext: An extension to append to the temporary filename used to
758            store the bytes while they are being written.  This can be used to
759            make sure that temporary files can be identified by their suffix,
760            for cleanup in case of crashes.
761
762        @type ext: C{str}
763        """
764        sib = self.temporarySibling(ext)
765        f = sib.open('w')
766        try:
767            f.write(content)
768        finally:
769            f.close()
770        if platform.isWindows() and exists(self.path):
771            os.unlink(self.path)
772        os.rename(sib.path, self.path)
773
774
775    # new in 2.2.0
776
777    def __cmp__(self, other):
778        if not isinstance(other, FilePath):
779            return NotImplemented
780        return cmp(self.path, other.path)
781
782    def createDirectory(self):
783        os.mkdir(self.path)
784
785    def requireCreate(self, val=1):
786        self.alwaysCreate = val
787
788    def create(self):
789        """Exclusively create a file, only if this file previously did not exist.
790        """
791        fdint = os.open(self.path, _CREATE_FLAGS)
792
793        # XXX TODO: 'name' attribute of returned files is not mutable or
794        # settable via fdopen, so this file is slighly less functional than the
795        # one returned from 'open' by default.  send a patch to Python...
796
797        return os.fdopen(fdint, 'w+b')
798
799
800    def temporarySibling(self, extension=""):
801        """
802        Construct a path referring to a sibling of this path.
803
804        The resulting path will be unpredictable, so that other subprocesses
805        should neither accidentally attempt to refer to the same path before it
806        is created, nor they should other processes be able to guess its name in
807        advance.
808
809        @param extension: A suffix to append to the created filename.  (Note
810            that if you want an extension with a '.' you must include the '.'
811            yourself.)
812
813        @type extension: C{str}
814
815        @return: a path object with the given extension suffix, C{alwaysCreate}
816            set to True.
817
818        @rtype: L{FilePath}
819        """
820        sib = self.sibling(_secureEnoughString() + self.basename() + extension)
821        sib.requireCreate()
822        return sib
823
824
825    _chunkSize = 2 ** 2 ** 2 ** 2
826
827    def copyTo(self, destination, followLinks=True):
828        """
829        Copies self to destination.
830
831        If self is a directory, this method copies its children (but not
832        itself) recursively to destination - if destination does not exist as a
833        directory, this method creates it.  If destination is a file, an
834        IOError will be raised.
835
836        If self is a file, this method copies it to destination.  If
837        destination is a file, this method overwrites it.  If destination is a
838        directory, an IOError will be raised.
839
840        If self is a link (and followLinks is False), self will be copied
841        over as a new symlink with the same target as returned by os.readlink.
842        That means that if it is absolute, both the old and new symlink will
843        link to the same thing.  If it's relative, then perhaps not (and
844        it's also possible that this relative link will be broken).
845
846        File/directory permissions and ownership will NOT be copied over.
847
848        If followLinks is True, symlinks are followed so that they're treated
849        as their targets.  In other words, if self is a link, the link's target
850        will be copied.  If destination is a link, self will be copied to the
851        destination's target (the actual destination will be destination's
852        target).  Symlinks under self (if self is a directory) will be
853        followed and its target's children be copied recursively.
854
855        If followLinks is False, symlinks will be copied over as symlinks.
856
857        @param destination: the destination (a FilePath) to which self
858            should be copied
859        @param followLinks: whether symlinks in self should be treated as links
860            or as their targets
861        """
862        if self.islink() and not followLinks:
863            os.symlink(os.readlink(self.path), destination.path)
864            return
865        # XXX TODO: *thorough* audit and documentation of the exact desired
866        # semantics of this code.  Right now the behavior of existent
867        # destination symlinks is convenient, and quite possibly correct, but
868        # its security properties need to be explained.
869        if self.isdir():
870            if not destination.exists():
871                destination.createDirectory()
872            for child in self.children():
873                destChild = destination.child(child.basename())
874                child.copyTo(destChild, followLinks)
875        elif self.isfile():
876            writefile = destination.open('w')
877            try:
878                readfile = self.open()
879                try:
880                    while 1:
881                        # XXX TODO: optionally use os.open, os.read and O_DIRECT
882                        # and use os.fstatvfs to determine chunk sizes and make
883                        # *****sure**** copy is page-atomic; the following is
884                        # good enough for 99.9% of everybody and won't take a
885                        # week to audit though.
886                        chunk = readfile.read(self._chunkSize)
887                        writefile.write(chunk)
888                        if len(chunk) < self._chunkSize:
889                            break
890                finally:
891                    readfile.close()
892            finally:
893                writefile.close()
894        else:
895            # If you see the following message because you want to copy
896            # symlinks, fifos, block devices, character devices, or unix
897            # sockets, please feel free to add support to do sensible things in
898            # reaction to those types!
899            raise NotImplementedError(
900                "Only copying of files and directories supported")
901
902
903    def moveTo(self, destination, followLinks=True):
904        """
905        Move self to destination - basically renaming self to whatever
906        destination is named.  If destination is an already-existing directory,
907        moves all children to destination if destination is empty.  If
908        destination is a non-empty directory, or destination is a file, an
909        OSError will be raised.
910
911        If moving between filesystems, self needs to be copied, and everything
912        that applies to copyTo applies to moveTo.
913
914        @param destination: the destination (a FilePath) to which self
915            should be copied
916        @param followLinks: whether symlinks in self should be treated as links
917            or as their targets (only applicable when moving between
918            filesystems)
919        """
920        try:
921            os.rename(self.path, destination.path)
922        except OSError, ose:
923            if ose.errno == errno.EXDEV:
924                # man 2 rename, ubuntu linux 5.10 "breezy":
925
926                #   oldpath and newpath are not on the same mounted filesystem.
927                #   (Linux permits a filesystem to be mounted at multiple
928                #   points, but rename(2) does not work across different mount
929                #   points, even if the same filesystem is mounted on both.)
930
931                # that means it's time to copy trees of directories!
932                secsib = destination.temporarySibling()
933                self.copyTo(secsib, followLinks) # slow
934                secsib.moveTo(destination, followLinks) # visible
935
936                # done creating new stuff.  let's clean me up.
937                mysecsib = self.temporarySibling()
938                self.moveTo(mysecsib, followLinks) # visible
939                mysecsib.remove() # slow
940            else:
941                raise
942        else:
943            self.changed()
944            destination.changed()
945
946
947FilePath.clonePath = FilePath
Note: See TracBrowser for help on using the browser.