ferrero-opentext/Python-Version/venv/lib/python3.12/site-packages/boxsdk/object/upload_session.py

205 lines
7.1 KiB
Python

import base64
import hashlib
import json
import os
from http import HTTPStatus
from typing import Any, Optional, TYPE_CHECKING, Iterable, IO
from boxsdk import BoxAPIException
from boxsdk.util.api_call_decorator import api_call
from boxsdk.util.chunked_uploader import ChunkedUploader
from boxsdk.session.session import Session
from boxsdk.config import API
from .base_object import BaseObject
from ..pagination.limit_offset_based_dict_collection import LimitOffsetBasedDictCollection
if TYPE_CHECKING:
from boxsdk.pagination.box_object_collection import BoxObjectCollection
from boxsdk.object.file import File
class UploadSession(BaseObject):
_item_type = 'upload_session'
_parent_item_type = 'file'
_default_upload_url = API.UPLOAD_URL
def __init__(
self, session: Session, object_id: str, response_object: dict = None, use_upload_session_urls: bool = True
):
super().__init__(session, object_id, response_object)
self._use_upload_session_urls = use_upload_session_urls
def get_url(self, *args: Any, url_key: str = None) -> str:
"""
Base class override. Endpoint is a little different - it's /files/upload_sessions.
"""
session_endpoints = getattr(self, 'session_endpoints', {})
if self._use_upload_session_urls and url_key in session_endpoints and self.session.api_config.UPLOAD_URL == self._default_upload_url:
return session_endpoints[url_key]
return self._session.get_url(
f'{self._parent_item_type}s/{self._item_type}s',
self._object_id,
*args
).replace(self.session.api_config.BASE_API_URL, self.session.api_config.UPLOAD_URL)
@api_call
def get_parts(self, limit: Optional[int] = None, offset: Optional[int] = None) -> 'BoxObjectCollection':
"""
Get a list of parts uploaded so far.
:param limit:
The maximum number of items to return per page. If not specified, then will use the server-side default.
:param offset:
The index at which to start returning items.
:returns:
Returns a :class:`BoxObjectCollection` object containing the uploaded parts.
"""
return LimitOffsetBasedDictCollection(
session=self.session,
url=self.get_url('parts', url_key='list_parts'),
limit=limit,
offset=offset,
fields=None,
return_full_pages=False,
)
@api_call
def upload_part_bytes(
self,
part_bytes: bytes,
offset: int,
total_size: int,
part_content_sha1: Optional[bytes] = None
) -> dict:
"""
Upload a part of a file.
:param part_bytes:
Part bytes
:param offset:
Offset, in number of bytes, of the part compared to the beginning of the file. This number should be a
multiple of the part size.
:param total_size:
The size of the file that this part belongs to.
:param part_content_sha1:
SHA-1 hash of the part's content. If not specified, this will be calculated.
:returns:
The uploaded part record.
"""
if part_content_sha1 is None:
sha1 = hashlib.sha1()
sha1.update(part_bytes)
part_content_sha1 = sha1.digest()
range_end = min(offset + self.part_size - 1, total_size - 1) # pylint:disable=no-member
headers = {
'Content-Type': 'application/octet-stream',
'Digest': f'SHA={base64.b64encode(part_content_sha1).decode("utf-8")}',
'Content-Range': f'bytes {offset}-{range_end}/{total_size}',
}
response = self._session.put(
self.get_url(url_key='upload_part'),
headers=headers,
data=part_bytes,
)
return response.json()['part']
@api_call
def commit(
self,
content_sha1: bytes,
parts: Iterable[Optional[dict]] = None,
file_attributes: dict = None,
etag: Optional[str] = None
) -> Optional['File']:
"""
Commit a multiput upload.
:param content_sha1:
SHA-1 hash of the file contents that was uploaded.
:param parts:
List of parts that were uploaded.
:param file_attributes:
A `dict` of attributes to set on the uploaded file.
:param etag:
If specified, instruct the Box API to delete the folder only if the current version's etag matches.
:returns:
The newly-uploaded file object or None if commit was not processed
"""
body = {}
if file_attributes is not None:
body['attributes'] = file_attributes
if parts is not None:
body['parts'] = parts
else:
body['parts'] = list(self.get_parts())
headers = {
'Content-Type': 'application/json',
'Digest': f'SHA={base64.b64encode(content_sha1).decode("utf-8")}',
}
if etag is not None:
headers['If-Match'] = etag
try:
response = self._session.post(
self.get_url('commit', url_key='commit'),
headers=headers,
data=json.dumps(body),
)
except BoxAPIException as box_api_exc:
if box_api_exc.status == HTTPStatus.ACCEPTED:
return None
raise box_api_exc
entry = response.json()['entries'][0]
return self.translator.translate(
session=self._session,
response_object=entry,
)
@api_call
def abort(self) -> bool:
"""
Abort an upload session, cancelling the upload and removing any parts that have already been uploaded.
:returns:
A boolean indication success of the upload abort.
"""
box_response = self._session.delete(
self.get_url(url_key='abort'),
expect_json_response=False
)
return box_response.ok
def get_chunked_uploader_for_stream(self, content_stream: IO[bytes], file_size: int) -> ChunkedUploader:
"""
Instantiate the chunked upload instance and create upload session.
:param content_stream:
File-like object containing the content of the part to be uploaded.
:param file_size:
The size of the file that this part belongs to.
:returns:
A :class:`ChunkedUploader` object.
"""
return ChunkedUploader(self, content_stream, file_size)
def get_chunked_uploader(self, file_path: str) -> ChunkedUploader:
# pylint: disable=consider-using-with
"""
Instantiate the chunked upload instance and create upload session with path to file.
:param file_path:
The local path to the file you wish to upload.
:returns:
A :class:`ChunkedUploader` object.
"""
total_size = os.stat(file_path).st_size
content_stream = open(file_path, 'rb')
return self.get_chunked_uploader_for_stream(
content_stream=content_stream,
file_size=total_size,
)