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.
7 from StringIO import StringIO
10 from appengine_blobstore import AppEngineBlobstore, BLOBSTORE_GITHUB
11 from appengine_url_fetcher import AppEngineUrlFetcher
12 from appengine_wrappers import urlfetch, blobstore
13 from docs_server_utils import StringIdentity
14 from file_system import FileSystem, StatInfo
15 from future import Future
16 from path_util import IsDirectory
18 from zipfile import ZipFile, BadZipfile
24 def _MakeBlobstoreKey(version):
25 return ZIP_KEY + '.' + str(version)
27 class _AsyncFetchFutureZip(object):
35 self._fetcher = fetcher
36 self._fetch = fetcher.FetchAsync(ZIP_KEY,
39 self._blobstore = blobstore
40 self._key_to_set = key_to_set
41 self._key_to_delete = key_to_delete
45 result = self._fetch.Get()
46 # Check if Github authentication failed.
47 if result.status_code == 401:
48 logging.error('Github authentication failed for %s, falling back to '
49 'unauthenticated.' % USERNAME)
50 blob = self._fetcher.Fetch(ZIP_KEY).content
53 except urlfetch.DownloadError as e:
54 logging.error('Bad github zip file: %s' % e)
56 if self._key_to_delete is not None:
57 self._blobstore.Delete(_MakeBlobstoreKey(self._key_to_delete),
60 return_zip = ZipFile(StringIO(blob))
61 except BadZipfile as e:
62 logging.error('Bad github zip file: %s' % e)
65 self._blobstore.Set(_MakeBlobstoreKey(self._key_to_set),
70 class GithubFileSystem(FileSystem):
72 def CreateChromeAppsSamples(object_store_creator):
73 return GithubFileSystem(
74 '%s/GoogleChrome/chrome-app-samples' % url_constants.GITHUB_REPOS,
78 def __init__(self, url, blobstore, object_store_creator):
79 # If we key the password store on the app version then the whole advantage
80 # of having it in the first place is greatly lessened (likewise it should
81 # always start populated).
82 password_store = object_store_creator.Create(
88 password_data = password_store.GetMulti(('username', 'password')).Get()
89 self._username, self._password = (password_data.get('username'),
90 password_data.get('password'))
92 password_store.SetMulti({'username': USERNAME, 'password': PASSWORD})
93 self._username, self._password = (USERNAME, PASSWORD)
96 self._fetcher = AppEngineUrlFetcher(url)
97 self._blobstore = blobstore
98 self._stat_object_store = object_store_creator.Create(GithubFileSystem)
100 self._GetZip(self.Stat(ZIP_KEY).version)
102 def _GetZip(self, version):
104 blob = self._blobstore.Get(_MakeBlobstoreKey(version), BLOBSTORE_GITHUB)
105 except blobstore.BlobNotFoundError:
106 self._zip_file = Future(value=None)
110 self._zip_file = Future(value=ZipFile(StringIO(blob)))
111 except BadZipfile as e:
112 self._blobstore.Delete(_MakeBlobstoreKey(version), BLOBSTORE_GITHUB)
113 logging.error('Bad github zip file: %s' % e)
114 self._zip_file = Future(value=None)
116 self._zip_file = Future(
117 delegate=_AsyncFetchFutureZip(self._fetcher,
122 key_to_delete=self._version))
123 self._version = version
125 def _ReadFile(self, path):
127 zip_file = self._zip_file.Get()
128 except Exception as e:
129 logging.error('Github ReadFile error: %s' % e)
132 logging.error('Bad github zip file.')
134 prefix = zip_file.namelist()[0]
135 return zip_file.read(prefix + path)
137 def _ListDir(self, path):
139 zip_file = self._zip_file.Get()
140 except Exception as e:
141 logging.error('Github ListDir error: %s' % e)
144 logging.error('Bad github zip file.')
146 filenames = zip_file.namelist()
147 # Take out parent directory name (GoogleChrome-chrome-app-samples-c78a30f)
148 filenames = [f[len(filenames[0]):] for f in filenames]
149 # Remove the path of the directory we're listing from the filenames.
150 filenames = [f[len(path):] for f in filenames
151 if f != path and f.startswith(path)]
152 # Remove all files not directly in this directory.
153 return [f for f in filenames if f[:-1].count('/') == 0]
155 def Read(self, paths):
156 version = self.Stat(ZIP_KEY).version
157 if version != self._version:
158 self._GetZip(version)
161 if IsDirectory(path):
162 result[path] = self._ListDir(path)
164 result[path] = self._ReadFile(path)
165 return Future(value=result)
167 def _DefaultStat(self, path):
169 # TODO(kalman): we should replace all of this by wrapping the
170 # GithubFileSystem in a CachingFileSystem. A lot of work has been put into
171 # CFS to be robust, and GFS is missing out.
172 # For example: the following line is wrong, but it could be moot.
173 self._stat_object_store.Set(path, version)
174 return StatInfo(version)
176 def Stat(self, path):
177 version = self._stat_object_store.Get(path).Get()
178 if version is not None:
179 return StatInfo(version)
181 result = self._fetcher.Fetch('commits/HEAD',
184 except urlfetch.DownloadError as e:
185 logging.warning('GithubFileSystem Stat: %s' % e)
186 return self._DefaultStat(path)
188 # Check if Github authentication failed.
189 if result.status_code == 401:
190 logging.warning('Github authentication failed for %s, falling back to '
191 'unauthenticated.' % USERNAME)
193 result = self._fetcher.Fetch('commits/HEAD')
194 except urlfetch.DownloadError as e:
195 logging.warning('GithubFileSystem Stat: %s' % e)
196 return self._DefaultStat(path)
198 # Parse response JSON - but sometimes github gives us invalid JSON.
200 version = json.loads(result.content)['sha']
201 self._stat_object_store.Set(path, version)
202 return StatInfo(version)
203 except StandardError as e:
205 ('%s: got invalid or unexpected JSON from github. Response status ' +
206 'was %s, content %s') % (e, result.status_code, result.content))
207 return self._DefaultStat(path)
209 def GetIdentity(self):
210 return '%s@%s' % (self.__class__.__name__, StringIdentity(self._url))