]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blob - src/allmydata/util/fileutil.py
Use "long" paths prefixed with \\?\ on Windows. refs #2235
[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 NamedTemporaryDirectory:
91     """
92     This calls tempfile.mkdtemp(), stores the name of the dir in
93     self.name, and rmrf's the dir when it gets garbage collected or
94     "shutdown()".
95     """
96     def __init__(self, cleanup=True, *args, **kwargs):
97         """ If cleanup, then the directory will be rmrf'ed when the object is shutdown. """
98         self.cleanup = cleanup
99         self.name = tempfile.mkdtemp(*args, **kwargs)
100
101     def __repr__(self):
102         return "<%s instance at %x %s>" % (self.__class__.__name__, id(self), self.name)
103
104     def __str__(self):
105         return self.__repr__()
106
107     def __del__(self):
108         try:
109             self.shutdown()
110         except:
111             import traceback
112             traceback.print_exc()
113
114     def shutdown(self):
115         if self.cleanup and hasattr(self, 'name'):
116             rm_dir(self.name)
117
118 class EncryptedTemporaryFile:
119     # not implemented: next, readline, readlines, xreadlines, writelines
120
121     def __init__(self):
122         self.file = tempfile.TemporaryFile()
123         self.key = os.urandom(16)  # AES-128
124
125     def _crypt(self, offset, data):
126         offset_big = offset // 16
127         offset_small = offset % 16
128         iv = binascii.unhexlify("%032x" % offset_big)
129         cipher = AES(self.key, iv=iv)
130         cipher.process("\x00"*offset_small)
131         return cipher.process(data)
132
133     def close(self):
134         self.file.close()
135
136     def flush(self):
137         self.file.flush()
138
139     def seek(self, offset, whence=0):  # 0 = SEEK_SET
140         self.file.seek(offset, whence)
141
142     def tell(self):
143         offset = self.file.tell()
144         return offset
145
146     def read(self, size=-1):
147         """A read must not follow a write, or vice-versa, without an intervening seek."""
148         index = self.file.tell()
149         ciphertext = self.file.read(size)
150         plaintext = self._crypt(index, ciphertext)
151         return plaintext
152
153     def write(self, plaintext):
154         """A read must not follow a write, or vice-versa, without an intervening seek.
155         If seeking and then writing causes a 'hole' in the file, the contents of the
156         hole are unspecified."""
157         index = self.file.tell()
158         ciphertext = self._crypt(index, plaintext)
159         self.file.write(ciphertext)
160
161     def truncate(self, newsize):
162         """Truncate or extend the file to 'newsize'. If it is extended, the contents after the
163         old end-of-file are unspecified. The file position after this operation is unspecified."""
164         self.file.truncate(newsize)
165
166
167 def make_dirs(dirname, mode=0777):
168     """
169     An idempotent version of os.makedirs().  If the dir already exists, do
170     nothing and return without raising an exception.  If this call creates the
171     dir, return without raising an exception.  If there is an error that
172     prevents creation or if the directory gets deleted after make_dirs() creates
173     it and before make_dirs() checks that it exists, raise an exception.
174     """
175     tx = None
176     try:
177         os.makedirs(dirname, mode)
178     except OSError, x:
179         tx = x
180
181     if not os.path.isdir(dirname):
182         if tx:
183             raise tx
184         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...
185
186 def rm_dir(dirname):
187     """
188     A threadsafe and idempotent version of shutil.rmtree().  If the dir is
189     already gone, do nothing and return without raising an exception.  If this
190     call removes the dir, return without raising an exception.  If there is an
191     error that prevents deletion or if the directory gets created again after
192     rm_dir() deletes it and before rm_dir() checks that it is gone, raise an
193     exception.
194     """
195     excs = []
196     try:
197         os.chmod(dirname, stat.S_IWRITE | stat.S_IEXEC | stat.S_IREAD)
198         for f in os.listdir(dirname):
199             fullname = os.path.join(dirname, f)
200             if os.path.isdir(fullname):
201                 rm_dir(fullname)
202             else:
203                 remove(fullname)
204         os.rmdir(dirname)
205     except Exception, le:
206         # Ignore "No such file or directory"
207         if (not isinstance(le, OSError)) or le.args[0] != 2:
208             excs.append(le)
209
210     # Okay, now we've recursively removed everything, ignoring any "No
211     # such file or directory" errors, and collecting any other errors.
212
213     if os.path.exists(dirname):
214         if len(excs) == 1:
215             raise excs[0]
216         if len(excs) == 0:
217             raise OSError, "Failed to remove dir for unknown reason."
218         raise OSError, excs
219
220
221 def remove_if_possible(f):
222     try:
223         remove(f)
224     except:
225         pass
226
227 def du(basedir):
228     size = 0
229
230     for root, dirs, files in os.walk(basedir):
231         for f in files:
232             fn = os.path.join(root, f)
233             size += os.path.getsize(fn)
234
235     return size
236
237 def move_into_place(source, dest):
238     """Atomically replace a file, or as near to it as the platform allows.
239     The dest file may or may not exist."""
240     if "win32" in sys.platform.lower():
241         remove_if_possible(dest)
242     os.rename(source, dest)
243
244 def write_atomically(target, contents, mode="b"):
245     f = open(target+".tmp", "w"+mode)
246     try:
247         f.write(contents)
248     finally:
249         f.close()
250     move_into_place(target+".tmp", target)
251
252 def write(path, data, mode="wb"):
253     wf = open(path, mode)
254     try:
255         wf.write(data)
256     finally:
257         wf.close()
258
259 def read(path):
260     rf = open(path, "rb")
261     try:
262         return rf.read()
263     finally:
264         rf.close()
265
266 def put_file(pathname, inf):
267     # TODO: create temporary file and move into place?
268     outf = open(os.path.expanduser(pathname), "wb")
269     try:
270         while True:
271             data = inf.read(32768)
272             if not data:
273                 break
274             outf.write(data)
275     finally:
276         outf.close()
277
278
279 # Work around <http://bugs.python.org/issue3426>. This code is adapted from
280 # <http://svn.python.org/view/python/trunk/Lib/ntpath.py?revision=78247&view=markup>
281 # with some simplifications.
282
283 _getfullpathname = None
284 try:
285     from nt import _getfullpathname
286 except ImportError:
287     pass
288
289 def abspath_expanduser_unicode(path):
290     """Return the absolute version of a path."""
291     assert isinstance(path, unicode), path
292
293     path = os.path.expanduser(path)
294
295     if _getfullpathname:
296         # On Windows, os.path.isabs will return True for paths without a drive letter,
297         # e.g. "\\". See <http://bugs.python.org/issue1669539>.
298         try:
299             path = _getfullpathname(path or u".")
300         except OSError:
301             pass
302
303     if not os.path.isabs(path):
304         path = os.path.join(os.getcwdu(), path)
305
306     # We won't hit <http://bugs.python.org/issue5827> because
307     # there is always at least one Unicode path component.
308     path = os.path.normpath(path)
309
310     if sys.platform == "win32":
311         path = to_windows_long_path(path)
312
313     return path
314
315 def to_windows_long_path(path):
316     # '/' is normally a perfectly valid path component separator in Windows.
317     # However, when using the "\\?\" syntax it is not recognized, so we
318     # replace it with '\' here.
319     path = path.replace(u"/", u"\\")
320
321     # Note that other normalizations such as removing '.' and '..' should
322     # be done outside this function.
323
324     if path.startswith(u"\\\\?\\") or path.startswith(u"\\\\.\\"):
325         return path
326     elif path.startswith(u"\\\\"):
327         return u"\\\\?\\UNC\\" + path[2 :]
328     else:
329         return u"\\\\?\\" + path
330
331
332 have_GetDiskFreeSpaceExW = False
333 if sys.platform == "win32":
334     try:
335         from ctypes import WINFUNCTYPE, windll, POINTER, byref, c_ulonglong
336         from ctypes.wintypes import BOOL, DWORD, LPCWSTR
337
338         # <http://msdn.microsoft.com/en-us/library/aa383742%28v=VS.85%29.aspx>
339         PULARGE_INTEGER = POINTER(c_ulonglong)
340
341         # <http://msdn.microsoft.com/en-us/library/aa364937%28VS.85%29.aspx>
342         GetDiskFreeSpaceExW = WINFUNCTYPE(BOOL, LPCWSTR, PULARGE_INTEGER, PULARGE_INTEGER, PULARGE_INTEGER)(
343             ("GetDiskFreeSpaceExW", windll.kernel32))
344
345         # <http://msdn.microsoft.com/en-us/library/ms679360%28v=VS.85%29.aspx>
346         GetLastError = WINFUNCTYPE(DWORD)(("GetLastError", windll.kernel32))
347
348         have_GetDiskFreeSpaceExW = True
349     except Exception:
350         import traceback
351         traceback.print_exc()
352
353 def get_disk_stats(whichdir, reserved_space=0):
354     """Return disk statistics for the storage disk, in the form of a dict
355     with the following fields.
356       total:            total bytes on disk
357       free_for_root:    bytes actually free on disk
358       free_for_nonroot: bytes free for "a non-privileged user" [Unix] or
359                           the current user [Windows]; might take into
360                           account quotas depending on platform
361       used:             bytes used on disk
362       avail:            bytes available excluding reserved space
363     An AttributeError can occur if the OS has no API to get disk information.
364     An EnvironmentError can occur if the OS call fails.
365
366     whichdir is a directory on the filesystem in question -- the
367     answer is about the filesystem, not about the directory, so the
368     directory is used only to specify which filesystem.
369
370     reserved_space is how many bytes to subtract from the answer, so
371     you can pass how many bytes you would like to leave unused on this
372     filesystem as reserved_space.
373     """
374
375     if have_GetDiskFreeSpaceExW:
376         # If this is a Windows system and GetDiskFreeSpaceExW is available, use it.
377         # (This might put up an error dialog unless
378         # SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOOPENFILEERRORBOX) has been called,
379         # which we do in allmydata.windows.fixups.initialize().)
380
381         n_free_for_nonroot = c_ulonglong(0)
382         n_total            = c_ulonglong(0)
383         n_free_for_root    = c_ulonglong(0)
384         retval = GetDiskFreeSpaceExW(whichdir, byref(n_free_for_nonroot),
385                                                byref(n_total),
386                                                byref(n_free_for_root))
387         if retval == 0:
388             raise OSError("Windows error %d attempting to get disk statistics for %r"
389                           % (GetLastError(), whichdir))
390         free_for_nonroot = n_free_for_nonroot.value
391         total            = n_total.value
392         free_for_root    = n_free_for_root.value
393     else:
394         # For Unix-like systems.
395         # <http://docs.python.org/library/os.html#os.statvfs>
396         # <http://opengroup.org/onlinepubs/7990989799/xsh/fstatvfs.html>
397         # <http://opengroup.org/onlinepubs/7990989799/xsh/sysstatvfs.h.html>
398         s = os.statvfs(whichdir)
399
400         # on my mac laptop:
401         #  statvfs(2) is a wrapper around statfs(2).
402         #    statvfs.f_frsize = statfs.f_bsize :
403         #     "minimum unit of allocation" (statvfs)
404         #     "fundamental file system block size" (statfs)
405         #    statvfs.f_bsize = statfs.f_iosize = stat.st_blocks : preferred IO size
406         # on an encrypted home directory ("FileVault"), it gets f_blocks
407         # wrong, and s.f_blocks*s.f_frsize is twice the size of my disk,
408         # but s.f_bavail*s.f_frsize is correct
409
410         total = s.f_frsize * s.f_blocks
411         free_for_root = s.f_frsize * s.f_bfree
412         free_for_nonroot = s.f_frsize * s.f_bavail
413
414     # valid for all platforms:
415     used = total - free_for_root
416     avail = max(free_for_nonroot - reserved_space, 0)
417
418     return { 'total': total,
419              'free_for_root': free_for_root,
420              'free_for_nonroot': free_for_nonroot,
421              'used': used,
422              'avail': avail,
423            }
424
425 def get_available_space(whichdir, reserved_space):
426     """Returns available space for share storage in bytes, or None if no
427     API to get this information is available.
428
429     whichdir is a directory on the filesystem in question -- the
430     answer is about the filesystem, not about the directory, so the
431     directory is used only to specify which filesystem.
432
433     reserved_space is how many bytes to subtract from the answer, so
434     you can pass how many bytes you would like to leave unused on this
435     filesystem as reserved_space.
436     """
437     try:
438         return get_disk_stats(whichdir, reserved_space)['avail']
439     except AttributeError:
440         return None
441     except EnvironmentError:
442         log.msg("OS call to get disk statistics failed")
443         return 0