1 # Copyright 2013 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 mimetypes 6 import posixpath 7 import traceback 8 9 from compiled_file_system import SingleFile 10 from directory_zipper import DirectoryZipper 11 from docs_server_utils import ToUnicode 12 from file_system import FileNotFoundError 13 from future import All, Future 14 from path_canonicalizer import PathCanonicalizer 15 from path_util import AssertIsValid, IsDirectory, Join, ToDirectory 16 from special_paths import SITE_VERIFICATION_FILE 17 from third_party.markdown import markdown 18 from third_party.motemplate import Motemplate 19 20 21 _MIMETYPE_OVERRIDES = { 22 # SVG is not supported by mimetypes.guess_type on AppEngine. 23 '.svg': 'image/svg+xml', 24 } 25 26 27 class ContentAndType(object): 28 '''Return value from ContentProvider.GetContentAndType. 29 ''' 30 31 def __init__(self, content, content_type, version): 32 self.content = content 33 self.content_type = content_type 34 self.version = version 35 36 37 class ContentProvider(object): 38 '''Returns file contents correctly typed for their content-types (in the HTTP 39 sense). Content-type is determined from Python's mimetype library which 40 guesses based on the file extension. 41 42 Typically the file contents will be either str (for binary content) or 43 unicode (for text content). However, HTML files *may* be returned as 44 Motemplate templates (if |supports_templates| is True on construction), in 45 which case the caller will presumably want to Render them. 46 47 Zip file are automatically created and returned for .zip file extensions if 48 |supports_zip| is True. 49 50 |default_extensions| is a list of file extensions which are queried when no 51 file extension is given to GetCanonicalPath/GetContentAndType. Typically 52 this will include .html. 53 ''' 54 55 def __init__(self, 56 name, 57 compiled_fs_factory, 58 file_system, 59 object_store_creator, 60 default_extensions=(), 61 supports_templates=False, 62 supports_zip=False): 63 # Public. 64 self.name = name 65 self.file_system = file_system 66 # Private. 67 self._content_cache = compiled_fs_factory.Create(file_system, 68 self._CompileContent, 69 ContentProvider) 70 self._path_canonicalizer = PathCanonicalizer(file_system, 71 object_store_creator, 72 default_extensions) 73 self._default_extensions = default_extensions 74 self._supports_templates = supports_templates 75 if supports_zip: 76 self._directory_zipper = DirectoryZipper(compiled_fs_factory, file_system) 77 else: 78 self._directory_zipper = None 79 80 @SingleFile 81 def _CompileContent(self, path, text): 82 assert text is not None, path 83 _, ext = posixpath.splitext(path) 84 mimetype = _MIMETYPE_OVERRIDES.get(ext, mimetypes.guess_type(path)[0]) 85 if ext == '.md': 86 # See http://pythonhosted.org/Markdown/extensions 87 # for details on "extensions=". 88 content = markdown(ToUnicode(text), 89 extensions=('extra', 'headerid', 'sane_lists')) 90 if self._supports_templates: 91 content = Motemplate(content, name=path) 92 mimetype = 'text/html' 93 elif mimetype is None: 94 content = text 95 mimetype = 'text/plain' 96 elif mimetype == 'text/html': 97 content = ToUnicode(text) 98 if self._supports_templates: 99 content = Motemplate(content, name=path) 100 elif (mimetype.startswith('text/') or 101 mimetype in ('application/javascript', 'application/json')): 102 content = ToUnicode(text) 103 else: 104 content = text 105 return ContentAndType(content, 106 mimetype, 107 self.file_system.Stat(path).version) 108 109 def GetCanonicalPath(self, path): 110 '''Gets the canonical location of |path|. This class is tolerant of 111 spelling errors and missing files that are in other directories, and this 112 returns the correct/canonical path for those. 113 114 For example, the canonical path of "browseraction" is probably 115 "extensions/browserAction.html". 116 117 Note that the canonical path is relative to this content provider i.e. 118 given relative to |path|. It does not add the "serveFrom" prefix which 119 would have been pulled out in ContentProviders, callers must do that 120 themselves. 121 ''' 122 AssertIsValid(path) 123 base, ext = posixpath.splitext(path) 124 if self._directory_zipper and ext == '.zip': 125 # The canonical location of zip files is the canonical location of the 126 # directory to zip + '.zip'. 127 return self._path_canonicalizer.Canonicalize(base + '/').rstrip('/') + ext 128 return self._path_canonicalizer.Canonicalize(path) 129 130 def GetContentAndType(self, path): 131 '''Returns a Future to the ContentAndType of the file at |path|. 132 ''' 133 AssertIsValid(path) 134 base, ext = posixpath.splitext(path) 135 if self._directory_zipper and ext == '.zip': 136 return (self._directory_zipper.Zip(ToDirectory(base)) 137 .Then(lambda zipped: ContentAndType(zipped, 138 'application/zip', 139 None))) 140 return self._FindFileForPath(path).Then(self._content_cache.GetFromFile) 141 142 def GetVersion(self, path): 143 '''Returns a Future to the version of the file at |path|. 144 ''' 145 AssertIsValid(path) 146 base, ext = posixpath.splitext(path) 147 if self._directory_zipper and ext == '.zip': 148 stat_future = self.file_system.StatAsync(ToDirectory(base)) 149 else: 150 stat_future = self._FindFileForPath(path).Then(self.file_system.StatAsync) 151 return stat_future.Then(lambda stat: stat.version) 152 153 def _FindFileForPath(self, path): 154 '''Finds the real file backing |path|. This may require looking for the 155 correct file extension, or looking for an 'index' file if it's a directory. 156 Returns None if no path is found. 157 ''' 158 AssertIsValid(path) 159 _, ext = posixpath.splitext(path) 160 161 if ext: 162 # There was already an extension, trust that it's a path. Elsewhere 163 # up the stack this will be caught if it's not. 164 return Future(value=path) 165 166 def find_file_with_name(name): 167 '''Tries to find a file in the file system called |name| with one of the 168 default extensions of this content provider. 169 If none is found, returns None. 170 ''' 171 paths = [name + ext for ext in self._default_extensions] 172 def get_first_path_which_exists(existence): 173 for exists, path in zip(existence, paths): 174 if exists: 175 return path 176 return None 177 return (All(self.file_system.Exists(path) for path in paths) 178 .Then(get_first_path_which_exists)) 179 180 def find_index_file(): 181 '''Tries to find an index file in |path|, if |path| is a directory. 182 If not, or if there is no index file, returns None. 183 ''' 184 def get_index_if_directory_exists(directory_exists): 185 if not directory_exists: 186 return None 187 return find_file_with_name(Join(path, 'index')) 188 return (self.file_system.Exists(ToDirectory(path)) 189 .Then(get_index_if_directory_exists)) 190 191 # Try to find a file with the right name. If not, and it's a directory, 192 # look for an index file in that directory. If nothing at all is found, 193 # return the original |path| - its nonexistence will be caught up the stack. 194 return (find_file_with_name(path) 195 .Then(lambda found: found or find_index_file()) 196 .Then(lambda found: found or path)) 197 198 def Refresh(self): 199 futures = [self._path_canonicalizer.Refresh()] 200 for root, _, files in self.file_system.Walk(''): 201 for f in files: 202 futures.append(self.GetContentAndType(Join(root, f))) 203 # Also cache the extension-less version of the file if needed. 204 base, ext = posixpath.splitext(f) 205 if f != SITE_VERIFICATION_FILE and ext in self._default_extensions: 206 futures.append(self.GetContentAndType(Join(root, base))) 207 # TODO(kalman): Cache .zip files for each directory (if supported). 208 return All(futures, except_pass=Exception, except_pass_log=True) 209 210 def __repr__(self): 211 return 'ContentProvider of <%s>' % repr(self.file_system) 212