github_file_system.py revision c2e0dbddbe15c98d52c4786dac06cb8952a8ae6d
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. 4 5import json 6import logging 7import os 8 9import appengine_blobstore as blobstore 10from appengine_wrappers import GetAppVersion, urlfetch 11from file_system import FileSystem, StatInfo 12from future import Future 13from object_store_creator import ObjectStoreCreator 14from StringIO import StringIO 15from zipfile import ZipFile, BadZipfile 16 17ZIP_KEY = 'zipball' 18USERNAME = None 19PASSWORD = None 20 21def _MakeBlobstoreKey(version): 22 return ZIP_KEY + '.' + str(version) 23 24class _AsyncFetchFutureZip(object): 25 def __init__(self, 26 fetcher, 27 username, 28 password, 29 blobstore, 30 key_to_set, 31 key_to_delete=None): 32 self._fetcher = fetcher 33 self._fetch = fetcher.FetchAsync(ZIP_KEY, 34 username=username, 35 password=password) 36 self._blobstore = blobstore 37 self._key_to_set = key_to_set 38 self._key_to_delete = key_to_delete 39 40 def Get(self): 41 try: 42 result = self._fetch.Get() 43 # Check if Github authentication failed. 44 if result.status_code == 401: 45 logging.error('Github authentication failed for %s, falling back to ' 46 'unauthenticated.' % USERNAME) 47 blob = self._fetcher.Fetch(ZIP_KEY).content 48 else: 49 blob = result.content 50 except urlfetch.DownloadError as e: 51 logging.error('Bad github zip file: %s' % e) 52 return None 53 if self._key_to_delete is not None: 54 self._blobstore.Delete(_MakeBlobstoreKey(self._key_to_delete), 55 blobstore.BLOBSTORE_GITHUB) 56 try: 57 return_zip = ZipFile(StringIO(blob)) 58 except BadZipfile as e: 59 logging.error('Bad github zip file: %s' % e) 60 return None 61 62 self._blobstore.Set(_MakeBlobstoreKey(self._key_to_set), 63 blob, 64 blobstore.BLOBSTORE_GITHUB) 65 return return_zip 66 67class GithubFileSystem(FileSystem): 68 """FileSystem implementation which fetches resources from github. 69 """ 70 def __init__(self, fetcher, blobstore): 71 # The password store is the same for all branches and versions. 72 password_store = (ObjectStoreCreator.GlobalFactory() 73 .Create(GithubFileSystem).Create(category='password')) 74 if USERNAME is None: 75 password_data = password_store.GetMulti(('username', 'password')).Get() 76 self._username, self._password = (password_data.get('username'), 77 password_data.get('password')) 78 else: 79 password_store.SetMulti({'username': USERNAME, 'password': PASSWORD}) 80 self._username, self._password = (USERNAME, PASSWORD) 81 82 self._fetcher = fetcher 83 self._blobstore = blobstore 84 self._stat_object_store = (ObjectStoreCreator.SharedFactory(GetAppVersion()) 85 .Create(GithubFileSystem).Create()) 86 self._version = None 87 self._GetZip(self.Stat(ZIP_KEY).version) 88 89 def _GetZip(self, version): 90 blob = self._blobstore.Get(_MakeBlobstoreKey(version), 91 blobstore.BLOBSTORE_GITHUB) 92 if blob is not None: 93 try: 94 self._zip_file = Future(value=ZipFile(StringIO(blob))) 95 except BadZipfile as e: 96 self._blobstore.Delete(_MakeBlobstoreKey(version), 97 blobstore.BLOBSTORE_GITHUB) 98 logging.error('Bad github zip file: %s' % e) 99 self._zip_file = Future(value=None) 100 else: 101 self._zip_file = Future( 102 delegate=_AsyncFetchFutureZip(self._fetcher, 103 self._username, 104 self._password, 105 self._blobstore, 106 version, 107 key_to_delete=self._version)) 108 self._version = version 109 110 def _ReadFile(self, path): 111 try: 112 zip_file = self._zip_file.Get() 113 except Exception as e: 114 logging.error('Github ReadFile error: %s' % e) 115 return '' 116 if zip_file is None: 117 logging.error('Bad github zip file.') 118 return '' 119 prefix = zip_file.namelist()[0][:-1] 120 return zip_file.read(prefix + path) 121 122 def _ListDir(self, path): 123 try: 124 zip_file = self._zip_file.Get() 125 except Exception as e: 126 logging.error('Github ListDir error: %s' % e) 127 return [] 128 if zip_file is None: 129 logging.error('Bad github zip file.') 130 return [] 131 filenames = zip_file.namelist() 132 # Take out parent directory name (GoogleChrome-chrome-app-samples-c78a30f) 133 filenames = [f[len(filenames[0]) - 1:] for f in filenames] 134 # Remove the path of the directory we're listing from the filenames. 135 filenames = [f[len(path):] for f in filenames 136 if f != path and f.startswith(path)] 137 # Remove all files not directly in this directory. 138 return [f for f in filenames if f[:-1].count('/') == 0] 139 140 def Read(self, paths, binary=False): 141 version = self.Stat(ZIP_KEY).version 142 if version != self._version: 143 self._GetZip(version) 144 result = {} 145 for path in paths: 146 if path.endswith('/'): 147 result[path] = self._ListDir(path) 148 else: 149 result[path] = self._ReadFile(path) 150 return Future(value=result) 151 152 def _DefaultStat(self, path): 153 version = 0 154 # TODO(kalman): we should replace all of this by wrapping the 155 # GithubFileSystem in a CachingFileSystem. A lot of work has been put into 156 # CFS to be robust, and GFS is missing out. 157 # For example: the following line is wrong, but it could be moot. 158 self._stat_object_store.Set(path, version) 159 return StatInfo(version) 160 161 def Stat(self, path): 162 version = self._stat_object_store.Get(path).Get() 163 if version is not None: 164 return StatInfo(version) 165 try: 166 result = self._fetcher.Fetch('commits/HEAD', 167 username=USERNAME, 168 password=PASSWORD) 169 except urlfetch.DownloadError as e: 170 logging.error('GithubFileSystem Stat: %s' % e) 171 return self._DefaultStat(path) 172 # Check if Github authentication failed. 173 if result.status_code == 401: 174 logging.error('Github authentication failed for %s, falling back to ' 175 'unauthenticated.' % USERNAME) 176 try: 177 result = self._fetcher.Fetch('commits/HEAD') 178 except urlfetch.DownloadError as e: 179 logging.error('GithubFileSystem Stat: %s' % e) 180 return self._DefaultStat(path) 181 version = (json.loads(result.content).get('commit', {}) 182 .get('tree', {}) 183 .get('sha', None)) 184 # Check if the JSON was valid, and set to 0 if not. 185 if version is not None: 186 self._stat_object_store.Set(path, version) 187 else: 188 logging.warning('Problem fetching commit hash from github.') 189 return self._DefaultStat(path) 190 return StatInfo(version) 191