1
# -*- test-case-name: twisted.test.test_paths -*-
2
# Copyright (c) 2001-2008 Twisted Matrix Laboratories.
3
# See LICENSE for details.
6
Object-oriented filesystem path representation.
14
from os.path import isabs, exists, normpath, abspath, splitext
15
from os.path import basename, dirname
16
from os.path import join as joinpath
17
from os import sep as slash
18
from os import listdir, utime, stat
20
from stat import S_ISREG, S_ISDIR
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
26
from twisted.python.runtime import platform
27
from twisted.python.hashlib import sha1
29
from twisted.python.win32 import ERROR_FILE_NOT_FOUND, ERROR_PATH_NOT_FOUND
30
from twisted.python.win32 import ERROR_INVALID_NAME, ERROR_DIRECTORY
31
from twisted.python.win32 import WindowsError
33
def _stub_islink(path):
35
Always return 'false' if the operating system does not support symlinks.
37
@param path: a path string.
46
Provide random data in versions of Python prior to 2.4. This is an
47
effectively compatible replacement for 'os.urandom'.
50
@param n: the number of bytes of data to return
51
@return: C{n} bytes of random data.
54
randomData = [random.randrange(256) for n in xrange(n)]
55
return ''.join(map(chr, randomData))
60
ASCII-armor for random data. This uses a hex encoding, although we will
61
prefer url-safe base64 encoding for features in this module if it is
64
return s.encode('hex')
66
islink = getattr(os.path, 'islink', _stub_islink)
67
randomBytes = getattr(os, 'urandom', _stub_urandom)
68
armor = getattr(base64, 'urlsafe_b64encode', _stub_armor)
70
class InsecurePath(Exception):
72
Error that is raised when the path provided to FilePath is invalid.
77
class LinkError(Exception):
79
An error with symlinks - either that there are cyclical symlinks or that
80
symlink are not supported on this platform.
85
class UnlistableError(OSError):
87
An exception which is used to distinguish between errors which mean 'this
88
is not a directory you can list' and other, more catastrophic errors.
90
This error will try to look as much like the original error as possible,
91
while still being catchable as an independent type.
93
@ivar originalException: the actual original exception instance, either an
94
L{OSError} or a L{WindowsError}.
96
def __init__(self, originalException):
98
Create an UnlistableError exception.
100
@param originalException: an instance of OSError.
102
self.__dict__.update(originalException.__dict__)
103
self.originalException = originalException
107
class _WindowsUnlistableError(UnlistableError, WindowsError):
109
This exception is raised on Windows, for compatibility with previous
110
releases of FilePath where unportable programs may have done "except
111
WindowsError:" around a call to children().
113
It is private because all application code may portably catch
114
L{UnlistableError} instead.
119
def _secureEnoughString():
121
Create a pseudorandom, 16-character string for use in secure filenames.
123
return armor(sha1(randomBytes(64)).digest())[:16]
129
Abstract helper class also used by ZipPath; implements certain utility
133
def getContent(self):
134
return self.open().read()
139
@return: an iterator of all the ancestors of this path, from the most
140
recent (its immediate parent) to the root of its filesystem.
143
parent = path.parent()
144
# root.parent() == root, so this means "are we the root"
145
while path != parent:
148
parent = parent.parent()
153
List the chilren of this path object.
155
@raise OSError: If an error occurs while listing the directory. If the
156
error is 'serious', meaning that the operation failed due to an access
157
violation, exhaustion of some kind of resource (file descriptors or
158
memory), OSError or a platform-specific variant will be raised.
160
@raise UnlistableError: If the inability to list the directory is due
161
to this path not existing or not being a directory, the more specific
162
OSError subclass L{UnlistableError} is raised instead.
164
@return: an iterable of all currently-existing children of this object
165
accessible with L{_PathHelper.child}.
168
subnames = self.listdir()
169
except WindowsError, winErrObj:
170
# WindowsError is an OSError subclass, so if not for this clause
171
# the OSError clause below would be handling these. Windows error
172
# codes aren't the same as POSIX error codes, so we need to handle
175
# Under Python 2.5 on Windows, WindowsError has a winerror
176
# attribute and an errno attribute. The winerror attribute is
177
# bound to the Windows error code while the errno attribute is
178
# bound to a translation of that code to a perhaps equivalent POSIX
181
# Under Python 2.4 on Windows, WindowsError only has an errno
182
# attribute. It is bound to the Windows error code.
184
# For simplicity of code and to keep the number of paths through
185
# this suite minimal, we grab the Windows error code under either
188
# Furthermore, attempting to use os.listdir on a non-existent path
189
# in Python 2.4 will result in a Windows error code of
190
# ERROR_PATH_NOT_FOUND. However, in Python 2.5,
191
# ERROR_FILE_NOT_FOUND results instead. -exarkun
192
winerror = getattr(winErrObj, 'winerror', winErrObj.errno)
193
if winerror not in (ERROR_PATH_NOT_FOUND,
194
ERROR_FILE_NOT_FOUND,
198
raise _WindowsUnlistableError(winErrObj)
200
if ose.errno not in (errno.ENOENT, errno.ENOTDIR):
201
# Other possible errors here, according to linux manpages:
202
# EACCES, EMIFLE, ENFILE, ENOMEM. None of these seem like the
203
# sort of thing which should be handled normally. -glyph
205
raise UnlistableError(ose)
206
return map(self.child, subnames)
208
def walk(self, descend=None):
210
Yield myself, then each of my children, and each of those children's
211
children in turn. The optional argument C{descend} is a predicate that
212
takes a FilePath, and determines whether or not that FilePath is
213
traversed/descended into. It will be called with each path for which
214
C{isdir} returns C{True}. If C{descend} is not specified, all
215
directories will be traversed (including symbolic links which refer to
218
@param descend: A one-argument callable that will return True for
219
FilePaths that should be traversed, False otherwise.
221
@return: a generator yielding FilePath-like objects.
225
for c in self.children():
226
# we should first see if it's what we want, then we
227
# can walk through the directory
228
if (descend is None or descend(c)):
229
for subc in c.walk(descend):
230
if os.path.realpath(self.path).startswith(
231
os.path.realpath(subc.path)):
232
raise LinkError("Cycle in file graph.")
238
def sibling(self, path):
239
return self.parent().child(path)
241
def segmentsFrom(self, ancestor):
243
Return a list of segments between a child and its ancestor.
245
For example, in the case of a path X representing /a/b/c/d and a path Y
246
representing /a/b, C{Y.segmentsFrom(X)} will return C{['c',
249
@param ancestor: an instance of the same class as self, ostensibly an
252
@raise: ValueError if the 'ancestor' parameter is not actually an
253
ancestor, i.e. a path for /x/y/z is passed as an ancestor for /a/b/c/d.
255
@return: a list of strs
257
# this might be an unnecessarily inefficient implementation but it will
258
# work on win32 and for zipfiles; later I will deterimine if the
259
# obvious fast implemenation does the right thing too
263
while f != ancestor and p != f:
264
segments[0:0] = [f.basename()]
267
if f == ancestor and segments:
269
raise ValueError("%r not parent of %r" % (ancestor, self))
275
Hash the same as another FilePath with the same path as mine.
277
return hash((self.__class__, self.path))
280
# pending deprecation in 8.0
283
Deprecated. Use getModificationTime instead.
285
return int(self.getModificationTime())
290
Deprecated. Use getAccessTime instead.
292
return int(self.getAccessTime())
297
Deprecated. Use getStatusChangeTime instead.
299
return int(self.getStatusChangeTime())
303
class FilePath(_PathHelper):
305
I am a path on the filesystem that only permits 'downwards' access.
307
Instantiate me with a pathname (for example,
308
FilePath('/home/myuser/public_html')) and I will attempt to only provide
309
access to files which reside inside that path. I may be a path to a file,
310
a directory, or a file which does not exist.
312
The correct way to use me is to instantiate me, and then do ALL filesystem
313
access through me. In other words, do not import the 'os' module; if you
314
need to open a file, call my 'open' method. If you need to list a
315
directory, call my 'path' method.
317
Even if you pass me a relative path, I will convert that to an absolute
320
Note: although time-related methods do return floating-point results, they
321
may still be only second resolution depending on the platform and the last
322
value passed to L{os.stat_float_times}. If you want greater-than-second
323
precision, call C{os.stat_float_times(True)}, or use Python 2.5.
324
Greater-than-second precision is only available in Windows on Python2.5 and
327
@type alwaysCreate: C{bool}
328
@ivar alwaysCreate: When opening this file, only succeed if the file does not
335
def __init__(self, path, alwaysCreate=False):
336
self.path = abspath(path)
337
self.alwaysCreate = alwaysCreate
339
def __getstate__(self):
340
d = self.__dict__.copy()
341
if d.has_key('statinfo'):
345
def child(self, path):
346
if platform.isWindows() and path.count(":"):
347
# Catch paths like C:blah that don't have a slash
348
raise InsecurePath("%r contains a colon." % (path,))
349
norm = normpath(path)
351
raise InsecurePath("%r contains one or more directory separators" % (path,))
352
newpath = abspath(joinpath(self.path, norm))
353
if not newpath.startswith(self.path):
354
raise InsecurePath("%r is not a child of %s" % (newpath, self.path))
355
return self.clonePath(newpath)
357
def preauthChild(self, path):
359
Use me if `path' might have slashes in it, but you know they're safe.
361
(NOT slashes at the beginning. It still needs to be a _child_).
363
newpath = abspath(joinpath(self.path, normpath(path)))
364
if not newpath.startswith(self.path):
365
raise InsecurePath("%s is not a child of %s" % (newpath, self.path))
366
return self.clonePath(newpath)
368
def childSearchPreauth(self, *paths):
369
"""Return my first existing child with a name in 'paths'.
371
paths is expected to be a list of *pre-secured* path fragments; in most
372
cases this will be specified by a system administrator and not an
375
If no appropriately-named children exist, this will return None.
379
jp = joinpath(p, child)
381
return self.clonePath(jp)
383
def siblingExtensionSearch(self, *exts):
384
"""Attempt to return a path with my name, given multiple possible
387
Each extension in exts will be tested and the first path which exists
388
will be returned. If no path exists, None will be returned. If '' is
389
in exts, then if the file referred to by this path exists, 'self' will
392
The extension '*' has a magic meaning, which means "any path that
393
begins with self.path+'.' is acceptable".
397
if not ext and self.exists():
400
basedot = basename(p)+'.'
401
for fn in listdir(dirname(p)):
402
if fn.startswith(basedot):
403
return self.clonePath(joinpath(dirname(p), fn))
406
return self.clonePath(p2)
411
Returns the absolute target as a FilePath if self is a link, self
412
otherwise. The absolute link is the ultimate file or directory the
413
link refers to (for instance, if the link refers to another link, and
414
another...). If the filesystem does not support symlinks, or
415
if the link is cyclical, raises a LinkError.
417
Behaves like L{os.path.realpath} in that it does not resolve link
418
names in the middle (ex. /x/y/z, y is a link to w - realpath on z
419
will return /x/y/z, not /x/w/z).
421
@return: FilePath of the target path
422
@raises LinkError: if links are not supported or links are cyclical.
425
result = os.path.realpath(self.path)
426
if result == self.path:
427
raise LinkError("Cyclical link - will loop forever")
428
return self.clonePath(result)
432
def siblingExtension(self, ext):
433
return self.clonePath(self.path+ext)
436
def linkTo(self, linkFilePath):
438
Creates a symlink to self to at the path in the L{FilePath}
439
C{linkFilePath}. Only works on posix systems due to its dependence on
440
C{os.symlink}. Propagates C{OSError}s up from C{os.symlink} if
441
C{linkFilePath.parent()} does not exist, or C{linkFilePath} already
444
@param linkFilePath: a FilePath representing the link to be created
445
@type linkFilePath: L{FilePath}
447
os.symlink(self.path, linkFilePath.path)
450
def open(self, mode='r'):
451
if self.alwaysCreate:
452
assert 'a' not in mode, "Appending not supported when alwaysCreate == True"
454
return open(self.path, mode+'b')
458
def restat(self, reraise=True):
460
Re-calculate cached effects of 'stat'. To refresh information on this path
461
after you know the filesystem may have changed, call this method.
463
@param reraise: a boolean. If true, re-raise exceptions from
464
L{os.stat}; otherwise, mark this path as not existing, and remove any
465
cached stat information.
468
self.statinfo = stat(self.path)
475
def chmod(self, mode):
477
Changes the permissions on self, if possible. Propagates errors from
480
@param mode: integer representing the new permissions desired (same as
481
the command line chmod)
484
os.chmod(self.path, mode)
495
def getModificationTime(self):
497
Retrieve the time of last access from this file.
499
@return: a number of seconds from the epoch.
506
return float(st.st_mtime)
509
def getStatusChangeTime(self):
511
Retrieve the time of the last status change for this file.
513
@return: a number of seconds from the epoch.
520
return float(st.st_ctime)
523
def getAccessTime(self):
525
Retrieve the time that this file was last accessed.
527
@return: a number of seconds from the epoch.
534
return float(st.st_atime)
539
Check if the C{path} exists.
541
@return: C{True} if the stats of C{path} can be retrieved successfully,
542
C{False} in the other cases.
562
return S_ISDIR(st.st_mode)
571
return S_ISREG(st.st_mode)
574
# We can't use cached stat results here, because that is the stat of
575
# the destination - (see #1773) which in *every case* but this one is
576
# the right thing to use. We could call lstat here and use that, but
577
# it seems unlikely we'd actually save any work that way. -glyph
578
return islink(self.path)
581
return isabs(self.path)
584
return listdir(self.path)
587
return splitext(self.path)
590
return 'FilePath(%r)' % (self.path,)
594
self.open('a').close()
597
utime(self.path, None)
601
Removes the file or directory that is represented by self. If
602
C{self.path} is a directory, recursively remove all its children
603
before removing the directory. If it's a file or link, just delete
606
if self.isdir() and not self.islink():
607
for child in self.children():
617
Create all directories not yet existing in C{path} segments, using
620
return os.makedirs(self.path)
623
def globChildren(self, pattern):
625
Assuming I am representing a directory, return a list of
626
FilePaths representing my children that match the given
630
path = self.path[-1] == '/' and self.path + pattern or slash.join([self.path, pattern])
631
return map(self.clonePath, glob.glob(path))
634
return basename(self.path)
637
return dirname(self.path)
640
return self.clonePath(self.dirname())
642
def setContent(self, content, ext='.new'):
643
sib = self.siblingExtension(ext)
647
if platform.isWindows() and exists(self.path):
649
os.rename(sib.path, self.path)
653
def __cmp__(self, other):
654
if not isinstance(other, FilePath):
655
return NotImplemented
656
return cmp(self.path, other.path)
658
def createDirectory(self):
661
def requireCreate(self, val=1):
662
self.alwaysCreate = val
665
"""Exclusively create a file, only if this file previously did not exist.
667
fdint = os.open(self.path, (os.O_EXCL |
671
# XXX TODO: 'name' attribute of returned files is not mutable or
672
# settable via fdopen, so this file is slighly less functional than the
673
# one returned from 'open' by default. send a patch to Python...
675
return os.fdopen(fdint, 'w+b')
677
def temporarySibling(self):
679
Create a path naming a temporary sibling of this path in a secure fashion.
681
sib = self.sibling(_secureEnoughString() + self.basename())
685
_chunkSize = 2 ** 2 ** 2 ** 2
688
def copyTo(self, destination, followLinks=True):
690
Copies self to destination.
692
If self is a directory, this method copies its children (but not
693
itself) recursively to destination - if destination does not exist as a
694
directory, this method creates it. If destination is a file, an
695
IOError will be raised.
697
If self is a file, this method copies it to destination. If
698
destination is a file, this method overwrites it. If destination is a
699
directory, an IOError will be raised.
701
If self is a link (and followLinks is False), self will be copied
702
over as a new symlink with the same target as returned by os.readlink.
703
That means that if it is absolute, both the old and new symlink will
704
link to the same thing. If it's relative, then perhaps not (and
705
it's also possible that this relative link will be broken).
707
File/directory permissions and ownership will NOT be copied over.
709
If followLinks is True, symlinks are followed so that they're treated
710
as their targets. In other words, if self is a link, the link's target
711
will be copied. If destination is a link, self will be copied to the
712
destination's target (the actual destination will be destination's
713
target). Symlinks under self (if self is a directory) will be
714
followed and its target's children be copied recursively.
716
If followLinks is False, symlinks will be copied over as symlinks.
718
@param destination: the destination (a FilePath) to which self
720
@param followLinks: whether symlinks in self should be treated as links
723
if self.islink() and not followLinks:
724
os.symlink(os.readlink(self.path), destination.path)
726
# XXX TODO: *thorough* audit and documentation of the exact desired
727
# semantics of this code. Right now the behavior of existent
728
# destination symlinks is convenient, and quite possibly correct, but
729
# its security properties need to be explained.
731
if not destination.exists():
732
destination.createDirectory()
733
for child in self.children():
734
destChild = destination.child(child.basename())
735
child.copyTo(destChild, followLinks)
737
writefile = destination.open('w')
738
readfile = self.open()
740
# XXX TODO: optionally use os.open, os.read and O_DIRECT and
741
# use os.fstatvfs to determine chunk sizes and make
742
# *****sure**** copy is page-atomic; the following is good
743
# enough for 99.9% of everybody and won't take a week to audit
745
chunk = readfile.read(self._chunkSize)
746
writefile.write(chunk)
747
if len(chunk) < self._chunkSize:
752
# If you see the following message because you want to copy
753
# symlinks, fifos, block devices, character devices, or unix
754
# sockets, please feel free to add support to do sensible things in
755
# reaction to those types!
756
raise NotImplementedError(
757
"Only copying of files and directories supported")
760
def moveTo(self, destination, followLinks=True):
762
Move self to destination - basically renaming self to whatever
763
destination is named. If destination is an already-existing directory,
764
moves all children to destination if destination is empty. If
765
destination is a non-empty directory, or destination is a file, an
766
OSError will be raised.
768
If moving between filesystems, self needs to be copied, and everything
769
that applies to copyTo applies to moveTo.
771
@param destination: the destination (a FilePath) to which self
773
@param followLinks: whether symlinks in self should be treated as links
774
or as their targets (only applicable when moving between
778
os.rename(self.path, destination.path)
781
if ose.errno == errno.EXDEV:
782
# man 2 rename, ubuntu linux 5.10 "breezy":
784
# oldpath and newpath are not on the same mounted filesystem.
785
# (Linux permits a filesystem to be mounted at multiple
786
# points, but rename(2) does not work across different mount
787
# points, even if the same filesystem is mounted on both.)
789
# that means it's time to copy trees of directories!
790
secsib = destination.temporarySibling()
791
self.copyTo(secsib, followLinks) # slow
792
secsib.moveTo(destination, followLinks) # visible
794
# done creating new stuff. let's clean me up.
795
mysecsib = self.temporarySibling()
796
self.moveTo(mysecsib, followLinks) # visible
797
mysecsib.remove() # slow
802
FilePath.clonePath = FilePath