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 from twisted.python import log
11 from pycryptopp.cipher.aes import AES
14 def rename(src, dst, tries=4, basedelay=0.1):
15 """ Here is a superkludge to workaround the fact that occasionally on
16 Windows some other process (e.g. an anti-virus scanner, a local search
17 engine, etc.) is looking at your file when you want to delete or move it,
18 and hence you can't. The horrible workaround is to sit and spin, trying
19 to delete it, for a short time and then give up.
21 With the default values of tries and basedelay this can block for less
24 @param tries: number of tries -- each time after the first we wait twice
25 as long as the previous wait
26 @param basedelay: how long to wait before the second try
28 for i in range(tries-1):
30 return os.rename(src, dst)
31 except EnvironmentError, le:
32 # 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.
33 log.msg("XXX KLUDGE Attempting to move file %s => %s; got %s; sleeping %s seconds" % (src, dst, le, basedelay,))
36 return os.rename(src, dst) # The last try.
38 def remove(f, tries=4, basedelay=0.1):
39 """ Here is a superkludge to workaround the fact that occasionally on
40 Windows some other process (e.g. an anti-virus scanner, a local search
41 engine, etc.) is looking at your file when you want to delete or move it,
42 and hence you can't. The horrible workaround is to sit and spin, trying
43 to delete it, for a short time and then give up.
45 With the default values of tries and basedelay this can block for less
48 @param tries: number of tries -- each time after the first we wait twice
49 as long as the previous wait
50 @param basedelay: how long to wait before the second try
53 os.chmod(f, stat.S_IWRITE | stat.S_IEXEC | stat.S_IREAD)
56 for i in range(tries-1):
59 except EnvironmentError, le:
60 # 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.
61 if not os.path.exists(f):
63 log.msg("XXX KLUDGE Attempting to remove file %s; got %s; sleeping %s seconds" % (f, le, basedelay,))
66 return os.remove(f) # The last try.
68 class ReopenableNamedTemporaryFile:
70 This uses tempfile.mkstemp() to generate a secure temp file. It then closes
71 the file, leaving a zero-length file as a placeholder. You can get the
72 filename with ReopenableNamedTemporaryFile.name. When the
73 ReopenableNamedTemporaryFile instance is garbage collected or its shutdown()
74 method is called, it deletes the file.
76 def __init__(self, *args, **kwargs):
77 fd, self.name = tempfile.mkstemp(*args, **kwargs)
81 return "<%s instance at %x %s>" % (self.__class__.__name__, id(self), self.name)
84 return self.__repr__()
92 class EncryptedTemporaryFile:
93 # not implemented: next, readline, readlines, xreadlines, writelines
96 self.file = tempfile.TemporaryFile()
97 self.key = os.urandom(16) # AES-128
99 def _crypt(self, offset, data):
100 offset_big = offset // 16
101 offset_small = offset % 16
102 iv = binascii.unhexlify("%032x" % offset_big)
103 cipher = AES(self.key, iv=iv)
104 cipher.process("\x00"*offset_small)
105 return cipher.process(data)
113 def seek(self, offset, whence=0): # 0 = SEEK_SET
114 self.file.seek(offset, whence)
117 offset = self.file.tell()
120 def read(self, size=-1):
121 """A read must not follow a write, or vice-versa, without an intervening seek."""
122 index = self.file.tell()
123 ciphertext = self.file.read(size)
124 plaintext = self._crypt(index, ciphertext)
127 def write(self, plaintext):
128 """A read must not follow a write, or vice-versa, without an intervening seek.
129 If seeking and then writing causes a 'hole' in the file, the contents of the
130 hole are unspecified."""
131 index = self.file.tell()
132 ciphertext = self._crypt(index, plaintext)
133 self.file.write(ciphertext)
135 def truncate(self, newsize):
136 """Truncate or extend the file to 'newsize'. If it is extended, the contents after the
137 old end-of-file are unspecified. The file position after this operation is unspecified."""
138 self.file.truncate(newsize)
141 def make_dirs(dirname, mode=0777):
143 An idempotent version of os.makedirs(). If the dir already exists, do
144 nothing and return without raising an exception. If this call creates the
145 dir, return without raising an exception. If there is an error that
146 prevents creation or if the directory gets deleted after make_dirs() creates
147 it and before make_dirs() checks that it exists, raise an exception.
151 os.makedirs(dirname, mode)
155 if not os.path.isdir(dirname):
158 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...
162 A threadsafe and idempotent version of shutil.rmtree(). If the dir is
163 already gone, do nothing and return without raising an exception. If this
164 call removes the dir, return without raising an exception. If there is an
165 error that prevents deletion or if the directory gets created again after
166 rm_dir() deletes it and before rm_dir() checks that it is gone, raise an
171 os.chmod(dirname, stat.S_IWRITE | stat.S_IEXEC | stat.S_IREAD)
172 for f in os.listdir(dirname):
173 fullname = os.path.join(dirname, f)
174 if os.path.isdir(fullname):
179 except Exception, le:
180 # Ignore "No such file or directory"
181 if (not isinstance(le, OSError)) or le.args[0] != 2:
184 # Okay, now we've recursively removed everything, ignoring any "No
185 # such file or directory" errors, and collecting any other errors.
187 if os.path.exists(dirname):
191 raise OSError, "Failed to remove dir for unknown reason."
195 def remove_if_possible(f):
204 for root, dirs, files in os.walk(basedir):
206 fn = os.path.join(root, f)
207 size += os.path.getsize(fn)
211 def move_into_place(source, dest):
212 """Atomically replace a file, or as near to it as the platform allows.
213 The dest file may or may not exist."""
214 if "win32" in sys.platform.lower():
215 remove_if_possible(dest)
216 os.rename(source, dest)
218 def write_atomically(target, contents, mode="b"):
219 f = open(target+".tmp", "w"+mode)
224 move_into_place(target+".tmp", target)
226 def write(path, data, mode="wb"):
227 wf = open(path, mode)
234 rf = open(path, "rb")
240 def put_file(path, inf):
241 precondition_abspath(path)
243 # TODO: create temporary file and move into place?
244 outf = open(path, "wb")
247 data = inf.read(32768)
255 def precondition_abspath(path):
256 if not isinstance(path, unicode):
257 raise AssertionError("an abspath must be a Unicode string")
259 if sys.platform == "win32":
260 # This intentionally doesn't view absolute paths starting with a drive specification, or
261 # paths relative to the current drive, as acceptable.
262 if not path.startswith("\\\\"):
263 raise AssertionError("an abspath should be normalized using abspath_expanduser_unicode")
265 # This intentionally doesn't view the path '~' or paths starting with '~/' as acceptable.
266 if not os.path.isabs(path):
267 raise AssertionError("an abspath should be normalized using abspath_expanduser_unicode")
269 # Work around <http://bugs.python.org/issue3426>. This code is adapted from
270 # <http://svn.python.org/view/python/trunk/Lib/ntpath.py?revision=78247&view=markup>
271 # with some simplifications.
273 _getfullpathname = None
275 from nt import _getfullpathname
279 def abspath_expanduser_unicode(path, base=None, long_path=True):
281 Return the absolute version of a path. If 'base' is given and 'path' is relative,
282 the path will be expanded relative to 'base'.
283 'path' must be a Unicode string. 'base', if given, must be a Unicode string
284 corresponding to an absolute path as returned by a previous call to
285 abspath_expanduser_unicode.
286 On Windows, the result will be a long path unless long_path is given as False.
288 if not isinstance(path, unicode):
289 raise AssertionError("paths must be Unicode strings")
291 precondition_abspath(base)
293 path = expanduser(path)
296 # On Windows, os.path.isabs will incorrectly return True
297 # for paths without a drive letter (that are not UNC paths),
298 # e.g. "\\". See <http://bugs.python.org/issue1669539>.
301 path = _getfullpathname(path or u".")
303 path = _getfullpathname(os.path.join(base, path))
307 if not os.path.isabs(path):
309 path = os.path.join(os.getcwdu(), path)
311 path = os.path.join(base, path)
313 # We won't hit <http://bugs.python.org/issue5827> because
314 # there is always at least one Unicode path component.
315 path = os.path.normpath(path)
317 if sys.platform == "win32" and long_path:
318 path = to_windows_long_path(path)
322 def to_windows_long_path(path):
323 # '/' is normally a perfectly valid path component separator in Windows.
324 # However, when using the "\\?\" syntax it is not recognized, so we
325 # replace it with '\' here.
326 path = path.replace(u"/", u"\\")
328 # Note that other normalizations such as removing '.' and '..' should
329 # be done outside this function.
331 if path.startswith(u"\\\\?\\") or path.startswith(u"\\\\.\\"):
333 elif path.startswith(u"\\\\"):
334 return u"\\\\?\\UNC\\" + path[2 :]
336 return u"\\\\?\\" + path
339 have_GetDiskFreeSpaceExW = False
340 if sys.platform == "win32":
341 from ctypes import WINFUNCTYPE, windll, POINTER, byref, c_ulonglong, create_unicode_buffer, \
343 from ctypes.wintypes import BOOL, DWORD, LPCWSTR, LPWSTR
345 # <http://msdn.microsoft.com/en-us/library/windows/desktop/ms683188%28v=vs.85%29.aspx>
346 GetEnvironmentVariableW = WINFUNCTYPE(
348 LPCWSTR, LPWSTR, DWORD,
350 )(("GetEnvironmentVariableW", windll.kernel32))
353 # <http://msdn.microsoft.com/en-us/library/aa383742%28v=VS.85%29.aspx>
354 PULARGE_INTEGER = POINTER(c_ulonglong)
356 # <http://msdn.microsoft.com/en-us/library/aa364937%28VS.85%29.aspx>
357 GetDiskFreeSpaceExW = WINFUNCTYPE(
359 LPCWSTR, PULARGE_INTEGER, PULARGE_INTEGER, PULARGE_INTEGER,
361 )(("GetDiskFreeSpaceExW", windll.kernel32))
363 have_GetDiskFreeSpaceExW = True
366 traceback.print_exc()
368 def expanduser(path):
369 # os.path.expanduser is hopelessly broken for Unicode paths on Windows (ticket #1674).
370 if sys.platform == "win32":
371 return windows_expanduser(path)
373 return os.path.expanduser(path)
375 def windows_expanduser(path):
376 if not path.startswith('~'):
379 home_dir = windows_getenv(u'USERPROFILE')
381 home_drive = windows_getenv(u'HOMEDRIVE')
382 home_path = windows_getenv(u'HOMEPATH')
383 if home_drive is None or home_path is None:
384 raise OSError("Could not find home directory: neither %USERPROFILE% nor (%HOMEDRIVE% and %HOMEPATH%) are set.")
385 home_dir = os.path.join(home_drive, home_path)
389 elif path.startswith('~/') or path.startswith('~\\'):
390 return os.path.join(home_dir, path[2 :])
394 # <https://msdn.microsoft.com/en-us/library/windows/desktop/ms681382%28v=vs.85%29.aspx>
395 ERROR_ENVVAR_NOT_FOUND = 203
397 def windows_getenv(name):
398 # Based on <http://stackoverflow.com/questions/2608200/problems-with-umlauts-in-python-appdata-environvent-variable/2608368#2608368>,
399 # with improved error handling. Returns None if there is no enivronment variable of the given name.
400 if not isinstance(name, unicode):
401 raise AssertionError("name must be Unicode")
403 n = GetEnvironmentVariableW(name, None, 0)
404 # GetEnvironmentVariableW returns DWORD, so n cannot be negative.
406 err = get_last_error()
407 if err == ERROR_ENVVAR_NOT_FOUND:
409 raise OSError("Windows error %d attempting to read size of environment variable %r"
412 # Avoid an ambiguity between a zero-length string and an error in the return value of the
413 # call to GetEnvironmentVariableW below.
416 buf = create_unicode_buffer(u'\0'*n)
417 retval = GetEnvironmentVariableW(name, buf, n)
419 err = get_last_error()
420 if err == ERROR_ENVVAR_NOT_FOUND:
422 raise OSError("Windows error %d attempting to read environment variable %r"
425 raise OSError("Unexpected result %d (expected less than %d) from GetEnvironmentVariableW attempting to read environment variable %r"
430 def get_disk_stats(whichdir, reserved_space=0):
431 """Return disk statistics for the storage disk, in the form of a dict
432 with the following fields.
433 total: total bytes on disk
434 free_for_root: bytes actually free on disk
435 free_for_nonroot: bytes free for "a non-privileged user" [Unix] or
436 the current user [Windows]; might take into
437 account quotas depending on platform
438 used: bytes used on disk
439 avail: bytes available excluding reserved space
440 An AttributeError can occur if the OS has no API to get disk information.
441 An EnvironmentError can occur if the OS call fails.
443 whichdir is a directory on the filesystem in question -- the
444 answer is about the filesystem, not about the directory, so the
445 directory is used only to specify which filesystem.
447 reserved_space is how many bytes to subtract from the answer, so
448 you can pass how many bytes you would like to leave unused on this
449 filesystem as reserved_space.
452 if have_GetDiskFreeSpaceExW:
453 # If this is a Windows system and GetDiskFreeSpaceExW is available, use it.
454 # (This might put up an error dialog unless
455 # SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOOPENFILEERRORBOX) has been called,
456 # which we do in allmydata.windows.fixups.initialize().)
458 n_free_for_nonroot = c_ulonglong(0)
459 n_total = c_ulonglong(0)
460 n_free_for_root = c_ulonglong(0)
461 retval = GetDiskFreeSpaceExW(whichdir, byref(n_free_for_nonroot),
463 byref(n_free_for_root))
465 raise OSError("Windows error %d attempting to get disk statistics for %r"
466 % (get_last_error(), whichdir))
467 free_for_nonroot = n_free_for_nonroot.value
468 total = n_total.value
469 free_for_root = n_free_for_root.value
471 # For Unix-like systems.
472 # <http://docs.python.org/library/os.html#os.statvfs>
473 # <http://opengroup.org/onlinepubs/7990989799/xsh/fstatvfs.html>
474 # <http://opengroup.org/onlinepubs/7990989799/xsh/sysstatvfs.h.html>
475 s = os.statvfs(whichdir)
478 # statvfs(2) is a wrapper around statfs(2).
479 # statvfs.f_frsize = statfs.f_bsize :
480 # "minimum unit of allocation" (statvfs)
481 # "fundamental file system block size" (statfs)
482 # statvfs.f_bsize = statfs.f_iosize = stat.st_blocks : preferred IO size
483 # on an encrypted home directory ("FileVault"), it gets f_blocks
484 # wrong, and s.f_blocks*s.f_frsize is twice the size of my disk,
485 # but s.f_bavail*s.f_frsize is correct
487 total = s.f_frsize * s.f_blocks
488 free_for_root = s.f_frsize * s.f_bfree
489 free_for_nonroot = s.f_frsize * s.f_bavail
491 # valid for all platforms:
492 used = total - free_for_root
493 avail = max(free_for_nonroot - reserved_space, 0)
495 return { 'total': total,
496 'free_for_root': free_for_root,
497 'free_for_nonroot': free_for_nonroot,
502 def get_available_space(whichdir, reserved_space):
503 """Returns available space for share storage in bytes, or None if no
504 API to get this information is available.
506 whichdir is a directory on the filesystem in question -- the
507 answer is about the filesystem, not about the directory, so the
508 directory is used only to specify which filesystem.
510 reserved_space is how many bytes to subtract from the answer, so
511 you can pass how many bytes you would like to leave unused on this
512 filesystem as reserved_space.
515 return get_disk_stats(whichdir, reserved_space)['avail']
516 except AttributeError:
518 except EnvironmentError:
519 log.msg("OS call to get disk statistics failed")
523 if sys.platform == "win32":
524 from ctypes.wintypes import BOOL, HANDLE, DWORD, LPCWSTR, LPVOID, WinError, get_last_error
526 # <http://msdn.microsoft.com/en-us/library/aa363858%28v=vs.85%29.aspx>
527 CreateFileW = WINFUNCTYPE(HANDLE, LPCWSTR, DWORD, DWORD, LPVOID, DWORD, DWORD, HANDLE) \
528 (("CreateFileW", windll.kernel32))
530 GENERIC_WRITE = 0x40000000
531 FILE_SHARE_READ = 0x00000001
532 FILE_SHARE_WRITE = 0x00000002
534 INVALID_HANDLE_VALUE = 0xFFFFFFFF
536 # <http://msdn.microsoft.com/en-us/library/aa364439%28v=vs.85%29.aspx>
537 FlushFileBuffers = WINFUNCTYPE(BOOL, HANDLE)(("FlushFileBuffers", windll.kernel32))
539 # <http://msdn.microsoft.com/en-us/library/ms724211%28v=vs.85%29.aspx>
540 CloseHandle = WINFUNCTYPE(BOOL, HANDLE)(("CloseHandle", windll.kernel32))
542 # <http://social.msdn.microsoft.com/forums/en-US/netfxbcl/thread/4465cafb-f4ed-434f-89d8-c85ced6ffaa8/>
543 def flush_volume(path):
544 drive = os.path.splitdrive(os.path.realpath(path))[0]
546 hVolume = CreateFileW(u"\\\\.\\" + drive,
548 FILE_SHARE_READ | FILE_SHARE_WRITE,
554 if hVolume == INVALID_HANDLE_VALUE:
557 if FlushFileBuffers(hVolume) == 0:
562 def flush_volume(path):
567 class ConflictError(Exception):
570 class UnableToUnlinkReplacementError(Exception):
573 def reraise(wrapper):
574 _, exc, tb = sys.exc_info()
575 wrapper_exc = wrapper("%s: %s" % (exc.__class__.__name__, exc))
576 raise wrapper_exc.__class__, wrapper_exc, tb
578 if sys.platform == "win32":
579 from ctypes import WINFUNCTYPE, windll, WinError, get_last_error
580 from ctypes.wintypes import BOOL, DWORD, LPCWSTR, LPVOID
582 # <https://msdn.microsoft.com/en-us/library/windows/desktop/aa365512%28v=vs.85%29.aspx>
583 ReplaceFileW = WINFUNCTYPE(
585 LPCWSTR, LPCWSTR, LPCWSTR, DWORD, LPVOID, LPVOID,
587 )(("ReplaceFileW", windll.kernel32))
589 REPLACEFILE_IGNORE_MERGE_ERRORS = 0x00000002
591 def rename_no_overwrite(source_path, dest_path):
592 os.rename(source_path, dest_path)
594 def replace_file(replaced_path, replacement_path, backup_path):
595 precondition_abspath(replaced_path)
596 precondition_abspath(replacement_path)
597 precondition_abspath(backup_path)
599 r = ReplaceFileW(replaced_path, replacement_path, backup_path,
600 REPLACEFILE_IGNORE_MERGE_ERRORS, None, None)
602 # The UnableToUnlinkReplacementError case does not happen on Windows;
603 # all errors should be treated as signalling a conflict.
604 err = get_last_error()
605 raise ConflictError("WinError: %s" % (WinError(err)))
607 def rename_no_overwrite(source_path, dest_path):
608 # link will fail with EEXIST if there is already something at dest_path.
609 os.link(source_path, dest_path)
611 os.unlink(source_path)
612 except EnvironmentError:
613 reraise(UnableToUnlinkReplacementError)
615 def replace_file(replaced_path, replacement_path, backup_path):
616 precondition_abspath(replaced_path)
617 precondition_abspath(replacement_path)
618 precondition_abspath(backup_path)
620 if not os.path.exists(replacement_path):
621 raise ConflictError("Replacement file not found: %r" % (replacement_path,))
624 os.rename(replaced_path, backup_path)
626 if e.errno != ENOENT:
629 rename_no_overwrite(replacement_path, replaced_path)
630 except EnvironmentError:
631 reraise(ConflictError)
633 PathInfo = namedtuple('PathInfo', 'isdir isfile islink exists size mtime ctime')
635 def get_pathinfo(path_u, now=None):
637 statinfo = os.lstat(path_u)
638 mode = statinfo.st_mode
639 return PathInfo(isdir =stat.S_ISDIR(mode),
640 isfile=stat.S_ISREG(mode),
641 islink=stat.S_ISLNK(mode),
643 size =statinfo.st_size,
644 mtime =statinfo.st_mtime,
645 ctime =statinfo.st_ctime,
648 if e.errno == ENOENT:
651 return PathInfo(isdir =False,