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 5 import posixpath 6 import traceback 7 8 from future import Future 9 from path_util import ( 10 AssertIsDirectory, AssertIsValid, IsDirectory, IsValid, SplitParent, 11 ToDirectory) 12 13 14 def IsFileSystemThrottledError(error): 15 return type(error).__name__ == 'FileSystemThrottledError' 16 17 18 class _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 29 class 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 36 class 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 44 class 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 52 class 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 78 class 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