]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blob - src/allmydata/util/fileutil.py
fileutil: add move_into_place(), to perform the standard unix trick of atomically...
[tahoe-lafs/tahoe-lafs.git] / src / allmydata / util / fileutil.py
1 #  Copyright (c) 2000 Autonomous Zone Industries
2 #  Copyright (c) 2002-2007 Bryce "Zooko" Wilcox-O'Hearn
3 #  This file is licensed under the
4 #    GNU Lesser General Public License v2.1.
5 #    See the file COPYING or visit http://www.gnu.org/ for details.
6 # Portions snarfed out of the Python standard library.
7 # The du part is due to Jim McCoy.
8
9 """
10 Futz with files like a pro.
11 """
12
13 import sys, exceptions, os, stat, tempfile, time
14
15 from twisted.python import log
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 NamedTemporaryDirectory:
96     """
97     This calls tempfile.mkdtemp(), stores the name of the dir in
98     self.name, and rmrf's the dir when it gets garbage collected or
99     "shutdown()".
100     """
101     def __init__(self, cleanup=True, *args, **kwargs):
102         """ If cleanup, then the directory will be rmrf'ed when the object is shutdown. """
103         self.cleanup = cleanup
104         self.name = tempfile.mkdtemp(*args, **kwargs)
105
106     def __repr__(self):
107         return "<%s instance at %x %s>" % (self.__class__.__name__, id(self), self.name)
108
109     def __str__(self):
110         return self.__repr__()
111
112     def __del__(self):
113         try:
114             self.shutdown()
115         except:
116             import traceback
117             traceback.print_exc()
118
119     def shutdown(self):
120         if self.cleanup and hasattr(self, 'name'):
121             rm_dir(self.name)
122
123 def make_dirs(dirname, mode=0777):
124     """
125     An idempotent version of os.makedirs().  If the dir already exists, do
126     nothing and return without raising an exception.  If this call creates the
127     dir, return without raising an exception.  If there is an error that
128     prevents creation or if the directory gets deleted after make_dirs() creates
129     it and before make_dirs() checks that it exists, raise an exception.
130     """
131     tx = None
132     try:
133         os.makedirs(dirname, mode)
134     except OSError, x:
135         tx = x
136
137     if not os.path.isdir(dirname):
138         if tx:
139             raise tx
140         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...
141
142 def rm_dir(dirname):
143     """
144     A threadsafe and idempotent version of shutil.rmtree().  If the dir is
145     already gone, do nothing and return without raising an exception.  If this
146     call removes the dir, return without raising an exception.  If there is an
147     error that prevents deletion or if the directory gets created again after
148     rm_dir() deletes it and before rm_dir() checks that it is gone, raise an
149     exception.
150     """
151     excs = []
152     try:
153         os.chmod(dirname, stat.S_IWRITE | stat.S_IEXEC | stat.S_IREAD)
154         for f in os.listdir(dirname):
155             fullname = os.path.join(dirname, f)
156             if os.path.isdir(fullname):
157                 rm_dir(fullname)
158             else:
159                 remove(fullname)
160         os.rmdir(dirname)
161     except Exception, le:
162         # Ignore "No such file or directory"
163         if (not isinstance(le, OSError)) or le.args[0] != 2:
164             excs.append(le)
165
166     # Okay, now we've recursively removed everything, ignoring any "No
167     # such file or directory" errors, and collecting any other errors.
168
169     if os.path.exists(dirname):
170         if len(excs) == 1:
171             raise excs[0]
172         if len(excs) == 0:
173             raise OSError, "Failed to remove dir for unknown reason."
174         raise OSError, excs
175
176
177 def remove_if_possible(f):
178     try:
179         remove(f)
180     except:
181         pass
182
183 def open_or_create(fname, binarymode=True):
184     try:
185         return open(fname, binarymode and "r+b" or "r+")
186     except EnvironmentError:
187         return open(fname, binarymode and "w+b" or "w+")
188
189 def du(basedir):
190     size = 0
191
192     for root, dirs, files in os.walk(basedir):
193         for f in files:
194             fn = os.path.join(root, f)
195             size += os.path.getsize(fn)
196
197     return size
198
199 def move_into_place(source, dest):
200     """Atomically replace a file, or as near to it as the platform allows.
201     The dest file may or may not exist."""
202     if "win32" in sys.platform.lower():
203         remove_if_possible(dest)
204     os.rename(source, dest)