diff --git a/README-uri-scheme.md b/README-uri-scheme.md index 18ebe0fc..71711f53 100644 --- a/README-uri-scheme.md +++ b/README-uri-scheme.md @@ -1,300 +1,348 @@ URI scheme ========== API --- ### Endpoints The api /api/1 is partially browsable on defined endpoints (/api, /api/1). * /api/ and /api/1/ List endpoint methods as per the client's 'Accept' header request. The following routes are to be anchored at at /api/1 * /revision/: show commit information $ curl -H 'Accept: application/json' http://localhost:6543/api/1/revision/18d8be353ed3480476f032475e7c233eff7371d5 { "author_email": "robot@softwareheritage.org", "author_name": "Software Heritage", "committer_date": "Mon, 17 Jan 2000 10:23:54 GMT", "committer_date_offset": 0, "committer_email": "robot@softwareheritage.org", "committer_name": "Software Heritage", "date": "Mon, 17 Jan 2000 10:23:54 GMT", "date_offset": 0, "directory": "7834ef7e7c357ce2af928115c6c6a42b7e2a44e6", "id": "18d8be353ed3480476f032475e7c233eff7371d5", "message": "synthetic revision message", "metadata": { "original_artifact": [ { "archive_type": "tar", "name": "webbase-5.7.0.tar.gz", "sha1": "147f73f369733d088b7a6fa9c4e0273dcd3c7ccd", "sha1_git": "6a15ea8b881069adedf11feceec35588f2cfe8f1", "sha256": "401d0df797110bea805d358b85bcc1ced29549d3d73f309d36484e7edf7bb912" } ] }, "parents": [ null ], "synthetic": true, "type": "tar" } * /directory/: show directory information (including ls) curl -X GET http://localhost:6543/api/1/directory/3126f46e2f7dc752227131a2a658265e58f53e38 [ { "dir_id": "3126f46e2f7dc752227131a2a658265e58f53e38", "name": "Makefile.am", "perms": 100644, "sha1": "b0283d8126f975e7b4a4348d13b07ddebe2cf8bf", "sha1_git": "e0522786777256d57c5210219bcbe8dacdad273d", "sha256": "897f3189dcfba96281b2190325c54afc74a42e2419c053baadfadc14386935ee", "status": "visible", "target": "e0522786777256d57c5210219bcbe8dacdad273d", "type": "file" }, { "dir_id": "3126f46e2f7dc752227131a2a658265e58f53e38", "name": "Makefile.in", "perms": 100644, "sha1": "81f5757b9451811cfb3ef84612e45a973c70b4e6", "sha1_git": "3b948d966fd8e99f93670025f63a550168d57d71", "sha256": "f5acd84a40f05d997a36b8846c4872a92ee57083abb77c82e05e9763c8edb59a", "status": "visible", "target": "3b948d966fd8e99f93670025f63a550168d57d71", "type": "file" }, ... snip ... { "dir_id": "3126f46e2f7dc752227131a2a658265e58f53e38", "name": "webtools.h", "perms": 100644, "sha1": "4b4c942ddd490ec1e312074ddfac352097886c02", "sha1_git": "e6fb8969d00e23dd152df5e7fb167118eab67342", "sha256": "95ffe6c0108f6ec48ccb0c93e966b54f1494f5cc353b066644c11fa47766620f", "status": "visible", "target": "e6fb8969d00e23dd152df5e7fb167118eab67342", "type": "file" }, { "dir_id": "3126f46e2f7dc752227131a2a658265e58f53e38", "name": "ylwrap", "perms": 100644, "sha1": "9073938df9ae47d585bfdf176bfff45d06f3e13e", "sha1_git": "13fc38d75f2a47bc55e90ad5bf8d8a0184b14878", "sha256": "184eb644e51154c79b42df70c22955b818d057491f84ca0e579e4f9e48a60d7b", "status": "visible", "target": "13fc38d75f2a47bc55e90ad5bf8d8a0184b14878", "type": "file" } ] * /content/[:]: show content information - content is specified by 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 - HASH_ALGO defaults to "sha1" (?) curl -X GET http://localhost:6543/api/1/content/sha1:486b486d2a4998929c68265fa85ab2326db5528a { "data": "/api/1/content/486b486d2a4998929c68265fa85ab2326db5528a/raw", "sha1": "486b486d2a4998929c68265fa85ab2326db5528a" } curl -X GET http://localhost:6543/api/1/content/sha1:4a1b6d7dd0a923ed90156c4e2f5db030095d8e08/ {"error": "Content with sha1:4a1b6d7dd0a923ed90156c4e2f5db030095d8e08 not found."} * /content/[/raw curl -H 'Accept: text/plain' http://localhost:6543/api/1/content/sha1:486b486d2a4998929c68265fa85ab2326db5528a/raw The GNU cfs-el web homepage is at @uref{http://www.gnu.org/software/cfs-el/cfs-el.html}. You can find the latest distribution of GNU cfs-el at @uref{ftp://ftp.gnu.org/gnu/} or at any of its mirrors. * /release/: show release information Sample: $ curl -X GET http://localhost:6543/api/1/release/4a1b6d7dd0a923ed90156c4e2f5db030095d8e08 { "author_name": "Software Heritage", "author_email": "robot@softwareheritage.org", "comment": "synthetic release message", "date": "Sat, 04 Mar 2000 07:50:35 GMT", "date_offset": 0, "id": "4a1b6d7dd0a923ed90156c4e2f5db030095d8e08", "name": "4.0.6", "revision": "5c7814ce9978d4e16f3858925b5cea611e500eec", "synthetic": true }% * /person/: show person information curl http://localhost:6543/api/1/person/1 { "email": "robot@softwareheritage.org", "id": 1, "name": "Software Heritage" } curl http://localhost:6543/api/1/person/2 {"error": "Person with id 2 not found."} * /origin/: show origin information Sample: $ curl -X GET http://localhost:6543/api/1/origin/1 { "id": 1, "lister": null, "project": null, "type": "ftp", "url": "rsync://ftp.gnu.org/old-gnu/solfege" }% * /browse/ Return content information up to one of its origin if the content is found. curl http://localhost:6543/api/1/browse/sha1:2e98ab73456aad8dfc6cc50d562ee1b80d201753 { "path": "republique.py", "origin_url": "file:///dev/null", "origin_type": "git", "revision": "8f8640a1c024c2ef85fa8e8d9297ea289134472d", "branch": "refs/remotes/origin/master" } * /uploadnsearch/ Post a file's content to api. Api computes the sha1 hash and checks in the storage if such sha1 exists. Json answer: {'sha1': hexadecimal sha1, 'found': true or false} Sample: $ curl -X POST -F filename=@/path/to/file http://localhost:6543/api/1/uploadnsearch { "found": false, "sha1": "e95097ad2d607b4c89c1ce7ca1fef2a1e4450558" }% +* /revision//log + +Show all revisions (~git log) starting from . +The first element is the given sha1_git. + +Sample: + + curl http://localhost:6543/api/1/revision/7026b7c1a2af56521e951c01ed20f255fa054238/log/ + + [ + { + "id": "7026b7c1a2af56521e951c01ed20f255fa054238", + "parents": [], + "type": "git", + "committer_date": "Mon, 12 Oct 2015 11:05:53 GMT", + "synthetic": false, + "committer": { + "email": "a3nm@a3nm.net", + "name": "Antoine Amarilli" + }, + "message": "+1 limitation\n", + "author": { + "email": "a3nm@a3nm.net", + "name": "Antoine Amarilli" + }, + "date": "Mon, 12 Oct 2015 11:05:53 GMT", + "metadata": null, + "directory": "a33a9acf2419b9a291e8a02302e6347dcffde5a6" + }, + { + "id": "368a48fe15b7db2383775f97c6b247011b3f14f4", + "parents": [], + "type": "git", + "committer_date": "Mon, 12 Oct 2015 10:57:11 GMT", + "synthetic": false, + "committer": { + "email": "a3nm@a3nm.net", + "name": "Antoine Amarilli" + }, + "message": "actually fix bug\n", + "author": { + "email": "a3nm@a3nm.net", + "name": "Antoine Amarilli" + }, + "date": "Mon, 12 Oct 2015 10:57:11 GMT", + "metadata": null, + "directory": "1d5188e4991510c74d62272f0301352c5c1b850b" + }, + ... + ] + * /project/: show project information * /organization/: show organization information * /directory//path/to/file-or-dir: ditto, but for file or directory pointed by path - note: This is the same as /directory/, where is the sha1_git ID of the directory pointed by path or /content/sha1_git: (for content) * /directory/path/to/file-or-dir?timestamp=&origin=&branch= - Same as /directory/ but looking up sha1 git using origin and branch at a given timestamp for a specific path /path/to/file-or-dir -* /revision/?timestamp=&origin=&branch= - - - Show all revisions (~git log) of origin and branch at a given timestamp * /revision/?timestamp=&origin= - Same as /revision/ but looking up sha1 git using origin at a given timestamp. * /revision/?timestamp=&origin= Show all branches of origin at a given timestamp. -* /revision//| - - - Show all branches of origin at a given timestamp ### Global behavior The api routes outputs 'application/json' as default. #### Accept header Also, you can specify the following 'Accept' header in your client query: - application/json - application/yaml - text/html The client can use specific filters and compose them as (s)he sees fit. #### Fields The client can filter the result output by field names when requesting `application/json` or `application/yaml` output. Ex: curl http://localhost:6543/api/1/stat/counters?fields=revision,release,content { "content": 133616, "revision": 1042, "release": 660 } #### JSONP When using the accept header 'application/json', the route can be enhanced by adding a `callback` parameter. This will output the result in a json function whose name is the callback parameter Ex: curl http://localhost:6543/api/1/stat/counters?callback=jsonp&fields=directory_entry_dir,revision,entity jsonp({ "directory_entry_dir": 12478, "revision": 1042, "entity": 0 }) #### Error When an error is raised, the error code response is used: - 400: user's input is not correct regarding the API - 404: user's input is ok but we did not found what (s)he was looking forbidden And the body of the response should be a dictionary with some more information on the error. Bad request sample: curl http://localhost:6543/api/1/revision/18d8be353ed3480476f032475e7c233eff7371d {"error": "Invalid checksum query string 18d8be353ed3480476f032475e7c233eff7371d"} curl http://localhost:6543/api/1/revision/sha1:18d8be353ed3480476f032475e7c233eff7371d {"error": "Invalid hash 18d8be353ed3480476f032475e7c233eff7371d for algorithm sha1"} Not found sample: curl http://localhost:6543/api/1/revision/sha1:18d8be353ed3480476f032475e7c233eff7371df {"error": "Revision with sha1_git sha1:18d8be353ed3480476f032475e7c233eff7371df not found."} diff --git a/swh/web/ui/api.py b/swh/web/ui/api.py index 497983e5..9ad653d0 100644 --- a/swh/web/ui/api.py +++ b/swh/web/ui/api.py @@ -1,306 +1,307 @@ # 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 from flask import request, url_for from flask.ext.api.decorators import set_renderers from swh.web.ui.main import app from swh.web.ui import service, renderers, utils from swh.web.ui.exc import BadInputExc, NotFoundExc @app.route('/browse/') def api_browse_endpoints(): """List the current api endpoints starting with /api or /api/. Returns: List of endpoints at /api """ return utils.filter_endpoints(app.url_map, '/browse') @app.route('/api/') def api_main_endpoints(): """List the current api endpoints starting with /api or /api/. Returns: List of endpoints at /api """ return utils.filter_endpoints(app.url_map, '/api') @app.route('/api/1/') def api_main_v1_endpoints(): """List the current api v1 endpoints starting with /api/1 or /api/1/. Returns: List of endpoints at /api/1 """ return utils.filter_endpoints(app.url_map, '/api/1') @app.route('/api/1/stat/counters/') def api_stats(): """Return statistics on SWH storage. Returns: SWH storage's statistics """ return service.stat_counters() @app.route('/api/1/search//') def api_search(q): """Search a content per hash. Args: q is of the form algo_hash:hash with algo_hash in (sha1, sha1_git, sha256) Returns: Dictionary with 'found' key and the associated result. Raises: BadInputExc in case of unknown algo_hash or bad hash """ r = service.lookup_hash(q).get('found') return {'found': True if r else False} def _api_lookup(criteria, lookup_fn, error_msg_if_not_found): """Factorize function regarding the api to lookup for data.""" res = lookup_fn(criteria) if not res: raise NotFoundExc(error_msg_if_not_found) if isinstance(res, map): return list(res) return res @app.route('/api/1/origin//') def api_origin(origin_id): """Return information about origin with id origin_id. Args: origin_id: the origin's identifier Returns: Information on the origin if found. Raises: NotFoundExc if the origin is not found. """ return _api_lookup( origin_id, lookup_fn=service.lookup_origin, error_msg_if_not_found='Origin with id %s not found.' % origin_id) @app.route('/api/1/person//') def api_person(person_id): """Return information about person with identifier person_id. Args: person_id: the person's identifier Returns: Information on the person if found. Raises: NotFoundExc if the person is not found. """ return _api_lookup( person_id, lookup_fn=service.lookup_person, error_msg_if_not_found='Person with id %s not found.' % person_id) @app.route('/api/1/release//') def api_release(sha1_git): """Return information about release with id sha1_git. Args: sha1_git: the release's hash Returns: Information on the release if found. Raises: BadInputExc in case of unknown algo_hash or bad hash NotFoundExc if the release is not found. """ error_msg = 'Release with sha1_git %s not found.' % sha1_git return _api_lookup( sha1_git, lookup_fn=service.lookup_release, error_msg_if_not_found=error_msg) @app.route('/api/1/revision//') def api_revision(sha1_git): """Return information about revision with id sha1_git. Args: sha1_git: the revision's hash Returns: Information on the revision if found. Raises: BadInputExc in case of unknown algo_hash or bad hash NotFoundExc if the revision is not found. """ error_msg = 'Revision with sha1_git %s not found.' % sha1_git return _api_lookup( sha1_git, lookup_fn=service.lookup_revision, error_msg_if_not_found=error_msg) @app.route('/api/1/revision//log/') def api_revision_log(sha1_git): - """Return information about revision with id sha1_git. + """Show all revisions (~git log) starting from sha1_git. + The first element returned is the given sha1_git. Args: sha1_git: the revision's hash Returns: Information on the revision if found. Raises: BadInputExc in case of unknown algo_hash or bad hash NotFoundExc if the revision is not found. """ error_msg = 'Revision with sha1_git %s not found.' % sha1_git return _api_lookup( sha1_git, lookup_fn=service.lookup_revision_log, error_msg_if_not_found=error_msg) @app.route('/api/1/directory//') def api_directory(sha1_git): """Return information about release with id sha1_git. Args: Directory's sha1_git Raises: BadInputExc in case of unknown algo_hash or bad hash NotFoundExc if the content is not found. """ error_msg = 'Directory with sha1_git %s not found.' % sha1_git return _api_lookup( sha1_git, lookup_fn=service.lookup_directory, error_msg_if_not_found=error_msg) @app.route('/api/1/browse//') def api_content_checksum_to_origin(q): """Return content information up to one of its origin if the content is found. Args: q is of the form algo_hash:hash with algo_hash in (sha1, sha1_git, sha256) Returns: Information on one possible origin for such content. Raises: BadInputExc in case of unknown algo_hash or bad hash NotFoundExc if the content is not found. """ found = service.lookup_hash(q)['found'] if not found: raise NotFoundExc('Content with %s not found.' % q) return service.lookup_hash_origin(q) @app.route('/api/1/content//raw/') @set_renderers(renderers.BytesRenderer) def api_content_raw(q): """Return content's raw data if content is found. Args: q is of the form (algo_hash:)hash with algo_hash in (sha1, sha1_git, sha256). When algo_hash is not provided, 'hash' is considered sha1. Returns: Content's raw data in text/plain. Raises: - BadInputExc in case of unknown algo_hash or bad hash - NotFoundExc if the content is not found. """ content = service.lookup_content_raw(q) if not content: raise NotFoundExc('Content with %s not found.' % q) return content['data'] @app.route('/api/1/content//') def api_content_with_details(q): """Return content information if content is found. Args: q is of the form (algo_hash:)hash with algo_hash in (sha1, sha1_git, sha256). When algo_hash is not provided, 'hash' is considered sha1. Returns: Content's information. Raises: - BadInputExc in case of unknown algo_hash or bad hash - NotFoundExc if the content is not found. """ content = service.lookup_content(q) if not content: raise NotFoundExc('Content with %s not found.' % q) content['data'] = url_for('api_content_raw', q=content['sha1']) return content @app.route('/api/1/uploadnsearch/', methods=['POST']) def api_uploadnsearch(): """Upload the file's content in the post body request. Compute its hash and determine if it exists in the storage. Args: request.files filled with the filename's data to upload. Returns: Dictionary with 'sha1', 'filename' and 'found' predicate depending on whether we find it or not. Raises: BadInputExc in case of the form submitted is incorrect. """ file = request.files.get('filename') if not file: raise BadInputExc('Bad request, missing \'filename\' entry in form.') return service.upload_and_search(file)