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 datetime | ||||
import hashlib | import hashlib | ||||
import json | import json | ||||
from typing import Any, Dict, Optional, Sequence, Tuple, Type, Union | from typing import Any, Dict, Optional, Sequence, Tuple, Type, Union | ||||
import attr | import attr | ||||
from django.core.files.uploadedfile import InMemoryUploadedFile | from django.core.files.uploadedfile import UploadedFile | ||||
from django.http import FileResponse, 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.request import Request | ||||
▲ Show 20 Lines • Show All 70 Lines • ▼ Show 20 Lines | class Receipt: | ||||
Deposit Receipt.""" | Deposit Receipt.""" | ||||
deposit_id = attr.ib(type=int) | deposit_id = attr.ib(type=int) | ||||
deposit_date = attr.ib(type=datetime.datetime) | deposit_date = attr.ib(type=datetime.datetime) | ||||
status = attr.ib(type=str) | status = attr.ib(type=str) | ||||
archive = attr.ib(type=Optional[str]) | archive = attr.ib(type=Optional[str]) | ||||
def _compute_md5(filehandler: InMemoryUploadedFile) -> bytes: | def _compute_md5(filehandler: UploadedFile) -> bytes: | ||||
h = hashlib.md5() | h = hashlib.md5() | ||||
for chunk in filehandler: | for chunk in filehandler: | ||||
h.update(chunk) # type: ignore | h.update(chunk) # type: ignore | ||||
return h.digest() | return h.digest() | ||||
def get_deposit_by_id( | def get_deposit_by_id( | ||||
deposit_id: int, collection_name: Optional[str] = None | deposit_id: int, collection_name: Optional[str] = None | ||||
Show All 29 Lines | class APIBase(APIConfig, AuthenticatedAPIView, metaclass=ABCMeta): | ||||
""" | """ | ||||
def _read_headers(self, request: Request) -> ParsedRequestHeaders: | def _read_headers(self, request: Request) -> ParsedRequestHeaders: | ||||
"""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: Input request | ||||
Returns: | Returns: | ||||
Dictionary with the following keys (some associated values may be | Dictionary with the following keys (some associated values may be | ||||
None): | None): | ||||
- content-type | - content-type | ||||
- content-length | - content-length | ||||
- in-progress | - in-progress | ||||
- content-disposition | - content-disposition | ||||
▲ Show 20 Lines • Show All 189 Lines • ▼ Show 20 Lines | def _delete_deposit(self, collection_name: str, deposit_id: int) -> Dict: | ||||
) | ) | ||||
DepositRequest.objects.filter(deposit=deposit).delete() | DepositRequest.objects.filter(deposit=deposit).delete() | ||||
deposit.delete() | deposit.delete() | ||||
return {} | return {} | ||||
def _check_preconditions_on( | def _check_preconditions_on( | ||||
self, filehandler, md5sum: Optional[bytes], content_length: Optional[int] = None | self, | ||||
filehandler: UploadedFile, | |||||
md5sum: Optional[bytes], | |||||
content_length: Optional[int] = None, | |||||
) -> None: | ) -> None: | ||||
"""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: The file to check | ||||
md5sum: md5 hash expected from the file's content | md5sum: md5 hash expected from the file's content | ||||
content_length: 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. | ||||
""" | """ | ||||
Show All 30 Lines | def _binary_upload( | ||||
replace_archives: bool = False, | replace_archives: bool = False, | ||||
check_slug_is_present: bool = False, | check_slug_is_present: bool = False, | ||||
) -> Receipt: | ) -> Receipt: | ||||
"""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: the request holding information to parse | ||||
and inject in db | and inject in db | ||||
headers (ParsedRequestHeaders): parsed request headers | headers: parsed request headers | ||||
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. | ||||
check_slug_is_present: Check for the slug header if True and raise | check_slug_is_present: Check for the slug header if True and raise | ||||
if not present | if not present | ||||
Raises: | Raises: | ||||
- 400 (bad request) if the request is not providing an external | - 400 (bad request) if the request is not providing an external | ||||
Show All 25 Lines | ) -> Receipt: | ||||
if packaging and packaging not in ACCEPT_PACKAGINGS: | if packaging and packaging not in ACCEPT_PACKAGINGS: | ||||
raise DepositError( | raise DepositError( | ||||
BAD_REQUEST, | BAD_REQUEST, | ||||
f"Only packaging {ACCEPT_PACKAGINGS} is supported", | f"Only packaging {ACCEPT_PACKAGINGS} is supported", | ||||
f"The packaging provided {packaging} is not supported", | f"The packaging provided {packaging} is not supported", | ||||
) | ) | ||||
filehandler = request.FILES["file"] | filehandler = request.FILES["file"] | ||||
assert isinstance(filehandler, UploadedFile), filehandler | |||||
self._check_preconditions_on( | self._check_preconditions_on( | ||||
filehandler, headers.content_md5sum, content_length | filehandler, headers.content_md5sum, content_length | ||||
) | ) | ||||
slug = headers.slug | slug = headers.slug | ||||
if check_slug_is_present and not slug: | if check_slug_is_present and not slug: | ||||
raise_missing_slug_error() | raise_missing_slug_error() | ||||
▲ Show 20 Lines • Show All 41 Lines • ▼ Show 20 Lines | class APIBase(APIConfig, AuthenticatedAPIView, metaclass=ABCMeta): | ||||
) -> Receipt: | ) -> Receipt: | ||||
"""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: the request holding information to parse | ||||
and inject in db | and inject in db | ||||
headers: parsed request headers | headers: parsed request headers | ||||
collection_name: the associated client | collection_name: the associated client | ||||
deposit_id: deposit identifier if provided | deposit_id: deposit identifier if provided | ||||
replace_metadata: '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: 'Update or add' request to existing | replace_archives: 'Update or add' request to existing | ||||
▲ Show 20 Lines • Show All 52 Lines • ▼ Show 20 Lines | ) -> Receipt: | ||||
"and 1 application/atom+xml content-disposition header " | "and 1 application/atom+xml content-disposition header " | ||||
"in the multipart deposit", | "in the multipart deposit", | ||||
) | ) | ||||
filehandler = data["application/zip"] | filehandler = data["application/zip"] | ||||
if not filehandler: | if not filehandler: | ||||
filehandler = data["application/x-tar"] | filehandler = data["application/x-tar"] | ||||
assert isinstance(filehandler, UploadedFile), filehandler | |||||
self._check_preconditions_on(filehandler, headers.content_md5sum) | self._check_preconditions_on(filehandler, headers.content_md5sum) | ||||
try: | try: | ||||
raw_metadata, metadata = self._read_metadata(data["application/atom+xml"]) | raw_metadata, metadata = self._read_metadata(data["application/atom+xml"]) | ||||
except ParserError: | except ParserError: | ||||
raise DepositError( | raise DepositError( | ||||
PARSING_ERROR, | PARSING_ERROR, | ||||
"Malformed xml metadata", | "Malformed xml metadata", | ||||
▲ Show 20 Lines • Show All 553 Lines • Show Last 20 Lines |