path_canonicalizer.py revision 0f1bc08d4cfcc34181b0b5cbf065c40f687bf740
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
5import logging
6import os
7import posixpath
8import traceback
9
10from branch_utility import BranchUtility
11from compiled_file_system import CompiledFileSystem, SingleFile
12from file_system import FileNotFoundError
13from third_party.json_schema_compiler.model import UnixName
14import svn_constants
15
16def _SimplifyFileName(file_name):
17  return (posixpath.splitext(file_name)[0]
18      .lower()
19      .replace('.', '')
20      .replace('-', '')
21      .replace('_', ''))
22
23class PathCanonicalizer(object):
24  '''Transforms paths into their canonical forms. Since the dev server has had
25  many incarnations - e.g. there didn't use to be apps/ - there may be old
26  paths lying around the webs. We try to redirect those to where they are now.
27  '''
28  def __init__(self, compiled_fs_factory, file_system):
29    # Map of simplified API names (for typo detection) to their real paths.
30    @SingleFile
31    def make_public_apis(_, file_names):
32      return dict((_SimplifyFileName(name), name) for name in file_names)
33    self._public_apis = compiled_fs_factory.Create(file_system,
34                                                   make_public_apis,
35                                                   PathCanonicalizer)
36
37  def Canonicalize(self, path):
38    '''Returns the canonical path for |path|, and whether that path is a
39    permanent canonicalisation (e.g. when we redirect from a channel to a
40    channel-less URL) or temporary (e.g. when we redirect from an apps-only API
41    to an extensions one - we may at some point enable it for extensions).
42    '''
43    class ReturnType(object):
44      def __init__(self, path, permanent):
45        self.path = path
46        self.permanent = permanent
47
48      # Catch incorrect comparisons by disabling ==/!=.
49      def __eq__(self, _): raise NotImplementedError()
50      def __ne__(self, _): raise NotImplementedError()
51
52    # Strip any channel info off it. There are no channels anymore.
53    for channel_name in BranchUtility.GetAllChannelNames():
54      channel_prefix = channel_name + '/'
55      if path.startswith(channel_prefix):
56        # Redirect now so that we can set the permanent-redirect bit.  Channel
57        # redirects are the only things that should be permanent redirects;
58        # anything else *could* change, so is temporary.
59        return ReturnType(path[len(channel_prefix):], True)
60
61    # No further work needed for static.
62    if path.startswith('static/'):
63      return ReturnType(path, False)
64
65    # People go to just "extensions" or "apps". Redirect to the directory.
66    if path in ('extensions', 'apps'):
67      return ReturnType(path + '/', False)
68
69    # The rest of this function deals with trying to figure out what API page
70    # for extensions/apps to redirect to, if any. We see a few different cases
71    # here:
72    #  - Unqualified names ("browserAction.html"). These are easy to resolve;
73    #    figure out whether it's an extension or app API and redirect.
74    #     - but what if it's both? Well, assume extensions. Maybe later we can
75    #       check analytics and see which is more popular.
76    #  - Wrong names ("apps/browserAction.html"). This really does happen,
77    #    damn it, so do the above logic but record which is the default.
78    if path.startswith(('extensions/', 'apps/')):
79      default_platform, reference_path = path.split('/', 1)
80    else:
81      default_platform, reference_path = ('extensions', path)
82
83    try:
84      apps_public = self._public_apis.GetFromFileListing(
85          '/'.join((svn_constants.PUBLIC_TEMPLATE_PATH, 'apps'))).Get()
86      extensions_public = self._public_apis.GetFromFileListing(
87          '/'.join((svn_constants.PUBLIC_TEMPLATE_PATH, 'extensions'))).Get()
88    except FileNotFoundError:
89      # Probably offline.
90      logging.warning(traceback.format_exc())
91      return ReturnType(path, False)
92
93    simple_reference_path = _SimplifyFileName(reference_path)
94    apps_path = apps_public.get(simple_reference_path)
95    extensions_path = extensions_public.get(simple_reference_path)
96
97    if apps_path is None:
98      if extensions_path is None:
99        # No idea. Just return the original path. It'll probably 404.
100        pass
101      else:
102        path = 'extensions/%s' % extensions_path
103    else:
104      if extensions_path is None:
105        path = 'apps/%s' % apps_path
106      else:
107        assert apps_path == extensions_path
108        path = '%s/%s' % (default_platform, apps_path)
109
110    return ReturnType(path, False)
111