]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blob - src/allmydata/util/fileutil.py
Improved error handling and cosmetics for ctypes calls on Windows.
[tahoe-lafs/tahoe-lafs.git] / src / allmydata / util / fileutil.py
1 """
2 Futz with files like a pro.
3 """
4
5 import sys, exceptions, os, stat, tempfile, time, binascii
6 from collections import namedtuple
7 from errno import ENOENT
8
9 if sys.platform == "win32":
10     from ctypes import WINFUNCTYPE, WinError, windll, POINTER, byref, c_ulonglong, \
11         create_unicode_buffer, get_last_error
12     from ctypes.wintypes import BOOL, DWORD, LPCWSTR, LPWSTR, LPVOID, HANDLE
13
14 if sys.platform == "win32":
15     from ctypes import WINFUNCTYPE, WinError, windll, POINTER, byref, c_ulonglong, \
16         create_unicode_buffer, get_last_error
17     from ctypes.wintypes import BOOL, DWORD, LPCWSTR, LPWSTR
18
19 from twisted.python import log
20
21 from pycryptopp.cipher.aes import AES
22
23
24 def rename(src, dst, tries=4, basedelay=0.1):
25     """ Here is a superkludge to workaround the fact that occasionally on
26     Windows some other process (e.g. an anti-virus scanner, a local search
27     engine, etc.) is looking at your file when you want to delete or move it,
28     and hence you can't.  The horrible workaround is to sit and spin, trying
29     to delete it, for a short time and then give up.
30
31     With the default values of tries and basedelay this can block for less
32     than a second.
33
34     @param tries: number of tries -- each time after the first we wait twice
35     as long as the previous wait
36     @param basedelay: how long to wait before the second try
37     """
38     for i in range(tries-1):
39         try:
40             return os.rename(src, dst)
41         except EnvironmentError, le:
42             # XXX Tighten this to check if this is a permission denied error (possibly due to another Windows process having the file open and execute the superkludge only in this case.
43             log.msg("XXX KLUDGE Attempting to move file %s => %s; got %s; sleeping %s seconds" % (src, dst, le, basedelay,))
44             time.sleep(basedelay)
45             basedelay *= 2
46     return os.rename(src, dst) # The last try.
47
48 def remove(f, tries=4, basedelay=0.1):
49     """ Here is a superkludge to workaround the fact that occasionally on
50     Windows some other process (e.g. an anti-virus scanner, a local search
51     engine, etc.) is looking at your file when you want to delete or move it,
52     and hence you can't.  The horrible workaround is to sit and spin, trying
53     to delete it, for a short time and then give up.
54
55     With the default values of tries and basedelay this can block for less
56     than a second.
57
58     @param tries: number of tries -- each time after the first we wait twice
59     as long as the previous wait
60     @param basedelay: how long to wait before the second try
61     """
62     try:
63         os.chmod(f, stat.S_IWRITE | stat.S_IEXEC | stat.S_IREAD)
64     except:
65         pass
66     for i in range(tries-1):
67         try:
68             return os.remove(f)
69         except EnvironmentError, le:
70             # XXX Tighten this to check if this is a permission denied error (possibly due to another Windows process having the file open and execute the superkludge only in this case.
71             if not os.path.exists(f):
72                 return
73             log.msg("XXX KLUDGE Attempting to remove file %s; got %s; sleeping %s seconds" % (f, le, basedelay,))
74             time.sleep(basedelay)
75             basedelay *= 2
76     return os.remove(f) # The last try.
77
78 class ReopenableNamedTemporaryFile:
79     """
80     This uses tempfile.mkstemp() to generate a secure temp file.  It then closes
81     the file, leaving a zero-length file as a placeholder.  You can get the
82     filename with ReopenableNamedTemporaryFile.name.  When the
83     ReopenableNamedTemporaryFile instance is garbage collected or its shutdown()
84     method is called, it deletes the file.
85     """
86     def __init__(self, *args, **kwargs):
87         fd, self.name = tempfile.mkstemp(*args, **kwargs)
88         os.close(fd)
89
90     def __repr__(self):
91         return "<%s instance at %x %s>" % (self.__class__.__name__, id(self), self.name)
92
93     def __str__(self):
94         return self.__repr__()
95
96     def __del__(self):
97         self.shutdown()
98
99     def shutdown(self):
100         remove(self.name)
101
102 class EncryptedTemporaryFile:
103     # not implemented: next, readline, readlines, xreadlines, writelines
104
105     def __init__(self):
106         self.file = tempfile.TemporaryFile()
107         self.key = os.urandom(16)  # AES-128
108
109     def _crypt(self, offset, data):
110         offset_big = offset // 16
111         offset_small = offset % 16
112         iv = binascii.unhexlify("%032x" % offset_big)
113         cipher = AES(self.key, iv=iv)
114         cipher.process("\x00"*offset_small)
115         return cipher.process(data)
116
117     def close(self):
118         self.file.close()
119
120     def flush(self):
121         self.file.flush()
122
123     def seek(self, offset, whence=0):  # 0 = SEEK_SET
124         self.file.seek(offset, whence)
125
126     def tell(self):
127         offset = self.file.tell()
128         return offset
129
130     def read(self, size=-1):
131         """A read must not follow a write, or vice-versa, without an intervening seek."""
132         index = self.file.tell()
133         ciphertext = self.file.read(size)
134         plaintext = self._crypt(index, ciphertext)
135         return plaintext
136
137     def write(self, plaintext):
138         """A read must not follow a write, or vice-versa, without an intervening seek.
139         If seeking and then writing causes a 'hole' in the file, the contents of the
140         hole are unspecified."""
141         index = self.file.tell()
142         ciphertext = self._crypt(index, plaintext)
143         self.file.write(ciphertext)
144
145     def truncate(self, newsize):
146         """Truncate or extend the file to 'newsize'. If it is extended, the contents after the
147         old end-of-file are unspecified. The file position after this operation is unspecified."""
148         self.file.truncate(newsize)
149
150
151 def make_dirs(dirname, mode=0777):
152     """
153     An idempotent version of os.makedirs().  If the dir already exists, do
154     nothing and return without raising an exception.  If this call creates the
155     dir, return without raising an exception.  If there is an error that
156     prevents creation or if the directory gets deleted after make_dirs() creates
157     it and before make_dirs() checks that it exists, raise an exception.
158     """
159     tx = None
160     try:
161         os.makedirs(dirname, mode)
162     except OSError, x:
163         tx = x
164
165     if not os.path.isdir(dirname):
166         if tx:
167             raise tx
168         raise exceptions.IOError, "unknown error prevented creation of directory, or deleted the directory immediately after creation: %s" % dirname # careful not to construct an IOError with a 2-tuple, as that has a special meaning...
169
170 def rm_dir(dirname):
171     """
172     A threadsafe and idempotent version of shutil.rmtree().  If the dir is
173     already gone, do nothing and return without raising an exception.  If this
174     call removes the dir, return without raising an exception.  If there is an
175     error that prevents deletion or if the directory gets created again after
176     rm_dir() deletes it and before rm_dir() checks that it is gone, raise an
177     exception.
178     """
179     excs = []
180     try:
181         os.chmod(dirname, stat.S_IWRITE | stat.S_IEXEC | stat.S_IREAD)
182         for f in os.listdir(dirname):
183             fullname = os.path.join(dirname, f)
184             if os.path.isdir(fullname):
185                 rm_dir(fullname)
186             else:
187                 remove(fullname)
188         os.rmdir(dirname)
189     except Exception, le:
190         # Ignore "No such file or directory"
191         if (not isinstance(le, OSError)) or le.args[0] != 2:
192             excs.append(le)
193
194     # Okay, now we've recursively removed everything, ignoring any "No
195     # such file or directory" errors, and collecting any other errors.
196
197     if os.path.exists(dirname):
198         if len(excs) == 1:
199             raise excs[0]
200         if len(excs) == 0:
201             raise OSError, "Failed to remove dir for unknown reason."
202         raise OSError, excs
203
204
205 def remove_if_possible(f):
206     try:
207         remove(f)
208     except:
209         pass
210
211 def du(basedir):
212     size = 0
213
214     for root, dirs, files in os.walk(basedir):
215         for f in files:
216             fn = os.path.join(root, f)
217             size += os.path.getsize(fn)
218
219     return size
220
221 def move_into_place(source, dest):
222     """Atomically replace a file, or as near to it as the platform allows.
223     The dest file may or may not exist."""
224     if "win32" in sys.platform.lower():
225         remove_if_possible(dest)
226     os.rename(source, dest)
227
228 def write_atomically(target, contents, mode="b"):
229     f = open(target+".tmp", "w"+mode)
230     try:
231         f.write(contents)
232     finally:
233         f.close()
234     move_into_place(target+".tmp", target)
235
236 def write(path, data, mode="wb"):
237     wf = open(path, mode)
238     try:
239         wf.write(data)
240     finally:
241         wf.close()
242
243 def read(path):
244     rf = open(path, "rb")
245     try:
246         return rf.read()
247     finally:
248         rf.close()
249
250 def put_file(path, inf):
251     precondition_abspath(path)
252
253     # TODO: create temporary file and move into place?
254     outf = open(path, "wb")
255     try:
256         while True:
257             data = inf.read(32768)
258             if not data:
259                 break
260             outf.write(data)
261     finally:
262         outf.close()
263
264
265 def precondition_abspath(path):
266     if not isinstance(path, unicode):
267         raise AssertionError("an abspath must be a Unicode string")
268
269     if sys.platform == "win32":
270         # This intentionally doesn't view absolute paths starting with a drive specification, or
271         # paths relative to the current drive, as acceptable.
272         if not path.startswith("\\\\"):
273             raise AssertionError("an abspath should be normalized using abspath_expanduser_unicode")
274     else:
275         # This intentionally doesn't view the path '~' or paths starting with '~/' as acceptable.
276         if not os.path.isabs(path):
277             raise AssertionError("an abspath should be normalized using abspath_expanduser_unicode")
278
279 # Work around <http://bugs.python.org/issue3426>. This code is adapted from
280 # <http://svn.python.org/view/python/trunk/Lib/ntpath.py?revision=78247&view=markup>
281 # with some simplifications.
282
283 _getfullpathname = None
284 try:
285     from nt import _getfullpathname
286 except ImportError:
287     pass
288
289 def abspath_expanduser_unicode(path, base=None, long_path=True):
290     """
291     Return the absolute version of a path. If 'base' is given and 'path' is relative,
292     the path will be expanded relative to 'base'.
293     'path' must be a Unicode string. 'base', if given, must be a Unicode string
294     corresponding to an absolute path as returned by a previous call to
295     abspath_expanduser_unicode.
296     On Windows, the result will be a long path unless long_path is given as False.
297     """
298     if not isinstance(path, unicode):
299         raise AssertionError("paths must be Unicode strings")
300     if base is not None and long_path:
301         precondition_abspath(base)
302
303     path = expanduser(path)
304
305     if _getfullpathname:
306         # On Windows, os.path.isabs will incorrectly return True
307         # for paths without a drive letter (that are not UNC paths),
308         # e.g. "\\". See <http://bugs.python.org/issue1669539>.
309         try:
310             if base is None:
311                 path = _getfullpathname(path or u".")
312             else:
313                 path = _getfullpathname(os.path.join(base, path))
314         except OSError:
315             pass
316
317     if not os.path.isabs(path):
318         if base is None:
319             path = os.path.join(os.getcwdu(), path)
320         else:
321             path = os.path.join(base, path)
322
323     # We won't hit <http://bugs.python.org/issue5827> because
324     # there is always at least one Unicode path component.
325     path = os.path.normpath(path)
326
327     if sys.platform == "win32" and long_path:
328         path = to_windows_long_path(path)
329
330     return path
331
332 def to_windows_long_path(path):
333     # '/' is normally a perfectly valid path component separator in Windows.
334     # However, when using the "\\?\" syntax it is not recognized, so we
335     # replace it with '\' here.
336     path = path.replace(u"/", u"\\")
337
338     # Note that other normalizations such as removing '.' and '..' should
339     # be done outside this function.
340
341     if path.startswith(u"\\\\?\\") or path.startswith(u"\\\\.\\"):
342         return path
343     elif path.startswith(u"\\\\"):
344         return u"\\\\?\\UNC\\" + path[2 :]
345     else:
346         return u"\\\\?\\" + path
347
348
349 have_GetDiskFreeSpaceExW = False
350 if sys.platform == "win32":
351     # <http://msdn.microsoft.com/en-us/library/windows/desktop/ms683188%28v=vs.85%29.aspx>
352     GetEnvironmentVariableW = WINFUNCTYPE(
353         DWORD,  LPCWSTR, LPWSTR, DWORD,
354         use_last_error=True
355     )(("GetEnvironmentVariableW", windll.kernel32))
356
357     try:
358         # <http://msdn.microsoft.com/en-us/library/aa383742%28v=VS.85%29.aspx>
359         PULARGE_INTEGER = POINTER(c_ulonglong)
360
361         # <http://msdn.microsoft.com/en-us/library/aa364937%28VS.85%29.aspx>
362         GetDiskFreeSpaceExW = WINFUNCTYPE(
363             BOOL,  LPCWSTR, PULARGE_INTEGER, PULARGE_INTEGER, PULARGE_INTEGER,
364             use_last_error=True
365         )(("GetDiskFreeSpaceExW", windll.kernel32))
366
367         have_GetDiskFreeSpaceExW = True
368     except Exception:
369         import traceback
370         traceback.print_exc()
371
372 def expanduser(path):
373     # os.path.expanduser is hopelessly broken for Unicode paths on Windows (ticket #1674).
374     if sys.platform == "win32":
375         return windows_expanduser(path)
376     else:
377         return os.path.expanduser(path)
378
379 def windows_expanduser(path):
380     if not path.startswith('~'):
381         return path
382
383     home_dir = windows_getenv(u'USERPROFILE')
384     if home_dir is None:
385         home_drive = windows_getenv(u'HOMEDRIVE')
386         home_path = windows_getenv(u'HOMEPATH')
387         if home_drive is None or home_path is None:
388             raise OSError("Could not find home directory: neither %USERPROFILE% nor (%HOMEDRIVE% and %HOMEPATH%) are set.")
389         home_dir = os.path.join(home_drive, home_path)
390
391     if path == '~':
392         return home_dir
393     elif path.startswith('~/') or path.startswith('~\\'):
394         return os.path.join(home_dir, path[2 :])
395     else:
396         return path
397
398 # <https://msdn.microsoft.com/en-us/library/windows/desktop/ms681382%28v=vs.85%29.aspx>
399 ERROR_ENVVAR_NOT_FOUND = 203
400
401 def windows_getenv(name):
402     # Based on <http://stackoverflow.com/questions/2608200/problems-with-umlauts-in-python-appdata-environvent-variable/2608368#2608368>,
403     # with improved error handling. Returns None if there is no enivronment variable of the given name.
404     if not isinstance(name, unicode):
405         raise AssertionError("name must be Unicode")
406
407     n = GetEnvironmentVariableW(name, None, 0)
408     # GetEnvironmentVariableW returns DWORD, so n cannot be negative.
409     if n == 0:
410         err = get_last_error()
411         if err == ERROR_ENVVAR_NOT_FOUND:
412             return None
413         raise OSError("WinError: %s\n attempting to read size of environment variable %r"
414                       % (WinError(err), name))
415     if n == 1:
416         # Avoid an ambiguity between a zero-length string and an error in the return value of the
417         # call to GetEnvironmentVariableW below.
418         return u""
419
420     buf = create_unicode_buffer(u'\0'*n)
421     retval = GetEnvironmentVariableW(name, buf, n)
422     if retval == 0:
423         err = get_last_error()
424         if err == ERROR_ENVVAR_NOT_FOUND:
425             return None
426         raise OSError("WinError: %s\n attempting to read environment variable %r"
427                       % (WinError(err), name))
428     if retval >= n:
429         raise OSError("Unexpected result %d (expected less than %d) from GetEnvironmentVariableW attempting to read environment variable %r"
430                       % (retval, n, name))
431
432     return buf.value
433
434 def get_disk_stats(whichdir, reserved_space=0):
435     """Return disk statistics for the storage disk, in the form of a dict
436     with the following fields.
437       total:            total bytes on disk
438       free_for_root:    bytes actually free on disk
439       free_for_nonroot: bytes free for "a non-privileged user" [Unix] or
440                           the current user [Windows]; might take into
441                           account quotas depending on platform
442       used:             bytes used on disk
443       avail:            bytes available excluding reserved space
444     An AttributeError can occur if the OS has no API to get disk information.
445     An EnvironmentError can occur if the OS call fails.
446
447     whichdir is a directory on the filesystem in question -- the
448     answer is about the filesystem, not about the directory, so the
449     directory is used only to specify which filesystem.
450
451     reserved_space is how many bytes to subtract from the answer, so
452     you can pass how many bytes you would like to leave unused on this
453     filesystem as reserved_space.
454     """
455
456     if have_GetDiskFreeSpaceExW:
457         # If this is a Windows system and GetDiskFreeSpaceExW is available, use it.
458         # (This might put up an error dialog unless
459         # SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOOPENFILEERRORBOX) has been called,
460         # which we do in allmydata.windows.fixups.initialize().)
461
462         n_free_for_nonroot = c_ulonglong(0)
463         n_total            = c_ulonglong(0)
464         n_free_for_root    = c_ulonglong(0)
465         retval = GetDiskFreeSpaceExW(whichdir, byref(n_free_for_nonroot),
466                                                byref(n_total),
467                                                byref(n_free_for_root))
468         if retval == 0:
469             raise OSError("WinError: %s\n attempting to get disk statistics for %r"
470                           % (WinError(get_last_error()), whichdir))
471         free_for_nonroot = n_free_for_nonroot.value
472         total            = n_total.value
473         free_for_root    = n_free_for_root.value
474     else:
475         # For Unix-like systems.
476         # <http://docs.python.org/library/os.html#os.statvfs>
477         # <http://opengroup.org/onlinepubs/7990989799/xsh/fstatvfs.html>
478         # <http://opengroup.org/onlinepubs/7990989799/xsh/sysstatvfs.h.html>
479         s = os.statvfs(whichdir)
480
481         # on my mac laptop:
482         #  statvfs(2) is a wrapper around statfs(2).
483         #    statvfs.f_frsize = statfs.f_bsize :
484         #     "minimum unit of allocation" (statvfs)
485         #     "fundamental file system block size" (statfs)
486         #    statvfs.f_bsize = statfs.f_iosize = stat.st_blocks : preferred IO size
487         # on an encrypted home directory ("FileVault"), it gets f_blocks
488         # wrong, and s.f_blocks*s.f_frsize is twice the size of my disk,
489         # but s.f_bavail*s.f_frsize is correct
490
491         total = s.f_frsize * s.f_blocks
492         free_for_root = s.f_frsize * s.f_bfree
493         free_for_nonroot = s.f_frsize * s.f_bavail
494
495     # valid for all platforms:
496     used = total - free_for_root
497     avail = max(free_for_nonroot - reserved_space, 0)
498
499     return { 'total': total,
500              'free_for_root': free_for_root,
501              'free_for_nonroot': free_for_nonroot,
502              'used': used,
503              'avail': avail,
504            }
505
506 def get_available_space(whichdir, reserved_space):
507     """Returns available space for share storage in bytes, or None if no
508     API to get this information is available.
509
510     whichdir is a directory on the filesystem in question -- the
511     answer is about the filesystem, not about the directory, so the
512     directory is used only to specify which filesystem.
513
514     reserved_space is how many bytes to subtract from the answer, so
515     you can pass how many bytes you would like to leave unused on this
516     filesystem as reserved_space.
517     """
518     try:
519         return get_disk_stats(whichdir, reserved_space)['avail']
520     except AttributeError:
521         return None
522     except EnvironmentError:
523         log.msg("OS call to get disk statistics failed")
524         return 0
525
526
527 if sys.platform == "win32":
528     # <http://msdn.microsoft.com/en-us/library/aa363858%28v=vs.85%29.aspx>
529     CreateFileW = WINFUNCTYPE(
530         HANDLE,  LPCWSTR, DWORD, DWORD, LPVOID, DWORD, DWORD, HANDLE,
531         use_last_error=True
532     )(("CreateFileW", windll.kernel32))
533
534     GENERIC_WRITE        = 0x40000000
535     FILE_SHARE_READ      = 0x00000001
536     FILE_SHARE_WRITE     = 0x00000002
537     OPEN_EXISTING        = 3
538     INVALID_HANDLE_VALUE = 0xFFFFFFFF
539
540     # <http://msdn.microsoft.com/en-us/library/aa364439%28v=vs.85%29.aspx>
541     FlushFileBuffers = WINFUNCTYPE(
542         BOOL,  HANDLE,
543         use_last_error=True
544     )(("FlushFileBuffers", windll.kernel32))
545
546     # <http://msdn.microsoft.com/en-us/library/ms724211%28v=vs.85%29.aspx>
547     CloseHandle = WINFUNCTYPE(
548         BOOL,  HANDLE,
549         use_last_error=True
550     )(("CloseHandle", windll.kernel32))
551
552     # <http://social.msdn.microsoft.com/forums/en-US/netfxbcl/thread/4465cafb-f4ed-434f-89d8-c85ced6ffaa8/>
553     def flush_volume(path):
554         abspath = os.path.realpath(path)
555         if abspath.startswith("\\\\?\\"):
556             abspath = abspath[4 :]
557         drive = os.path.splitdrive(abspath)[0]
558
559         print "flushing %r" % (drive,)
560         hVolume = CreateFileW(u"\\\\.\\" + drive,
561                               GENERIC_WRITE,
562                               FILE_SHARE_READ | FILE_SHARE_WRITE,
563                               None,
564                               OPEN_EXISTING,
565                               0,
566                               None
567                              )
568         if hVolume == INVALID_HANDLE_VALUE:
569             raise WinError(get_last_error())
570
571         if FlushFileBuffers(hVolume) == 0:
572             raise WinError(get_last_error())
573
574         CloseHandle(hVolume)
575 else:
576     def flush_volume(path):
577         # use sync()?
578         pass
579
580
581 class ConflictError(Exception):
582     pass
583
584 class UnableToUnlinkReplacementError(Exception):
585     pass
586
587 def reraise(wrapper):
588     _, exc, tb = sys.exc_info()
589     wrapper_exc = wrapper("%s: %s" % (exc.__class__.__name__, exc))
590     raise wrapper_exc.__class__, wrapper_exc, tb
591
592 if sys.platform == "win32":
593     # <https://msdn.microsoft.com/en-us/library/windows/desktop/aa365512%28v=vs.85%29.aspx>
594     ReplaceFileW = WINFUNCTYPE(
595         BOOL,  LPCWSTR, LPCWSTR, LPCWSTR, DWORD, LPVOID, LPVOID,
596         use_last_error=True
597     )(("ReplaceFileW", windll.kernel32))
598
599     REPLACEFILE_IGNORE_MERGE_ERRORS = 0x00000002
600
601     # <https://msdn.microsoft.com/en-us/library/windows/desktop/ms681382%28v=vs.85%29.aspx>
602     ERROR_FILE_NOT_FOUND = 2
603
604     def rename_no_overwrite(source_path, dest_path):
605         os.rename(source_path, dest_path)
606
607     def replace_file(replaced_path, replacement_path, backup_path):
608         precondition_abspath(replaced_path)
609         precondition_abspath(replacement_path)
610         precondition_abspath(backup_path)
611
612         r = ReplaceFileW(replaced_path, replacement_path, backup_path,
613                          REPLACEFILE_IGNORE_MERGE_ERRORS, None, None)
614         if r == 0:
615             # The UnableToUnlinkReplacementError case does not happen on Windows;
616             # all errors should be treated as signalling a conflict.
617             err = get_last_error()
618             if err != ERROR_FILE_NOT_FOUND:
619                 raise ConflictError("WinError: %s" % (WinError(err),))
620
621             try:
622                 rename_no_overwrite(replacement_path, replaced_path)
623             except EnvironmentError:
624                 reraise(ConflictError)
625 else:
626     def rename_no_overwrite(source_path, dest_path):
627         # link will fail with EEXIST if there is already something at dest_path.
628         os.link(source_path, dest_path)
629         try:
630             os.unlink(source_path)
631         except EnvironmentError:
632             reraise(UnableToUnlinkReplacementError)
633
634     def replace_file(replaced_path, replacement_path, backup_path):
635         precondition_abspath(replaced_path)
636         precondition_abspath(replacement_path)
637         precondition_abspath(backup_path)
638
639         if not os.path.exists(replacement_path):
640             raise ConflictError("Replacement file not found: %r" % (replacement_path,))
641
642         try:
643             os.rename(replaced_path, backup_path)
644         except OSError as e:
645             if e.errno != ENOENT:
646                 raise
647         try:
648             rename_no_overwrite(replacement_path, replaced_path)
649         except EnvironmentError:
650             reraise(ConflictError)
651
652 PathInfo = namedtuple('PathInfo', 'isdir isfile islink exists size mtime ctime')
653
654 def get_pathinfo(path_u, now=None):
655     try:
656         statinfo = os.lstat(path_u)
657         mode = statinfo.st_mode
658         return PathInfo(isdir =stat.S_ISDIR(mode),
659                         isfile=stat.S_ISREG(mode),
660                         islink=stat.S_ISLNK(mode),
661                         exists=True,
662                         size  =statinfo.st_size,
663                         mtime =statinfo.st_mtime,
664                         ctime =statinfo.st_ctime,
665                        )
666     except OSError as e:
667         if e.errno == ENOENT:
668             if now is None:
669                 now = time.time()
670             return PathInfo(isdir =False,
671                             isfile=False,
672                             islink=False,
673                             exists=False,
674                             size  =None,
675                             mtime =now,
676                             ctime =now,
677                            )
678         raise