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
9 from appengine_blobstore
import AppEngineBlobstore
, BLOBSTORE_GITHUB
10 from appengine_url_fetcher
import AppEngineUrlFetcher
11 from appengine_wrappers
import urlfetch
, blobstore
12 from docs_server_utils
import StringIdentity
13 from file_system
import FileSystem
, StatInfo
14 from future
import Future
16 from zipfile
import ZipFile
, BadZipfile
22 def _MakeBlobstoreKey(version
):
23 return ZIP_KEY
+ '.' + str(version
)
25 class _AsyncFetchFutureZip(object):
33 self
._fetcher
= fetcher
34 self
._fetch
= fetcher
.FetchAsync(ZIP_KEY
,
37 self
._blobstore
= blobstore
38 self
._key
_to
_set
= key_to_set
39 self
._key
_to
_delete
= key_to_delete
43 result
= self
._fetch
.Get()
44 # Check if Github authentication failed.
45 if result
.status_code
== 401:
46 logging
.error('Github authentication failed for %s, falling back to '
47 'unauthenticated.' % USERNAME
)
48 blob
= self
._fetcher
.Fetch(ZIP_KEY
).content
51 except urlfetch
.DownloadError
as e
:
52 logging
.error('Bad github zip file: %s' % e
)
54 if self
._key
_to
_delete
is not None:
55 self
._blobstore
.Delete(_MakeBlobstoreKey(self
._key
_to
_delete
),
58 return_zip
= ZipFile(StringIO(blob
))
59 except BadZipfile
as e
:
60 logging
.error('Bad github zip file: %s' % e
)
63 self
._blobstore
.Set(_MakeBlobstoreKey(self
._key
_to
_set
),
68 class GithubFileSystem(FileSystem
):
70 def CreateChromeAppsSamples(object_store_creator
):
71 return GithubFileSystem(
72 '%s/GoogleChrome/chrome-app-samples' % url_constants
.GITHUB_REPOS
,
76 def __init__(self
, url
, blobstore
, object_store_creator
):
77 # If we key the password store on the app version then the whole advantage
78 # of having it in the first place is greatly lessened (likewise it should
79 # always start populated).
80 password_store
= object_store_creator
.Create(
86 password_data
= password_store
.GetMulti(('username', 'password')).Get()
87 self
._username
, self
._password
= (password_data
.get('username'),
88 password_data
.get('password'))
90 password_store
.SetMulti({'username': USERNAME
, 'password': PASSWORD
})
91 self
._username
, self
._password
= (USERNAME
, PASSWORD
)
94 self
._fetcher
= AppEngineUrlFetcher(url
)
95 self
._blobstore
= blobstore
96 self
._stat
_object
_store
= object_store_creator
.Create(GithubFileSystem
)
98 self
._GetZip
(self
.Stat(ZIP_KEY
).version
)
100 def _GetZip(self
, version
):
102 blob
= self
._blobstore
.Get(_MakeBlobstoreKey(version
), BLOBSTORE_GITHUB
)
103 except blobstore
.BlobNotFoundError
:
104 self
._zip
_file
= Future(value
=None)
108 self
._zip
_file
= Future(value
=ZipFile(StringIO(blob
)))
109 except BadZipfile
as e
:
110 self
._blobstore
.Delete(_MakeBlobstoreKey(version
), BLOBSTORE_GITHUB
)
111 logging
.error('Bad github zip file: %s' % e
)
112 self
._zip
_file
= Future(value
=None)
114 self
._zip
_file
= Future(
115 delegate
=_AsyncFetchFutureZip(self
._fetcher
,
120 key_to_delete
=self
._version
))
121 self
._version
= version
123 def _ReadFile(self
, path
):
125 zip_file
= self
._zip
_file
.Get()
126 except Exception as e
:
127 logging
.error('Github ReadFile error: %s' % e
)
130 logging
.error('Bad github zip file.')
132 prefix
= zip_file
.namelist()[0][:-1]
133 return zip_file
.read(prefix
+ path
)
135 def _ListDir(self
, path
):
137 zip_file
= self
._zip
_file
.Get()
138 except Exception as e
:
139 logging
.error('Github ListDir error: %s' % e
)
142 logging
.error('Bad github zip file.')
144 filenames
= zip_file
.namelist()
145 # Take out parent directory name (GoogleChrome-chrome-app-samples-c78a30f)
146 filenames
= [f
[len(filenames
[0]) - 1:] for f
in filenames
]
147 # Remove the path of the directory we're listing from the filenames.
148 filenames
= [f
[len(path
):] for f
in filenames
149 if f
!= path
and f
.startswith(path
)]
150 # Remove all files not directly in this directory.
151 return [f
for f
in filenames
if f
[:-1].count('/') == 0]
153 def Read(self
, paths
):
154 version
= self
.Stat(ZIP_KEY
).version
155 if version
!= self
._version
:
156 self
._GetZip
(version
)
159 if path
.endswith('/'):
160 result
[path
] = self
._ListDir
(path
)
162 result
[path
] = self
._ReadFile
(path
)
163 return Future(value
=result
)
165 def _DefaultStat(self
, path
):
167 # TODO(kalman): we should replace all of this by wrapping the
168 # GithubFileSystem in a CachingFileSystem. A lot of work has been put into
169 # CFS to be robust, and GFS is missing out.
170 # For example: the following line is wrong, but it could be moot.
171 self
._stat
_object
_store
.Set(path
, version
)
172 return StatInfo(version
)
174 def Stat(self
, path
):
175 version
= self
._stat
_object
_store
.Get(path
).Get()
176 if version
is not None:
177 return StatInfo(version
)
179 result
= self
._fetcher
.Fetch('commits/HEAD',
182 except urlfetch
.DownloadError
as e
:
183 logging
.warning('GithubFileSystem Stat: %s' % e
)
184 return self
._DefaultStat
(path
)
186 # Check if Github authentication failed.
187 if result
.status_code
== 401:
188 logging
.warning('Github authentication failed for %s, falling back to '
189 'unauthenticated.' % USERNAME
)
191 result
= self
._fetcher
.Fetch('commits/HEAD')
192 except urlfetch
.DownloadError
as e
:
193 logging
.warning('GithubFileSystem Stat: %s' % e
)
194 return self
._DefaultStat
(path
)
196 # Parse response JSON - but sometimes github gives us invalid JSON.
198 version
= json
.loads(result
.content
)['sha']
199 self
._stat
_object
_store
.Set(path
, version
)
200 return StatInfo(version
)
201 except StandardError as e
:
203 ('%s: got invalid or unexpected JSON from github. Response status ' +
204 'was %s, content %s') % (e
, result
.status_code
, result
.content
))
205 return self
._DefaultStat
(path
)
207 def GetIdentity(self
):
208 return '%s@%s' % (self
.__class
__.__name
__, StringIdentity(self
._url
))