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, windll, POINTER, byref, c_ulonglong, create_unicode_buffer
11 from ctypes.wintypes import BOOL, HANDLE, DWORD, LPCWSTR, LPWSTR, LPVOID, WinError, get_last_error
14 from twisted.python import log
16 from pycryptopp.cipher.aes import AES
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.
26 With the default values of tries and basedelay this can block for less
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
33 for i in range(tries-1):
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,))
41 return os.rename(src, dst) # The last try.
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.
50 With the default values of tries and basedelay this can block for less
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
58 os.chmod(f, stat.S_IWRITE | stat.S_IEXEC | stat.S_IREAD)
61 for i in range(tries-1):
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):
68 log.msg("XXX KLUDGE Attempting to remove file %s; got %s; sleeping %s seconds" % (f, le, basedelay,))
71 return os.remove(f) # The last try.
73 class ReopenableNamedTemporaryFile:
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.
81 def __init__(self, *args, **kwargs):
82 fd, self.name = tempfile.mkstemp(*args, **kwargs)
86 return "<%s instance at %x %s>" % (self.__class__.__name__, id(self), self.name)
89 return self.__repr__()
97 class EncryptedTemporaryFile:
98 # not implemented: next, readline, readlines, xreadlines, writelines
101 self.file = tempfile.TemporaryFile()
102 self.key = os.urandom(16) # AES-128
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)
118 def seek(self, offset, whence=0): # 0 = SEEK_SET
119 self.file.seek(offset, whence)
122 offset = self.file.tell()
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)
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)
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)
146 def make_dirs(dirname, mode=0777):
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.
156 os.makedirs(dirname, mode)
160 if not os.path.isdir(dirname):
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...
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
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):
184 except Exception, le:
185 # Ignore "No such file or directory"
186 if (not isinstance(le, OSError)) or le.args[0] != 2:
189 # Okay, now we've recursively removed everything, ignoring any "No
190 # such file or directory" errors, and collecting any other errors.
192 if os.path.exists(dirname):
196 raise OSError, "Failed to remove dir for unknown reason."
200 def remove_if_possible(f):
209 for root, dirs, files in os.walk(basedir):
211 fn = os.path.join(root, f)
212 size += os.path.getsize(fn)
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)
223 def write_atomically(target, contents, mode="b"):
224 f = open(target+".tmp", "w"+mode)
229 move_into_place(target+".tmp", target)
231 def write(path, data, mode="wb"):
232 wf = open(path, mode)
239 rf = open(path, "rb")
245 def put_file(path, inf):
246 precondition_abspath(path)
248 # TODO: create temporary file and move into place?
249 outf = open(path, "wb")
252 data = inf.read(32768)
260 def precondition_abspath(path):
261 if not isinstance(path, unicode):
262 raise AssertionError("an abspath must be a Unicode string")
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")
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")
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.
278 _getfullpathname = None
280 from nt import _getfullpathname
284 def abspath_expanduser_unicode(path, base=None, long_path=True):
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.
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)
298 path = expanduser(path)
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>.
306 path = _getfullpathname(path or u".")
308 path = _getfullpathname(os.path.join(base, path))
312 if not os.path.isabs(path):
314 path = os.path.join(os.getcwdu(), path)
316 path = os.path.join(base, path)
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)
322 if sys.platform == "win32" and long_path:
323 path = to_windows_long_path(path)
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"\\")
333 # Note that other normalizations such as removing '.' and '..' should
334 # be done outside this function.
336 if path.startswith(u"\\\\?\\") or path.startswith(u"\\\\.\\"):
338 elif path.startswith(u"\\\\"):
339 return u"\\\\?\\UNC\\" + path[2 :]
341 return u"\\\\?\\" + path
344 have_GetDiskFreeSpaceExW = False
345 if sys.platform == "win32":
346 # <http://msdn.microsoft.com/en-us/library/windows/desktop/ms683188%28v=vs.85%29.aspx>
347 GetEnvironmentVariableW = WINFUNCTYPE(
349 LPCWSTR, LPWSTR, DWORD,
351 )(("GetEnvironmentVariableW", windll.kernel32))
354 # <http://msdn.microsoft.com/en-us/library/aa383742%28v=VS.85%29.aspx>
355 PULARGE_INTEGER = POINTER(c_ulonglong)
357 # <http://msdn.microsoft.com/en-us/library/aa364937%28VS.85%29.aspx>
358 GetDiskFreeSpaceExW = WINFUNCTYPE(
360 LPCWSTR, PULARGE_INTEGER, PULARGE_INTEGER, PULARGE_INTEGER,
362 )(("GetDiskFreeSpaceExW", windll.kernel32))
364 have_GetDiskFreeSpaceExW = True
367 traceback.print_exc()
369 def expanduser(path):
370 # os.path.expanduser is hopelessly broken for Unicode paths on Windows (ticket #1674).
371 if sys.platform == "win32":
372 return windows_expanduser(path)
374 return os.path.expanduser(path)
376 def windows_expanduser(path):
377 if not path.startswith('~'):
380 home_dir = windows_getenv(u'USERPROFILE')
382 home_drive = windows_getenv(u'HOMEDRIVE')
383 home_path = windows_getenv(u'HOMEPATH')
384 if home_drive is None or home_path is None:
385 raise OSError("Could not find home directory: neither %USERPROFILE% nor (%HOMEDRIVE% and %HOMEPATH%) are set.")
386 home_dir = os.path.join(home_drive, home_path)
390 elif path.startswith('~/') or path.startswith('~\\'):
391 return os.path.join(home_dir, path[2 :])
395 # <https://msdn.microsoft.com/en-us/library/windows/desktop/ms681382%28v=vs.85%29.aspx>
396 ERROR_ENVVAR_NOT_FOUND = 203
398 def windows_getenv(name):
399 # Based on <http://stackoverflow.com/questions/2608200/problems-with-umlauts-in-python-appdata-environvent-variable/2608368#2608368>,
400 # with improved error handling. Returns None if there is no enivronment variable of the given name.
401 if not isinstance(name, unicode):
402 raise AssertionError("name must be Unicode")
404 n = GetEnvironmentVariableW(name, None, 0)
405 # GetEnvironmentVariableW returns DWORD, so n cannot be negative.
407 err = get_last_error()
408 if err == ERROR_ENVVAR_NOT_FOUND:
410 raise OSError("Windows error %d attempting to read size of environment variable %r"
413 # Avoid an ambiguity between a zero-length string and an error in the return value of the
414 # call to GetEnvironmentVariableW below.
417 buf = create_unicode_buffer(u'\0'*n)
418 retval = GetEnvironmentVariableW(name, buf, n)
420 err = get_last_error()
421 if err == ERROR_ENVVAR_NOT_FOUND:
423 raise OSError("Windows error %d attempting to read environment variable %r"
426 raise OSError("Unexpected result %d (expected less than %d) from GetEnvironmentVariableW attempting to read environment variable %r"
431 def get_disk_stats(whichdir, reserved_space=0):
432 """Return disk statistics for the storage disk, in the form of a dict
433 with the following fields.
434 total: total bytes on disk
435 free_for_root: bytes actually free on disk
436 free_for_nonroot: bytes free for "a non-privileged user" [Unix] or
437 the current user [Windows]; might take into
438 account quotas depending on platform
439 used: bytes used on disk
440 avail: bytes available excluding reserved space
441 An AttributeError can occur if the OS has no API to get disk information.
442 An EnvironmentError can occur if the OS call fails.
444 whichdir is a directory on the filesystem in question -- the
445 answer is about the filesystem, not about the directory, so the
446 directory is used only to specify which filesystem.
448 reserved_space is how many bytes to subtract from the answer, so
449 you can pass how many bytes you would like to leave unused on this
450 filesystem as reserved_space.
453 if have_GetDiskFreeSpaceExW:
454 # If this is a Windows system and GetDiskFreeSpaceExW is available, use it.
455 # (This might put up an error dialog unless
456 # SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOOPENFILEERRORBOX) has been called,
457 # which we do in allmydata.windows.fixups.initialize().)
459 n_free_for_nonroot = c_ulonglong(0)
460 n_total = c_ulonglong(0)
461 n_free_for_root = c_ulonglong(0)
462 retval = GetDiskFreeSpaceExW(whichdir, byref(n_free_for_nonroot),
464 byref(n_free_for_root))
466 raise OSError("Windows error %d attempting to get disk statistics for %r"
467 % (get_last_error(), whichdir))
468 free_for_nonroot = n_free_for_nonroot.value
469 total = n_total.value
470 free_for_root = n_free_for_root.value
472 # For Unix-like systems.
473 # <http://docs.python.org/library/os.html#os.statvfs>
474 # <http://opengroup.org/onlinepubs/7990989799/xsh/fstatvfs.html>
475 # <http://opengroup.org/onlinepubs/7990989799/xsh/sysstatvfs.h.html>
476 s = os.statvfs(whichdir)
479 # statvfs(2) is a wrapper around statfs(2).
480 # statvfs.f_frsize = statfs.f_bsize :
481 # "minimum unit of allocation" (statvfs)
482 # "fundamental file system block size" (statfs)
483 # statvfs.f_bsize = statfs.f_iosize = stat.st_blocks : preferred IO size
484 # on an encrypted home directory ("FileVault"), it gets f_blocks
485 # wrong, and s.f_blocks*s.f_frsize is twice the size of my disk,
486 # but s.f_bavail*s.f_frsize is correct
488 total = s.f_frsize * s.f_blocks
489 free_for_root = s.f_frsize * s.f_bfree
490 free_for_nonroot = s.f_frsize * s.f_bavail
492 # valid for all platforms:
493 used = total - free_for_root
494 avail = max(free_for_nonroot - reserved_space, 0)
496 return { 'total': total,
497 'free_for_root': free_for_root,
498 'free_for_nonroot': free_for_nonroot,
503 def get_available_space(whichdir, reserved_space):
504 """Returns available space for share storage in bytes, or None if no
505 API to get this information is available.
507 whichdir is a directory on the filesystem in question -- the
508 answer is about the filesystem, not about the directory, so the
509 directory is used only to specify which filesystem.
511 reserved_space is how many bytes to subtract from the answer, so
512 you can pass how many bytes you would like to leave unused on this
513 filesystem as reserved_space.
516 return get_disk_stats(whichdir, reserved_space)['avail']
517 except AttributeError:
519 except EnvironmentError:
520 log.msg("OS call to get disk statistics failed")
524 if sys.platform == "win32":
525 # <http://msdn.microsoft.com/en-us/library/aa363858%28v=vs.85%29.aspx>
526 CreateFileW = WINFUNCTYPE(HANDLE, LPCWSTR, DWORD, DWORD, LPVOID, DWORD, DWORD, HANDLE) \
527 (("CreateFileW", windll.kernel32))
529 GENERIC_WRITE = 0x40000000
530 FILE_SHARE_READ = 0x00000001
531 FILE_SHARE_WRITE = 0x00000002
533 INVALID_HANDLE_VALUE = 0xFFFFFFFF
535 # <http://msdn.microsoft.com/en-us/library/aa364439%28v=vs.85%29.aspx>
536 FlushFileBuffers = WINFUNCTYPE(BOOL, HANDLE)(("FlushFileBuffers", windll.kernel32))
538 # <http://msdn.microsoft.com/en-us/library/ms724211%28v=vs.85%29.aspx>
539 CloseHandle = WINFUNCTYPE(BOOL, HANDLE)(("CloseHandle", windll.kernel32))
541 # <http://social.msdn.microsoft.com/forums/en-US/netfxbcl/thread/4465cafb-f4ed-434f-89d8-c85ced6ffaa8/>
542 def flush_volume(path):
543 drive = os.path.splitdrive(os.path.realpath(path))[0]
545 hVolume = CreateFileW(u"\\\\.\\" + drive,
547 FILE_SHARE_READ | FILE_SHARE_WRITE,
553 if hVolume == INVALID_HANDLE_VALUE:
556 if FlushFileBuffers(hVolume) == 0:
561 def flush_volume(path):
566 class ConflictError(Exception):
569 class UnableToUnlinkReplacementError(Exception):
572 def reraise(wrapper):
573 _, exc, tb = sys.exc_info()
574 wrapper_exc = wrapper("%s: %s" % (exc.__class__.__name__, exc))
575 raise wrapper_exc.__class__, wrapper_exc, tb
577 if sys.platform == "win32":
578 # <https://msdn.microsoft.com/en-us/library/windows/desktop/aa365512%28v=vs.85%29.aspx>
579 ReplaceFileW = WINFUNCTYPE(
581 LPCWSTR, LPCWSTR, LPCWSTR, DWORD, LPVOID, LPVOID,
583 )(("ReplaceFileW", windll.kernel32))
585 REPLACEFILE_IGNORE_MERGE_ERRORS = 0x00000002
587 # <https://msdn.microsoft.com/en-us/library/windows/desktop/ms681382%28v=vs.85%29.aspx>
588 ERROR_FILE_NOT_FOUND = 2
590 def rename_no_overwrite(source_path, dest_path):
591 os.rename(source_path, dest_path)
593 def replace_file(replaced_path, replacement_path, backup_path):
594 precondition_abspath(replaced_path)
595 precondition_abspath(replacement_path)
596 precondition_abspath(backup_path)
598 r = ReplaceFileW(replaced_path, replacement_path, backup_path,
599 REPLACEFILE_IGNORE_MERGE_ERRORS, None, None)
601 # The UnableToUnlinkReplacementError case does not happen on Windows;
602 # all errors should be treated as signalling a conflict.
603 err = get_last_error()
604 if err != ERROR_FILE_NOT_FOUND:
605 raise ConflictError("WinError: %s" % (WinError(err),))
608 rename_no_overwrite(replacement_path, replaced_path)
609 except EnvironmentError:
610 reraise(ConflictError)
612 def rename_no_overwrite(source_path, dest_path):
613 # link will fail with EEXIST if there is already something at dest_path.
614 os.link(source_path, dest_path)
616 os.unlink(source_path)
617 except EnvironmentError:
618 reraise(UnableToUnlinkReplacementError)
620 def replace_file(replaced_path, replacement_path, backup_path):
621 precondition_abspath(replaced_path)
622 precondition_abspath(replacement_path)
623 precondition_abspath(backup_path)
625 if not os.path.exists(replacement_path):
626 raise ConflictError("Replacement file not found: %r" % (replacement_path,))
629 os.rename(replaced_path, backup_path)
631 if e.errno != ENOENT:
634 rename_no_overwrite(replacement_path, replaced_path)
635 except EnvironmentError:
636 reraise(ConflictError)
638 PathInfo = namedtuple('PathInfo', 'isdir isfile islink exists size mtime ctime')
640 def get_pathinfo(path_u, now=None):
642 statinfo = os.lstat(path_u)
643 mode = statinfo.st_mode
644 return PathInfo(isdir =stat.S_ISDIR(mode),
645 isfile=stat.S_ISREG(mode),
646 islink=stat.S_ISLNK(mode),
648 size =statinfo.st_size,
649 mtime =statinfo.st_mtime,
650 ctime =statinfo.st_ctime,
653 if e.errno == ENOENT:
656 return PathInfo(isdir =False,