diff --git a/docs/endpoints/collection.rst b/docs/endpoints/collection.rst index 7debb7f2..53219258 100644 --- a/docs/endpoints/collection.rst +++ b/docs/endpoints/collection.rst @@ -1,73 +1,73 @@ Create deposit ^^^^^^^^^^^^^^^ .. http:post:: /1// Create deposit in a collection. - + The client sends a deposit request to a specific collection with: * an archive holding the software source code (binary upload) * an envelop with metadata describing information regarding a deposit (atom entry deposit) Also known as: COL-IRI :param text : the client's credentials :param text Content-Type: accepted mimetype :param int Content-Length: tarball size :param text Content-MD5: md5 checksum hex encoded of the tarball :param text Content-Disposition: attachment; filename=[filename]; the filename parameter must be text (ascii) :param text Content-Disposition: for the metadata file set name parameter to 'atom'. :param bool In-progress: true if not final; false when final request. :statuscode 201: success for deposit on POST :statuscode 401: Unauthorized :statuscode 404: access to an unknown collection :statuscode 415: unsupported media type Sample request ~~~~~~~~~~~~~~~ .. code:: shell curl -i -u hal: \ -F "file=@../deposit.json;type=application/zip;filename=payload" \ -F "atom=@../atom-entry.xml;type=application/atom+xml;charset=UTF-8" \ -H 'In-Progress: false' \ -H 'Slug: some-external-id' \ -XPOST https://deposit.softwareheritage.org/1/hal/ Sample response ~~~~~~~~~~~~~~~ .. code:: shell HTTP/1.0 201 Created Date: Tue, 26 Sep 2017 10:32:35 GMT Server: WSGIServer/0.2 CPython/3.5.3 Vary: Accept, Cookie Allow: GET, POST, PUT, DELETE, HEAD, OPTIONS Location: /1/hal/10/metadata/ X-Frame-Options: SAMEORIGIN Content-Type: application/xml 10 Sept. 26, 2017, 10:32 a.m. None deposited http://purl.org/net/sword/package/SimpleZip diff --git a/swh/deposit/api/converters.py b/swh/deposit/api/converters.py index 928fb248..2f251ef5 100644 --- a/swh/deposit/api/converters.py +++ b/swh/deposit/api/converters.py @@ -1,58 +1,57 @@ # Copyright (C) 2017-2018 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 def convert_status_detail(status_detail): """Given a status_detail dict, transforms it into a human readable string. - Dict has the following form (all first level keys are optional): - { - 'url': { - 'summary': , - 'fields': - }, - 'metadata': [{ - 'summary': , - 'fields': , - }], - 'archive': [{ - 'summary': , - 'fields': , - }] - - - } + Dict has the following form (all first level keys are optional): + { + 'url': { + 'summary': "summary-string", + 'fields': [impacted-fields-list] + }, + 'metadata': [{ + 'summary': "summary-string", + 'fields': [impacted-fields-list], + }], + 'archive': [{ + 'summary': "summary-string", + 'fields': [impacted-fields-list], + }] + } Args: - status_detail (dict): + status_detail (dict): The status detail dict with the syntax + mentioned Returns: - Status detail as inlined string. + the status detail as inlined string """ if not status_detail: return None def _str_fields(data): fields = data.get('fields') if not fields: return '' return ' (%s)' % ', '.join(map(str, fields)) msg = [] for key in ['metadata', 'archive']: _detail = status_detail.get(key) if _detail: for data in _detail: msg.append('- %s%s\n' % (data['summary'], _str_fields(data))) _detail = status_detail.get('url') if _detail: msg.append('- %s%s\n' % (_detail['summary'], _str_fields(_detail))) if not msg: return None return ''.join(msg) diff --git a/swh/deposit/api/deposit_update.py b/swh/deposit/api/deposit_update.py index 42af22e0..109902a7 100644 --- a/swh/deposit/api/deposit_update.py +++ b/swh/deposit/api/deposit_update.py @@ -1,155 +1,148 @@ # Copyright (C) 2017-2018 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 rest_framework import status from .common import SWHPostDepositAPI, SWHPutDepositAPI, SWHDeleteDepositAPI from .common import ACCEPT_ARCHIVE_CONTENT_TYPES from ..config import CONT_FILE_IRI, EDIT_SE_IRI, EM_IRI from ..errors import make_error_dict, BAD_REQUEST from ..parsers import SWHFileUploadZipParser, SWHFileUploadTarParser from ..parsers import SWHAtomEntryParser from ..parsers import SWHMultiPartParser class SWHUpdateArchiveDeposit(SWHPostDepositAPI, SWHPutDepositAPI, SWHDeleteDepositAPI): """Deposit request class defining api endpoints for sword deposit. - What's known as 'EM IRI' in the sword specification. + What's known as 'EM IRI' in the sword specification. - HTTP verbs supported: PUT, POST, DELETE + HTTP verbs supported: PUT, POST, DELETE """ parser_classes = (SWHFileUploadZipParser, SWHFileUploadTarParser, ) def process_put(self, req, headers, collection_name, deposit_id): """Replace existing content for the existing deposit. - source: http://swordapp.github.io/SWORDv2-Profile/SWORDProfile.html - #protocoloperations_editingcontent_binary + source: http://swordapp.github.io/SWORDv2-Profile/SWORDProfile.html#protocoloperations_editingcontent_binary # noqa Returns: 204 No content """ if req.content_type not in ACCEPT_ARCHIVE_CONTENT_TYPES: msg = 'Packaging format supported is restricted to %s' % ( ', '.join(ACCEPT_ARCHIVE_CONTENT_TYPES)) return make_error_dict(BAD_REQUEST, msg) return self._binary_upload(req, headers, collection_name, deposit_id=deposit_id, replace_archives=True) def process_post(self, req, headers, collection_name, deposit_id): """Add new content to the existing deposit. - source: http://swordapp.github.io/SWORDv2-Profile/SWORDProfile.html - #protocoloperations_addingcontent_mediaresource + source: http://swordapp.github.io/SWORDv2-Profile/SWORDProfile.html#protocoloperations_addingcontent_mediaresource # noqa Returns: 201 Created Headers: Location: [Cont-File-IRI] Body: [optional Deposit Receipt] """ if req.content_type not in ACCEPT_ARCHIVE_CONTENT_TYPES: msg = 'Packaging format supported is restricted to %s' % ( ', '.join(ACCEPT_ARCHIVE_CONTENT_TYPES)) return 'unused', 'unused', make_error_dict(BAD_REQUEST, msg) return (status.HTTP_201_CREATED, CONT_FILE_IRI, self._binary_upload(req, headers, collection_name, deposit_id)) def process_delete(self, req, collection_name, deposit_id): """Delete content (archives) from existing deposit. - source: http://swordapp.github.io/SWORDv2-Profile/SWORDProfile.html - #protocoloperations_deletingcontent + source: http://swordapp.github.io/SWORDv2-Profile/SWORDProfile.html#protocoloperations_deletingcontent # noqa Returns: 204 Created """ return self._delete_archives(collection_name, deposit_id) class SWHUpdateMetadataDeposit(SWHPostDepositAPI, SWHPutDepositAPI, SWHDeleteDepositAPI): """Deposit request class defining api endpoints for sword deposit. - What's known as 'Edit IRI' (and SE IRI) in the sword specification. + What's known as 'Edit IRI' (and SE IRI) in the sword specification. - HTTP verbs supported: POST (SE IRI), PUT (Edit IRI), DELETE + HTTP verbs supported: POST (SE IRI), PUT (Edit IRI), DELETE """ parser_classes = (SWHMultiPartParser, SWHAtomEntryParser) def process_put(self, req, headers, collection_name, deposit_id): """Replace existing deposit's metadata/archive with new ones. - source: - - http://swordapp.github.io/SWORDv2-Profile/SWORDProfile.html - #protocoloperations_editingcontent_metadata - - http://swordapp.github.io/SWORDv2-Profile/SWORDProfile.html - #protocoloperations_editingcontent_multipart + source: + - http://swordapp.github.io/SWORDv2-Profile/SWORDProfile.html#protocoloperations_editingcontent_metadata # noqa + - http://swordapp.github.io/SWORDv2-Profile/SWORDProfile.html#protocoloperations_editingcontent_multipart # noqa Returns: 204 No content """ if req.content_type.startswith('multipart/'): return self._multipart_upload(req, headers, collection_name, deposit_id=deposit_id, replace_archives=True, replace_metadata=True) return self._atom_entry(req, headers, collection_name, deposit_id=deposit_id, replace_metadata=True) def process_post(self, req, headers, collection_name, deposit_id): """Add new metadata/archive to existing deposit. - source: - - http://swordapp.github.io/SWORDv2-Profile/SWORDProfile.html - #protocoloperations_addingcontent_metadata - - http://swordapp.github.io/SWORDv2-Profile/SWORDProfile.html - #protocoloperations_addingcontent_multipart + source: + - http://swordapp.github.io/SWORDv2-Profile/SWORDProfile.html#protocoloperations_addingcontent_metadata # noqa + - http://swordapp.github.io/SWORDv2-Profile/SWORDProfile.html#protocoloperations_addingcontent_multipart # noqa This also deals with an empty post corner case to finalize a deposit. Returns: In optimal case for a multipart and atom-entry update, a 201 Created response. The body response will hold a deposit. And the response headers will contain an entry 'Location' with the EM-IRI. For the empty post case, this returns a 200. """ if req.content_type.startswith('multipart/'): return (status.HTTP_201_CREATED, EM_IRI, self._multipart_upload(req, headers, collection_name, deposit_id=deposit_id)) # check for final empty post # source: http://swordapp.github.io/SWORDv2-Profile/SWORDProfile.html # #continueddeposit_complete if headers['content-length'] == 0 and headers['in-progress'] is False: data = self._empty_post(req, headers, collection_name, deposit_id) return (status.HTTP_200_OK, EDIT_SE_IRI, data) return (status.HTTP_201_CREATED, EM_IRI, self._atom_entry(req, headers, collection_name, deposit_id=deposit_id)) def process_delete(self, req, collection_name, deposit_id): """Delete the container (deposit). - Source: http://swordapp.github.io/SWORDv2-Profile/SWORDProfile.html - #protocoloperations_deleteconteiner + source: http://swordapp.github.io/SWORDv2-Profile/SWORDProfile.html#protocoloperations_deleteconteiner # noqa + """ return self._delete_deposit(collection_name, deposit_id) diff --git a/swh/deposit/api/urls.py b/swh/deposit/api/urls.py index 1aba2429..cc516092 100644 --- a/swh/deposit/api/urls.py +++ b/swh/deposit/api/urls.py @@ -1,68 +1,57 @@ -# Copyright (C) 2017 The Software Heritage developers +# Copyright (C) 2017-2018 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 """swh URL Configuration -The `urlpatterns` list routes URLs to views. For more information please see: - https://docs.djangoproject.com/en/1.10/topics/http/urls/ -Examples: -Function views - 1. Add an import: from my_app import views - 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') -Class-based views - 1. Add an import: from other_app.views import Home - 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') -Including another URLconf - 1. Import the include() function: from django.conf.urls import url, include - 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) """ + from django.conf.urls import url from ..config import EDIT_SE_IRI, EM_IRI, CONT_FILE_IRI from ..config import SD_IRI, COL_IRI, STATE_IRI from .deposit import SWHDeposit from .deposit_status import SWHDepositStatus from .deposit_update import SWHUpdateMetadataDeposit from .deposit_update import SWHUpdateArchiveDeposit from .deposit_content import SWHDepositContent from .service_document import SWHServiceDocument urlpatterns = [ # PUBLIC API # SD IRI - Service Document IRI # -> GET url(r'^servicedocument/', SWHServiceDocument.as_view(), name=SD_IRI), # Col IRI - Collection IRI # -> POST url(r'^(?P[^/]+)/$', SWHDeposit.as_view(), name=COL_IRI), # EM IRI - Atom Edit Media IRI (update archive IRI) # -> PUT (update-in-place existing archive) # -> POST (add new archive) url(r'^(?P[^/]+)/(?P[^/]+)/media/$', SWHUpdateArchiveDeposit.as_view(), name=EM_IRI), # Edit IRI - Atom Entry Edit IRI (update metadata IRI) # SE IRI - Sword Edit IRI ;; possibly same as Edit IRI # -> PUT (update in place) # -> POST (add new metadata) url(r'^(?P[^/]+)/(?P[^/]+)/metadata/$', SWHUpdateMetadataDeposit.as_view(), name=EDIT_SE_IRI), # State IRI # -> GET url(r'^(?P[^/]+)/(?P[^/]+)/status/$', SWHDepositStatus.as_view(), name=STATE_IRI), # Cont/File IRI # -> GET url(r'^(?P[^/]+)/(?P[^/]+)/content/$', SWHDepositContent.as_view(), name=CONT_FILE_IRI), # specification is not clear about # FILE-IRI, we assume it's the same as # the CONT-IRI one ] diff --git a/swh/deposit/urls.py b/swh/deposit/urls.py index e889fabe..355029b5 100644 --- a/swh/deposit/urls.py +++ b/swh/deposit/urls.py @@ -1,50 +1,30 @@ # Copyright (C) 2017-2018 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 """swhdeposit URL Configuration -The :data:`urlpatterns` list routes URLs to views. For more information please - see: https://docs.djangoproject.com/en/1.11/topics/http/urls/ - -Examples: - -- Function views: - - 1. Add an import: ``from my_app import views`` - 2. Add a URL to urlpatterns: ``url(r'^$', views.home, name='home')`` - -- Class-based views: - - 1. Add an import: ``from other_app.views import Home`` - 2. Add a URL to urlpatterns: ``url(r'^$', Home.as_view(), name='home')`` - -- Including another URLconf: - - 1. Import the include function: ``from django.conf.urls import url, include`` - 2. Add a URL to urlpatterns: ``url(r'^blog/', include('blog.urls'))`` - """ from django.conf.urls import url, include from django.shortcuts import render from django.views.generic.base import RedirectView from rest_framework.urlpatterns import format_suffix_patterns favicon_view = RedirectView.as_view(url='/static/img/icons/swh-logo-32x32.png', permanent=True) def default_view(req): return render(req, "homepage.html") urlpatterns = [ url(r'^favicon\.ico$', favicon_view), url(r'^1/', include('swh.deposit.api.urls')), url(r'^1/private/', include('swh.deposit.api.private.urls')), url(r'^$', default_view, name='home'), ] urlpatterns = format_suffix_patterns(urlpatterns)