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