1 # Copyright (C) 2010-2012 Chris Ball <cjb@laptop.org>
2 # W. Trevor King <wking@drexel.edu>
4 # This file is part of Bugs Everywhere.
6 # Bugs Everywhere is free software: you can redistribute it and/or modify it
7 # under the terms of the GNU General Public License as published by the Free
8 # Software Foundation, either version 2 of the License, or (at your option) any
11 # Bugs Everywhere is distributed in the hope that it will be useful, but
12 # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
16 # You should have received a copy of the GNU General Public License along with
17 # Bugs Everywhere. If not, see <http://www.gnu.org/licenses/>.
19 """Define an HTTP-based :class:`~libbe.storage.base.VersionedStorage`
24 :mod:`libbe.command.serve_storage` : the associated server
27 from __future__ import absolute_import
34 import libbe.util.http
35 from libbe.util.http import HTTP_VALID, HTTP_USER_ERROR
38 from libbe import TESTING
47 import libbe.command.serve_storage
48 import libbe.util.http
49 import libbe.util.wsgi
52 class HTTP (base.VersionedStorage):
53 """:class:`~libbe.storage.base.VersionedStorage` implementation over
56 Uses GET to retrieve information and POST to set information.
59 user_agent = 'BE-HTTP-Storage'
61 def __init__(self, repo, *args, **kwargs):
62 repo,self.uname,self.password = self.parse_repo(repo)
63 base.VersionedStorage.__init__(self, repo, *args, **kwargs)
65 def parse_repo(self, repo):
66 """Grab username and password (if any) from the repo URL.
71 >>> s = HTTP('http://host.com/path/to/repo')
73 'http://host.com/path/to/repo'
76 >>> s.password == None
78 >>> s.parse_repo('http://joe:secret@host.com/path/to/repo')
79 ('http://host.com/path/to/repo', 'joe', 'secret')
81 scheme,netloc,path,params,query,fragment = urlparse.urlparse(repo)
82 parts = netloc.split('@', 1)
84 uname,password = parts[0].split(':')
85 repo = urlparse.urlunparse(
86 (scheme, parts[1], path, params, query, fragment))
88 uname,password = (None, None)
89 return (repo, uname, password)
91 def get_post_url(self, url, get=True, data_dict=None, headers=[]):
92 if self.uname != None and self.password != None:
93 headers.append(('Authorization','Basic %s' % \
94 ('%s:%s' % (self.uname, self.password)).encode('base64')))
95 return libbe.util.http.get_post_url(
96 url, get, data_dict=data_dict, headers=headers,
97 agent=self.user_agent)
99 def storage_version(self, revision=None):
100 """Return the storage format for this backend."""
101 return libbe.storage.STORAGE_VERSION
104 """Create a new storage repository."""
105 raise base.NotSupported(
106 'init', 'Cannot initialize this repository format.')
109 """Remove the storage repository."""
110 raise base.NotSupported(
111 'destroy', 'Cannot destroy this repository format.')
114 self.check_storage_version()
116 def _disconnect(self):
119 def _add(self, id, parent=None, directory=False):
120 url = urlparse.urljoin(self.repo, 'add')
121 page,final_url,info = self.get_post_url(
123 data_dict={'id':id, 'parent':parent, 'directory':directory})
125 def _exists(self, id, revision=None):
126 url = urlparse.urljoin(self.repo, 'exists')
127 page,final_url,info = self.get_post_url(
129 data_dict={'id':id, 'revision':revision})
134 def _remove(self, id):
135 url = urlparse.urljoin(self.repo, 'remove')
136 page,final_url,info = self.get_post_url(
138 data_dict={'id':id, 'recursive':False})
140 def _recursive_remove(self, id):
141 url = urlparse.urljoin(self.repo, 'remove')
142 page,final_url,info = self.get_post_url(
144 data_dict={'id':id, 'recursive':True})
146 def _ancestors(self, id=None, revision=None):
147 url = urlparse.urljoin(self.repo, 'ancestors')
148 page,final_url,info = self.get_post_url(
150 data_dict={'id':id, 'revision':revision})
151 return page.strip('\n').splitlines()
153 def _children(self, id=None, revision=None):
154 url = urlparse.urljoin(self.repo, 'children')
155 page,final_url,info = self.get_post_url(
157 data_dict={'id':id, 'revision':revision})
158 return page.strip('\n').splitlines()
160 def _get(self, id, default=base.InvalidObject, revision=None):
161 url = urlparse.urljoin(self.repo, '/'.join(['get', id]))
163 page,final_url,info = self.get_post_url(
165 data_dict={'revision':revision})
166 except libbe.util.http.HTTPError, e:
167 if not (hasattr(e.error, 'code') and e.error.code in HTTP_VALID):
169 elif default == base.InvalidObject:
170 raise base.InvalidID(id)
172 version = info['X-BE-Version']
173 if version != libbe.storage.STORAGE_VERSION:
174 raise base.InvalidStorageVersion(
175 version, libbe.storage.STORAGE_VERSION)
178 def _set(self, id, value):
179 url = urlparse.urljoin(self.repo, '/'.join(['set', id]))
181 page,final_url,info = self.get_post_url(
183 data_dict={'value':value})
184 except libbe.util.http.HTTPError, e:
185 if not (hasattr(e.error, 'code') and e.error.code in HTTP_VALID):
187 if e.error.code == HTTP_USER_ERROR \
188 and not 'InvalidID' in str(e.error):
189 raise base.InvalidDirectory(
190 'Directory %s cannot have data' % id)
191 raise base.InvalidID(id)
193 def _commit(self, summary, body=None, allow_empty=False):
194 url = urlparse.urljoin(self.repo, 'commit')
196 page,final_url,info = self.get_post_url(
198 data_dict={'summary':summary, 'body':body,
199 'allow_empty':allow_empty})
200 except libbe.util.http.HTTPError, e:
201 if not (hasattr(e.error, 'code') and e.error.code in HTTP_VALID):
203 if e.error.code == HTTP_USER_ERROR:
204 raise base.EmptyCommit
205 raise base.InvalidID(id)
206 return page.rstrip('\n')
208 def revision_id(self, index=None):
209 """Return the name of the <index>th revision.
211 The choice of which branch to follow when crossing
212 branches/merges is not defined. Revision indices start at 1;
213 ID 0 is the blank repository.
215 Return None if index==None.
220 If the specified revision does not exist.
225 if int(index) != index:
226 raise base.InvalidRevision(index)
228 raise base.InvalidRevision(index)
229 url = urlparse.urljoin(self.repo, 'revision-id')
231 page,final_url,info = self.get_post_url(
233 data_dict={'index':index})
234 except libbe.util.http.HTTPError, e:
235 if not (hasattr(e.error, 'code') and e.error.code in HTTP_VALID):
237 if e.error.code == HTTP_USER_ERROR:
238 raise base.InvalidRevision(index)
239 raise base.InvalidID(id)
240 return page.rstrip('\n')
242 def changed(self, revision=None):
243 url = urlparse.urljoin(self.repo, 'changed')
244 page,final_url,info = self.get_post_url(
246 data_dict={'revision':revision})
247 lines = page.strip('\n')
248 new,mod,rem = [p.splitlines() for p in page.split('\n\n')]
249 return (new, mod, rem)
251 def check_storage_version(self):
252 version = self.storage_version()
253 if version != libbe.storage.STORAGE_VERSION:
254 raise base.InvalidStorageVersion(
255 version, libbe.storage.STORAGE_VERSION)
257 def storage_version(self, revision=None):
258 url = urlparse.urljoin(self.repo, 'version')
259 page,final_url,info = self.get_post_url(
260 url, get=True, data_dict={'revision':revision})
261 return page.rstrip('\n')
264 class TestingHTTP (HTTP):
266 def __init__(self, repo, *args, **kwargs):
267 self._storage_backend = base.VersionedStorage(repo)
268 app = libbe.command.serve_storage.ServerApp(
269 storage=self._storage_backend)
270 self.app = libbe.util.wsgi.BEExceptionApp(app=app)
271 HTTP.__init__(self, repo='http://localhost:8000/', *args, **kwargs)
272 self.intitialized = False
273 # duplicated from libbe.util.wsgi.WSGITestCase
274 self.default_environ = {
275 'REQUEST_METHOD': 'GET', # 'POST', 'HEAD'
276 'REMOTE_ADDR': '192.168.0.123',
279 #'QUERY_STRING':'', # may be empty or absent
280 #'CONTENT_TYPE':'', # may be empty or absent
281 #'CONTENT_LENGTH':'', # may be empty or absent
282 'SERVER_NAME':'example.com',
284 'SERVER_PROTOCOL':'HTTP/1.1',
285 'wsgi.version':(1,0),
286 'wsgi.url_scheme':'http',
287 'wsgi.input':StringIO.StringIO(),
288 'wsgi.errors':StringIO.StringIO(),
289 'wsgi.multithread':False,
290 'wsgi.multiprocess':False,
291 'wsgi.run_once':False,
293 def getURL(self, app, path='/', method='GET', data=None,
294 scheme='http', environ={}):
295 # duplicated from libbe.util.wsgi.WSGITestCase
296 env = copy.copy(self.default_environ)
297 env['PATH_INFO'] = path
298 env['REQUEST_METHOD'] = method
299 env['scheme'] = scheme
301 enc_data = urllib.urlencode(data)
303 env['CONTENT_LENGTH'] = len(enc_data)
304 env['wsgi.input'] = StringIO.StringIO(enc_data)
306 assert method in ['GET', 'HEAD'], method
307 env['QUERY_STRING'] = enc_data
308 for key,value in environ.items():
311 result = app(env, self.start_response)
312 except libbe.util.wsgi.HandlerError as e:
313 raise libbe.util.http.HTTPError(error=e, url=path, msg=str(e))
314 return ''.join(result)
315 def start_response(self, status, response_headers, exc_info=None):
317 self.response_headers = response_headers
318 self.exc_info = exc_info
319 def get_post_url(self, url, get=True, data_dict=None, headers=[]):
324 scheme,netloc,path,params,query,fragment = urlparse.urlparse(url)
326 for header_name,header_value in headers:
327 environ['HTTP_%s' % header_name] = header_value
328 output = self.getURL(
329 self.app, path, method, data_dict, scheme, environ)
330 if self.status != '200 OK':
331 class __estr (object):
332 def __init__(self, string):
334 self.code = int(string.split()[0])
337 error = __estr(self.status)
338 raise libbe.util.http.HTTPError(
339 error=error, url=url, msg=output)
340 info = dict(self.response_headers)
341 return (output, url, info)
346 except base.NotSupported:
348 self._storage_backend._init()
353 except base.NotSupported:
355 self._storage_backend._destroy()
357 self._storage_backend._connect()
359 def _disconnect(self):
360 HTTP._disconnect(self)
361 self._storage_backend._disconnect()
364 base.make_versioned_storage_testcase_subclasses(
365 TestingHTTP, sys.modules[__name__])
367 unitsuite =unittest.TestLoader().loadTestsFromModule(sys.modules[__name__])
368 suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()])