]> 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 open_or_create(fname, binarymode=True):
228     try:
229         return open(fname, binarymode and "r+b" or "r+")
230     except EnvironmentError:
231         return open(fname, binarymode and "w+b" or "w+")
232
233 def du(basedir):
234     size = 0
235
236     for root, dirs, files in os.walk(basedir):
237         for f in files:
238             fn = os.path.join(root, f)
239             size += os.path.getsize(fn)
240
241     return size
242
243 def move_into_place(source, dest):
244     """Atomically replace a file, or as near to it as the platform allows.
245     The dest file may or may not exist."""
246     if "win32" in sys.platform.lower():
247         remove_if_possible(dest)
248     os.rename(source, dest)
249
250 def write_atomically(target, contents, mode="b"):
251     f = open(target+".tmp", "w"+mode)
252     try:
253         f.write(contents)
254     finally:
255         f.close()
256     move_into_place(target+".tmp", target)
257
258 def write(path, data, mode="wb"):
259     wf = open(path, mode)
260     try:
261         wf.write(data)
262     finally:
263         wf.close()
264
265 def read(path):
266     rf = open(path, "rb")
267     try:
268         return rf.read()
269     finally:
270         rf.close()
271
272 def put_file(pathname, inf):
273     # TODO: create temporary file and move into place?
274     outf = open(os.path.expanduser(pathname), "wb")
275     try:
276         while True:
277             data = inf.read(32768)
278             if not data:
279                 break
280             outf.write(data)
281     finally:
282         outf.close()
283
284
285 # Work around <http://bugs.python.org/issue3426>. This code is adapted from
286 # <http://svn.python.org/view/python/trunk/Lib/ntpath.py?revision=78247&view=markup>
287 # with some simplifications.
288
289 _getfullpathname = None
290 try:
291     from nt import _getfullpathname
292 except ImportError:
293     pass
294
295 def abspath_expanduser_unicode(path):
296     """Return the absolute version of a path."""
297     assert isinstance(path, unicode), path
298
299     path = os.path.expanduser(path)
300
301     if _getfullpathname:
302         # On Windows, os.path.isabs will return True for paths without a drive letter,
303         # e.g. "\\". See <http://bugs.python.org/issue1669539>.
304         try:
305             path = _getfullpathname(path or u".")
306         except OSError:
307             pass
308
309     if not os.path.isabs(path):
310         path = os.path.join(os.getcwdu(), path)
311
312     # We won't hit <http://bugs.python.org/issue5827> because
313     # there is always at least one Unicode path component.
314     path = os.path.normpath(path)
315
316     if sys.platform == "win32" and not path.startswith(u"\\\\"):
317         path = u"\\\\?\\" + path
318
319     return path
320
321
322 have_GetDiskFreeSpaceExW = False
323 if sys.platform == "win32":
324     try:
325         from ctypes import WINFUNCTYPE, windll, POINTER, byref, c_ulonglong
326         from ctypes.wintypes import BOOL, DWORD, LPCWSTR
327
328         # <http://msdn.microsoft.com/en-us/library/aa383742%28v=VS.85%29.aspx>
329         PULARGE_INTEGER = POINTER(c_ulonglong)
330
331         # <http://msdn.microsoft.com/en-us/library/aa364937%28VS.85%29.aspx>
332         GetDiskFreeSpaceExW = WINFUNCTYPE(BOOL, LPCWSTR, PULARGE_INTEGER, PULARGE_INTEGER, PULARGE_INTEGER)(
333             ("GetDiskFreeSpaceExW", windll.kernel32))
334
335         # <http://msdn.microsoft.com/en-us/library/ms679360%28v=VS.85%29.aspx>
336         GetLastError = WINFUNCTYPE(DWORD)(("GetLastError", windll.kernel32))
337
338         have_GetDiskFreeSpaceExW = True
339     except Exception:
340         import traceback
341         traceback.print_exc()
342
343 def get_disk_stats(whichdir, reserved_space=0):
344     """Return disk statistics for the storage disk, in the form of a dict
345     with the following fields.
346       total:            total bytes on disk
347       free_for_root:    bytes actually free on disk
348       free_for_nonroot: bytes free for "a non-privileged user" [Unix] or
349                           the current user [Windows]; might take into
350                           account quotas depending on platform
351       used:             bytes used on disk
352       avail:            bytes available excluding reserved space
353     An AttributeError can occur if the OS has no API to get disk information.
354     An EnvironmentError can occur if the OS call fails.
355
356     whichdir is a directory on the filesystem in question -- the
357     answer is about the filesystem, not about the directory, so the
358     directory is used only to specify which filesystem.
359
360     reserved_space is how many bytes to subtract from the answer, so
361     you can pass how many bytes you would like to leave unused on this
362     filesystem as reserved_space.
363     """
364
365     if have_GetDiskFreeSpaceExW:
366         # If this is a Windows system and GetDiskFreeSpaceExW is available, use it.
367         # (This might put up an error dialog unless
368         # SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOOPENFILEERRORBOX) has been called,
369         # which we do in allmydata.windows.fixups.initialize().)
370
371         n_free_for_nonroot = c_ulonglong(0)
372         n_total            = c_ulonglong(0)
373         n_free_for_root    = c_ulonglong(0)
374         retval = GetDiskFreeSpaceExW(whichdir, byref(n_free_for_nonroot),
375                                                byref(n_total),
376                                                byref(n_free_for_root))
377         if retval == 0:
378             raise OSError("Windows error %d attempting to get disk statistics for %r"
379                           % (GetLastError(), whichdir))
380         free_for_nonroot = n_free_for_nonroot.value
381         total            = n_total.value
382         free_for_root    = n_free_for_root.value
383     else:
384         # For Unix-like systems.
385         # <http://docs.python.org/library/os.html#os.statvfs>
386         # <http://opengroup.org/onlinepubs/7990989799/xsh/fstatvfs.html>
387         # <http://opengroup.org/onlinepubs/7990989799/xsh/sysstatvfs.h.html>
388         s = os.statvfs(whichdir)
389
390         # on my mac laptop:
391         #  statvfs(2) is a wrapper around statfs(2).
392         #    statvfs.f_frsize = statfs.f_bsize :
393         #     "minimum unit of allocation" (statvfs)
394         #     "fundamental file system block size" (statfs)
395         #    statvfs.f_bsize = statfs.f_iosize = stat.st_blocks : preferred IO size
396         # on an encrypted home directory ("FileVault"), it gets f_blocks
397         # wrong, and s.f_blocks*s.f_frsize is twice the size of my disk,
398         # but s.f_bavail*s.f_frsize is correct
399
400         total = s.f_frsize * s.f_blocks
401         free_for_root = s.f_frsize * s.f_bfree
402         free_for_nonroot = s.f_frsize * s.f_bavail
403
404     # valid for all platforms:
405     used = total - free_for_root
406     avail = max(free_for_nonroot - reserved_space, 0)
407
408     return { 'total': total,
409              'free_for_root': free_for_root,
410              'free_for_nonroot': free_for_nonroot,
411              'used': used,
412              'avail': avail,
413            }
414
415 def get_available_space(whichdir, reserved_space):
416     """Returns available space for share storage in bytes, or None if no
417     API to get this information is available.
418
419     whichdir is a directory on the filesystem in question -- the
420     answer is about the filesystem, not about the directory, so the
421     directory is used only to specify which filesystem.
422
423     reserved_space is how many bytes to subtract from the answer, so
424     you can pass how many bytes you would like to leave unused on this
425     filesystem as reserved_space.
426     """
427     try:
428         return get_disk_stats(whichdir, reserved_space)['avail']
429     except AttributeError:
430         return None
431     except EnvironmentError:
432         log.msg("OS call to get disk statistics failed")
433         return 0