]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blob - src/allmydata/util/fileutil.py
5fc35c0561d9d02889d1f15c60dfb32b7c7ca0c0
[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 if sys.platform == "win32":
8     from ctypes import WINFUNCTYPE, WinError, windll, POINTER, byref, c_ulonglong, \
9         create_unicode_buffer, get_last_error
10     from ctypes.wintypes import BOOL, DWORD, LPCWSTR, LPWSTR
11
12 from twisted.python import log
13
14 from pycryptopp.cipher.aes import AES
15
16
17 def rename(src, dst, tries=4, basedelay=0.1):
18     """ Here is a superkludge to workaround the fact that occasionally on
19     Windows some other process (e.g. an anti-virus scanner, a local search
20     engine, etc.) is looking at your file when you want to delete or move it,
21     and hence you can't.  The horrible workaround is to sit and spin, trying
22     to delete it, for a short time and then give up.
23
24     With the default values of tries and basedelay this can block for less
25     than a second.
26
27     @param tries: number of tries -- each time after the first we wait twice
28     as long as the previous wait
29     @param basedelay: how long to wait before the second try
30     """
31     for i in range(tries-1):
32         try:
33             return os.rename(src, dst)
34         except EnvironmentError, le:
35             # 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.
36             log.msg("XXX KLUDGE Attempting to move file %s => %s; got %s; sleeping %s seconds" % (src, dst, le, basedelay,))
37             time.sleep(basedelay)
38             basedelay *= 2
39     return os.rename(src, dst) # The last try.
40
41 def remove(f, tries=4, basedelay=0.1):
42     """ Here is a superkludge to workaround the fact that occasionally on
43     Windows some other process (e.g. an anti-virus scanner, a local search
44     engine, etc.) is looking at your file when you want to delete or move it,
45     and hence you can't.  The horrible workaround is to sit and spin, trying
46     to delete it, for a short time and then give up.
47
48     With the default values of tries and basedelay this can block for less
49     than a second.
50
51     @param tries: number of tries -- each time after the first we wait twice
52     as long as the previous wait
53     @param basedelay: how long to wait before the second try
54     """
55     try:
56         os.chmod(f, stat.S_IWRITE | stat.S_IEXEC | stat.S_IREAD)
57     except:
58         pass
59     for i in range(tries-1):
60         try:
61             return os.remove(f)
62         except EnvironmentError, le:
63             # 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.
64             if not os.path.exists(f):
65                 return
66             log.msg("XXX KLUDGE Attempting to remove file %s; got %s; sleeping %s seconds" % (f, le, basedelay,))
67             time.sleep(basedelay)
68             basedelay *= 2
69     return os.remove(f) # The last try.
70
71 class ReopenableNamedTemporaryFile:
72     """
73     This uses tempfile.mkstemp() to generate a secure temp file.  It then closes
74     the file, leaving a zero-length file as a placeholder.  You can get the
75     filename with ReopenableNamedTemporaryFile.name.  When the
76     ReopenableNamedTemporaryFile instance is garbage collected or its shutdown()
77     method is called, it deletes the file.
78     """
79     def __init__(self, *args, **kwargs):
80         fd, self.name = tempfile.mkstemp(*args, **kwargs)
81         os.close(fd)
82
83     def __repr__(self):
84         return "<%s instance at %x %s>" % (self.__class__.__name__, id(self), self.name)
85
86     def __str__(self):
87         return self.__repr__()
88
89     def __del__(self):
90         self.shutdown()
91
92     def shutdown(self):
93         remove(self.name)
94
95 class EncryptedTemporaryFile:
96     # not implemented: next, readline, readlines, xreadlines, writelines
97
98     def __init__(self):
99         self.file = tempfile.TemporaryFile()
100         self.key = os.urandom(16)  # AES-128
101
102     def _crypt(self, offset, data):
103         offset_big = offset // 16
104         offset_small = offset % 16
105         iv = binascii.unhexlify("%032x" % offset_big)
106         cipher = AES(self.key, iv=iv)
107         cipher.process("\x00"*offset_small)
108         return cipher.process(data)
109
110     def close(self):
111         self.file.close()
112
113     def flush(self):
114         self.file.flush()
115
116     def seek(self, offset, whence=0):  # 0 = SEEK_SET
117         self.file.seek(offset, whence)
118
119     def tell(self):
120         offset = self.file.tell()
121         return offset
122
123     def read(self, size=-1):
124         """A read must not follow a write, or vice-versa, without an intervening seek."""
125         index = self.file.tell()
126         ciphertext = self.file.read(size)
127         plaintext = self._crypt(index, ciphertext)
128         return plaintext
129
130     def write(self, plaintext):
131         """A read must not follow a write, or vice-versa, without an intervening seek.
132         If seeking and then writing causes a 'hole' in the file, the contents of the
133         hole are unspecified."""
134         index = self.file.tell()
135         ciphertext = self._crypt(index, plaintext)
136         self.file.write(ciphertext)
137
138     def truncate(self, newsize):
139         """Truncate or extend the file to 'newsize'. If it is extended, the contents after the
140         old end-of-file are unspecified. The file position after this operation is unspecified."""
141         self.file.truncate(newsize)
142
143
144 def make_dirs(dirname, mode=0777):
145     """
146     An idempotent version of os.makedirs().  If the dir already exists, do
147     nothing and return without raising an exception.  If this call creates the
148     dir, return without raising an exception.  If there is an error that
149     prevents creation or if the directory gets deleted after make_dirs() creates
150     it and before make_dirs() checks that it exists, raise an exception.
151     """
152     tx = None
153     try:
154         os.makedirs(dirname, mode)
155     except OSError, x:
156         tx = x
157
158     if not os.path.isdir(dirname):
159         if tx:
160             raise tx
161         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
163 def rm_dir(dirname):
164     """
165     A threadsafe and idempotent version of shutil.rmtree().  If the dir is
166     already gone, do nothing and return without raising an exception.  If this
167     call removes the dir, return without raising an exception.  If there is an
168     error that prevents deletion or if the directory gets created again after
169     rm_dir() deletes it and before rm_dir() checks that it is gone, raise an
170     exception.
171     """
172     excs = []
173     try:
174         os.chmod(dirname, stat.S_IWRITE | stat.S_IEXEC | stat.S_IREAD)
175         for f in os.listdir(dirname):
176             fullname = os.path.join(dirname, f)
177             if os.path.isdir(fullname):
178                 rm_dir(fullname)
179             else:
180                 remove(fullname)
181         os.rmdir(dirname)
182     except Exception, le:
183         # Ignore "No such file or directory"
184         if (not isinstance(le, OSError)) or le.args[0] != 2:
185             excs.append(le)
186
187     # Okay, now we've recursively removed everything, ignoring any "No
188     # such file or directory" errors, and collecting any other errors.
189
190     if os.path.exists(dirname):
191         if len(excs) == 1:
192             raise excs[0]
193         if len(excs) == 0:
194             raise OSError, "Failed to remove dir for unknown reason."
195         raise OSError, excs
196
197
198 def remove_if_possible(f):
199     try:
200         remove(f)
201     except:
202         pass
203
204 def du(basedir):
205     size = 0
206
207     for root, dirs, files in os.walk(basedir):
208         for f in files:
209             fn = os.path.join(root, f)
210             size += os.path.getsize(fn)
211
212     return size
213
214 def move_into_place(source, dest):
215     """Atomically replace a file, or as near to it as the platform allows.
216     The dest file may or may not exist."""
217     if "win32" in sys.platform.lower():
218         remove_if_possible(dest)
219     os.rename(source, dest)
220
221 def write_atomically(target, contents, mode="b"):
222     f = open(target+".tmp", "w"+mode)
223     try:
224         f.write(contents)
225     finally:
226         f.close()
227     move_into_place(target+".tmp", target)
228
229 def write(path, data, mode="wb"):
230     wf = open(path, mode)
231     try:
232         wf.write(data)
233     finally:
234         wf.close()
235
236 def read(path):
237     rf = open(path, "rb")
238     try:
239         return rf.read()
240     finally:
241         rf.close()
242
243 def put_file(path, inf):
244     precondition_abspath(path)
245
246     # TODO: create temporary file and move into place?
247     outf = open(path, "wb")
248     try:
249         while True:
250             data = inf.read(32768)
251             if not data:
252                 break
253             outf.write(data)
254     finally:
255         outf.close()
256
257
258 def precondition_abspath(path):
259     if not isinstance(path, unicode):
260         raise AssertionError("an abspath must be a Unicode string")
261
262     if sys.platform == "win32":
263         # This intentionally doesn't view absolute paths starting with a drive specification, or
264         # paths relative to the current drive, as acceptable.
265         if not path.startswith("\\\\"):
266             raise AssertionError("an abspath should be normalized using abspath_expanduser_unicode")
267     else:
268         # This intentionally doesn't view the path '~' or paths starting with '~/' as acceptable.
269         if not os.path.isabs(path):
270             raise AssertionError("an abspath should be normalized using abspath_expanduser_unicode")
271
272 # Work around <http://bugs.python.org/issue3426>. This code is adapted from
273 # <http://svn.python.org/view/python/trunk/Lib/ntpath.py?revision=78247&view=markup>
274 # with some simplifications.
275
276 _getfullpathname = None
277 try:
278     from nt import _getfullpathname
279 except ImportError:
280     pass
281
282 def abspath_expanduser_unicode(path, base=None, long_path=True):
283     """
284     Return the absolute version of a path. If 'base' is given and 'path' is relative,
285     the path will be expanded relative to 'base'.
286     'path' must be a Unicode string. 'base', if given, must be a Unicode string
287     corresponding to an absolute path as returned by a previous call to
288     abspath_expanduser_unicode.
289     On Windows, the result will be a long path unless long_path is given as False.
290     """
291     if not isinstance(path, unicode):
292         raise AssertionError("paths must be Unicode strings")
293     if base is not None and long_path:
294         precondition_abspath(base)
295
296     path = expanduser(path)
297
298     if _getfullpathname:
299         # On Windows, os.path.isabs will incorrectly return True
300         # for paths without a drive letter (that are not UNC paths),
301         # e.g. "\\". See <http://bugs.python.org/issue1669539>.
302         try:
303             if base is None:
304                 path = _getfullpathname(path or u".")
305             else:
306                 path = _getfullpathname(os.path.join(base, path))
307         except OSError:
308             pass
309
310     if not os.path.isabs(path):
311         if base is None:
312             path = os.path.join(os.getcwdu(), path)
313         else:
314             path = os.path.join(base, path)
315
316     # We won't hit <http://bugs.python.org/issue5827> because
317     # there is always at least one Unicode path component.
318     path = os.path.normpath(path)
319
320     if sys.platform == "win32" and long_path:
321         path = to_windows_long_path(path)
322
323     return path
324
325 def to_windows_long_path(path):
326     # '/' is normally a perfectly valid path component separator in Windows.
327     # However, when using the "\\?\" syntax it is not recognized, so we
328     # replace it with '\' here.
329     path = path.replace(u"/", u"\\")
330
331     # Note that other normalizations such as removing '.' and '..' should
332     # be done outside this function.
333
334     if path.startswith(u"\\\\?\\") or path.startswith(u"\\\\.\\"):
335         return path
336     elif path.startswith(u"\\\\"):
337         return u"\\\\?\\UNC\\" + path[2 :]
338     else:
339         return u"\\\\?\\" + path
340
341
342 have_GetDiskFreeSpaceExW = False
343 if sys.platform == "win32":
344     # <http://msdn.microsoft.com/en-us/library/windows/desktop/ms683188%28v=vs.85%29.aspx>
345     GetEnvironmentVariableW = WINFUNCTYPE(
346         DWORD,  LPCWSTR, LPWSTR, DWORD,
347         use_last_error=True
348     )(("GetEnvironmentVariableW", windll.kernel32))
349
350     try:
351         # <http://msdn.microsoft.com/en-us/library/aa383742%28v=VS.85%29.aspx>
352         PULARGE_INTEGER = POINTER(c_ulonglong)
353
354         # <http://msdn.microsoft.com/en-us/library/aa364937%28VS.85%29.aspx>
355         GetDiskFreeSpaceExW = WINFUNCTYPE(
356             BOOL,  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("WinError: %s\n attempting to read size of environment variable %r"
407                       % (WinError(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("WinError: %s\n attempting to read environment variable %r"
420                       % (WinError(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("WinError: %s\n attempting to get disk statistics for %r"
463                           % (WinError(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