diff options
author | W. Trevor King <wking@tremily.us> | 2012-08-29 23:55:37 -0400 |
---|---|---|
committer | W. Trevor King <wking@tremily.us> | 2012-08-29 23:55:37 -0400 |
commit | 0336db8d4052a319f1d959a33fc847814551e3f8 (patch) | |
tree | 706d9f132199c3feb1621a2e324215ae9a2b535a /libbe/command/serve_storage.py | |
parent | 4db1a045a0606bead191a563abc54dfa8352efe0 (diff) | |
download | bugseverywhere-0336db8d4052a319f1d959a33fc847814551e3f8.tar.gz |
command:serve-storage: rename `be serve` -> `be serve-storage`.
This will help avoid confusion between
be serve-storage
and
be serve-commands
Diffstat (limited to 'libbe/command/serve_storage.py')
-rw-r--r-- | libbe/command/serve_storage.py | 353 |
1 files changed, 353 insertions, 0 deletions
diff --git a/libbe/command/serve_storage.py b/libbe/command/serve_storage.py new file mode 100644 index 0000000..966c932 --- /dev/null +++ b/libbe/command/serve_storage.py @@ -0,0 +1,353 @@ +# Copyright (C) 2010-2012 Chris Ball <cjb@laptop.org> +# W. Trevor King <wking@drexel.edu> +# +# This file is part of Bugs Everywhere. +# +# Bugs Everywhere is free software: you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the Free +# Software Foundation, either version 2 of the License, or (at your option) any +# later version. +# +# Bugs Everywhere is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along with +# Bugs Everywhere. If not, see <http://www.gnu.org/licenses/>. + +"""Define the :class:`Serve` serving BE Storage over HTTP. + +See Also +-------- +:mod:`libbe.storage.http` : the associated client +""" + +import logging +import os.path + +import libbe +import libbe.command +import libbe.command.util +import libbe.util.subproc +import libbe.util.wsgi +import libbe.version + +if libbe.TESTING: + import copy + import doctest + import StringIO + import sys + import unittest + import wsgiref.validate + try: + import cherrypy.test.webtest + cherrypy_test_webtest = True + except ImportError: + cherrypy_test_webtest = None + + import libbe.bugdir + import libbe.util.wsgi + + +class ServerApp (libbe.util.wsgi.WSGI_AppObject, + libbe.util.wsgi.WSGI_DataObject): + """WSGI server for a BE Storage instance over HTTP. + + RESTful_ WSGI request handler for serving the + libbe.storage.http.HTTP backend with GET, POST, and HEAD commands. + For more information on authentication and REST, see John + Calcote's `Open Sourcery article`_ + + .. _RESTful: http://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm + .. _Open Sourcery article: http://jcalcote.wordpress.com/2009/08/10/restful-authentication/ + + This serves files from a connected storage instance, usually + a VCS-based repository located on the local machine. + + Notes + ----- + + The GET and HEAD requests are identical except that the HEAD + request omits the actual content of the file. + """ + server_version = "BE-server/" + libbe.version.version() + + def __init__(self, storage=None, notify=False, **kwargs): + super(ServerApp, self).__init__( + urls=[ + (r'^add/?', self.add), + (r'^exists/?', self.exists), + (r'^remove/?', self.remove), + (r'^ancestors/?', self.ancestors), + (r'^children/?', self.children), + (r'^get/(.+)', self.get), + (r'^set/(.+)', self.set), + (r'^commit/?', self.commit), + (r'^revision-id/?', self.revision_id), + (r'^changed/?', self.changed), + (r'^version/?', self.version), + ], + **kwargs) + self.storage = storage + self.notify = notify + self.http_user_error = 418 + + # handlers + def add(self, environ, start_response): + self.check_login(environ) + data = self.post_data(environ) + source = 'post' + id = self.data_get_id(data, source=source) + parent = self.data_get_string( + data, 'parent', default=None, source=source) + directory = self.data_get_boolean( + data, 'directory', default=False, source=source) + self.storage.add(id, parent=parent, directory=directory) + if self.notify: + self._notify(environ, 'add', id, + [('parent', parent), ('directory', directory)]) + return self.ok_response(environ, start_response, None) + + def exists(self, environ, start_response): + self.check_login(environ) + data = self.query_data(environ) + source = 'query' + id = self.data_get_id(data, source=source) + revision = self.data_get_string( + data, 'revision', default=None, source=source) + content = str(self.storage.exists(id, revision)) + return self.ok_response(environ, start_response, content) + + def remove(self, environ, start_response): + self.check_login(environ) + data = self.post_data(environ) + source = 'post' + id = self.data_get_id(data, source=source) + recursive = self.data_get_boolean( + data, 'recursive', default=False, source=source) + if recursive == True: + self.storage.recursive_remove(id) + else: + self.storage.remove(id) + if self.notify: + self._notify(environ, 'remove', id, [('recursive', recursive)]) + return self.ok_response(environ, start_response, None) + + def ancestors(self, environ, start_response): + self.check_login(environ) + data = self.query_data(environ) + source = 'query' + id = self.data_get_id(data, source=source) + revision = self.data_get_string( + data, 'revision', default=None, source=source) + content = '\n'.join(self.storage.ancestors(id, revision))+'\n' + return self.ok_response(environ, start_response, content) + + def children(self, environ, start_response): + self.check_login(environ) + data = self.query_data(environ) + source = 'query' + id = self.data_get_id(data, default=None, source=source) + revision = self.data_get_string( + data, 'revision', default=None, source=source) + content = '\n'.join(self.storage.children(id, revision)) + return self.ok_response(environ, start_response, content) + + def get(self, environ, start_response): + self.check_login(environ) + data = self.query_data(environ) + source = 'query' + try: + id = environ['be-server.url_args'][0] + except: + raise libbe.util.wsgi.HandlerError(404, 'Not Found') + revision = self.data_get_string( + data, 'revision', default=None, source=source) + content = self.storage.get(id, revision=revision) + be_version = self.storage.storage_version(revision) + return self.ok_response(environ, start_response, content, + headers=[('X-BE-Version', be_version)]) + + def set(self, environ, start_response): + self.check_login(environ) + data = self.post_data(environ) + try: + id = environ['be-server.url_args'][0] + except: + raise libbe.util.wsgi.HandlerError(404, 'Not Found') + if not 'value' in data: + raise libbe.util.wsgi.HandlerError(406, 'Missing query key value') + value = data['value'] + self.storage.set(id, value) + if self.notify: + self._notify(environ, 'set', id, [('value', value)]) + return self.ok_response(environ, start_response, None) + + def commit(self, environ, start_response): + self.check_login(environ) + data = self.post_data(environ) + if not 'summary' in data: + raise libbe.util.wsgi.HandlerError( + 406, 'Missing query key summary') + summary = data['summary'] + if not 'body' in data or data['body'] == 'None': + data['body'] = None + body = data['body'] + if not 'allow_empty' in data \ + or data['allow_empty'] == 'True': + allow_empty = True + else: + allow_empty = False + try: + revision = self.storage.commit(summary, body, allow_empty) + except libbe.storage.EmptyCommit, e: + raise libbe.util.wsgi.HandlerError( + self.http_user_error, 'EmptyCommit') + if self.notify: + self._notify(environ, 'commit', id, + [('allow_empty', allow_empty), ('summary', summary), + ('body', body)]) + return self.ok_response(environ, start_response, revision) + + def revision_id(self, environ, start_response): + self.check_login(environ) + data = self.query_data(environ) + source = 'query' + index = int(self.data_get_string( + data, 'index', default=libbe.util.wsgi.HandlerError, + source=source)) + content = self.storage.revision_id(index) + return self.ok_response(environ, start_response, content) + + def changed(self, environ, start_response): + self.check_login(environ) + data = self.query_data(environ) + source = 'query' + revision = self.data_get_string( + data, 'revision', default=None, source=source) + add,mod,rem = self.storage.changed(revision) + content = '\n\n'.join(['\n'.join(p) for p in (add,mod,rem)]) + return self.ok_response(environ, start_response, content) + + def version(self, environ, start_response): + self.check_login(environ) + data = self.query_data(environ) + source = 'query' + revision = self.data_get_string( + data, 'revision', default=None, source=source) + content = self.storage.storage_version(revision) + return self.ok_response(environ, start_response, content) + + # handler utility functions + def check_login(self, environ): + user = environ.get('be-auth.user', None) + if user is not None: # we're running under AuthenticationApp + if environ['REQUEST_METHOD'] == 'POST': + if user == 'guest' or self.storage.is_writeable() == False: + raise _Unauthorized() # only non-guests allowed to write + # allow read-only commands for all users + + def _notify(self, environ, command, id, params): + message = self._format_notification(environ, command, id, params) + self._submit_notification(message) + + def _format_notification(self, environ, command, id, params): + key_length = len('command') + for key,value in params: + if len(key) > key_length and '\n' not in str(value): + key_length = len(key) + key_length += 1 + lines = [] + multi_line_params = [] + for key,value in [('address', environ.get('REMOTE_ADDR', '-')), + ('command', command), ('id', id)]+params: + v = str(value) + if '\n' in v: + multi_line_params.append((key,v)) + continue + lines.append('%*.*s %s' % (key_length, key_length, key+':', v)) + lines.append('') + for key,value in multi_line_params: + lines.extend(['=== START %s ===' % key, v, + '=== STOP %s ===' % key, '']) + lines.append('') + return '\n'.join(lines) + + def _submit_notification(self, message): + libbe.util.subproc.invoke(self.notify, stdin=message, shell=True) + + +class ServeStorage (libbe.util.wsgi.ServerCommand): + """Serve bug directory storage over HTTP. + + This allows you to run local `be` commands interfacing with remote + data, transmitting file reads/writes/etc. over the network. + + :class:`~libbe.command.base.Command` wrapper around + :class:`ServerApp`. + """ + + name = 'serve-storage' + + def _get_app(self, logger, storage, **kwargs): + return ServerApp( + logger=logger, storage=storage, notify=kwargs.get('notify', False)) + + def _long_help(self): + return """ +Example usage:: + + $ be serve-storage + +And in another terminal (or after backgrounding the server):: + + $ be --repo http://localhost:8000/ list + +If you bind your server to a public interface, take a look at the +``--read-only`` option or the combined ``--ssl --auth FILE`` +options so other people can't mess with your repository. If you do use +authentication, you'll need to send in your username and password with, +for example:: + + $ be --repo http://username:password@localhost:8000/ list +""" + + +# alias for libbe.command.base.get_command_class() +Serve_storage = ServeStorage + + +if libbe.TESTING: + class ServerAppTestCase (libbe.util.wsgi.WSGITestCase): + def setUp(self): + super(ServerAppTestCase, self).setUp() + self.bd = libbe.bugdir.SimpleBugDir(memory=False) + self.app = ServerApp(self.bd.storage, logger=self.logger) + + def tearDown(self): + self.bd.cleanup() + super(ServerAppTestCase, self).tearDown() + + def test_add_get(self): + try: + self.getURL(self.app, '/add/', method='GET') + except libbe.util.wsgi.HandlerError as e: + self.failUnless(e.code == 404, e) + else: + self.fail('GET /add/ did not raise 404') + + def test_add_post(self): + self.getURL(self.app, '/add/', method='POST', + data_dict={'id':'123456', 'parent':'abc123', + 'directory':'True'}) + self.failUnless(self.status == '200 OK', self.status) + self.failUnless(self.response_headers == [], + self.response_headers) + self.failUnless(self.exc_info is None, self.exc_info) + # Note: other methods tested in libbe.storage.http + + # TODO: integration tests on Serve? + + unitsuite =unittest.TestLoader().loadTestsFromModule(sys.modules[__name__]) + suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()]) |