2 Futz with files like a pro.
5 import sys, exceptions, os, stat, tempfile, time, binascii
6 from collections import namedtuple
7 from errno import ENOENT
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
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
19 from twisted.python import log
21 from pycryptopp.cipher.aes import AES
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.
31 With the default values of tries and basedelay this can block for less
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
38 for i in range(tries-1):
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,))
46 return os.rename(src, dst) # The last try.
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.
55 With the default values of tries and basedelay this can block for less
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
63 os.chmod(f, stat.S_IWRITE | stat.S_IEXEC | stat.S_IREAD)
66 for i in range(tries-1):
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):
73 log.msg("XXX KLUDGE Attempting to remove file %s; got %s; sleeping %s seconds" % (f, le, basedelay,))
76 return os.remove(f) # The last try.
78 class ReopenableNamedTemporaryFile:
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.
86 def __init__(self, *args, **kwargs):
87 fd, self.name = tempfile.mkstemp(*args, **kwargs)
91 return "<%s instance at %x %s>" % (self.__class__.__name__, id(self), self.name)
94 return self.__repr__()
102 class EncryptedTemporaryFile:
103 # not implemented: next, readline, readlines, xreadlines, writelines
106 self.file = tempfile.TemporaryFile()
107 self.key = os.urandom(16) # AES-128
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)
123 def seek(self, offset, whence=0): # 0 = SEEK_SET
124 self.file.seek(offset, whence)
127 offset = self.file.tell()
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)
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)
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)
151 def make_dirs(dirname, mode=0777):
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.
161 os.makedirs(dirname, mode)
165 if not os.path.isdir(dirname):
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...
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
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):
189 except Exception, le:
190 # Ignore "No such file or directory"
191 if (not isinstance(le, OSError)) or le.args[0] != 2:
194 # Okay, now we've recursively removed everything, ignoring any "No
195 # such file or directory" errors, and collecting any other errors.
197 if os.path.exists(dirname):
201 raise OSError, "Failed to remove dir for unknown reason."
205 def remove_if_possible(f):
214 for root, dirs, files in os.walk(basedir):
216 fn = os.path.join(root, f)
217 size += os.path.getsize(fn)
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)
228 def write_atomically(target, contents, mode="b"):
229 f = open(target+".tmp", "w"+mode)
234 move_into_place(target+".tmp", target)
236 def write(path, data, mode="wb"):
237 wf = open(path, mode)
244 rf = open(path, "rb")
250 def put_file(path, inf):
251 precondition_abspath(path)
253 # TODO: create temporary file and move into place?
254 outf = open(path, "wb")
257 data = inf.read(32768)
265 def precondition_abspath(path):
266 if not isinstance(path, unicode):
267 raise AssertionError("an abspath must be a Unicode string")
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")
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")
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.
283 _getfullpathname = None
285 from nt import _getfullpathname
289 def abspath_expanduser_unicode(path, base=None, long_path=True):
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.
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)
303 path = expanduser(path)
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>.
311 path = _getfullpathname(path or u".")
313 path = _getfullpathname(os.path.join(base, path))
317 if not os.path.isabs(path):
319 path = os.path.join(os.getcwdu(), path)
321 path = os.path.join(base, path)
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)
327 if sys.platform == "win32" and long_path:
328 path = to_windows_long_path(path)
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"\\")
338 # Note that other normalizations such as removing '.' and '..' should
339 # be done outside this function.
341 if path.startswith(u"\\\\?\\") or path.startswith(u"\\\\.\\"):
343 elif path.startswith(u"\\\\"):
344 return u"\\\\?\\UNC\\" + path[2 :]
346 return u"\\\\?\\" + path
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,
355 )(("GetEnvironmentVariableW", windll.kernel32))
358 # <http://msdn.microsoft.com/en-us/library/aa383742%28v=VS.85%29.aspx>
359 PULARGE_INTEGER = POINTER(c_ulonglong)
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,
365 )(("GetDiskFreeSpaceExW", windll.kernel32))
367 have_GetDiskFreeSpaceExW = True
370 traceback.print_exc()
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)
377 return os.path.expanduser(path)
379 def windows_expanduser(path):
380 if not path.startswith('~'):
383 home_dir = windows_getenv(u'USERPROFILE')
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)
393 elif path.startswith('~/') or path.startswith('~\\'):
394 return os.path.join(home_dir, path[2 :])
398 # <https://msdn.microsoft.com/en-us/library/windows/desktop/ms681382%28v=vs.85%29.aspx>
399 ERROR_ENVVAR_NOT_FOUND = 203
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")
407 n = GetEnvironmentVariableW(name, None, 0)
408 # GetEnvironmentVariableW returns DWORD, so n cannot be negative.
410 err = get_last_error()
411 if err == ERROR_ENVVAR_NOT_FOUND:
413 raise OSError("WinError: %s\n attempting to read size of environment variable %r"
414 % (WinError(err), name))
416 # Avoid an ambiguity between a zero-length string and an error in the return value of the
417 # call to GetEnvironmentVariableW below.
420 buf = create_unicode_buffer(u'\0'*n)
421 retval = GetEnvironmentVariableW(name, buf, n)
423 err = get_last_error()
424 if err == ERROR_ENVVAR_NOT_FOUND:
426 raise OSError("WinError: %s\n attempting to read environment variable %r"
427 % (WinError(err), name))
429 raise OSError("Unexpected result %d (expected less than %d) from GetEnvironmentVariableW attempting to read environment variable %r"
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.
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.
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.
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().)
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),
467 byref(n_free_for_root))
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
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)
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
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
495 # valid for all platforms:
496 used = total - free_for_root
497 avail = max(free_for_nonroot - reserved_space, 0)
499 return { 'total': total,
500 'free_for_root': free_for_root,
501 'free_for_nonroot': free_for_nonroot,
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.
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.
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.
519 return get_disk_stats(whichdir, reserved_space)['avail']
520 except AttributeError:
522 except EnvironmentError:
523 log.msg("OS call to get disk statistics failed")
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,
532 )(("CreateFileW", windll.kernel32))
534 GENERIC_WRITE = 0x40000000
535 FILE_SHARE_READ = 0x00000001
536 FILE_SHARE_WRITE = 0x00000002
538 INVALID_HANDLE_VALUE = 0xFFFFFFFF
540 # <http://msdn.microsoft.com/en-us/library/aa364439%28v=vs.85%29.aspx>
541 FlushFileBuffers = WINFUNCTYPE(
544 )(("FlushFileBuffers", windll.kernel32))
546 # <http://msdn.microsoft.com/en-us/library/ms724211%28v=vs.85%29.aspx>
547 CloseHandle = WINFUNCTYPE(
550 )(("CloseHandle", windll.kernel32))
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]
559 print "flushing %r" % (drive,)
560 hVolume = CreateFileW(u"\\\\.\\" + drive,
562 FILE_SHARE_READ | FILE_SHARE_WRITE,
568 if hVolume == INVALID_HANDLE_VALUE:
569 raise WinError(get_last_error())
571 if FlushFileBuffers(hVolume) == 0:
572 raise WinError(get_last_error())
576 def flush_volume(path):
581 class ConflictError(Exception):
584 class UnableToUnlinkReplacementError(Exception):
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
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,
597 )(("ReplaceFileW", windll.kernel32))
599 REPLACEFILE_IGNORE_MERGE_ERRORS = 0x00000002
601 # <https://msdn.microsoft.com/en-us/library/windows/desktop/ms681382%28v=vs.85%29.aspx>
602 ERROR_FILE_NOT_FOUND = 2
604 def rename_no_overwrite(source_path, dest_path):
605 os.rename(source_path, dest_path)
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)
612 r = ReplaceFileW(replaced_path, replacement_path, backup_path,
613 REPLACEFILE_IGNORE_MERGE_ERRORS, None, None)
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),))
622 rename_no_overwrite(replacement_path, replaced_path)
623 except EnvironmentError:
624 reraise(ConflictError)
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)
630 os.unlink(source_path)
631 except EnvironmentError:
632 reraise(UnableToUnlinkReplacementError)
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)
639 if not os.path.exists(replacement_path):
640 raise ConflictError("Replacement file not found: %r" % (replacement_path,))
643 os.rename(replaced_path, backup_path)
645 if e.errno != ENOENT:
648 rename_no_overwrite(replacement_path, replaced_path)
649 except EnvironmentError:
650 reraise(ConflictError)
652 PathInfo = namedtuple('PathInfo', 'isdir isfile islink exists size mtime ctime')
654 def get_pathinfo(path_u, now=None):
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),
662 size =statinfo.st_size,
663 mtime =statinfo.st_mtime,
664 ctime =statinfo.st_ctime,
667 if e.errno == ENOENT:
670 return PathInfo(isdir =False,