diff --git a/swh/backend/api.py b/swh/backend/api.py index 28baa86..2578499 100755 --- a/swh/backend/api.py +++ b/swh/backend/api.py @@ -1,292 +1,292 @@ #!/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 General Public License version 3, or any later version # See top-level LICENSE file for more information import logging from flask import Flask, Response, make_response, request from swh.storage import store, db, service from swh.protocols import serial # api's definition app = Flask(__name__) def read_request_payload(request): """Read the request's payload. """ # TODO: Check the signed pickled data? return serial.load(request.stream) def write_response(data): """Write response from data. """ return Response(serial.dumps(data), mimetype=serial.MIMETYPE) @app.route('/') def hello(): """A simple api to define what the server is all about. FIXME: A redirect towards a static page defining the routes would be nice. """ return 'Dev SWH API' # from uri to type _uri_types = {'revisions': store.Type.revision, 'directories': store.Type.directory, 'contents': store.Type.content, 'releases': store.Type.release, 'occurrences': store.Type.occurrence} def _do_action_with_payload(conf, action_fn, uri_type, id, map_result_fn): uri_type_ok = _uri_types.get(uri_type, None) if uri_type_ok is None: return make_response('Bad request!', 400) vcs_object = read_request_payload(request) vcs_object.update({'id': id, 'type': uri_type_ok}) return action_fn(conf, vcs_object, map_result_fn) # occurrence type is not dealt the same way _post_all_uri_types = {'revisions': store.Type.revision, 'directories': store.Type.directory, 'contents': store.Type.content} @app.route('/vcs//', methods=['POST']) def filter_unknowns_type(uri_type): """Filters unknown sha1 to the backend and returns them. """ if request.headers.get('Content-Type') != serial.MIMETYPE: return make_response('Bad request. Expected %s data!' % serial.MIMETYPE, 400) obj_type = _post_all_uri_types.get(uri_type) if obj_type is None: return make_response('Bad request. Type not supported!', 400) sha1s = read_request_payload(request) config = app.config['conf'] with db.connect(config['db_url']) as db_conn: unknowns_sha1s = service.filter_unknowns_type(db_conn, obj_type, sha1s) if unknowns_sha1s is None: return make_response('Bad request!', 400) else: return write_response(unknowns_sha1s) @app.route('/vcs/persons/', methods=['POST']) def post_person(): """Post a person. """ if request.headers.get('Content-Type') != serial.MIMETYPE: return make_response('Bad request. Expected %s data!' % serial.MIMETYPE, 400) origin = read_request_payload(request) config = app.config['conf'] with db.connect(config['db_url']) as db_conn: try: person_found = service.find_person(db_conn, origin) if person_found: return write_response(person_found) else: return make_response('Person not found!', 404) except: return make_response('Bad request!', 400) @app.route('/origins/', methods=['POST']) def post_origin(): """Post an origin. """ if request.headers.get('Content-Type') != serial.MIMETYPE: return make_response('Bad request. Expected %s data!' % serial.MIMETYPE, 400) origin = read_request_payload(request) config = app.config['conf'] with db.connect(config['db_url']) as db_conn: try: origin_found = service.find_origin(db_conn, origin) if origin_found: return write_response(origin_found) else: return make_response('Origin not found!', 404) except: return make_response('Bad request!', 400) @app.route('/origins/', methods=['PUT']) def put_origin(): """Create an origin or returns it if already existing. """ if request.headers.get('Content-Type') != serial.MIMETYPE: return make_response('Bad request. Expected %s data!' % serial.MIMETYPE, 400) origin = read_request_payload(request) config = app.config['conf'] with db.connect(config['db_url']) as db_conn: try: origin_found = service.add_origin(db_conn, origin) - return write_response(origin_found) # FIXME 204 + return write_response(origin_found) # FIXME: 204 except: return make_response('Bad request!', 400) @app.route('/vcs/persons/', methods=['PUT']) def put_all_persons(): """Store or update given revisions. FIXME: Refactor same behavior with `put_all`. """ if request.headers.get('Content-Type') != serial.MIMETYPE: return make_response('Bad request. Expected %s data!' % serial.MIMETYPE, 400) payload = read_request_payload(request) obj_type = store.Type.person config = app.config['conf'] with db.connect(config['db_url']) as db_conn: service.add_persons(db_conn, config, obj_type, payload) return make_response('Successful creation!', 204) @app.route('/vcs/revisions/', methods=['PUT']) def put_all_revisions(): """Store or update given revisions. FIXME: Refactor same behavior with `put_all`. """ if request.headers.get('Content-Type') != serial.MIMETYPE: return make_response('Bad request. Expected %s data!' % serial.MIMETYPE, 400) payload = read_request_payload(request) obj_type = store.Type.revision config = app.config['conf'] with db.connect(config['db_url']) as db_conn: service.add_revisions(db_conn, config, obj_type, payload) return make_response('Successful creation!', 204) @app.route('/vcs//', methods=['PUT']) def put_all(uri_type): """Store or update given objects (uri_type in {contents, directories, releases). """ if request.headers.get('Content-Type') != serial.MIMETYPE: return make_response('Bad request. Expected %s data!' % serial.MIMETYPE, 400) payload = read_request_payload(request) obj_type = _uri_types[uri_type] config = app.config['conf'] with db.connect(config['db_url']) as db_conn: service.add_objects(db_conn, config, obj_type, payload) return make_response('Successful creation!', 204) def add_object(config, vcs_object, map_result_fn): """Add object in storage. - config is the configuration needed for the backend to execute query - vcs_object is the object to look for in the backend - map_result_fn is a mapping function which takes the backend's result and transform its output accordingly. This function returns an http response of the result. """ type = vcs_object['type'] - id = vcs_object['id'] # FIXME: remove useless key and send direct list + id = vcs_object['id'] logging.debug('store %s %s' % (type, id)) with db.connect(config['db_url']) as db_conn: if store.find(db_conn, vcs_object): logging.debug('update %s %s' % (id, type)) return make_response('Successful update!', 200) # immutable else: logging.debug('store %s %s' % (id, type)) res = store.add(db_conn, config, vcs_object) if res is None: return make_response('Bad request!', 400) elif res is False: logging.error('store %s %s' % (id, type)) return make_response('Internal server error!', 500) else: return make_response(map_result_fn(id, res), 204) def _do_lookup(conf, uri_type, id, map_result_fn): """Looking up type object with sha1. - config is the configuration needed for the backend to execute query - vcs_object is the object to look for in the backend - map_result_fn is a mapping function which takes the backend's result and transform its output accordingly. This function returns an http response of the result. """ uri_type_ok = _uri_types.get(uri_type, None) if not uri_type_ok: return make_response('Bad request!', 400) vcs_object = {'id': id, 'type': uri_type_ok} with db.connect(conf['db_url']) as db_conn: res = store.find(db_conn, vcs_object) if res: return write_response(map_result_fn(id, res)) # 200 return make_response('Not found!', 404) @app.route('/vcs/occurrences/') def list_occurrences_for(id): """Return the occurrences pointing to the revision id. """ return _do_lookup(app.config['conf'], 'occurrences', id, lambda _, result: list(map(lambda col: col[1], result))) @app.route('/vcs//') def object_exists_p(uri_type, id): """Assert if the object with sha1 id, of type uri_type, exists. """ return _do_lookup(app.config['conf'], uri_type, id, lambda sha1, _: {'id': sha1}) @app.route('/vcs//', methods=['PUT']) def put_object(uri_type, id): """Put an object in storage. """ return _do_action_with_payload(app.config['conf'], add_object, uri_type, id, - lambda _1, _2: 'Successful Creation!') # FIXME use id or result instead + lambda _1, _2: 'Successful Creation!') # FIXME: use id or result instead def run(conf): """Run the api's server. 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 - 'port' to override the default of 5000 (from the underlying layer: flask) - 'debug' activate the verbose logs """ app.config['conf'] = conf # app.config is the app's state (accessible) app.run(port=conf.get('port', None), debug=conf['debug'] == 'true') diff --git a/swh/gitloader/local_store.py b/swh/gitloader/local_store.py index 5080c62..f3a742f 100644 --- a/swh/gitloader/local_store.py +++ b/swh/gitloader/local_store.py @@ -1,86 +1,86 @@ # Copyright (C) 2015 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU General Public License version 3, or any later version # See top-level LICENSE file for more information from swh.storage import store, db, service from swh.conf import reader -# FIXME duplicated from bin/swh-backend... +# FIXME: duplicated from bin/swh-backend... # Default configuration file DEFAULT_CONF_FILE = '~/.config/swh/back.ini' # default configuration DEFAULT_CONF = { 'content_storage_dir': ('string', '/tmp/swh-git-loader/content-storage'), 'log_dir': ('string', '/tmp/swh-git-loader/log'), 'db_url': ('string', 'dbname=softwareheritage-dev'), 'storage_compression': ('bool', None), 'folder_depth': ('int', 4), 'debug': ('bool', None), 'port': ('int', 5000) } def store_only_new(db_conn, conf, obj_type, obj): """Store object if not already present. """ obj.update({'type': obj_type}) if not store.find(db_conn, obj): store.add(db_conn, conf, obj) _obj_to_persist_fn = {store.Type.revision: service.add_revisions} def store_unknown_objects(db_conn, conf, obj_type, swhmap): """Load objects to the backend. """ sha1s = swhmap.keys() # have: filter unknown obj unknown_obj_sha1s = service.filter_unknowns_type(db_conn, obj_type, sha1s) if not unknown_obj_sha1s: return True # seen: now store in backend persist_fn = _obj_to_persist_fn.get(obj_type, service.add_objects) obj_fulls = map(swhmap.get, unknown_obj_sha1s) return persist_fn(db_conn, conf, obj_type, obj_fulls) def load_to_back(backend_setup_file, swhrepo): """Load to the backend the repository swhrepo. """ # Read the configuration file (no check yet) conf = reader.read(backend_setup_file or DEFAULT_CONF_FILE, DEFAULT_CONF) with db.connect(conf['db_url']) as db_conn: # First, store/retrieve the origin identifier # FIXME: should be done by the cloner worker (which is not yet plugged # on the right swh db ftm) service.add_origin(db_conn, swhrepo.get_origin()) # First reference all unknown persons service.add_persons(db_conn, conf, store.Type.person, swhrepo.get_persons()) res = store_unknown_objects(db_conn, conf, store.Type.content, swhrepo.get_contents()) if res: res = store_unknown_objects(db_conn, conf, store.Type.directory, swhrepo.get_directories()) if res: res = store_unknown_objects(db_conn, conf, store.Type.revision, swhrepo.get_revisions()) if res: # brutally send all remaining occurrences service.add_objects(db_conn, conf, store.Type.occurrence, swhrepo.get_occurrences()) # and releases (the idea here is that compared to existing # objects, the quantity is less) service.add_objects(db_conn, conf, store.Type.release, swhrepo.get_releases()) diff --git a/swh/tests/test_api_occurrence.py b/swh/tests/test_api_occurrence.py index a06e9c2..d1977f8 100644 --- a/swh/tests/test_api_occurrence.py +++ b/swh/tests/test_api_occurrence.py @@ -1,130 +1,130 @@ # Copyright (C) 2015 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU General Public License version 3, or any later version # See top-level LICENSE file for more information import unittest from nose.tools import istest from nose.plugins.attrib import attr from swh.storage import db, models from swh.protocols import serial from test_utils import now, app_client @attr('slow') class OccurrenceTestCase(unittest.TestCase): def setUp(self): self.app, db_url = app_client() with db.connect(db_url) as db_conn: self.directory_sha1_hex = 'directory-sha16ee476a8be155ab049994f717e' models.add_directory(db_conn, self.directory_sha1_hex) authorAndCommitter = {'name': 'some-name', 'email': 'some-email'} models.add_person(db_conn, authorAndCommitter['name'], authorAndCommitter['email']) self.revision_sha1_hex = 'revision-sha1-to-test-existence9994f717e' models.add_revision(db_conn, self.revision_sha1_hex, now(), self.directory_sha1_hex, "revision message", authorAndCommitter, authorAndCommitter) self.origin_url = "https://github.com/user/repo" models.add_origin(db_conn, self.origin_url, 'git') self.reference_name = 'master' models.add_occurrence(db_conn, self.origin_url, self.reference_name, self.revision_sha1_hex) self.reference_name2 = 'master2' models.add_occurrence(db_conn, self.origin_url, self.reference_name2, self.revision_sha1_hex) self.revision_sha1_hex_2 = '2-revision-sha1-to-test-existence9994f71' models.add_revision(db_conn, self.revision_sha1_hex_2, now(), self.directory_sha1_hex, "revision message 2", authorAndCommitter, authorAndCommitter) @istest def get_occurrence_ok(self): # when rv = self.app.get('/vcs/occurrences/%s' % self.revision_sha1_hex) # then assert rv.status_code == 200 assert serial.loads(rv.data) == [self.reference_name, self.reference_name2] @istest def get_occurrence_not_found(self): # when rv = self.app.get('/vcs/occurrences/inexistant-sha1') # then assert rv.status_code == 404 assert rv.data == b'Not found!' @istest def get_occurrence_not_found_with_bad_format(self): # when rv = self.app.get('/vcs/occurrences/1') # then assert rv.status_code == 404 assert rv.data == b'Not found!' @istest def put_occurrence_create_and_update(self): occ_revision_sha1_hex = self.revision_sha1_hex_2 rv = self.app.get('/vcs/occurrences/%s' % occ_revision_sha1_hex) # then assert rv.status_code == 404 assert rv.data == b'Not found!' # we create it body = serial.dumps({'content': b'occurrence content', - 'revision': occ_revision_sha1_hex, # FIXME redundant with the one from uri... + 'revision': occ_revision_sha1_hex, # FIXME: redundant with the one from uri.. 'reference': 'master', 'url-origin': self.origin_url}) rv = self.app.put('/vcs/occurrences/%s' % occ_revision_sha1_hex, # ... here data=body, headers={'Content-Type': serial.MIMETYPE}) assert rv.status_code == 204 assert rv.data == b'' # now it exists rv = self.app.get('/vcs/occurrences/%s' % occ_revision_sha1_hex) # then assert rv.status_code == 200 assert serial.loads(rv.data) == ['master'] # we update it rv = self.app.put('/vcs/occurrences/%s' % occ_revision_sha1_hex, data=body, headers={'Content-Type': serial.MIMETYPE}) assert rv.status_code == 200 assert rv.data == b'Successful update!' # still the same rv = self.app.get('/vcs/occurrences/%s' % occ_revision_sha1_hex) # then assert rv.status_code == 200 assert serial.loads(rv.data) == ['master'] diff --git a/swh/tests/test_api_origin.py b/swh/tests/test_api_origin.py index 5410422..f08c9ed 100644 --- a/swh/tests/test_api_origin.py +++ b/swh/tests/test_api_origin.py @@ -1,98 +1,98 @@ # Copyright (C) 2015 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU General Public License version 3, or any later version # See top-level LICENSE file for more information import unittest from nose.tools import istest from nose.plugins.attrib import attr from swh.storage import db, models from swh.protocols import serial from test_utils import app_client @attr('slow') class OriginTestCase(unittest.TestCase): def setUp(self): self.app, db_url = app_client() with db.connect(db_url) as db_conn: self.origin_url = 'https://github.com/torvalds/linux.git' self.origin_type = 'git' self.origin_id = models.add_origin(db_conn, self.origin_url, self.origin_type) @istest def get_origin_ok(self): # when payload = {'url': self.origin_url, 'type': self.origin_type} rv = self.app.post('/origins/', data=serial.dumps(payload), headers={'Content-Type': serial.MIMETYPE}) # then assert rv.status_code == 200 assert serial.loads(rv.data)['id'] == self.origin_id @istest def get_origin_not_found(self): # when payload = {'url': 'unknown', 'type': 'blah'} rv = self.app.post('/origins/', data=serial.dumps(payload), headers={'Content-Type': serial.MIMETYPE}) # then assert rv.status_code == 404 assert rv.data == b'Origin not found!' @istest def get_origin_not_found_with_bad_format(self): # when rv = self.app.post('/origins/', data=serial.dumps({'url': 'unknown'}), headers={'Content-Type': serial.MIMETYPE}) # then assert rv.status_code == 400 @istest def put_origin(self): # when payload = {'url': 'unknown', 'type': 'blah'} rv = self.app.post('/origins/', data=serial.dumps(payload), headers={'Content-Type': serial.MIMETYPE}) # then assert rv.status_code == 404 assert rv.data == b'Origin not found!' # when rv = self.app.put('/origins/', data=serial.dumps(payload), headers={'Content-Type': serial.MIMETYPE}) # then - assert rv.status_code == 200 # fixme 201 + assert rv.status_code == 200 # FIXME: 201 assert serial.loads(rv.data)['id'] payload = {'url': 'unknown', 'type': 'blah'} rv = self.app.post('/origins/', data=serial.dumps(payload), headers={'Content-Type': serial.MIMETYPE}) # then assert rv.status_code == 200 origin_id = serial.loads(rv.data)['id'] assert origin_id # when rv = self.app.put('/origins/', data=serial.dumps(payload), headers={'Content-Type': serial.MIMETYPE}) # then - assert rv.status_code == 200 # fixme 204 + assert rv.status_code == 200 # FIXME: 204 assert serial.loads(rv.data)['id'] == origin_id