]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blob - src/allmydata/util/fileutil.py
Allow abspath_expanduser_unicode to optionally produce paths without the long-path...
[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, long_path=True):
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     On Windows, the result will be a long path unless long_path is given as False.
285     """
286     if not isinstance(path, unicode):
287         raise AssertionError("paths must be Unicode strings")
288     if base is not None and long_path:
289         precondition_abspath(base)
290
291     path = expanduser(path)
292
293     if _getfullpathname:
294         # On Windows, os.path.isabs will incorrectly return True
295         # for paths without a drive letter (that are not UNC paths),
296         # e.g. "\\". See <http://bugs.python.org/issue1669539>.
297         try:
298             if base is None:
299                 path = _getfullpathname(path or u".")
300             else:
301                 path = _getfullpathname(os.path.join(base, path))
302         except OSError:
303             pass
304
305     if not os.path.isabs(path):
306         if base is None:
307             path = os.path.join(os.getcwdu(), path)
308         else:
309             path = os.path.join(base, path)
310
311     # We won't hit <http://bugs.python.org/issue5827> because
312     # there is always at least one Unicode path component.
313     path = os.path.normpath(path)
314
315     if sys.platform == "win32" and long_path:
316         path = to_windows_long_path(path)
317
318     return path
319
320 def to_windows_long_path(path):
321     # '/' is normally a perfectly valid path component separator in Windows.
322     # However, when using the "\\?\" syntax it is not recognized, so we
323     # replace it with '\' here.
324     path = path.replace(u"/", u"\\")
325
326     # Note that other normalizations such as removing '.' and '..' should
327     # be done outside this function.
328
329     if path.startswith(u"\\\\?\\") or path.startswith(u"\\\\.\\"):
330         return path
331     elif path.startswith(u"\\\\"):
332         return u"\\\\?\\UNC\\" + path[2 :]
333     else:
334         return u"\\\\?\\" + path
335
336
337 have_GetDiskFreeSpaceExW = False
338 if sys.platform == "win32":
339     from ctypes import WINFUNCTYPE, windll, POINTER, byref, c_ulonglong, create_unicode_buffer, \
340         get_last_error
341     from ctypes.wintypes import BOOL, DWORD, LPCWSTR, LPWSTR
342
343     # <http://msdn.microsoft.com/en-us/library/windows/desktop/ms683188%28v=vs.85%29.aspx>
344     GetEnvironmentVariableW = WINFUNCTYPE(
345         DWORD,  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,  LPCWSTR, PULARGE_INTEGER, PULARGE_INTEGER, PULARGE_INTEGER,
356             use_last_error=True
357         )(("GetDiskFreeSpaceExW", windll.kernel32))
358
359         have_GetDiskFreeSpaceExW = True
360     except Exception:
361         import traceback
362         traceback.print_exc()
363
364 def expanduser(path):
365     # os.path.expanduser is hopelessly broken for Unicode paths on Windows (ticket #1674).
366     if sys.platform == "win32":
367         return windows_expanduser(path)
368     else:
369         return os.path.expanduser(path)
370
371 def windows_expanduser(path):
372     if not path.startswith('~'):
373         return path
374
375     home_dir = windows_getenv(u'USERPROFILE')
376     if home_dir is None:
377         home_drive = windows_getenv(u'HOMEDRIVE')
378         home_path = windows_getenv(u'HOMEPATH')
379         if home_drive is None or home_path is None:
380             raise OSError("Could not find home directory: neither %USERPROFILE% nor (%HOMEDRIVE% and %HOMEPATH%) are set.")
381         home_dir = os.path.join(home_drive, home_path)
382
383     if path == '~':
384         return home_dir
385     elif path.startswith('~/') or path.startswith('~\\'):
386         return os.path.join(home_dir, path[2 :])
387     else:
388         return path
389
390 # <https://msdn.microsoft.com/en-us/library/windows/desktop/ms681382%28v=vs.85%29.aspx>
391 ERROR_ENVVAR_NOT_FOUND = 203
392
393 def windows_getenv(name):
394     # Based on <http://stackoverflow.com/questions/2608200/problems-with-umlauts-in-python-appdata-environvent-variable/2608368#2608368>,
395     # with improved error handling. Returns None if there is no enivronment variable of the given name.
396     if not isinstance(name, unicode):
397         raise AssertionError("name must be Unicode")
398
399     n = GetEnvironmentVariableW(name, None, 0)
400     # GetEnvironmentVariableW returns DWORD, so n cannot be negative.
401     if n == 0:
402         err = get_last_error()
403         if err == ERROR_ENVVAR_NOT_FOUND:
404             return None
405         raise OSError("WinError: %s\n attempting to read size of environment variable %r"
406                       % (WinError(err), name))
407     if n == 1:
408         # Avoid an ambiguity between a zero-length string and an error in the return value of the
409         # call to GetEnvironmentVariableW below.
410         return u""
411
412     buf = create_unicode_buffer(u'\0'*n)
413     retval = GetEnvironmentVariableW(name, buf, n)
414     if retval == 0:
415         err = get_last_error()
416         if err == ERROR_ENVVAR_NOT_FOUND:
417             return None
418         raise OSError("WinError: %s\n attempting to read environment variable %r"
419                       % (WinError(err), name))
420     if retval >= n:
421         raise OSError("Unexpected result %d (expected less than %d) from GetEnvironmentVariableW attempting to read environment variable %r"
422                       % (retval, n, name))
423
424     return buf.value
425
426 def get_disk_stats(whichdir, reserved_space=0):
427     """Return disk statistics for the storage disk, in the form of a dict
428     with the following fields.
429       total:            total bytes on disk
430       free_for_root:    bytes actually free on disk
431       free_for_nonroot: bytes free for "a non-privileged user" [Unix] or
432                           the current user [Windows]; might take into
433                           account quotas depending on platform
434       used:             bytes used on disk
435       avail:            bytes available excluding reserved space
436     An AttributeError can occur if the OS has no API to get disk information.
437     An EnvironmentError can occur if the OS call fails.
438
439     whichdir is a directory on the filesystem in question -- the
440     answer is about the filesystem, not about the directory, so the
441     directory is used only to specify which filesystem.
442
443     reserved_space is how many bytes to subtract from the answer, so
444     you can pass how many bytes you would like to leave unused on this
445     filesystem as reserved_space.
446     """
447
448     if have_GetDiskFreeSpaceExW:
449         # If this is a Windows system and GetDiskFreeSpaceExW is available, use it.
450         # (This might put up an error dialog unless
451         # SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOOPENFILEERRORBOX) has been called,
452         # which we do in allmydata.windows.fixups.initialize().)
453
454         n_free_for_nonroot = c_ulonglong(0)
455         n_total            = c_ulonglong(0)
456         n_free_for_root    = c_ulonglong(0)
457         retval = GetDiskFreeSpaceExW(whichdir, byref(n_free_for_nonroot),
458                                                byref(n_total),
459                                                byref(n_free_for_root))
460         if retval == 0:
461             raise OSError("WinError: %s\n attempting to get disk statistics for %r"
462                           % (WinError(get_last_error()), whichdir))
463         free_for_nonroot = n_free_for_nonroot.value
464         total            = n_total.value
465         free_for_root    = n_free_for_root.value
466     else:
467         # For Unix-like systems.
468         # <http://docs.python.org/library/os.html#os.statvfs>
469         # <http://opengroup.org/onlinepubs/7990989799/xsh/fstatvfs.html>
470         # <http://opengroup.org/onlinepubs/7990989799/xsh/sysstatvfs.h.html>
471         s = os.statvfs(whichdir)
472
473         # on my mac laptop:
474         #  statvfs(2) is a wrapper around statfs(2).
475         #    statvfs.f_frsize = statfs.f_bsize :
476         #     "minimum unit of allocation" (statvfs)
477         #     "fundamental file system block size" (statfs)
478         #    statvfs.f_bsize = statfs.f_iosize = stat.st_blocks : preferred IO size
479         # on an encrypted home directory ("FileVault"), it gets f_blocks
480         # wrong, and s.f_blocks*s.f_frsize is twice the size of my disk,
481         # but s.f_bavail*s.f_frsize is correct
482
483         total = s.f_frsize * s.f_blocks
484         free_for_root = s.f_frsize * s.f_bfree
485         free_for_nonroot = s.f_frsize * s.f_bavail
486
487     # valid for all platforms:
488     used = total - free_for_root
489     avail = max(free_for_nonroot - reserved_space, 0)
490
491     return { 'total': total,
492              'free_for_root': free_for_root,
493              'free_for_nonroot': free_for_nonroot,
494              'used': used,
495              'avail': avail,
496            }
497
498 def get_available_space(whichdir, reserved_space):
499     """Returns available space for share storage in bytes, or None if no
500     API to get this information is available.
501
502     whichdir is a directory on the filesystem in question -- the
503     answer is about the filesystem, not about the directory, so the
504     directory is used only to specify which filesystem.
505
506     reserved_space is how many bytes to subtract from the answer, so
507     you can pass how many bytes you would like to leave unused on this
508     filesystem as reserved_space.
509     """
510     try:
511         return get_disk_stats(whichdir, reserved_space)['avail']
512     except AttributeError:
513         return None
514     except EnvironmentError:
515         log.msg("OS call to get disk statistics failed")
516         return 0