diff --git a/bin/swh-web-ui b/bin/swh-web-ui index 1547264d..5622bf5c 100755 --- a/bin/swh-web-ui +++ b/bin/swh-web-ui @@ -1,42 +1,35 @@ #!/usr/bin/env python3 # Copyright (C) 2015 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU Affero General Public License version 3, or any later version # See top-level LICENSE file for more information import argparse -import logging -import os -from swh.web.ui import controller, main +from swh.web.ui import main # Default configuration file DEFAULT_CONF_FILE = '~/.config/swh/webapp.ini' -# Default configuration in swh.web.ui.main - def parse_args(): """Parse the configuration for the cli. """ cli = argparse.ArgumentParser(description="SWH's web ui.") cli.add_argument('--verbose', '-v', action='store_true', help='Verbosity level in log file.') cli.add_argument('--config', '-c', help='configuration file path') args = cli.parse_args() return args if __name__ == '__main__': args = parse_args() - conf = main.read_config(args.config or DEFAULT_CONF_FILE) - - logging.basicConfig(filename=os.path.join(conf['log_dir'], 'web-ui.log'), - level=logging.DEBUG if args.verbose else logging.INFO) + config_path = args.config or DEFAULT_CONF_FILE - controller.run(conf) + main.run_debug_from(config_path, args.verbose) diff --git a/swh/web/ui/controller.py b/swh/web/ui/controller.py index 0ad983d6..4475a18a 100644 --- a/swh/web/ui/controller.py +++ b/swh/web/ui/controller.py @@ -1,362 +1,330 @@ # Copyright (C) 2015 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU Affero General Public License version 3, or any later version # See top-level LICENSE file for more information import logging from flask import redirect, render_template, url_for, jsonify, request from flask import make_response from swh.core.hashutil import ALGORITHMS from swh.web.ui.main import app from swh.web.ui import service from swh.web.ui.decorators import jsonp hash_filter_keys = ALGORITHMS @app.route('/') def main(): """Main application view. At the moment, redirect to the content search view. """ return redirect(url_for('info')) @app.route('/info') def info(): """A simple api to define what the server is all about. """ logging.info('Dev SWH UI') return 'Dev SWH UI' @app.route('/search') def search(): """Search for hashes in swh-storage. """ q = request.args.get('q', '') env = {'q': q, 'message': '', 'found': None} try: if q: env['found'] = service.lookup_hash(q) except ValueError: env['message'] = 'Error: invalid query string' return render_template('search.html', **env) @app.route('/browse/revision/') def revision(sha1_git): """Show commit information. Args: sha1_git: the revision's sha1 Returns: Revision information """ return render_template('revision.html', sha1_git=sha1_git) @app.route('/browse/directory/') def directory(sha1_git): """Show directory information. Args: sha1_git: the directory's sha1 Returns: Directory information """ return render_template('directory.html', sha1_git=sha1_git) @app.route('/browse/directory//') def directory_at_path(sha1_git, p): """Show directory information for the sha1_git at path. Args: sha1_git: the directory's sha1 path: file or directory pointed to Returns: Directory information at sha1_git + path """ return render_template('directory.html', sha1_git=sha1_git, path=p) def _origin_seen(hash, data): """Given an origin, compute a message string with the right information. Args: origin: a dictionary with keys: - origin: a dictionary with type and url keys - occurrence: a dictionary with a validity range Returns: message as a string """ if data is None: return 'Content with hash %s is unknown as of now.' % hash origin_type = data['origin_type'] origin_url = data['origin_url'] revision = data['revision'] branch = data['branch'] path = data['path'] return """The content with hash %s has been seen on origin with type '%s' at url '%s'. The revision was identified at '%s' on branch '%s'. The file's path referenced was '%s'.""" % (hash, origin_type, origin_url, revision, branch, path) @app.route('/browse/content/:') def content(hash, sha): """Show content information. Args: hash: hash according to HASH_ALGO, where HASH_ALGO is one of: sha1, sha1_git, sha256. This means that several different URLs (at least one per HASH_ALGO) will point to the same content sha: the sha with 'hash' format Returns: The content's information at sha1_git """ # Checks user input if hash not in hash_filter_keys: return make_response( 'Bad request, sha must be one of sha1, sha1_git, sha256', 400) q = "%s:%s" % (hash, sha) found = service.lookup_hash(q) if not found: message = "Hash %s was not found." % hash else: origin = service.lookup_hash_origin(q) message = _origin_seen(hash, origin) return render_template('content.html', hash=hash, sha=sha, message=message) @app.route('/browse/release/') def release(sha1_git): """Show release's information. Args: sha1_git: sha1_git for this particular release Returns: Release's information """ return 'Release information at %s' % sha1_git @app.route('/browse/person/') def person(id): """Show Person's information at id. Args: id: person's unique identifier Returns: Person's information """ return 'Person information at %s' % id @app.route('/browse/origin/') def origin(id): """Show origin's information at id. Args: id: origin's unique identifier Returns: Origin's information """ return 'Origin information at %s' % id @app.route('/browse/project/') def project(id): """Show project's information at id. Args: id: project's unique identifier Returns: Project's information """ return 'Project information at %s' % id @app.route('/browse/organization/') def organization(id): """Show organization's information at id. Args: id: organization's unique identifier Returns: Organization's information """ return 'Organization information at %s' % id @app.route('/browse/directory//' '+|/' '|/') def directory_at_origin(timestamp, origin_type, origin_url, branch, path): """Show directory information at timestamp, origin-type, origin-url, branch and path. Those parameters are separated by the `|` terminator. Args: timestamp: the timestamp to look for. can be latest or some iso8601 date format. (TODO: decide the time matching policy.) origin_type: origin's type origin_url: origin's url (can contain `/`) branch: branch name which can contain `/` path: path to directory or file Returns: Directory information at the given parameters. """ return 'Directory at (%s, %s, %s, %s, %s)' % (timestamp, origin_type, origin_url, branch, path) @app.route('/browse/revision//' '+|/') def revision_at_origin_and_branch(timestamp, origin_type, origin_url, branch): """Show revision information at timestamp, origin, and branch. Those parameters are separated by the `|` terminator. Args: timestamp: the timestamp to look for. can be latest or some iso8601 date format. (TODO: decide the time matching policy.) origin_type: origin's type origin_url: origin's url (can contain `/`) branch: branch name which can contain / Returns: Revision information at the given parameters. """ return 'Revision at (ts=%s, type=%s, url=%s, branch=%s)' % (timestamp, origin_type, origin_url, branch) @app.route('/browse/revision//' '+|') def revision_at_origin(timestamp, origin_type, origin_url): """Show revision information at timestamp, origin, and branch. Those parameters are separated by the `|` terminator. Args: timestamp: the timestamp to look for. can be latest or iso8601 date format. (TODO: decide the time matching policy.) origin_type: origin's type origin_url: origin's url (can contain `/`) Returns: Revision information at the given parameters. """ return 'Revision at (timestamp=%s, type=%s, url=%s)' % (timestamp, origin_type, origin_url) @app.route('/api/1/stat/counters') @jsonp def api_stats(): """Return statistics as a JSON object""" return jsonify(service.stat_counters()) @app.route('/api/1/search//') @jsonp def api_search(q): """Return search results as a JSON object""" return jsonify({'query': q, 'found': service.lookup_hash(q)}) @app.route('/api/1/browse//') @jsonp def api_browse(q): """Return search results as a JSON object""" return jsonify({'query': q, 'origin': service.lookup_hash_origin(q)}) - - -def run(conf): - """Run the api's server. - - Args: - conf is a dictionary of keywords: - - 'db_url' the db url's access (through psycopg2 format) - - 'content_storage_dir' revisions/directories/contents storage on disk - - 'host' to override the default 127.0.0.1 to open or not the server - to the world - - 'port' to override the default of 5000 (from the underlying layer: - flask) - - 'debug' activate the verbose logs - - 'secret_key' the flask secret key - - Returns: - Never - - Raises: - ? - - """ - print("""SWH Web UI available at http://%s:%s/ -debug: %s""" % (conf['host'], conf.get('port', None), conf['debug'])) - - app.secret_key = conf['secret_key'] - app.config.update({'conf': conf}) - - app.run(host=conf['host'], - port=conf.get('port', None), - debug=conf['debug']) diff --git a/swh/web/ui/main.py b/swh/web/ui/main.py index 2420c146..027ef251 100644 --- a/swh/web/ui/main.py +++ b/swh/web/ui/main.py @@ -1,73 +1,109 @@ # Copyright (C) 2015 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU Affero General Public License version 3, or any later version # See top-level LICENSE file for more information import logging import os from flask import Flask from swh.core import config + DEFAULT_CONFIG = { 'storage_args': ('list[str]', ['http://localhost:5000/']), 'storage_class': ('str', 'remote_storage'), 'log_dir': ('string', '/tmp/swh/log'), 'debug': ('bool', None), 'host': ('string', '127.0.0.1'), 'port': ('int', 6543), 'secret_key': ('string', 'development key'), } # api's definition app = Flask(__name__) def read_config(config_file): """Read the configuration file `config_file`, update the app with parameters (secret_key, conf) and return the parsed configuration as a dict""" conf = config.read(config_file, DEFAULT_CONFIG) config.prepare_folders(conf, 'log_dir') if conf['storage_class'] == 'remote_storage': from swh.storage.api.client import RemoteStorage as Storage else: from swh.storage import Storage conf['storage'] = Storage(*conf['storage_args']) return conf def load_controllers(): """Load the controllers for the application""" from swh.web.ui import controller # flake8: noqa +def storage(): + """Return the current application's storage. + + """ + return app.config['conf']['storage'] + + def run_from_webserver(environ, start_response): """Run the WSGI app from the webserver, loading the configuration.""" load_controllers() config_path = '/etc/softwareheritage/webapp/webapp.ini' conf = read_config(config_path) app.secret_key = conf['secret_key'] app.config['conf'] = conf logging.basicConfig(filename=os.path.join(conf['log_dir'], 'web-ui.log'), level=logging.INFO) return app(environ, start_response) -def storage(): - """Return the current application's storage. +def run_debug_from(config_path, verbose=False): + """Run the api's server in dev mode. + + Args: + conf is a dictionary of keywords: + - 'db_url' the db url's access (through psycopg2 format) + - 'content_storage_dir' revisions/directories/contents storage on disk + - 'host' to override the default 127.0.0.1 to open or not the server + to the world + - 'port' to override the default of 5000 (from the underlying layer: + flask) + - 'debug' activate the verbose logs + - 'secret_key' the flask secret key + + Returns: + Never """ - return app.config['conf']['storage'] + load_controllers() + + conf = read_config(config_path) + + app.secret_key = conf['secret_key'] + app.config['conf'] = conf + + host = conf.get('host', '127.0.0.1') + port = conf.get('port') + debug = conf.get('debug') + + logging.basicConfig(filename=os.path.join(conf['log_dir'], 'web-ui.log'), + level=logging.DEBUG if verbose else logging.INFO) + + app.run(host=host, port=port, debug=debug) # , use_reloader=False)