Home | History | Annotate | Download | only in server2
      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