Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 # Copyright (c) 2012 The Chromium Authors. All rights reserved. | 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 | 2 # Use of this source code is governed by a BSD-style license that can be |
| 3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
| 4 | 4 |
| 5 import logging | 5 import logging |
| 6 import os | 6 import os |
| 7 from StringIO import StringIO | 7 from StringIO import StringIO |
| 8 import sys | 8 import sys |
| 9 | 9 |
| 10 from appengine_wrappers import webapp | 10 from appengine_wrappers import webapp |
| 11 from appengine_wrappers import memcache | 11 from appengine_wrappers import memcache |
| 12 from appengine_wrappers import urlfetch | 12 from appengine_wrappers import urlfetch |
| 13 | 13 |
| 14 from api_data_source import APIDataSource | 14 from api_data_source import APIDataSource |
| 15 from api_list_data_source import APIListDataSource | 15 from api_list_data_source import APIListDataSource |
| 16 from appengine_blobstore import AppEngineBlobstore | 16 from appengine_blobstore import AppEngineBlobstore |
| 17 from in_memory_object_store import InMemoryObjectStore | |
| 18 from appengine_url_fetcher import AppEngineUrlFetcher | 17 from appengine_url_fetcher import AppEngineUrlFetcher |
| 18 from availability_data_source import AvailabilityDataSource | |
| 19 from branch_utility import BranchUtility | 19 from branch_utility import BranchUtility |
| 20 from example_zipper import ExampleZipper | 20 from chrome_version_data_source import ChromeVersionDataSource |
| 21 from compiled_file_system import CompiledFileSystem | 21 from compiled_file_system import CompiledFileSystem |
| 22 import compiled_file_system as compiled_fs | 22 import compiled_file_system as compiled_fs |
| 23 from example_zipper import ExampleZipper | |
| 23 from github_file_system import GithubFileSystem | 24 from github_file_system import GithubFileSystem |
| 25 from in_memory_object_store import InMemoryObjectStore | |
| 24 from intro_data_source import IntroDataSource | 26 from intro_data_source import IntroDataSource |
| 25 from known_issues_data_source import KnownIssuesDataSource | 27 from known_issues_data_source import KnownIssuesDataSource |
| 26 from local_file_system import LocalFileSystem | 28 from local_file_system import LocalFileSystem |
| 27 from memcache_file_system import MemcacheFileSystem | 29 from memcache_file_system import MemcacheFileSystem |
| 28 from reference_resolver import ReferenceResolver | 30 from reference_resolver import ReferenceResolver |
| 29 from samples_data_source import SamplesDataSource | 31 from samples_data_source import SamplesDataSource |
| 30 from server_instance import ServerInstance | 32 from server_instance import ServerInstance |
| 31 from sidenav_data_source import SidenavDataSource | 33 from sidenav_data_source import SidenavDataSource |
| 32 from subversion_file_system import SubversionFileSystem | 34 from subversion_file_system import SubversionFileSystem |
| 33 from template_data_source import TemplateDataSource | 35 from template_data_source import TemplateDataSource |
| 34 from third_party.json_schema_compiler.model import UnixName | 36 from third_party.json_schema_compiler.model import UnixName |
| 35 import url_constants | 37 import url_constants |
| 36 | 38 |
| 37 # Increment this version to force the server to reload all pages in the first | 39 # Increment this version to force the server to reload all pages in the first |
| 38 # cron job that is run. | 40 # cron job that is run. |
| 39 _VERSION = 1 | 41 _VERSION = 2 |
| 40 | 42 |
| 41 # The default channel to serve docs for if no channel is specified. | 43 # The default channel to serve docs for if no channel is specified. |
| 42 _DEFAULT_CHANNEL = 'stable' | 44 _DEFAULT_CHANNEL = 'stable' |
| 43 | 45 |
| 44 BRANCH_UTILITY_MEMCACHE = InMemoryObjectStore('branch_utility') | 46 BRANCH_UTILITY_MEMCACHE = InMemoryObjectStore('branch_utility') |
| 45 BRANCH_UTILITY = BranchUtility(url_constants.OMAHA_PROXY_URL, | 47 BRANCH_UTILITY = BranchUtility(url_constants.OMAHA_PROXY_URL, |
| 46 AppEngineUrlFetcher(None), | 48 AppEngineUrlFetcher(None), |
| 47 BRANCH_UTILITY_MEMCACHE) | 49 BRANCH_UTILITY_MEMCACHE) |
| 48 | 50 |
| 51 AVAILABILITY_DATA_SOURCE_MEMCACHE = InMemoryObjectStore( | |
| 52 'availability_data_source') | |
| 53 | |
| 54 | |
| 49 GITHUB_MEMCACHE = InMemoryObjectStore('github') | 55 GITHUB_MEMCACHE = InMemoryObjectStore('github') |
| 50 GITHUB_FILE_SYSTEM = GithubFileSystem( | 56 GITHUB_FILE_SYSTEM = GithubFileSystem( |
| 51 AppEngineUrlFetcher(url_constants.GITHUB_URL), | 57 AppEngineUrlFetcher(url_constants.GITHUB_URL), |
| 52 GITHUB_MEMCACHE, | 58 GITHUB_MEMCACHE, |
| 53 AppEngineBlobstore()) | 59 AppEngineBlobstore()) |
| 54 GITHUB_COMPILED_FILE_SYSTEM = CompiledFileSystem.Factory(GITHUB_FILE_SYSTEM, | 60 GITHUB_COMPILED_FILE_SYSTEM = CompiledFileSystem.Factory(GITHUB_FILE_SYSTEM, |
| 55 GITHUB_MEMCACHE) | 61 GITHUB_MEMCACHE) |
| 56 | 62 |
| 57 EXTENSIONS_PATH = 'chrome/common/extensions' | 63 EXTENSIONS_PATH = 'chrome/common/extensions' |
| 58 DOCS_PATH = 'docs' | 64 DOCS_PATH = 'docs' |
| (...skipping 18 matching lines...) Expand all Loading... | |
| 77 return [UnixName(os.path.splitext(f.split('/')[-1])[0]) for f in files] | 83 return [UnixName(os.path.splitext(f.split('/')[-1])[0]) for f in files] |
| 78 | 84 |
| 79 def _CreateMemcacheFileSystem(branch, branch_memcache): | 85 def _CreateMemcacheFileSystem(branch, branch_memcache): |
| 80 svn_url = _GetURLFromBranch(branch) + '/' + EXTENSIONS_PATH | 86 svn_url = _GetURLFromBranch(branch) + '/' + EXTENSIONS_PATH |
| 81 stat_fetcher = AppEngineUrlFetcher( | 87 stat_fetcher = AppEngineUrlFetcher( |
| 82 svn_url.replace(url_constants.SVN_URL, url_constants.VIEWVC_URL)) | 88 svn_url.replace(url_constants.SVN_URL, url_constants.VIEWVC_URL)) |
| 83 fetcher = AppEngineUrlFetcher(svn_url) | 89 fetcher = AppEngineUrlFetcher(svn_url) |
| 84 return MemcacheFileSystem(SubversionFileSystem(fetcher, stat_fetcher), | 90 return MemcacheFileSystem(SubversionFileSystem(fetcher, stat_fetcher), |
| 85 branch_memcache) | 91 branch_memcache) |
| 86 | 92 |
| 87 _default_branch = BRANCH_UTILITY.GetBranchNumberForChannelName(_DEFAULT_CHANNEL) | 93 def _CreateMemcacheForBranch(branch_number): |
|
epeterson
2013/03/25 19:48:02
This is now a global method, which is passed to th
cduvall
2013/03/25 22:59:43
Which objects does it depend on? Can you just pass
| |
| 94 """Passed to the global instance of chromeVersionDataSource. Allows for | |
| 95 the creation of api_data_sources linked to |branch_number|, which is the | |
| 96 maximum branch number from a given version of chrome. | |
| 97 """ | |
| 98 branch_memcache = InMemoryObjectStore(branch_number) | |
| 99 file_system = _CreateMemcacheFileSystem(branch_number, branch_memcache) | |
| 100 cache_factory = CompiledFileSystem.Factory(file_system, branch_memcache) | |
| 101 api_list_data_source_factory = APIListDataSource.Factory( | |
| 102 cache_factory, | |
| 103 file_system, | |
| 104 API_PATH, | |
| 105 PUBLIC_TEMPLATE_PATH) | |
| 106 api_data_source_factory = APIDataSource.Factory(cache_factory, API_PATH) | |
| 107 ref_resolver_factory = ReferenceResolver.Factory( | |
| 108 api_data_source_factory, | |
| 109 api_list_data_source_factory, | |
| 110 branch_memcache) | |
| 111 api_data_source_factory.SetReferenceResolverFactory(ref_resolver_factory) | |
| 112 samples_data_source_factory = SamplesDataSource.Factory( | |
| 113 #channel_name, | |
| 114 'trunk', | |
|
epeterson
2013/03/25 19:48:02
This is really the only part that prevents this me
cduvall
2013/03/25 22:59:43
Can you just pass the channel name in as a functio
| |
| 115 file_system, | |
| 116 GITHUB_FILE_SYSTEM, | |
| 117 cache_factory, | |
| 118 GITHUB_COMPILED_FILE_SYSTEM, | |
| 119 ref_resolver_factory, | |
| 120 EXAMPLES_PATH) | |
| 121 api_data_source_factory.SetSamplesDataSourceFactory( | |
| 122 samples_data_source_factory) | |
| 123 return api_data_source_factory.Create(None) | |
| 124 | |
| 125 _default_branch = BRANCH_UTILITY.GetChannelInfoForChannelName( | |
| 126 _DEFAULT_CHANNEL)['branch'] | |
| 88 APPS_MEMCACHE = InMemoryObjectStore(_default_branch) | 127 APPS_MEMCACHE = InMemoryObjectStore(_default_branch) |
| 89 APPS_FILE_SYSTEM = _CreateMemcacheFileSystem(_default_branch, APPS_MEMCACHE) | 128 APPS_FILE_SYSTEM = _CreateMemcacheFileSystem(_default_branch, APPS_MEMCACHE) |
| 90 APPS_COMPILED_FILE_SYSTEM = CompiledFileSystem.Factory( | 129 APPS_COMPILED_FILE_SYSTEM = CompiledFileSystem.Factory( |
| 91 APPS_FILE_SYSTEM, | 130 APPS_FILE_SYSTEM, |
| 92 APPS_MEMCACHE).Create(_SplitFilenameUnix, compiled_fs.APPS_FS) | 131 APPS_MEMCACHE).Create(_SplitFilenameUnix, compiled_fs.APPS_FS) |
| 93 | 132 |
| 94 EXTENSIONS_MEMCACHE = InMemoryObjectStore(_default_branch) | 133 EXTENSIONS_MEMCACHE = InMemoryObjectStore(_default_branch) |
| 95 EXTENSIONS_FILE_SYSTEM = _CreateMemcacheFileSystem(_default_branch, | 134 EXTENSIONS_FILE_SYSTEM = _CreateMemcacheFileSystem(_default_branch, |
| 96 EXTENSIONS_MEMCACHE) | 135 EXTENSIONS_MEMCACHE) |
| 97 EXTENSIONS_COMPILED_FILE_SYSTEM = CompiledFileSystem.Factory( | 136 EXTENSIONS_COMPILED_FILE_SYSTEM = CompiledFileSystem.Factory( |
| 98 EXTENSIONS_FILE_SYSTEM, | 137 EXTENSIONS_FILE_SYSTEM, |
| 99 EXTENSIONS_MEMCACHE).Create(_SplitFilenameUnix, compiled_fs.EXTENSIONS_FS) | 138 EXTENSIONS_MEMCACHE).Create(_SplitFilenameUnix, compiled_fs.EXTENSIONS_FS) |
| 100 | 139 |
| 101 KNOWN_ISSUES_DATA_SOURCE = KnownIssuesDataSource( | 140 KNOWN_ISSUES_DATA_SOURCE = KnownIssuesDataSource( |
| 102 InMemoryObjectStore('KnownIssues'), | 141 InMemoryObjectStore('KnownIssues'), |
| 103 AppEngineUrlFetcher(None)) | 142 AppEngineUrlFetcher(None)) |
| 104 | 143 |
| 144 CHROME_VERSION_DATA_SOURCE_MEMCACHE = InMemoryObjectStore( | |
| 145 'chrome_version_data_source') | |
| 146 CHROME_VERSION_DATA_SOURCE = ChromeVersionDataSource( | |
| 147 url_constants.OMAHA_BETA_HISTORY, | |
| 148 AppEngineUrlFetcher(None), | |
| 149 CHROME_VERSION_DATA_SOURCE_MEMCACHE, | |
| 150 _CreateMemcacheForBranch) | |
| 151 | |
| 105 def _MakeInstanceKey(branch, number): | 152 def _MakeInstanceKey(branch, number): |
| 106 return '%s/%s' % (branch, number) | 153 return '%s/%s' % (branch, number) |
| 107 | 154 |
| 108 def _GetInstanceForBranch(channel_name, local_path): | 155 def _GetInstanceForBranch(channel_name, local_path): |
| 109 branch = BRANCH_UTILITY.GetBranchNumberForChannelName(channel_name) | 156 channel_info = BRANCH_UTILITY.GetChannelInfoForChannelName( |
| 157 channel_name) | |
| 158 branch = channel_info['branch'] | |
| 159 version = channel_info['version'] | |
| 110 | 160 |
| 111 # The key for the server is a tuple of |channel_name| with |branch|, since | 161 # The key for the server is a tuple of |channel_name| with |branch|, since |
| 112 # sometimes stable and beta point to the same branch. | 162 # sometimes stable and beta point to the same branch. |
| 113 instance_key = _MakeInstanceKey(channel_name, branch) | 163 instance_key = _MakeInstanceKey(channel_name, branch) |
| 114 instance = SERVER_INSTANCES.get(instance_key, None) | 164 instance = SERVER_INSTANCES.get(instance_key, None) |
| 115 if instance is not None: | 165 if instance is not None: |
| 116 return instance | 166 return instance |
| 117 | |
| 118 branch_memcache = InMemoryObjectStore(branch) | 167 branch_memcache = InMemoryObjectStore(branch) |
| 119 file_system = _CreateMemcacheFileSystem(branch, branch_memcache) | 168 file_system = _CreateMemcacheFileSystem(branch, branch_memcache) |
| 120 cache_factory = CompiledFileSystem.Factory(file_system, branch_memcache) | 169 cache_factory = CompiledFileSystem.Factory(file_system, branch_memcache) |
| 121 api_list_data_source_factory = APIListDataSource.Factory(cache_factory, | 170 api_list_data_source_factory = APIListDataSource.Factory(cache_factory, |
| 122 file_system, | 171 file_system, |
| 123 API_PATH, | 172 API_PATH, |
| 124 PUBLIC_TEMPLATE_PATH) | 173 PUBLIC_TEMPLATE_PATH) |
| 174 availability_data_source = AvailabilityDataSource( | |
| 175 CHROME_VERSION_DATA_SOURCE, | |
| 176 version, | |
| 177 AVAILABILITY_DATA_SOURCE_MEMCACHE) | |
| 125 api_data_source_factory = APIDataSource.Factory( | 178 api_data_source_factory = APIDataSource.Factory( |
| 126 cache_factory, | 179 cache_factory, |
| 127 API_PATH) | 180 API_PATH, |
| 181 availability_data_source) | |
| 128 | 182 |
| 129 # Give the ReferenceResolver a memcache, to speed up the lookup of | 183 # Give the ReferenceResolver a memcache, to speed up the lookup of |
| 130 # duplicate $refs. | 184 # duplicate $refs. |
| 131 ref_resolver_factory = ReferenceResolver.Factory( | 185 ref_resolver_factory = ReferenceResolver.Factory( |
| 132 api_data_source_factory, | 186 api_data_source_factory, |
| 133 api_list_data_source_factory, | 187 api_list_data_source_factory, |
| 134 branch_memcache) | 188 branch_memcache) |
| 135 api_data_source_factory.SetReferenceResolverFactory(ref_resolver_factory) | 189 api_data_source_factory.SetReferenceResolverFactory(ref_resolver_factory) |
| 136 samples_data_source_factory = SamplesDataSource.Factory( | 190 samples_data_source_factory = SamplesDataSource.Factory( |
| 137 channel_name, | 191 channel_name, |
| 138 file_system, | 192 file_system, |
| 139 GITHUB_FILE_SYSTEM, | 193 GITHUB_FILE_SYSTEM, |
| 140 cache_factory, | 194 cache_factory, |
| 141 GITHUB_COMPILED_FILE_SYSTEM, | 195 GITHUB_COMPILED_FILE_SYSTEM, |
| 142 ref_resolver_factory, | 196 ref_resolver_factory, |
| 143 EXAMPLES_PATH) | 197 EXAMPLES_PATH) |
| 144 api_data_source_factory.SetSamplesDataSourceFactory( | 198 api_data_source_factory.SetSamplesDataSourceFactory( |
| 145 samples_data_source_factory) | 199 samples_data_source_factory) |
| 146 intro_data_source_factory = IntroDataSource.Factory( | 200 intro_data_source_factory = IntroDataSource.Factory( |
| 147 cache_factory, | 201 cache_factory, |
| 148 ref_resolver_factory, | 202 ref_resolver_factory, |
| 149 [INTRO_PATH, ARTICLE_PATH]) | 203 [INTRO_PATH, ARTICLE_PATH]) |
| 150 sidenav_data_source_factory = SidenavDataSource.Factory(cache_factory, | 204 sidenav_data_source_factory = SidenavDataSource.Factory(cache_factory, |
| 151 JSON_PATH) | 205 JSON_PATH) |
|
cduvall
2013/03/25 22:59:43
revert
epeterson
2013/03/27 22:36:09
Done, in the new global method.
| |
| 152 template_data_source_factory = TemplateDataSource.Factory( | 206 template_data_source_factory = TemplateDataSource.Factory( |
| 153 channel_name, | 207 channel_name, |
| 154 api_data_source_factory, | 208 api_data_source_factory, |
| 155 api_list_data_source_factory, | 209 api_list_data_source_factory, |
| 156 intro_data_source_factory, | 210 intro_data_source_factory, |
| 157 samples_data_source_factory, | 211 samples_data_source_factory, |
| 158 KNOWN_ISSUES_DATA_SOURCE, | 212 KNOWN_ISSUES_DATA_SOURCE, |
| 159 sidenav_data_source_factory, | 213 sidenav_data_source_factory, |
| 160 cache_factory, | 214 cache_factory, |
| 161 ref_resolver_factory, | 215 ref_resolver_factory, |
| 162 PUBLIC_TEMPLATE_PATH, | 216 PUBLIC_TEMPLATE_PATH, |
| 163 PRIVATE_TEMPLATE_PATH) | 217 PRIVATE_TEMPLATE_PATH, |
| 218 BRANCH_UTILITY.GetChannelInfoForAllChannels()) | |
| 164 example_zipper = ExampleZipper(file_system, | 219 example_zipper = ExampleZipper(file_system, |
| 165 cache_factory, | 220 cache_factory, |
| 166 DOCS_PATH) | 221 DOCS_PATH) |
| 167 | |
| 168 instance = ServerInstance(template_data_source_factory, | 222 instance = ServerInstance(template_data_source_factory, |
| 169 example_zipper, | 223 example_zipper, |
| 170 cache_factory) | 224 cache_factory) |
| 171 SERVER_INSTANCES[instance_key] = instance | 225 SERVER_INSTANCES[instance_key] = instance |
| 172 return instance | 226 return instance |
| 173 | 227 |
| 174 def _CleanBranches(): | 228 def _CleanBranches(): |
| 175 keys = [_MakeInstanceKey(branch, number) | 229 keys = [_MakeInstanceKey(branch, number) |
| 176 for branch, number in BRANCH_UTILITY.GetAllBranchNumbers()] | 230 for branch, number in BRANCH_UTILITY.GetAllBranchNumbers()] |
| 177 for key in SERVER_INSTANCES.keys(): | 231 for key in SERVER_INSTANCES.keys(): |
| (...skipping 41 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 219 | 273 |
| 220 _CleanBranches() | 274 _CleanBranches() |
| 221 | 275 |
| 222 # Yes, do this after it's passed to RedirectBadPaths. That needs to know | 276 # Yes, do this after it's passed to RedirectBadPaths. That needs to know |
| 223 # whether or not a branch was specified. | 277 # whether or not a branch was specified. |
| 224 if channel_name is None: | 278 if channel_name is None: |
| 225 channel_name = _DEFAULT_CHANNEL | 279 channel_name = _DEFAULT_CHANNEL |
| 226 _GetInstanceForBranch(channel_name, self._local_path).Get(real_path, | 280 _GetInstanceForBranch(channel_name, self._local_path).Get(real_path, |
| 227 self.request, | 281 self.request, |
| 228 self.response) | 282 self.response) |
| 229 | |
| 230 def _Render(self, files, channel): | 283 def _Render(self, files, channel): |
| 231 original_response = self.response | 284 original_response = self.response |
| 232 for f in files: | 285 for f in files: |
| 233 if f.endswith('404.html'): | 286 if f.endswith('404.html'): |
| 234 continue | 287 continue |
| 235 path = channel + f.split(PUBLIC_TEMPLATE_PATH)[-1] | 288 path = channel + f.split(PUBLIC_TEMPLATE_PATH)[-1] |
| 236 self.request = _MockRequest(path) | 289 self.request = _MockRequest(path) |
| 237 self.response = _MockResponse() | 290 self.response = _MockResponse() |
| 238 try: | 291 try: |
| 239 self._HandleGet(path) | 292 self._HandleGet(path) |
| (...skipping 19 matching lines...) Expand all Loading... | |
| 259 # We could list all files in PUBLIC_TEMPLATE_PATH then render them. However, | 312 # We could list all files in PUBLIC_TEMPLATE_PATH then render them. However, |
| 260 # this would be inefficient in the common case where files haven't changed | 313 # this would be inefficient in the common case where files haven't changed |
| 261 # since the last cron. | 314 # since the last cron. |
| 262 # | 315 # |
| 263 # Instead, let the CompiledFileSystem give us clues when to re-render: we | 316 # Instead, let the CompiledFileSystem give us clues when to re-render: we |
| 264 # use the CFS to check whether the templates, examples, or API folders have | 317 # use the CFS to check whether the templates, examples, or API folders have |
| 265 # been changed. If there has been a change, the compilation function will | 318 # been changed. If there has been a change, the compilation function will |
| 266 # be called. The same is then done separately with the apps samples page, | 319 # be called. The same is then done separately with the apps samples page, |
| 267 # since it pulls its data from Github. | 320 # since it pulls its data from Github. |
| 268 channel = path.split('/')[-1] | 321 channel = path.split('/')[-1] |
| 269 branch = BRANCH_UTILITY.GetBranchNumberForChannelName(channel) | 322 branch = BRANCH_UTILITY.GetChannelInfoForChannelName(channel)['branch'] |
| 270 logging.info('Running cron job for %s.' % branch) | 323 logging.info('Running cron job for %s.' % branch) |
| 271 branch_memcache = InMemoryObjectStore(branch) | 324 branch_memcache = InMemoryObjectStore(branch) |
| 272 file_system = _CreateMemcacheFileSystem(branch, branch_memcache) | 325 file_system = _CreateMemcacheFileSystem(branch, branch_memcache) |
| 273 factory = CompiledFileSystem.Factory(file_system, branch_memcache) | 326 factory = CompiledFileSystem.Factory(file_system, branch_memcache) |
| 274 | 327 |
| 275 needs_render = self._ValueHolder(False) | 328 needs_render = self._ValueHolder(False) |
| 276 invalidation_cache = factory.Create(lambda _, __: needs_render.Set(True), | 329 invalidation_cache = factory.Create(lambda _, __: needs_render.Set(True), |
| 277 compiled_fs.CRON_INVALIDATION, | 330 compiled_fs.CRON_INVALIDATION, |
| 278 version=_VERSION) | 331 version=_VERSION) |
| 279 for path in [TEMPLATE_PATH, EXAMPLES_PATH, API_PATH]: | 332 for path in [TEMPLATE_PATH, EXAMPLES_PATH, API_PATH]: |
| (...skipping 87 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 367 | 420 |
| 368 # Redirect paths like "directory" to "directory/". This is so relative | 421 # Redirect paths like "directory" to "directory/". This is so relative |
| 369 # file paths will know to treat this as a directory. | 422 # file paths will know to treat this as a directory. |
| 370 if os.path.splitext(path)[1] == '' and path[-1] != '/': | 423 if os.path.splitext(path)[1] == '' and path[-1] != '/': |
| 371 self.redirect(path + '/') | 424 self.redirect(path + '/') |
| 372 return | 425 return |
| 373 | 426 |
| 374 path = path.strip('/') | 427 path = path.strip('/') |
| 375 if not self._RedirectFromCodeDotGoogleDotCom(path): | 428 if not self._RedirectFromCodeDotGoogleDotCom(path): |
| 376 self._HandleGet(path) | 429 self._HandleGet(path) |
| OLD | NEW |