1# Copyright (c) 2012 The Chromium Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5import posixpath
6import traceback
7
8from future import Future
9from path_util import (
10    AssertIsDirectory, AssertIsValid, IsDirectory, IsValid, SplitParent,
11    ToDirectory)
12
13
14def IsFileSystemThrottledError(error):
15  return type(error).__name__ == 'FileSystemThrottledError'
16
17
18class _BaseFileSystemException(Exception):
19  def __init__(self, message):
20    Exception.__init__(self, message)
21
22  @classmethod
23  def RaiseInFuture(cls, message):
24    stack = traceback.format_stack()
25    def boom(): raise cls('%s. Creation stack:\n%s' % (message, ''.join(stack)))
26    return Future(callback=boom)
27
28
29class FileNotFoundError(_BaseFileSystemException):
30  '''Raised when a file isn't found for read or stat.
31  '''
32  def __init__(self, filename):
33    _BaseFileSystemException.__init__(self, filename)
34
35
36class FileSystemThrottledError(_BaseFileSystemException):
37  '''Raised when access to a file system resource is temporarily unavailable
38  due to service throttling.
39  '''
40  def __init__(self, filename):
41    _BaseFileSystemException.__init__(self, filename)
42
43
44class FileSystemError(_BaseFileSystemException):
45  '''Raised on when there are errors reading or statting files, such as a
46  network timeout.
47  '''
48  def __init__(self, filename):
49    _BaseFileSystemException.__init__(self, filename)
50
51
52class StatInfo(object):
53  '''The result of calling Stat on a FileSystem.
54  '''
55  def __init__(self, version, child_versions=None):
56    if child_versions:
57      assert all(IsValid(path) for path in child_versions.iterkeys()), \
58             child_versions
59    self.version = version
60    self.child_versions = child_versions
61
62  def __eq__(self, other):
63    return (isinstance(other, StatInfo) and
64            self.version == other.version and
65            self.child_versions == other.child_versions)
66
67  def __ne__(self, other):
68    return not (self == other)
69
70  def __str__(self):
71    return '{version: %s, child_versions: %s}' % (self.version,
72                                                  self.child_versions)
73
74  def __repr__(self):
75    return str(self)
76
77
78class FileSystem(object):
79  '''A FileSystem interface that can read files and directories.
80  '''
81  def Read(self, paths, skip_not_found=False):
82    '''Reads each file in paths and returns a dictionary mapping the path to the
83    contents. If a path in paths ends with a '/', it is assumed to be a
84    directory, and a list of files in the directory is mapped to the path.
85
86    The contents will be a str.
87
88    If any path cannot be found:
89      - If |skip_not_found| is True, the resulting object will not contain any
90        mapping for that path.
91      - Otherwise, and by default, a FileNotFoundError is raised. This is
92        guaranteed to only happen once the Future has been resolved (Get()
93        called).
94
95    For any other failure, raises a FileSystemError.
96    '''
97    raise NotImplementedError(self.__class__)
98
99  def ReadSingle(self, path, skip_not_found=False):
100    '''Reads a single file from the FileSystem. Returns a Future with the same
101    rules as Read(). If |path| is not found raise a FileNotFoundError on Get(),
102    or if |skip_not_found| is True then return None.
103    '''
104    AssertIsValid(path)
105    read_single = self.Read([path], skip_not_found=skip_not_found)
106    return Future(callback=lambda: read_single.Get().get(path, None))
107
108  def Exists(self, path):
109    '''Returns a Future to the existence of |path|; True if |path| exists,
110    False if not. This method will not throw a FileNotFoundError unlike
111    the Read* methods, however it may still throw a FileSystemError.
112
113    There are several ways to implement this method via the interface but this
114    method exists to do so in a canonical and most efficient way for caching.
115    '''
116    AssertIsValid(path)
117    if path == '':
118      # There is always a root directory.
119      return Future(value=True)
120
121    parent, base = SplitParent(path)
122    def handle(error):
123      if isinstance(error, FileNotFoundError):
124        return False
125      raise error
126    return self.ReadSingle(ToDirectory(parent)).Then(lambda l: base in l,
127                                                     handle)
128
129  def Refresh(self):
130    '''Asynchronously refreshes the content of the FileSystem, returning a
131    future to its completion.
132    '''
133    raise NotImplementedError(self.__class__)
134
135  # TODO(cduvall): Allow Stat to take a list of paths like Read.
136  def Stat(self, path):
137    '''DEPRECATED: Please try to use StatAsync instead.
138
139    Returns a |StatInfo| object containing the version of |path|. If |path|
140    is a directory, |StatInfo| will have the versions of all the children of
141    the directory in |StatInfo.child_versions|.
142
143    If the path cannot be found, raises a FileNotFoundError.
144    For any other failure, raises a FileSystemError.
145    '''
146    # Delegate to this implementation's StatAsync if it has been implemented.
147    if type(self).StatAsync != FileSystem.StatAsync:
148      return self.StatAsync(path).Get()
149    raise NotImplementedError(self.__class__)
150
151  def StatAsync(self, path):
152    '''An async version of Stat. Returns a Future to a StatInfo rather than a
153    raw StatInfo.
154
155    This is a bandaid for a lack of an async Stat function. Stat() should be
156    async by default but for now just let implementations override this if they
157    like.
158    '''
159    return Future(callback=lambda: self.Stat(path))
160
161  def GetIdentity(self):
162    '''The identity of the file system, exposed for caching classes to
163    namespace their caches. this will usually depend on the configuration of
164    that file system - e.g. a LocalFileSystem with a base path of /var is
165    different to that of a SubversionFileSystem with a base path of /bar, is
166    different to a LocalFileSystem with a base path of /usr.
167    '''
168    raise NotImplementedError(self.__class__)
169
170  def Walk(self, root, depth=-1, file_lister=None):
171    '''Recursively walk the directories in a file system, starting with root.
172
173    Behaviour is very similar to os.walk from the standard os module, yielding
174    (base, dirs, files) recursively, where |base| is the base path of |files|,
175    |dirs| relative to |root|, and |files| and |dirs| the list of files/dirs in
176    |base| respectively. If |depth| is specified and greater than 0, Walk will
177    only recurse |depth| times.
178
179    |file_lister|, if specified, should be a callback of signature
180
181      def my_file_lister(root):,
182
183    which returns a tuple (dirs, files), where |dirs| is a list of directory
184    names under |root|, and |files| is a list of file names under |root|. Note
185    that the listing of files and directories should be for a *single* level
186    only, i.e. it should not recursively list anything.
187
188    Note that directories will always end with a '/', files never will.
189
190    If |root| cannot be found, raises a FileNotFoundError.
191    For any other failure, raises a FileSystemError.
192    '''
193    AssertIsDirectory(root)
194    basepath = root
195
196    def walk(root, depth):
197      if depth == 0:
198        return
199      AssertIsDirectory(root)
200
201      if file_lister:
202        dirs, files = file_lister(root)
203      else:
204        dirs, files = [], []
205        for f in self.ReadSingle(root).Get():
206          if IsDirectory(f):
207            dirs.append(f)
208          else:
209            files.append(f)
210
211      yield root[len(basepath):].rstrip('/'), dirs, files
212
213      for d in dirs:
214        for walkinfo in walk(root + d, depth - 1):
215          yield walkinfo
216
217    for walkinfo in walk(root, depth):
218      yield walkinfo
219
220  def __eq__(self, other):
221    return (isinstance(other, FileSystem) and
222            self.GetIdentity() == other.GetIdentity())
223
224  def __ne__(self, other):
225    return not (self == other)
226
227  def __repr__(self):
228    return '<%s>' % type(self).__name__
229
230  def __str__(self):
231    return repr(self)
232