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