1# Copyright (C) 2010 Google Inc. All rights reserved.
2#
3# Redistribution and use in source and binary forms, with or without
4# modification, are permitted provided that the following conditions are
5# met:
6#
7#     * Redistributions of source code must retain the above copyright
8# notice, this list of conditions and the following disclaimer.
9#     * Redistributions in binary form must reproduce the above
10# copyright notice, this list of conditions and the following disclaimer
11# in the documentation and/or other materials provided with the
12# distribution.
13#     * Neither the name of Google Inc. nor the names of its
14# contributors may be used to endorse or promote products derived from
15# this software without specific prior written permission.
16#
17# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
18# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
19# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
20# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
21# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
22# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
23# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
24# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
25# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28
29"""Wrapper object for the file system / source tree."""
30
31import codecs
32import errno
33import exceptions
34import glob
35import hashlib
36import os
37import shutil
38import sys
39import tempfile
40import time
41
42class FileSystem(object):
43    """FileSystem interface for webkitpy.
44
45    Unless otherwise noted, all paths are allowed to be either absolute
46    or relative."""
47    sep = os.sep
48    pardir = os.pardir
49
50    def abspath(self, path):
51        return os.path.abspath(path)
52
53    def realpath(self, path):
54        return os.path.realpath(path)
55
56    def path_to_module(self, module_name):
57        """A wrapper for all calls to __file__ to allow easy unit testing."""
58        # FIXME: This is the only use of sys in this file. It's possible this function should move elsewhere.
59        return sys.modules[module_name].__file__  # __file__ is always an absolute path.
60
61    def expanduser(self, path):
62        return os.path.expanduser(path)
63
64    def basename(self, path):
65        return os.path.basename(path)
66
67    def chdir(self, path):
68        return os.chdir(path)
69
70    def copyfile(self, source, destination):
71        shutil.copyfile(source, destination)
72
73    def dirname(self, path):
74        return os.path.dirname(path)
75
76    def exists(self, path):
77        return os.path.exists(path)
78
79    def files_under(self, path, dirs_to_skip=[], file_filter=None):
80        """Return the list of all files under the given path in topdown order.
81
82        Args:
83            dirs_to_skip: a list of directories to skip over during the
84                traversal (e.g., .svn, resources, etc.)
85            file_filter: if not None, the filter will be invoked
86                with the filesystem object and the dirname and basename of
87                each file found. The file is included in the result if the
88                callback returns True.
89        """
90        def filter_all(fs, dirpath, basename):
91            return True
92
93        file_filter = file_filter or filter_all
94        files = []
95        if self.isfile(path):
96            if file_filter(self, self.dirname(path), self.basename(path)):
97                files.append(path)
98            return files
99
100        if self.basename(path) in dirs_to_skip:
101            return []
102
103        for (dirpath, dirnames, filenames) in os.walk(path):
104            for d in dirs_to_skip:
105                if d in dirnames:
106                    dirnames.remove(d)
107
108            for filename in filenames:
109                if file_filter(self, dirpath, filename):
110                    files.append(self.join(dirpath, filename))
111        return files
112
113    def getcwd(self):
114        return os.getcwd()
115
116    def glob(self, path):
117        return glob.glob(path)
118
119    def isabs(self, path):
120        return os.path.isabs(path)
121
122    def isfile(self, path):
123        return os.path.isfile(path)
124
125    def isdir(self, path):
126        return os.path.isdir(path)
127
128    def join(self, *comps):
129        return os.path.join(*comps)
130
131    def listdir(self, path):
132        return os.listdir(path)
133
134    def walk(self, top):
135        return os.walk(top)
136
137    def mkdtemp(self, **kwargs):
138        """Create and return a uniquely named directory.
139
140        This is like tempfile.mkdtemp, but if used in a with statement
141        the directory will self-delete at the end of the block (if the
142        directory is empty; non-empty directories raise errors). The
143        directory can be safely deleted inside the block as well, if so
144        desired.
145
146        Note that the object returned is not a string and does not support all of the string
147        methods. If you need a string, coerce the object to a string and go from there.
148        """
149        class TemporaryDirectory(object):
150            def __init__(self, **kwargs):
151                self._kwargs = kwargs
152                self._directory_path = tempfile.mkdtemp(**self._kwargs)
153
154            def __str__(self):
155                return self._directory_path
156
157            def __enter__(self):
158                return self._directory_path
159
160            def __exit__(self, type, value, traceback):
161                # Only self-delete if necessary.
162
163                # FIXME: Should we delete non-empty directories?
164                if os.path.exists(self._directory_path):
165                    os.rmdir(self._directory_path)
166
167        return TemporaryDirectory(**kwargs)
168
169    def maybe_make_directory(self, *path):
170        """Create the specified directory if it doesn't already exist."""
171        try:
172            os.makedirs(self.join(*path))
173        except OSError, e:
174            if e.errno != errno.EEXIST:
175                raise
176
177    def move(self, source, destination):
178        shutil.move(source, destination)
179
180    def mtime(self, path):
181        return os.stat(path).st_mtime
182
183    def normpath(self, path):
184        return os.path.normpath(path)
185
186    def open_binary_tempfile(self, suffix):
187        """Create, open, and return a binary temp file. Returns a tuple of the file and the name."""
188        temp_fd, temp_name = tempfile.mkstemp(suffix)
189        f = os.fdopen(temp_fd, 'wb')
190        return f, temp_name
191
192    def open_binary_file_for_reading(self, path):
193        return codecs.open(path, 'rb')
194
195    def read_binary_file(self, path):
196        """Return the contents of the file at the given path as a byte string."""
197        with file(path, 'rb') as f:
198            return f.read()
199
200    def write_binary_file(self, path, contents):
201        with file(path, 'wb') as f:
202            f.write(contents)
203
204    def open_text_file_for_reading(self, path):
205        # Note: There appears to be an issue with the returned file objects
206        # not being seekable. See http://stackoverflow.com/questions/1510188/can-seek-and-tell-work-with-utf-8-encoded-documents-in-python .
207        return codecs.open(path, 'r', 'utf8')
208
209    def open_text_file_for_writing(self, path):
210        return codecs.open(path, 'w', 'utf8')
211
212    def read_text_file(self, path):
213        """Return the contents of the file at the given path as a Unicode string.
214
215        The file is read assuming it is a UTF-8 encoded file with no BOM."""
216        with codecs.open(path, 'r', 'utf8') as f:
217            return f.read()
218
219    def write_text_file(self, path, contents):
220        """Write the contents to the file at the given location.
221
222        The file is written encoded as UTF-8 with no BOM."""
223        with codecs.open(path, 'w', 'utf8') as f:
224            f.write(contents)
225
226    def sha1(self, path):
227        contents = self.read_binary_file(path)
228        return hashlib.sha1(contents).hexdigest()
229
230    def relpath(self, path, start='.'):
231        return os.path.relpath(path, start)
232
233    class _WindowsError(exceptions.OSError):
234        """Fake exception for Linux and Mac."""
235        pass
236
237    def remove(self, path, osremove=os.remove):
238        """On Windows, if a process was recently killed and it held on to a
239        file, the OS will hold on to the file for a short while.  This makes
240        attempts to delete the file fail.  To work around that, this method
241        will retry for a few seconds until Windows is done with the file."""
242        try:
243            exceptions.WindowsError
244        except AttributeError:
245            exceptions.WindowsError = FileSystem._WindowsError
246
247        retry_timeout_sec = 3.0
248        sleep_interval = 0.1
249        while True:
250            try:
251                osremove(path)
252                return True
253            except exceptions.WindowsError, e:
254                time.sleep(sleep_interval)
255                retry_timeout_sec -= sleep_interval
256                if retry_timeout_sec < 0:
257                    raise e
258
259    def rmtree(self, path):
260        """Delete the directory rooted at path, whether empty or not."""
261        shutil.rmtree(path, ignore_errors=True)
262
263    def copytree(self, source, destination):
264        shutil.copytree(source, destination)
265
266    def split(self, path):
267        """Return (dirname, basename + '.' + ext)"""
268        return os.path.split(path)
269
270    def splitext(self, path):
271        """Return (dirname + os.sep + basename, '.' + ext)"""
272        return os.path.splitext(path)
273