Changeset View
Changeset View
Standalone View
Standalone View
swh/deposit/api/common.py
# Copyright (C) 2017-2020 The Software Heritage developers | # Copyright (C) 2017-2020 The Software Heritage developers | ||||
# See the AUTHORS file at the top-level directory of this distribution | # See the AUTHORS file at the top-level directory of this distribution | ||||
# License: GNU General Public License version 3, or any later version | # License: GNU General Public License version 3, or any later version | ||||
# See top-level LICENSE file for more information | # See top-level LICENSE file for more information | ||||
from abc import ABCMeta, abstractmethod | from abc import ABCMeta, abstractmethod | ||||
import datetime | |||||
import hashlib | import hashlib | ||||
from typing import Sequence, Type | import json | ||||
from typing import Any, Dict, Optional, Sequence, Tuple, Type, Union | |||||
from django.http import HttpResponse | from django.http import FileResponse, HttpResponse | ||||
from django.shortcuts import render | from django.shortcuts import render | ||||
from django.urls import reverse | from django.urls import reverse | ||||
from django.utils import timezone | from django.utils import timezone | ||||
from rest_framework import status | from rest_framework import status | ||||
from rest_framework.authentication import BaseAuthentication, BasicAuthentication | from rest_framework.authentication import BaseAuthentication, BasicAuthentication | ||||
from rest_framework.permissions import BasePermission, IsAuthenticated | from rest_framework.permissions import BasePermission, IsAuthenticated | ||||
from rest_framework.request import Request | |||||
from rest_framework.views import APIView | from rest_framework.views import APIView | ||||
from swh.model import hashutil | from swh.model import hashutil | ||||
from swh.scheduler.utils import create_oneshot_task_dict | from swh.scheduler.utils import create_oneshot_task_dict | ||||
from ..config import ( | from ..config import ( | ||||
ARCHIVE_KEY, | ARCHIVE_KEY, | ||||
ARCHIVE_TYPE, | ARCHIVE_TYPE, | ||||
▲ Show 20 Lines • Show All 41 Lines • ▼ Show 20 Lines | class AuthenticatedAPIView(APIView): | ||||
permission_classes: Sequence[Type[BasePermission]] = (IsAuthenticated,) | permission_classes: Sequence[Type[BasePermission]] = (IsAuthenticated,) | ||||
class APIBase(APIConfig, AuthenticatedAPIView, metaclass=ABCMeta): | class APIBase(APIConfig, AuthenticatedAPIView, metaclass=ABCMeta): | ||||
"""Base deposit request class sharing multiple common behaviors. | """Base deposit request class sharing multiple common behaviors. | ||||
""" | """ | ||||
def _read_headers(self, request): | def _read_headers(self, request: Request) -> Dict[str, Any]: | ||||
"""Read and unify the necessary headers from the request (those are | """Read and unify the necessary headers from the request (those are | ||||
not stored in the same location or not properly formatted). | not stored in the same location or not properly formatted). | ||||
Args: | Args: | ||||
request (Request): Input request | request (Request): Input request | ||||
Returns: | Returns: | ||||
Dictionary with the following keys (some associated values may be | Dictionary with the following keys (some associated values may be | ||||
Show All 35 Lines | def _read_headers(self, request: Request) -> Dict[str, Any]: | ||||
"content-disposition": content_disposition, | "content-disposition": content_disposition, | ||||
"content-md5sum": content_md5sum, | "content-md5sum": content_md5sum, | ||||
"packaging": packaging, | "packaging": packaging, | ||||
"slug": slug, | "slug": slug, | ||||
"on-behalf-of": on_behalf_of, | "on-behalf-of": on_behalf_of, | ||||
"metadata-relevant": metadata_relevant, | "metadata-relevant": metadata_relevant, | ||||
} | } | ||||
def _compute_md5(self, filehandler): | def _compute_md5(self, filehandler) -> bytes: | ||||
"""Compute uploaded file's md5 sum. | """Compute uploaded file's md5 sum. | ||||
Args: | Args: | ||||
filehandler (InMemoryUploadedFile): the file to compute the md5 | filehandler (InMemoryUploadedFile): the file to compute the md5 | ||||
hash | hash | ||||
Returns: | Returns: | ||||
the md5 checksum (str) | the md5 checksum (str) | ||||
""" | """ | ||||
h = hashlib.md5() | h = hashlib.md5() | ||||
for chunk in filehandler: | for chunk in filehandler: | ||||
h.update(chunk) | h.update(chunk) | ||||
return h.digest() | return h.digest() | ||||
def _deposit_put( | def _deposit_put( | ||||
self, request, deposit_id=None, in_progress=False, external_id=None | self, | ||||
): | request: Request, | ||||
deposit_id: Optional[int] = None, | |||||
in_progress: bool = False, | |||||
external_id: Optional[str] = None, | |||||
) -> Deposit: | |||||
"""Save/Update a deposit in db. | """Save/Update a deposit in db. | ||||
Args: | Args: | ||||
deposit_id (int): deposit identifier | request: request data | ||||
in_progress (dict): The deposit's status | deposit_id: deposit identifier | ||||
external_id (str): The external identifier to associate to | in_progress: deposit status | ||||
the deposit | external_id: external identifier to associate to the deposit | ||||
Returns: | Returns: | ||||
The Deposit instance saved or updated. | The Deposit instance saved or updated. | ||||
""" | """ | ||||
complete_date: Optional[datetime.datetime] = None | |||||
deposit_parent: Optional[Deposit] = None | |||||
if in_progress is False: | if in_progress is False: | ||||
complete_date = timezone.now() | complete_date = timezone.now() | ||||
status_type = DEPOSIT_STATUS_DEPOSITED | status_type = DEPOSIT_STATUS_DEPOSITED | ||||
else: | else: | ||||
complete_date = None | |||||
status_type = DEPOSIT_STATUS_PARTIAL | status_type = DEPOSIT_STATUS_PARTIAL | ||||
if not deposit_id: | if not deposit_id: | ||||
try: | try: | ||||
# find a deposit parent (same external id, status load | # find a deposit parent (same external id, status load to success) | ||||
# to success) | |||||
deposit_parent = ( | deposit_parent = ( | ||||
Deposit.objects.filter( | Deposit.objects.filter( | ||||
external_id=external_id, status=DEPOSIT_STATUS_LOAD_SUCCESS | external_id=external_id, status=DEPOSIT_STATUS_LOAD_SUCCESS | ||||
) | ) | ||||
.order_by("-id")[0:1] | .order_by("-id")[0:1] | ||||
.get() | .get() | ||||
) # noqa | ) # noqa | ||||
except Deposit.DoesNotExist: | except Deposit.DoesNotExist: | ||||
deposit_parent = None | # then no parent for that deposit, deposit_parent already None | ||||
pass | |||||
assert external_id is not None | |||||
deposit = Deposit( | deposit = Deposit( | ||||
collection=self._collection, | collection=self._collection, | ||||
external_id=external_id, | external_id=external_id, | ||||
complete_date=complete_date, | complete_date=complete_date, | ||||
status=status_type, | status=status_type, | ||||
client=self._client, | client=self._client, | ||||
parent=deposit_parent, | parent=deposit_parent, | ||||
) | ) | ||||
Show All 17 Lines | ) -> Deposit: | ||||
deposit.check_task_id = check_task_id | deposit.check_task_id = check_task_id | ||||
deposit.save() | deposit.save() | ||||
return deposit | return deposit | ||||
def _deposit_request_put( | def _deposit_request_put( | ||||
self, | self, | ||||
deposit, | deposit: Deposit, | ||||
deposit_request_data, | deposit_request_data: Dict[str, Any], | ||||
replace_metadata=False, | replace_metadata: bool = False, | ||||
replace_archives=False, | replace_archives: bool = False, | ||||
): | ) -> None: | ||||
"""Save a deposit request with metadata attached to a deposit. | """Save a deposit request with metadata attached to a deposit. | ||||
Args: | Args: | ||||
deposit (Deposit): The deposit concerned by the request | deposit: The deposit concerned by the request | ||||
deposit_request_data (dict): The dictionary with at most 2 deposit | deposit_request_data: The dictionary with at most 2 deposit | ||||
request types (archive, metadata) to associate to the deposit | request types (archive, metadata) to associate to the deposit | ||||
replace_metadata (bool): Flag defining if we add or update | replace_metadata: Flag defining if we add or update | ||||
existing metadata to the deposit | existing metadata to the deposit | ||||
replace_archives (bool): Flag defining if we add or update | replace_archives: Flag defining if we add or update | ||||
archives to existing deposit | archives to existing deposit | ||||
Returns: | Returns: | ||||
None | None | ||||
""" | """ | ||||
if replace_metadata: | if replace_metadata: | ||||
DepositRequest.objects.filter(deposit=deposit, type=METADATA_TYPE).delete() | DepositRequest.objects.filter(deposit=deposit, type=METADATA_TYPE).delete() | ||||
if replace_archives: | if replace_archives: | ||||
DepositRequest.objects.filter(deposit=deposit, type=ARCHIVE_TYPE).delete() | DepositRequest.objects.filter(deposit=deposit, type=ARCHIVE_TYPE).delete() | ||||
deposit_request = None | deposit_request = None | ||||
archive_file = deposit_request_data.get(ARCHIVE_KEY) | archive_file = deposit_request_data.get(ARCHIVE_KEY) | ||||
if archive_file: | if archive_file: | ||||
deposit_request = DepositRequest( | deposit_request = DepositRequest( | ||||
type=ARCHIVE_TYPE, deposit=deposit, archive=archive_file | type=ARCHIVE_TYPE, deposit=deposit, archive=archive_file | ||||
) | ) | ||||
deposit_request.save() | deposit_request.save() | ||||
metadata = deposit_request_data.get(METADATA_KEY) | metadata = deposit_request_data.get(METADATA_KEY) | ||||
if metadata: | if metadata: | ||||
raw_metadata = deposit_request_data.get(RAW_METADATA_KEY) | raw_metadata = deposit_request_data[RAW_METADATA_KEY] | ||||
deposit_request = DepositRequest( | deposit_request = DepositRequest( | ||||
type=METADATA_TYPE, | type=METADATA_TYPE, | ||||
deposit=deposit, | deposit=deposit, | ||||
metadata=metadata, | metadata=metadata, | ||||
raw_metadata=raw_metadata.decode("utf-8"), | raw_metadata=raw_metadata.decode("utf-8"), | ||||
) | ) | ||||
deposit_request.save() | deposit_request.save() | ||||
assert deposit_request is not None | assert deposit_request is not None | ||||
def _delete_archives(self, collection_name, deposit_id): | def _delete_archives(self, collection_name: str, deposit_id: int) -> Dict: | ||||
"""Delete archives reference from the deposit id. | """Delete archive references from the deposit id. | ||||
""" | """ | ||||
try: | try: | ||||
deposit = Deposit.objects.get(pk=deposit_id) | deposit = Deposit.objects.get(pk=deposit_id) | ||||
except Deposit.DoesNotExist: | except Deposit.DoesNotExist: | ||||
return make_error_dict( | return make_error_dict( | ||||
NOT_FOUND, f"The deposit {deposit_id} does not exist" | NOT_FOUND, f"The deposit {deposit_id} does not exist" | ||||
) | ) | ||||
DepositRequest.objects.filter(deposit=deposit, type=ARCHIVE_TYPE).delete() | DepositRequest.objects.filter(deposit=deposit, type=ARCHIVE_TYPE).delete() | ||||
return {} | return {} | ||||
def _delete_deposit(self, collection_name, deposit_id): | def _delete_deposit(self, collection_name: str, deposit_id: int) -> Dict: | ||||
"""Delete deposit reference. | """Delete deposit reference. | ||||
Args: | Args: | ||||
collection_name (str): Client's name | collection_name: Client's collection | ||||
deposit_id (id): The deposit to delete | deposit_id: The deposit to delete | ||||
Returns | Returns | ||||
Empty dict when ok. | Empty dict when ok. | ||||
Dict with error key to describe the failure. | Dict with error key to describe the failure. | ||||
""" | """ | ||||
try: | try: | ||||
deposit = Deposit.objects.get(pk=deposit_id) | deposit = Deposit.objects.get(pk=deposit_id) | ||||
Show All 12 Lines | def _delete_deposit(self, collection_name: str, deposit_id: int) -> Dict: | ||||
BAD_REQUEST, summary=summary, verbose_description=description | BAD_REQUEST, summary=summary, verbose_description=description | ||||
) | ) | ||||
DepositRequest.objects.filter(deposit=deposit).delete() | DepositRequest.objects.filter(deposit=deposit).delete() | ||||
deposit.delete() | deposit.delete() | ||||
return {} | return {} | ||||
def _check_preconditions_on(self, filehandler, md5sum, content_length=None): | def _check_preconditions_on( | ||||
self, filehandler, md5sum: str, content_length: Optional[int] = None | |||||
) -> Optional[Dict]: | |||||
"""Check preconditions on provided file are respected. That is the | """Check preconditions on provided file are respected. That is the | ||||
length and/or the md5sum hash match the file's content. | length and/or the md5sum hash match the file's content. | ||||
Args: | Args: | ||||
filehandler (InMemoryUploadedFile): The file to check | filehandler (InMemoryUploadedFile): The file to check | ||||
md5sum (hex str): md5 hash expected from the file's content | md5sum: md5 hash expected from the file's content | ||||
content_length (int): the expected length if provided. | content_length: the expected length if provided. | ||||
Returns: | Returns: | ||||
Either none if no error or a dictionary with a key error | Either none if no error or a dictionary with a key error | ||||
detailing the problem. | detailing the problem. | ||||
""" | """ | ||||
max_upload_size = self.config["max_upload_size"] | max_upload_size = self.config["max_upload_size"] | ||||
if content_length: | if content_length: | ||||
Show All 19 Lines | ) -> Optional[Dict]: | ||||
f"The checksum sent {hashutil.hash_to_hex(md5sum)} and the actual " | f"The checksum sent {hashutil.hash_to_hex(md5sum)} and the actual " | ||||
f"checksum {hashutil.hash_to_hex(_md5sum)} does not match.", | f"checksum {hashutil.hash_to_hex(_md5sum)} does not match.", | ||||
) | ) | ||||
return None | return None | ||||
def _binary_upload( | def _binary_upload( | ||||
self, | self, | ||||
request, | request: Request, | ||||
headers, | headers: Dict[str, Any], | ||||
collection_name, | collection_name: str, | ||||
deposit_id=None, | deposit_id: Optional[int] = None, | ||||
replace_metadata=False, | replace_metadata: bool = False, | ||||
replace_archives=False, | replace_archives: bool = False, | ||||
): | ) -> Dict[str, Any]: | ||||
"""Binary upload routine. | """Binary upload routine. | ||||
Other than such a request, a 415 response is returned. | Other than such a request, a 415 response is returned. | ||||
Args: | Args: | ||||
request (Request): the request holding information to parse | request (Request): the request holding information to parse | ||||
and inject in db | and inject in db | ||||
headers (dict): request headers formatted | headers (dict): request headers formatted | ||||
▲ Show 20 Lines • Show All 77 Lines • ▼ Show 20 Lines | ) -> Dict[str, Any]: | ||||
return { | return { | ||||
"deposit_id": deposit.id, | "deposit_id": deposit.id, | ||||
"deposit_date": deposit.reception_date, | "deposit_date": deposit.reception_date, | ||||
"status": deposit.status, | "status": deposit.status, | ||||
"archive": filehandler.name, | "archive": filehandler.name, | ||||
} | } | ||||
def _read_metadata(self, metadata_stream): | def _read_metadata(self, metadata_stream) -> Tuple[bytes, Dict[str, Any]]: | ||||
"""Given a metadata stream, reads the metadata and returns both the | """Given a metadata stream, reads the metadata and returns both the | ||||
parsed and the raw metadata. | parsed and the raw metadata. | ||||
""" | """ | ||||
raw_metadata = metadata_stream.read() | raw_metadata = metadata_stream.read() | ||||
metadata = parse_xml(raw_metadata) | metadata = parse_xml(raw_metadata) | ||||
return raw_metadata, metadata | return raw_metadata, metadata | ||||
def _multipart_upload( | def _multipart_upload( | ||||
self, | self, | ||||
request, | request: Request, | ||||
headers, | headers: Dict[str, Any], | ||||
collection_name, | collection_name: str, | ||||
deposit_id=None, | deposit_id: Optional[int] = None, | ||||
replace_metadata=False, | replace_metadata: bool = False, | ||||
replace_archives=False, | replace_archives: bool = False, | ||||
): | ) -> Dict: | ||||
"""Multipart upload supported with exactly: | """Multipart upload supported with exactly: | ||||
- 1 archive (zip) | - 1 archive (zip) | ||||
- 1 atom entry | - 1 atom entry | ||||
Other than such a request, a 415 response is returned. | Other than such a request, a 415 response is returned. | ||||
Args: | Args: | ||||
request (Request): the request holding information to parse | request (Request): the request holding information to parse | ||||
and inject in db | and inject in db | ||||
headers (dict): request headers formatted | headers: request headers formatted | ||||
collection_name (str): the associated client | collection_name: the associated client | ||||
deposit_id (id): deposit identifier if provided | deposit_id: deposit identifier if provided | ||||
replace_metadata (bool): 'Update or add' request to existing | replace_metadata: 'Update or add' request to existing | ||||
deposit. If False (default), this adds new metadata request to | deposit. If False (default), this adds new metadata request to | ||||
existing ones. Otherwise, this will replace existing metadata. | existing ones. Otherwise, this will replace existing metadata. | ||||
replace_archives (bool): 'Update or add' request to existing | replace_archives: 'Update or add' request to existing | ||||
deposit. If False (default), this adds new archive request to | deposit. If False (default), this adds new archive request to | ||||
existing ones. Otherwise, this will replace existing archives. | existing ones. Otherwise, this will replace existing archives. | ||||
ones. | ones. | ||||
Returns: | Returns: | ||||
In the optimal case a dict with the following keys: | In the optimal case a dict with the following keys: | ||||
- deposit_id (int): Deposit identifier | - deposit_id (int): Deposit identifier | ||||
- deposit_date (date): Deposit date | - deposit_date (date): Deposit date | ||||
Show All 10 Lines | ) -> Dict: | ||||
archive exceeds the max size configured | archive exceeds the max size configured | ||||
- 415 (unsupported media type) if a wrong media type is provided | - 415 (unsupported media type) if a wrong media type is provided | ||||
""" | """ | ||||
external_id = headers["slug"] | external_id = headers["slug"] | ||||
content_types_present = set() | content_types_present = set() | ||||
data = { | data: Dict[str, Optional[Any]] = { | ||||
"application/zip": None, # expected either zip | "application/zip": None, # expected either zip | ||||
"application/x-tar": None, # or x-tar | "application/x-tar": None, # or x-tar | ||||
"application/atom+xml": None, | "application/atom+xml": None, | ||||
} | } | ||||
for key, value in request.FILES.items(): | for key, value in request.FILES.items(): | ||||
fh = value | fh = value | ||||
if fh.content_type in content_types_present: | content_type = fh.content_type | ||||
if content_type in content_types_present: | |||||
return make_error_dict( | return make_error_dict( | ||||
ERROR_CONTENT, | ERROR_CONTENT, | ||||
"Only 1 application/zip (or application/x-tar) archive " | "Only 1 application/zip (or application/x-tar) archive " | ||||
"and 1 atom+xml entry is supported (as per sword2.0 " | "and 1 atom+xml entry is supported (as per sword2.0 " | ||||
"specification)", | "specification)", | ||||
"You provided more than 1 application/(zip|x-tar) " | "You provided more than 1 application/(zip|x-tar) " | ||||
"or more than 1 application/atom+xml content-disposition " | "or more than 1 application/atom+xml content-disposition " | ||||
"header in the multipart deposit", | "header in the multipart deposit", | ||||
) | ) | ||||
content_types_present.add(fh.content_type) | content_types_present.add(content_type) | ||||
data[fh.content_type] = fh | assert content_type is not None | ||||
data[content_type] = fh | |||||
if len(content_types_present) != 2: | if len(content_types_present) != 2: | ||||
return make_error_dict( | return make_error_dict( | ||||
ERROR_CONTENT, | ERROR_CONTENT, | ||||
"You must provide both 1 application/zip (or " | "You must provide both 1 application/zip (or " | ||||
"application/x-tar) and 1 atom+xml entry for multipart " | "application/x-tar) and 1 atom+xml entry for multipart " | ||||
"deposit", | "deposit", | ||||
"You need to provide only 1 application/(zip|x-tar) " | "You need to provide only 1 application/(zip|x-tar) " | ||||
Show All 33 Lines | ) -> Dict: | ||||
ARCHIVE_KEY: filehandler, | ARCHIVE_KEY: filehandler, | ||||
METADATA_KEY: metadata, | METADATA_KEY: metadata, | ||||
RAW_METADATA_KEY: raw_metadata, | RAW_METADATA_KEY: raw_metadata, | ||||
} | } | ||||
self._deposit_request_put( | self._deposit_request_put( | ||||
deposit, deposit_request_data, replace_metadata, replace_archives | deposit, deposit_request_data, replace_metadata, replace_archives | ||||
) | ) | ||||
assert filehandler is not None | |||||
return { | return { | ||||
"deposit_id": deposit.id, | "deposit_id": deposit.id, | ||||
"deposit_date": deposit.reception_date, | "deposit_date": deposit.reception_date, | ||||
"archive": filehandler.name, | "archive": filehandler.name, | ||||
"status": deposit.status, | "status": deposit.status, | ||||
} | } | ||||
def _atom_entry( | def _atom_entry( | ||||
self, | self, | ||||
request, | request: Request, | ||||
headers, | headers: Dict[str, Any], | ||||
collection_name, | collection_name: str, | ||||
deposit_id=None, | deposit_id: Optional[int] = None, | ||||
replace_metadata=False, | replace_metadata: bool = False, | ||||
replace_archives=False, | replace_archives: bool = False, | ||||
): | ) -> Dict[str, Any]: | ||||
"""Atom entry deposit. | """Atom entry deposit. | ||||
Args: | Args: | ||||
request (Request): the request holding information to parse | request (Request): the request holding information to parse | ||||
and inject in db | and inject in db | ||||
headers (dict): request headers formatted | headers: request headers formatted | ||||
collection_name (str): the associated client | collection_name: the associated client | ||||
deposit_id (id): deposit identifier if provided | deposit_id: deposit identifier if provided | ||||
replace_metadata (bool): 'Update or add' request to existing | replace_metadata: 'Update or add' request to existing | ||||
deposit. If False (default), this adds new metadata request to | deposit. If False (default), this adds new metadata request to | ||||
existing ones. Otherwise, this will replace existing metadata. | existing ones. Otherwise, this will replace existing metadata. | ||||
replace_archives (bool): 'Update or add' request to existing | replace_archives: 'Update or add' request to existing | ||||
deposit. If False (default), this adds new archive request to | deposit. If False (default), this adds new archive request to | ||||
existing ones. Otherwise, this will replace existing archives. | existing ones. Otherwise, this will replace existing archives. | ||||
ones. | ones. | ||||
Returns: | Returns: | ||||
In the optimal case a dict with the following keys: | In the optimal case a dict with the following keys: | ||||
- deposit_id: deposit id associated to the deposit | - deposit_id: deposit id associated to the deposit | ||||
Show All 24 Lines | ) -> Dict[str, Any]: | ||||
BAD_REQUEST, | BAD_REQUEST, | ||||
"Empty body request is not supported", | "Empty body request is not supported", | ||||
"Atom entry deposit is supposed to send for metadata. " | "Atom entry deposit is supposed to send for metadata. " | ||||
"If the body is empty, there is no metadata.", | "If the body is empty, there is no metadata.", | ||||
) | ) | ||||
external_id = metadata.get("external_identifier", headers["slug"]) | external_id = metadata.get("external_identifier", headers["slug"]) | ||||
# TODO: Determine if we are in the metadata-only deposit case. If it is, then | |||||
# save deposit and deposit request typed 'metadata' and send metadata to the | |||||
# metadata storage. Otherwise, do as existing deposit. | |||||
deposit = self._deposit_put( | deposit = self._deposit_put( | ||||
request, | request, | ||||
deposit_id=deposit_id, | deposit_id=deposit_id, | ||||
in_progress=headers["in-progress"], | in_progress=headers["in-progress"], | ||||
external_id=external_id, | external_id=external_id, | ||||
) | ) | ||||
self._deposit_request_put( | self._deposit_request_put( | ||||
deposit, | deposit, | ||||
{METADATA_KEY: metadata, RAW_METADATA_KEY: raw_metadata}, | {METADATA_KEY: metadata, RAW_METADATA_KEY: raw_metadata}, | ||||
replace_metadata, | replace_metadata, | ||||
replace_archives, | replace_archives, | ||||
) | ) | ||||
return { | return { | ||||
"deposit_id": deposit.id, | "deposit_id": deposit.id, | ||||
"deposit_date": deposit.reception_date, | "deposit_date": deposit.reception_date, | ||||
"archive": None, | "archive": None, | ||||
"status": deposit.status, | "status": deposit.status, | ||||
} | } | ||||
def _empty_post(self, request, headers, collection_name, deposit_id): | def _empty_post( | ||||
self, request: Request, headers: Dict, collection_name: str, deposit_id: int | |||||
) -> Dict[str, Any]: | |||||
"""Empty post to finalize an empty deposit. | """Empty post to finalize an empty deposit. | ||||
Args: | Args: | ||||
request (Request): the request holding information to parse | request: the request holding information to parse | ||||
and inject in db | and inject in db | ||||
headers (dict): request headers formatted | headers: request headers formatted | ||||
collection_name (str): the associated client | collection_name: the associated client | ||||
deposit_id (id): deposit identifier | deposit_id: deposit identifier | ||||
Returns: | Returns: | ||||
Dictionary of result with the deposit's id, the date | Dictionary of result with the deposit's id, the date | ||||
it was completed and no archive. | it was completed and no archive. | ||||
""" | """ | ||||
deposit = Deposit.objects.get(pk=deposit_id) | deposit = Deposit.objects.get(pk=deposit_id) | ||||
deposit.complete_date = timezone.now() | deposit.complete_date = timezone.now() | ||||
deposit.status = DEPOSIT_STATUS_DEPOSITED | deposit.status = DEPOSIT_STATUS_DEPOSITED | ||||
deposit.save() | deposit.save() | ||||
return { | return { | ||||
"deposit_id": deposit_id, | "deposit_id": deposit_id, | ||||
"deposit_date": deposit.complete_date, | "deposit_date": deposit.complete_date, | ||||
"status": deposit.status, | "status": deposit.status, | ||||
"archive": None, | "archive": None, | ||||
} | } | ||||
def _make_iris(self, request, collection_name, deposit_id): | def _make_iris( | ||||
self, request: Request, collection_name: str, deposit_id: int | |||||
) -> Dict[str, Any]: | |||||
"""Define the IRI endpoints | """Define the IRI endpoints | ||||
Args: | Args: | ||||
request (Request): The initial request | request (Request): The initial request | ||||
collection_name (str): client/collection's name | collection_name (str): client/collection's name | ||||
deposit_id (id): Deposit identifier | deposit_id (id): Deposit identifier | ||||
Returns: | Returns: | ||||
Dictionary of keys with the iris' urls. | Dictionary of keys with the iris' urls. | ||||
""" | """ | ||||
args = [collection_name, deposit_id] | args = [collection_name, deposit_id] | ||||
return { | return { | ||||
iri: request.build_absolute_uri(reverse(iri, args=args)) | iri: request.build_absolute_uri(reverse(iri, args=args)) | ||||
for iri in [EM_IRI, EDIT_SE_IRI, CONT_FILE_IRI, STATE_IRI] | for iri in [EM_IRI, EDIT_SE_IRI, CONT_FILE_IRI, STATE_IRI] | ||||
} | } | ||||
def additional_checks(self, request, headers, collection_name, deposit_id=None): | def additional_checks( | ||||
self, | |||||
request: Request, | |||||
headers: Dict[str, Any], | |||||
collection_name: str, | |||||
deposit_id: Optional[int] = None, | |||||
) -> Dict[str, Any]: | |||||
"""Permit the child class to enrich additional checks. | """Permit the child class to enrich additional checks. | ||||
Returns: | Returns: | ||||
dict with 'error' detailing the problem. | dict with 'error' detailing the problem. | ||||
""" | """ | ||||
return {} | return {} | ||||
def checks(self, request, collection_name, deposit_id=None): | def checks( | ||||
self, request: Request, collection_name: str, deposit_id: Optional[int] = None | |||||
) -> Dict[str, Any]: | |||||
try: | try: | ||||
self._collection = DepositCollection.objects.get(name=collection_name) | self._collection = DepositCollection.objects.get(name=collection_name) | ||||
except DepositCollection.DoesNotExist: | except DepositCollection.DoesNotExist: | ||||
return make_error_dict( | return make_error_dict( | ||||
NOT_FOUND, f"Unknown collection name {collection_name}" | NOT_FOUND, f"Unknown collection name {collection_name}" | ||||
) | ) | ||||
assert self._collection is not None | |||||
username = request.user.username | username = request.user.username | ||||
if username: # unauthenticated request can have the username empty | if username: # unauthenticated request can have the username empty | ||||
try: | try: | ||||
self._client = DepositClient.objects.get(username=username) | self._client: DepositClient = DepositClient.objects.get( # type: ignore | ||||
username=username | |||||
) | |||||
ardumont: otherwise cryptic message: `error: Cannot determine type of 'objects'` | |||||
except DepositClient.DoesNotExist: | except DepositClient.DoesNotExist: | ||||
return make_error_dict(NOT_FOUND, f"Unknown client name {username}") | return make_error_dict(NOT_FOUND, f"Unknown client name {username}") | ||||
if self._collection.id not in self._client.collections: | collection_id = self._collection.id | ||||
collections = self._client.collections | |||||
assert collections is not None | |||||
if collection_id not in collections: | |||||
return make_error_dict( | return make_error_dict( | ||||
FORBIDDEN, | FORBIDDEN, | ||||
f"Client {username} cannot access collection {collection_name}", | f"Client {username} cannot access collection {collection_name}", | ||||
) | ) | ||||
if deposit_id: | if deposit_id: | ||||
try: | try: | ||||
deposit = Deposit.objects.get(pk=deposit_id) | deposit = Deposit.objects.get(pk=deposit_id) | ||||
Show All 11 Lines | ) -> Dict[str, Any]: | ||||
return make_error_dict(MEDIATION_NOT_ALLOWED, "Mediation is not supported.") | return make_error_dict(MEDIATION_NOT_ALLOWED, "Mediation is not supported.") | ||||
checks = self.additional_checks(request, headers, collection_name, deposit_id) | checks = self.additional_checks(request, headers, collection_name, deposit_id) | ||||
if "error" in checks: | if "error" in checks: | ||||
return checks | return checks | ||||
return {"headers": headers} | return {"headers": headers} | ||||
def restrict_access(self, request, deposit=None): | def restrict_access( | ||||
self, request: Request, deposit: Optional[Deposit] = None | |||||
) -> Dict[str, Any]: | |||||
if deposit: | if deposit: | ||||
if request.method != "GET" and deposit.status != DEPOSIT_STATUS_PARTIAL: | if request.method != "GET" and deposit.status != DEPOSIT_STATUS_PARTIAL: | ||||
summary = "You can only act on deposit with status '%s'" % ( | summary = "You can only act on deposit with status '%s'" % ( | ||||
DEPOSIT_STATUS_PARTIAL, | DEPOSIT_STATUS_PARTIAL, | ||||
) | ) | ||||
description = f"This deposit has status '{deposit.status}'" | description = f"This deposit has status '{deposit.status}'" | ||||
return make_error_dict( | return make_error_dict( | ||||
BAD_REQUEST, summary=summary, verbose_description=description | BAD_REQUEST, summary=summary, verbose_description=description | ||||
) | ) | ||||
return {} | |||||
def _basic_not_allowed_method(self, request, method): | def _basic_not_allowed_method(self, request: Request, method: str): | ||||
return make_error_response( | return make_error_response( | ||||
request, | request, | ||||
METHOD_NOT_ALLOWED, | METHOD_NOT_ALLOWED, | ||||
f"{method} method is not supported on this endpoint", | f"{method} method is not supported on this endpoint", | ||||
) | ) | ||||
def get(self, request, *args, **kwargs): | def get( | ||||
self, request: Request, collection_name: str, deposit_id: int | |||||
) -> Union[HttpResponse, FileResponse]: | |||||
return self._basic_not_allowed_method(request, "GET") | return self._basic_not_allowed_method(request, "GET") | ||||
def post(self, request, *args, **kwargs): | def post( | ||||
self, request: Request, collection_name: str, deposit_id: Optional[int] = None | |||||
) -> HttpResponse: | |||||
return self._basic_not_allowed_method(request, "POST") | return self._basic_not_allowed_method(request, "POST") | ||||
def put(self, request, *args, **kwargs): | def put( | ||||
self, request: Request, collection_name: str, deposit_id: int | |||||
) -> HttpResponse: | |||||
return self._basic_not_allowed_method(request, "PUT") | return self._basic_not_allowed_method(request, "PUT") | ||||
def delete(self, request, *args, **kwargs): | def delete( | ||||
self, request: Request, collection_name: str, deposit_id: Optional[int] = None | |||||
) -> HttpResponse: | |||||
return self._basic_not_allowed_method(request, "DELETE") | return self._basic_not_allowed_method(request, "DELETE") | ||||
class APIGet(APIBase, metaclass=ABCMeta): | class APIGet(APIBase, metaclass=ABCMeta): | ||||
"""Mixin for class to support GET method. | """Mixin for class to support GET method. | ||||
""" | """ | ||||
def get(self, request, collection_name, deposit_id, format=None): | def get( | ||||
self, request: Request, collection_name: str, deposit_id: int | |||||
) -> Union[HttpResponse, FileResponse]: | |||||
"""Endpoint to create/add resources to deposit. | """Endpoint to create/add resources to deposit. | ||||
Returns: | Returns: | ||||
200 response when no error during routine occurred | 200 response when no error during routine occurred | ||||
400 if the deposit does not belong to the collection | 400 if the deposit does not belong to the collection | ||||
404 if the deposit or the collection does not exist | 404 if the deposit or the collection does not exist | ||||
""" | """ | ||||
checks = self.checks(request, collection_name, deposit_id) | checks = self.checks(request, collection_name, deposit_id) | ||||
if "error" in checks: | if "error" in checks: | ||||
return make_error_response_from_dict(request, checks["error"]) | return make_error_response_from_dict(request, checks["error"]) | ||||
r = self.process_get(request, collection_name, deposit_id) | r = self.process_get(request, collection_name, deposit_id) | ||||
if isinstance(r, tuple): | |||||
status, content, content_type = r | status, content, content_type = r | ||||
if content_type == "swh/generator": | |||||
with content as path: | |||||
return FileResponse( | |||||
open(path, "rb"), status=status, content_type="application/zip" | |||||
) | |||||
if content_type == "application/json": | |||||
return HttpResponse( | |||||
json.dumps(content), status=status, content_type=content_type | |||||
) | |||||
return HttpResponse(content, status=status, content_type=content_type) | return HttpResponse(content, status=status, content_type=content_type) | ||||
return r | |||||
@abstractmethod | @abstractmethod | ||||
def process_get(self, request, collection_name, deposit_id): | def process_get( | ||||
self, request: Request, collection_name: str, deposit_id: int | |||||
) -> Tuple[int, Any, str]: | |||||
"""Routine to deal with the deposit's get processing. | """Routine to deal with the deposit's get processing. | ||||
Returns: | Returns: | ||||
Tuple status, stream of content, content-type | Tuple status, stream of content, content-type | ||||
""" | """ | ||||
pass | pass | ||||
class APIPost(APIBase, metaclass=ABCMeta): | class APIPost(APIBase, metaclass=ABCMeta): | ||||
"""Mixin for class to support DELETE method. | """Mixin for class to support DELETE method. | ||||
""" | """ | ||||
def post(self, request, collection_name, deposit_id=None, format=None): | def post( | ||||
self, request: Request, collection_name: str, deposit_id: Optional[int] = None | |||||
) -> HttpResponse: | |||||
"""Endpoint to create/add resources to deposit. | """Endpoint to create/add resources to deposit. | ||||
Returns: | Returns: | ||||
204 response when no error during routine occurred. | 204 response when no error during routine occurred. | ||||
400 if the deposit does not belong to the collection | 400 if the deposit does not belong to the collection | ||||
404 if the deposit or the collection does not exist | 404 if the deposit or the collection does not exist | ||||
""" | """ | ||||
Show All 15 Lines | ) -> HttpResponse: | ||||
data.update(iris) | data.update(iris) | ||||
response = render( | response = render( | ||||
request, | request, | ||||
"deposit/deposit_receipt.xml", | "deposit/deposit_receipt.xml", | ||||
context=data, | context=data, | ||||
content_type="application/xml", | content_type="application/xml", | ||||
status=_status, | status=_status, | ||||
) | ) | ||||
response._headers["location"] = "Location", data[_iri_key] | response._headers["location"] = "Location", data[_iri_key] # type: ignore | ||||
return response | return response | ||||
@abstractmethod | @abstractmethod | ||||
def process_post(self, request, headers, collection_name, deposit_id=None): | def process_post( | ||||
self, | |||||
request, | |||||
headers: Dict, | |||||
collection_name: str, | |||||
deposit_id: Optional[int] = None, | |||||
) -> Tuple[int, str, Dict]: | |||||
"""Routine to deal with the deposit's processing. | """Routine to deal with the deposit's processing. | ||||
Returns | Returns | ||||
Tuple of: | Tuple of: | ||||
- response status code (200, 201, etc...) | - response status code (200, 201, etc...) | ||||
- key iri (EM_IRI, EDIT_SE_IRI, etc...) | - key iri (EM_IRI, EDIT_SE_IRI, etc...) | ||||
- dictionary of the processing result | - dictionary of the processing result | ||||
""" | """ | ||||
pass | pass | ||||
class APIPut(APIBase, metaclass=ABCMeta): | class APIPut(APIBase, metaclass=ABCMeta): | ||||
"""Mixin for class to support PUT method. | """Mixin for class to support PUT method. | ||||
""" | """ | ||||
def put(self, request, collection_name, deposit_id, format=None): | def put( | ||||
self, request: Request, collection_name: str, deposit_id: int | |||||
) -> HttpResponse: | |||||
"""Endpoint to update deposit resources. | """Endpoint to update deposit resources. | ||||
Returns: | Returns: | ||||
204 response when no error during routine occurred. | 204 response when no error during routine occurred. | ||||
400 if the deposit does not belong to the collection | 400 if the deposit does not belong to the collection | ||||
404 if the deposit or the collection does not exist | 404 if the deposit or the collection does not exist | ||||
""" | """ | ||||
checks = self.checks(request, collection_name, deposit_id) | checks = self.checks(request, collection_name, deposit_id) | ||||
if "error" in checks: | if "error" in checks: | ||||
return make_error_response_from_dict(request, checks["error"]) | return make_error_response_from_dict(request, checks["error"]) | ||||
headers = checks["headers"] | headers = checks["headers"] | ||||
data = self.process_put(request, headers, collection_name, deposit_id) | data = self.process_put(request, headers, collection_name, deposit_id) | ||||
error = data.get("error") | error = data.get("error") | ||||
if error: | if error: | ||||
return make_error_response_from_dict(request, error) | return make_error_response_from_dict(request, error) | ||||
return HttpResponse(status=status.HTTP_204_NO_CONTENT) | return HttpResponse(status=status.HTTP_204_NO_CONTENT) | ||||
@abstractmethod | @abstractmethod | ||||
def process_put(self, request, headers, collection_name, deposit_id): | def process_put( | ||||
self, request: Request, headers: Dict, collection_name: str, deposit_id: int | |||||
) -> Dict[str, Any]: | |||||
"""Routine to deal with updating a deposit in some way. | """Routine to deal with updating a deposit in some way. | ||||
Returns | Returns | ||||
dictionary of the processing result | dictionary of the processing result | ||||
""" | """ | ||||
pass | pass | ||||
class APIDelete(APIBase, metaclass=ABCMeta): | class APIDelete(APIBase, metaclass=ABCMeta): | ||||
"""Mixin for class to support DELETE method. | """Mixin for class to support DELETE method. | ||||
""" | """ | ||||
def delete(self, request, collection_name, deposit_id): | def delete( | ||||
self, request: Request, collection_name: str, deposit_id: Optional[int] = None | |||||
) -> HttpResponse: | |||||
"""Endpoint to delete some deposit's resources (archives, deposit). | """Endpoint to delete some deposit's resources (archives, deposit). | ||||
Returns: | Returns: | ||||
204 response when no error during routine occurred. | 204 response when no error during routine occurred. | ||||
400 if the deposit does not belong to the collection | 400 if the deposit does not belong to the collection | ||||
404 if the deposit or the collection does not exist | 404 if the deposit or the collection does not exist | ||||
""" | """ | ||||
checks = self.checks(request, collection_name, deposit_id) | checks = self.checks(request, collection_name, deposit_id) | ||||
if "error" in checks: | if "error" in checks: | ||||
return make_error_response_from_dict(request, checks["error"]) | return make_error_response_from_dict(request, checks["error"]) | ||||
assert deposit_id is not None | |||||
data = self.process_delete(request, collection_name, deposit_id) | data = self.process_delete(request, collection_name, deposit_id) | ||||
error = data.get("error") | error = data.get("error") | ||||
if error: | if error: | ||||
return make_error_response_from_dict(request, error) | return make_error_response_from_dict(request, error) | ||||
return HttpResponse(status=status.HTTP_204_NO_CONTENT) | return HttpResponse(status=status.HTTP_204_NO_CONTENT) | ||||
@abstractmethod | @abstractmethod | ||||
def process_delete(self, request, collection_name, deposit_id): | def process_delete( | ||||
self, request: Request, collection_name: str, deposit_id: int | |||||
) -> Dict: | |||||
"""Routine to delete a resource. | """Routine to delete a resource. | ||||
This is mostly not allowed except for the | This is mostly not allowed except for the | ||||
EM_IRI (cf. .api.deposit_update.APIUpdateArchive) | EM_IRI (cf. .api.deposit_update.APIUpdateArchive) | ||||
""" | """ | ||||
pass | return {} |
otherwise cryptic message: error: Cannot determine type of 'objects'