Futz with files like a pro.
"""
-import sys, exceptions, os, stat, tempfile, time
+import sys, exceptions, os, stat, tempfile, time, binascii
from twisted.python import log
+from pycryptopp.cipher.aes import AES
+
+
def rename(src, dst, tries=4, basedelay=0.1):
""" Here is a superkludge to workaround the fact that occasionally on
Windows some other process (e.g. an anti-virus scanner, a local search
if self.cleanup and hasattr(self, 'name'):
rm_dir(self.name)
+class EncryptedTemporaryFile:
+ # not implemented: next, readline, readlines, xreadlines, writelines
+
+ def __init__(self):
+ self.file = tempfile.TemporaryFile()
+ self.key = os.urandom(16) # AES-128
+
+ def _crypt(self, offset, data):
+ offset_big = offset // 16
+ offset_small = offset % 16
+ iv = binascii.unhexlify("%032x" % offset_big)
+ cipher = AES(self.key, iv=iv)
+ cipher.process("\x00"*offset_small)
+ return cipher.process(data)
+
+ def close(self):
+ self.file.close()
+
+ def flush(self):
+ self.file.flush()
+
+ def seek(self, offset, whence=0): # 0 = SEEK_SET
+ self.file.seek(offset, whence)
+
+ def tell(self):
+ offset = self.file.tell()
+ return offset
+
+ def read(self, size=-1):
+ """A read must not follow a write, or vice-versa, without an intervening seek."""
+ index = self.file.tell()
+ ciphertext = self.file.read(size)
+ plaintext = self._crypt(index, ciphertext)
+ return plaintext
+
+ def write(self, plaintext):
+ """A read must not follow a write, or vice-versa, without an intervening seek.
+ If seeking and then writing causes a 'hole' in the file, the contents of the
+ hole are unspecified."""
+ index = self.file.tell()
+ ciphertext = self._crypt(index, plaintext)
+ self.file.write(ciphertext)
+
+ def truncate(self, newsize):
+ """Truncate or extend the file to 'newsize'. If it is extended, the contents after the
+ old end-of-file are unspecified. The file position after this operation is unspecified."""
+ self.file.truncate(newsize)
+
+
def make_dirs(dirname, mode=0777):
"""
An idempotent version of os.makedirs(). If the dir already exists, do
remove_if_possible(dest)
os.rename(source, dest)
-def write(path, data):
- wf = open(path, "wb")
+def write_atomically(target, contents, mode="b"):
+ f = open(target+".tmp", "w"+mode)
+ try:
+ f.write(contents)
+ finally:
+ f.close()
+ move_into_place(target+".tmp", target)
+
+def write(path, data, mode="wb"):
+ wf = open(path, mode)
try:
wf.write(data)
finally:
outf.write(data)
finally:
outf.close()
+
+
+# Work around <http://bugs.python.org/issue3426>. This code is adapted from
+# <http://svn.python.org/view/python/trunk/Lib/ntpath.py?revision=78247&view=markup>
+# with some simplifications.
+
+_getfullpathname = None
+try:
+ from nt import _getfullpathname
+except ImportError:
+ pass
+
+def abspath_expanduser_unicode(path):
+ """Return the absolute version of a path."""
+ assert isinstance(path, unicode), path
+
+ path = os.path.expanduser(path)
+
+ if _getfullpathname:
+ # On Windows, os.path.isabs will return True for paths without a drive letter,
+ # e.g. "\\". See <http://bugs.python.org/issue1669539>.
+ try:
+ path = _getfullpathname(path or u".")
+ except OSError:
+ pass
+
+ if not os.path.isabs(path):
+ path = os.path.join(os.getcwdu(), path)
+
+ # We won't hit <http://bugs.python.org/issue5827> because
+ # there is always at least one Unicode path component.
+ return os.path.normpath(path)
+
+
+have_GetDiskFreeSpaceExW = False
+if sys.platform == "win32":
+ try:
+ from ctypes import WINFUNCTYPE, windll, POINTER, byref, c_ulonglong
+ from ctypes.wintypes import BOOL, DWORD, LPCWSTR
+
+ # <http://msdn.microsoft.com/en-us/library/aa383742%28v=VS.85%29.aspx>
+ PULARGE_INTEGER = POINTER(c_ulonglong)
+
+ # <http://msdn.microsoft.com/en-us/library/aa364937%28VS.85%29.aspx>
+ GetDiskFreeSpaceExW = WINFUNCTYPE(BOOL, LPCWSTR, PULARGE_INTEGER, PULARGE_INTEGER, PULARGE_INTEGER)(
+ ("GetDiskFreeSpaceExW", windll.kernel32))
+
+ # <http://msdn.microsoft.com/en-us/library/ms679360%28v=VS.85%29.aspx>
+ GetLastError = WINFUNCTYPE(DWORD)(("GetLastError", windll.kernel32))
+
+ have_GetDiskFreeSpaceExW = True
+ except Exception:
+ import traceback
+ traceback.print_exc()
+
+def get_disk_stats(whichdir, reserved_space=0):
+ """Return disk statistics for the storage disk, in the form of a dict
+ with the following fields.
+ total: total bytes on disk
+ free_for_root: bytes actually free on disk
+ free_for_nonroot: bytes free for "a non-privileged user" [Unix] or
+ the current user [Windows]; might take into
+ account quotas depending on platform
+ used: bytes used on disk
+ avail: bytes available excluding reserved space
+ An AttributeError can occur if the OS has no API to get disk information.
+ An EnvironmentError can occur if the OS call fails.
+
+ whichdir is a directory on the filesystem in question -- the
+ answer is about the filesystem, not about the directory, so the
+ directory is used only to specify which filesystem.
+
+ reserved_space is how many bytes to subtract from the answer, so
+ you can pass how many bytes you would like to leave unused on this
+ filesystem as reserved_space.
+ """
+
+ if have_GetDiskFreeSpaceExW:
+ # If this is a Windows system and GetDiskFreeSpaceExW is available, use it.
+ # (This might put up an error dialog unless
+ # SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOOPENFILEERRORBOX) has been called,
+ # which we do in allmydata.windows.fixups.initialize().)
+
+ n_free_for_nonroot = c_ulonglong(0)
+ n_total = c_ulonglong(0)
+ n_free_for_root = c_ulonglong(0)
+ retval = GetDiskFreeSpaceExW(whichdir, byref(n_free_for_nonroot),
+ byref(n_total),
+ byref(n_free_for_root))
+ if retval == 0:
+ raise OSError("Windows error %d attempting to get disk statistics for %r"
+ % (GetLastError(), whichdir))
+ free_for_nonroot = n_free_for_nonroot.value
+ total = n_total.value
+ free_for_root = n_free_for_root.value
+ else:
+ # For Unix-like systems.
+ # <http://docs.python.org/library/os.html#os.statvfs>
+ # <http://opengroup.org/onlinepubs/7990989799/xsh/fstatvfs.html>
+ # <http://opengroup.org/onlinepubs/7990989799/xsh/sysstatvfs.h.html>
+ s = os.statvfs(whichdir)
+
+ # on my mac laptop:
+ # statvfs(2) is a wrapper around statfs(2).
+ # statvfs.f_frsize = statfs.f_bsize :
+ # "minimum unit of allocation" (statvfs)
+ # "fundamental file system block size" (statfs)
+ # statvfs.f_bsize = statfs.f_iosize = stat.st_blocks : preferred IO size
+ # on an encrypted home directory ("FileVault"), it gets f_blocks
+ # wrong, and s.f_blocks*s.f_frsize is twice the size of my disk,
+ # but s.f_bavail*s.f_frsize is correct
+
+ total = s.f_frsize * s.f_blocks
+ free_for_root = s.f_frsize * s.f_bfree
+ free_for_nonroot = s.f_frsize * s.f_bavail
+
+ # valid for all platforms:
+ used = total - free_for_root
+ avail = max(free_for_nonroot - reserved_space, 0)
+
+ return { 'total': total,
+ 'free_for_root': free_for_root,
+ 'free_for_nonroot': free_for_nonroot,
+ 'used': used,
+ 'avail': avail,
+ }
+
+def get_available_space(whichdir, reserved_space):
+ """Returns available space for share storage in bytes, or None if no
+ API to get this information is available.
+
+ whichdir is a directory on the filesystem in question -- the
+ answer is about the filesystem, not about the directory, so the
+ directory is used only to specify which filesystem.
+
+ reserved_space is how many bytes to subtract from the answer, so
+ you can pass how many bytes you would like to leave unused on this
+ filesystem as reserved_space.
+ """
+ try:
+ return get_disk_stats(whichdir, reserved_space)['avail']
+ except AttributeError:
+ return None
+ except EnvironmentError:
+ log.msg("OS call to get disk statistics failed")
+ return 0