893 lines
37 KiB
Python
893 lines
37 KiB
Python
import json
|
|
import os
|
|
from datetime import datetime
|
|
from typing import TYPE_CHECKING, Optional, Tuple, Union, IO, Iterable, List, Any
|
|
from boxsdk.exception import BoxAPIException
|
|
|
|
from boxsdk.util.datetime_formatter import normalize_date_to_rfc3339_format
|
|
from .item import Item
|
|
from ..util.api_call_decorator import api_call
|
|
from ..util.default_arg_value import SDK_VALUE_NOT_SET
|
|
from ..util.deprecation_decorator import deprecated
|
|
from ..pagination.marker_based_object_collection import MarkerBasedObjectCollection
|
|
from ..pagination.limit_offset_based_object_collection import LimitOffsetBasedObjectCollection
|
|
|
|
if TYPE_CHECKING:
|
|
from boxsdk.object.upload_session import UploadSession
|
|
from boxsdk.util.chunked_uploader import ChunkedUploader
|
|
from boxsdk.object.file_version import FileVersion
|
|
from boxsdk.pagination.box_object_collection import BoxObjectCollection
|
|
from boxsdk.object.comment import Comment
|
|
from boxsdk.object.task import Task
|
|
from boxsdk.object.folder import Folder
|
|
|
|
|
|
class File(Item):
|
|
"""Box API endpoint for interacting with files."""
|
|
|
|
_item_type = 'file'
|
|
|
|
@api_call
|
|
def preflight_check(self, size: int, name: Optional[str] = None) -> Optional[str]:
|
|
"""
|
|
Make an API call to check if the file can be updated with the new name and size of the file.
|
|
Returns an accelerator URL if one is available.
|
|
|
|
:param size:
|
|
The size of the file in bytes. Specify 0 for unknown file-sizes.
|
|
:param name:
|
|
The name of the file to be updated. It's optional, if the name is not being changed.
|
|
:return:
|
|
The Accelerator upload url or None if cannot get the Accelerator upload url.
|
|
:raises:
|
|
:class:`BoxAPIException` when preflight check fails.
|
|
"""
|
|
return self._preflight_check(
|
|
size=size,
|
|
name=name,
|
|
file_id=self._object_id,
|
|
)
|
|
|
|
@api_call
|
|
def create_upload_session(
|
|
self, file_size: int, file_name: Optional[str] = None, use_upload_session_urls: bool = True
|
|
) -> 'UploadSession':
|
|
"""
|
|
Create a new chunked upload session for uploading a new version of the file.
|
|
|
|
:param file_size:
|
|
The size of the file in bytes that will be uploaded.
|
|
:param file_name:
|
|
The new name of the file version that will be uploaded.
|
|
:param use_upload_session_urls:
|
|
The parameter detrermining what urls to use to perform chunked upload.
|
|
If True, the urls returned by create_upload_session() endpoint response will be used,
|
|
unless a custom API.UPLOAD_URL was set in the config.
|
|
If False, the base upload url will be used.
|
|
:returns:
|
|
A :class:`UploadSession` object.
|
|
"""
|
|
body_params = {
|
|
'file_id': self.object_id,
|
|
'file_size': file_size,
|
|
}
|
|
if file_name is not None:
|
|
body_params['file_name'] = file_name
|
|
url = self.get_url('upload_sessions').replace(self.session.api_config.BASE_API_URL, self.session.api_config.UPLOAD_URL)
|
|
response = self._session.post(url, data=json.dumps(body_params)).json()
|
|
upload_session = self.translator.translate(
|
|
session=self._session,
|
|
response_object=response,
|
|
)
|
|
# pylint:disable=protected-access
|
|
upload_session._use_upload_session_urls = use_upload_session_urls
|
|
return upload_session
|
|
|
|
@api_call
|
|
def get_chunked_uploader(
|
|
self, file_path: str, rename_file: bool = False, use_upload_session_urls: bool = True
|
|
) -> '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.
|
|
:param rename_file:
|
|
Indicates whether the file should be renamed or not.
|
|
:param use_upload_session_urls:
|
|
The parameter detrermining what urls to use to perform chunked upload.
|
|
If True, the urls returned by create_upload_session() endpoint response will be used,
|
|
unless a custom API.UPLOAD_URL was set in the config.
|
|
If False, the base upload url will be used.
|
|
:returns:
|
|
A :class:`ChunkedUploader` object.
|
|
"""
|
|
total_size = os.stat(file_path).st_size
|
|
content_stream = open(file_path, 'rb')
|
|
file_name = os.path.basename(file_path) if rename_file else None
|
|
upload_session = self.create_upload_session(total_size, file_name, use_upload_session_urls)
|
|
return upload_session.get_chunked_uploader_for_stream(content_stream, total_size)
|
|
|
|
def _get_accelerator_upload_url_for_update(self) -> Optional[str]:
|
|
"""
|
|
Get Accelerator upload url for updating the file.
|
|
|
|
:return:
|
|
The Accelerator upload url for updating the file or None if cannot get one
|
|
"""
|
|
return self._get_accelerator_upload_url(file_id=self._object_id)
|
|
|
|
@staticmethod
|
|
def _construct_range_header(boundaries: Union[Tuple[int], Tuple[int, int]]) -> str:
|
|
"""
|
|
Construct the correct value for the Range header, given a closed or open-ended range.
|
|
|
|
:param boundaries:
|
|
The range of bytes (inclusive)
|
|
:returns:
|
|
The value for the Range header
|
|
:raises ValueError:
|
|
"""
|
|
if len(boundaries) == 1:
|
|
return f'bytes={boundaries[0]}-'
|
|
if len(boundaries) == 2:
|
|
return f'bytes={boundaries[0]}-{boundaries[1]}'
|
|
|
|
raise ValueError('Expected a 1-tuple or 2-tuple for byte range')
|
|
|
|
@api_call
|
|
def content(self, file_version: Optional['FileVersion'] = None, byte_range: Tuple[int, int] = None) -> bytes:
|
|
"""
|
|
Get the content of a file on Box.
|
|
|
|
:param file_version:
|
|
The specific version of the file to retrieve the contents of.
|
|
:param byte_range:
|
|
A tuple of inclusive byte offsets to download, e.g. (100, 199) to download the second 100 bytes of a file
|
|
:returns:
|
|
File content as bytes.
|
|
"""
|
|
url = self.get_url('content')
|
|
params = {'version': file_version.object_id} if file_version is not None else None
|
|
headers = {'Range': self._construct_range_header(byte_range)} if byte_range is not None else None
|
|
box_response = self._session.get(url, expect_json_response=False, params=params, headers=headers)
|
|
return box_response.content
|
|
|
|
@api_call
|
|
def download_to(
|
|
self,
|
|
writeable_stream: IO[bytes],
|
|
file_version: Optional['FileVersion'] = None,
|
|
byte_range: Tuple[int, int] = None
|
|
) -> None:
|
|
"""
|
|
Download the file; write it to the given stream.
|
|
|
|
:param writeable_stream:
|
|
A file-like object where bytes can be written into.
|
|
:param file_version:
|
|
The specific version of the file to retrieve the contents of.
|
|
:param byte_range:
|
|
A tuple of inclusive byte offsets to download, e.g. (100, 199) to download the second 100 bytes of a file
|
|
"""
|
|
url = self.get_url('content')
|
|
params = {'version': file_version.object_id} if file_version is not None else None
|
|
headers = {'Range': self._construct_range_header(byte_range)} if byte_range is not None else None
|
|
box_response = self._session.get(url, expect_json_response=False, stream=True, params=params, headers=headers)
|
|
for chunk in box_response.network_response.response_as_stream.stream(decode_content=True):
|
|
writeable_stream.write(chunk)
|
|
|
|
@api_call
|
|
def get_download_url(self, file_version: Optional['FileVersion'] = None) -> str:
|
|
"""
|
|
Get the url to download the file.
|
|
|
|
:param file_version:
|
|
The specific version of the file to retrieve the contents of.
|
|
:return: Url to download the file
|
|
"""
|
|
url = self.get_url('content')
|
|
params = {'version': file_version.object_id} if file_version is not None else None
|
|
box_response = self._session.get(
|
|
url,
|
|
params=params,
|
|
expect_json_response=False,
|
|
allow_redirects=False,
|
|
)
|
|
network_response = box_response.network_response
|
|
if 'location' not in box_response.headers:
|
|
raise BoxAPIException(
|
|
status=network_response.status_code,
|
|
headers=network_response.headers,
|
|
message='Download URL is not present in the response.',
|
|
url=url,
|
|
method='GET',
|
|
network_response=network_response,
|
|
)
|
|
|
|
return box_response.headers['location']
|
|
|
|
@api_call
|
|
def update_contents_with_stream(
|
|
self,
|
|
file_stream: IO[bytes],
|
|
etag: Optional[str] = None,
|
|
preflight_check: bool = False,
|
|
preflight_expected_size: int = 0,
|
|
upload_using_accelerator: bool = False,
|
|
file_name: Optional[str] = None,
|
|
content_modified_at: Union[datetime, str] = None,
|
|
additional_attributes: Optional[dict] = None,
|
|
sha1: Optional[str] = None,
|
|
) -> 'File':
|
|
"""
|
|
Upload a new version of a file, taking the contents from the given file stream.
|
|
|
|
:param file_stream:
|
|
The file-like object containing the bytes
|
|
:param etag:
|
|
If specified, instruct the Box API to update the item only if the current version's etag matches.
|
|
:param preflight_check:
|
|
If specified, preflight check will be performed before actually uploading the file.
|
|
:param preflight_expected_size:
|
|
The size of the file to be uploaded in bytes, which is used for preflight check. The default value is '0',
|
|
which means the file size is unknown.
|
|
:param upload_using_accelerator:
|
|
If specified, the upload will try to use Box Accelerator to speed up the uploads for big files.
|
|
It will make an extra API call before the actual upload to get the Accelerator upload url, and then make
|
|
a POST request to that url instead of the default Box upload url. It falls back to normal upload endpoint,
|
|
if cannot get the Accelerator upload url.
|
|
|
|
Please notice that this is a premium feature, which might not be available to your app.
|
|
:param file_name:
|
|
The new name to give the file on Box.
|
|
:param content_modified_at:
|
|
The A datetime string in a format supported by the dateutil library or datetime object,
|
|
which specifies when the file content was last modified.
|
|
If no timezone info provided, local timezone will be applied.
|
|
:param additional_attributes:
|
|
A dictionary containing attributes to add to the file that are not covered by other parameters.
|
|
:param sha1:
|
|
A sha1 checksum for the new content.
|
|
:returns:
|
|
A new file object
|
|
:raises:
|
|
:class:`BoxAPIException` if the specified etag doesn't match the latest version of the file or preflight
|
|
check fails.
|
|
"""
|
|
accelerator_upload_url = None
|
|
if preflight_check:
|
|
# Preflight check does double duty, returning the accelerator URL if one is available in the response.
|
|
accelerator_upload_url = self.preflight_check(size=preflight_expected_size)
|
|
elif upload_using_accelerator:
|
|
accelerator_upload_url = self._get_accelerator_upload_url_for_update()
|
|
|
|
url = self.get_url('content').replace(
|
|
self._session.api_config.BASE_API_URL,
|
|
self._session.api_config.UPLOAD_URL,
|
|
)
|
|
if upload_using_accelerator and accelerator_upload_url:
|
|
url = accelerator_upload_url
|
|
|
|
attributes = {
|
|
'name': file_name,
|
|
'content_modified_at': normalize_date_to_rfc3339_format(content_modified_at),
|
|
}
|
|
if additional_attributes:
|
|
attributes.update(additional_attributes)
|
|
|
|
data = {'attributes': json.dumps(attributes)}
|
|
files = {'file': ('unused', file_stream)}
|
|
headers = {}
|
|
if etag is not None:
|
|
headers['If-Match'] = etag
|
|
if sha1 is not None:
|
|
# The Content-MD5 field accepts sha1
|
|
headers['Content-MD5'] = sha1
|
|
if not headers:
|
|
headers = None
|
|
file_response = self._session.post(
|
|
url,
|
|
expect_json_response=False,
|
|
data=data,
|
|
files=files,
|
|
headers=headers,
|
|
).json()
|
|
if 'entries' in file_response:
|
|
file_response = file_response['entries'][0]
|
|
return self.translator.translate(
|
|
session=self._session,
|
|
response_object=file_response,
|
|
)
|
|
|
|
@api_call
|
|
def update_contents(
|
|
self,
|
|
file_path: str,
|
|
etag: Optional[str] = None,
|
|
preflight_check: bool = False,
|
|
preflight_expected_size: int = 0,
|
|
upload_using_accelerator: bool = False,
|
|
file_name: Optional[str] = None,
|
|
content_modified_at: Union[datetime, str] = None,
|
|
additional_attributes: Optional[dict] = None,
|
|
sha1: Optional[str] = None,
|
|
) -> 'File':
|
|
"""Upload a new version of a file. The contents are taken from the given file path.
|
|
|
|
:param file_path:
|
|
The path of the file that should be uploaded.
|
|
:param etag:
|
|
If specified, instruct the Box API to update the item only if the current version's etag matches.
|
|
:param preflight_check:
|
|
If specified, preflight check will be performed before actually uploading the file.
|
|
:param preflight_expected_size:
|
|
The size of the file to be uploaded in bytes, which is used for preflight check. The default value is '0',
|
|
which means the file size is unknown.
|
|
:param upload_using_accelerator:
|
|
If specified, the upload will try to use Box Accelerator to speed up the uploads for big files.
|
|
It will make an extra API call before the actual upload to get the Accelerator upload url, and then make
|
|
a POST request to that url instead of the default Box upload url. It falls back to normal upload endpoint,
|
|
if cannot get the Accelerator upload url.
|
|
|
|
Please notice that this is a premium feature, which might not be available to your app.
|
|
:param file_name:
|
|
The new name to give the file on Box.
|
|
:param content_modified_at:
|
|
A datetime string in a format supported by the dateutil library or a datetime.datetime object,
|
|
which specifies when the file content was last modified.
|
|
If no timezone info provided, local timezone will be applied.
|
|
:param additional_attributes:
|
|
A dictionary containing attributes to add to the file that are not covered by other parameters.
|
|
:param sha1:
|
|
A sha1 checksum for the new content.
|
|
:returns:
|
|
A new file object
|
|
:raises:
|
|
:class:`BoxAPIException` if the specified etag doesn't match the latest version of the file or preflight
|
|
check fails.
|
|
"""
|
|
with open(file_path, 'rb') as file_stream:
|
|
return self.update_contents_with_stream(
|
|
file_stream,
|
|
etag,
|
|
preflight_check,
|
|
preflight_expected_size=preflight_expected_size,
|
|
upload_using_accelerator=upload_using_accelerator,
|
|
file_name=file_name,
|
|
content_modified_at=content_modified_at,
|
|
additional_attributes=additional_attributes,
|
|
sha1=sha1,
|
|
)
|
|
|
|
@api_call
|
|
def lock(self, prevent_download: bool = False, expire_time: Union[datetime, str] = None) -> 'File':
|
|
"""
|
|
Lock a file, preventing others from modifying (or possibly even downloading) it.
|
|
|
|
:param prevent_download:
|
|
Whether or not the lock should prevent other users from downloading the file.
|
|
:param expire_time:
|
|
A datetime string in a format supported by the dateutil library or a datetime.datetime object,
|
|
which specifies when the lock should automatically expire, unlocking the file.
|
|
If no timezone info provided, local timezone will be applied.
|
|
:return:
|
|
A new :class:`File` instance reflecting that the file has been locked.
|
|
"""
|
|
data = {
|
|
'lock': {
|
|
'type': 'lock',
|
|
'is_download_prevented': prevent_download,
|
|
}
|
|
}
|
|
if expire_time is not None:
|
|
data['lock']['expires_at'] = normalize_date_to_rfc3339_format(expire_time)
|
|
return self.update_info(data=data)
|
|
|
|
@api_call
|
|
def unlock(self) -> 'File':
|
|
"""
|
|
Unlock a file, releasing any restrictions that the lock maintained.
|
|
|
|
:return:
|
|
A new :class:`File` instance reflecting that the file has been unlocked.
|
|
"""
|
|
data = {'lock': None}
|
|
return self.update_info(data=data)
|
|
|
|
@api_call
|
|
def create_shared_link(
|
|
self,
|
|
*,
|
|
access: Optional[str] = None,
|
|
etag: Optional[str] = None,
|
|
unshared_at: Union[datetime, str, None] = SDK_VALUE_NOT_SET,
|
|
allow_download: Optional[bool] = None,
|
|
allow_preview: Optional[bool] = None,
|
|
allow_edit: Optional[bool] = None,
|
|
password: Optional[str] = None,
|
|
vanity_name: Optional[str] = None,
|
|
**kwargs: Any
|
|
) -> 'File':
|
|
"""
|
|
Baseclass override.
|
|
|
|
:param access:
|
|
Determines who can access the shared link. May be open, company, or collaborators. If no access is
|
|
specified, the default access will be used.
|
|
:param etag:
|
|
If specified, instruct the Box API to create the link only if the current version's etag matches.
|
|
:param unshared_at:
|
|
The date on which this link should be disabled. May only be set if the current user is not a free user
|
|
and has permission to set expiration dates. Takes a datetime string supported by the dateutil library
|
|
or a datetime.datetime object. If no timezone info provided, local timezone will be applied.
|
|
The time portion can be omitted, which defaults to midnight (00:00:00) on that date.
|
|
:param allow_download:
|
|
Whether the file being shared can be downloaded when accessed via the shared link.
|
|
If this parameter is None, the default setting will be used.
|
|
:param allow_preview:
|
|
Whether the file being shared can be previewed when accessed via the shared link.
|
|
If this parameter is None, the default setting will be used.
|
|
:param allow_edit:
|
|
Whether the file being shared can be edited when accessed via the shared link.
|
|
If this parameter is None, the default setting will be used.
|
|
:param password:
|
|
The password required to view this link. If no password is specified then no password will be set.
|
|
Please notice that this is a premium feature, which might not be available to your app.
|
|
:param vanity_name:
|
|
Defines a custom vanity name to use in the shared link URL, eg. https://app.box.com/v/my-custom-vanity-name.
|
|
If this parameter is None, the standard shared link URL will be used.
|
|
:param kwargs:
|
|
Used to fulfill the contract of overriden method
|
|
:return:
|
|
The updated object with shared link.
|
|
Returns a new object of the same type, without modifying the original object passed as self.
|
|
:raises: :class:`BoxAPIException` if the specified etag doesn't match the latest version of the file.
|
|
"""
|
|
# pylint:disable=arguments-differ
|
|
return super().create_shared_link(
|
|
access=access,
|
|
etag=etag,
|
|
unshared_at=unshared_at,
|
|
allow_download=allow_download,
|
|
allow_preview=allow_preview,
|
|
allow_edit=allow_edit,
|
|
password=password,
|
|
vanity_name=vanity_name
|
|
)
|
|
|
|
@api_call
|
|
def get_shared_link(
|
|
self,
|
|
*,
|
|
access: Optional[str] = None,
|
|
etag: Optional[str] = None,
|
|
unshared_at: Union[datetime, str, None] = SDK_VALUE_NOT_SET,
|
|
allow_download: Optional[bool] = None,
|
|
allow_preview: Optional[bool] = None,
|
|
allow_edit: Optional[bool] = None,
|
|
password: Optional[str] = None,
|
|
vanity_name: Optional[str] = None,
|
|
**kwargs: Any
|
|
) -> 'str':
|
|
"""
|
|
Baseclass override.
|
|
|
|
:param access:
|
|
Determines who can access the shared link. May be open, company, or collaborators. If no access is
|
|
specified, the default access will be used.
|
|
:param etag:
|
|
If specified, instruct the Box API to create the link only if the current version's etag matches.
|
|
:param unshared_at:
|
|
The date on which this link should be disabled. May only be set if the current user is not a free user
|
|
and has permission to set expiration dates. Takes a datetime string supported by the dateutil library
|
|
or a datetime.datetime object. If no timezone info provided, local timezone will be applied.
|
|
The time portion can be omitted, which defaults to midnight (00:00:00) on that date.
|
|
:param allow_download:
|
|
Whether the file being shared can be downloaded when accessed via the shared link.
|
|
If this parameter is None, the default setting will be used.
|
|
:param allow_preview:
|
|
Whether the file being shared can be previewed when accessed via the shared link.
|
|
If this parameter is None, the default setting will be used.
|
|
:param allow_edit:
|
|
Whether the file being shared can be edited when accessed via the shared link.
|
|
If this parameter is None, the default setting will be used.
|
|
:param password:
|
|
The password required to view this link. If no password is specified then no password will be set.
|
|
Please notice that this is a premium feature, which might not be available to your app.
|
|
:param vanity_name:
|
|
Defines a custom vanity name to use in the shared link URL, eg. https://app.box.com/v/my-custom-vanity-name.
|
|
If this parameter is None, the standard shared link URL will be used.
|
|
:param kwargs:
|
|
Used to fulfill the contract of overriden method
|
|
:returns:
|
|
The URL of the shared link.
|
|
:raises: :class:`BoxAPIException` if the specified etag doesn't match the latest version of the file.
|
|
"""
|
|
# pylint:disable=arguments-differ
|
|
return super().get_shared_link(
|
|
access=access,
|
|
etag=etag,
|
|
unshared_at=unshared_at,
|
|
allow_download=allow_download,
|
|
allow_preview=allow_preview,
|
|
allow_edit=allow_edit,
|
|
password=password,
|
|
vanity_name=vanity_name
|
|
)
|
|
|
|
@api_call
|
|
def get_shared_link_download_url(
|
|
self,
|
|
access: Optional[str] = None,
|
|
etag: Optional[str] = None,
|
|
unshared_at: Union[datetime, str, None] = SDK_VALUE_NOT_SET,
|
|
allow_preview: Optional[bool] = None,
|
|
password: Optional[str] = None,
|
|
vanity_name: Optional[str] = None
|
|
) -> str:
|
|
"""
|
|
Get a shared link download url for the file with the given access permissions.
|
|
This url is a direct download url for the file.
|
|
|
|
:param access:
|
|
Determines who can access the shared link. May be open, company, or collaborators. If no access is
|
|
specified, the default access will be used.
|
|
:param etag:
|
|
If specified, instruct the Box API to create the link only if the current version's etag matches.
|
|
:param unshared_at:
|
|
The date on which this link should be disabled. May only be set if the current user is not a free user
|
|
and has permission to set expiration dates. Takes a datetime string supported by the dateutil library
|
|
or a datetime.datetime object. If no timezone info provided, local timezone will be applied.
|
|
The time portion can be omitted, which defaults to midnight (00:00:00) on that date.
|
|
:param allow_preview:
|
|
Whether or not the item being shared can be previewed when accessed via the shared link.
|
|
If this parameter is None, the default setting will be used.
|
|
:param password:
|
|
The password required to view this link. If no password is specified then no password will be set.
|
|
Please notice that this is a premium feature, which might not be available to your app.
|
|
:param vanity_name:
|
|
Defines a custom vanity name to use in the shared link URL, eg. https://app.box.com/v/my-custom-vanity-name.
|
|
If this parameter is None, the standard shared link URL will be used.
|
|
:returns:
|
|
The URL of the shared link that allows direct download.
|
|
:raises: :class:`BoxAPIException` if the specified etag doesn't match the latest version of the item.
|
|
"""
|
|
item = self.create_shared_link(
|
|
access=access,
|
|
etag=etag,
|
|
unshared_at=unshared_at,
|
|
allow_preview=allow_preview,
|
|
password=password,
|
|
vanity_name=vanity_name
|
|
)
|
|
return item.shared_link['download_url'] # pylint:disable=no-member
|
|
|
|
@api_call
|
|
def get_comments(
|
|
self,
|
|
limit: Optional[int] = None,
|
|
offset: int = 0,
|
|
fields: Iterable[str] = None
|
|
) -> 'BoxObjectCollection':
|
|
"""
|
|
Get the comments on the file.
|
|
|
|
: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.
|
|
:param fields:
|
|
List of fields to request.
|
|
:returns:
|
|
An iterator of the items in the folder.
|
|
"""
|
|
return LimitOffsetBasedObjectCollection(
|
|
self.session,
|
|
self.get_url('comments'),
|
|
limit=limit,
|
|
fields=fields,
|
|
offset=offset,
|
|
return_full_pages=False,
|
|
)
|
|
|
|
@api_call
|
|
def add_comment(self, message: str) -> 'Comment':
|
|
"""
|
|
Add a comment to the file.
|
|
|
|
:param message:
|
|
The content of the reply comment.
|
|
:return: Added comment
|
|
"""
|
|
url = self._session.get_url('comments')
|
|
comment_class = self._session.translator.get('comment')
|
|
data = comment_class.construct_params_from_message(message)
|
|
data['item'] = {
|
|
'type': 'file',
|
|
'id': self.object_id
|
|
}
|
|
box_response = self._session.post(url, data=json.dumps(data))
|
|
response = box_response.json()
|
|
return self._session.translator.translate(
|
|
session=self._session,
|
|
response_object=response,
|
|
)
|
|
|
|
@api_call
|
|
def create_task(
|
|
self,
|
|
message: Optional[str] = None,
|
|
due_at: Union[datetime, str] = None,
|
|
action: str = 'review',
|
|
completion_rule: Optional[str] = None
|
|
) -> 'Task':
|
|
"""
|
|
Create a task on the given file.
|
|
|
|
:param message:
|
|
An optional message to include in the task.
|
|
:param due_at:
|
|
When this task is due. Takes a datetime string supported by the dateutil library
|
|
or a datetime.datetime object. If no timezone info provided, local timezone will be applied
|
|
:param action:
|
|
The type of task the task assignee will be prompted to perform.
|
|
Value is one of review,complete
|
|
:param completion_rule:
|
|
Defines which assignees need to complete this task before the task
|
|
is considered completed.
|
|
Value is one of all_assignees,any_assignee
|
|
:return:
|
|
The newly created task
|
|
"""
|
|
url = self._session.get_url('tasks')
|
|
task_attributes = {
|
|
'item': {
|
|
'type': 'file',
|
|
'id': self.object_id
|
|
},
|
|
'action': action,
|
|
}
|
|
if message is not None:
|
|
task_attributes['message'] = message
|
|
if due_at is not None:
|
|
task_attributes['due_at'] = normalize_date_to_rfc3339_format(due_at)
|
|
if completion_rule is not None:
|
|
task_attributes['completion_rule'] = completion_rule
|
|
box_response = self._session.post(url, data=json.dumps(task_attributes))
|
|
response = box_response.json()
|
|
return self.translator.translate(
|
|
session=self._session,
|
|
response_object=response,
|
|
)
|
|
|
|
@api_call
|
|
def get_tasks(self, fields: Iterable[str] = None) -> 'BoxObjectCollection':
|
|
"""
|
|
Get the entries in the file tasks.
|
|
|
|
:param fields:
|
|
List of fields to request.
|
|
:returns:
|
|
An iterator of the entries in the file tasks
|
|
"""
|
|
return MarkerBasedObjectCollection(
|
|
session=self._session,
|
|
url=self.get_url('tasks'),
|
|
limit=None,
|
|
marker=None,
|
|
fields=fields,
|
|
return_full_pages=False,
|
|
)
|
|
|
|
@api_call
|
|
def get_previous_versions(
|
|
self,
|
|
limit: Optional[int] = None,
|
|
offset: int = None,
|
|
fields: Iterable[str] = None
|
|
) -> 'BoxObjectCollection':
|
|
"""
|
|
Get previous versions of the file.
|
|
|
|
: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.
|
|
:param fields:
|
|
List of fields to request.
|
|
:returns:
|
|
An iterator of the previous versions of the file.
|
|
"""
|
|
return LimitOffsetBasedObjectCollection(
|
|
session=self.session,
|
|
url=self.get_url('versions'),
|
|
limit=limit,
|
|
fields=fields,
|
|
offset=offset,
|
|
return_full_pages=False,
|
|
)
|
|
|
|
@api_call
|
|
def promote_version(self, file_version: 'FileVersion') -> 'FileVersion':
|
|
"""
|
|
Promote a file version to become the current version of this file. This will create a new file version
|
|
identical to the previous version as the new current version.
|
|
|
|
:param file_version:
|
|
The file version to promote.
|
|
:returns:
|
|
The new file version created as the current.
|
|
"""
|
|
url = self.get_url('versions', 'current')
|
|
body = {
|
|
'type': 'file_version',
|
|
'id': file_version.object_id,
|
|
}
|
|
response = self._session.post(url, data=json.dumps(body)).json()
|
|
return self.translator.translate(
|
|
session=self._session,
|
|
response_object=response,
|
|
)
|
|
|
|
@api_call
|
|
def delete_version(self, file_version: 'FileVersion', etag: Optional[str] = None) -> bool:
|
|
"""
|
|
Delete a specific version of a file.
|
|
|
|
:param file_version:
|
|
The file version to delete.
|
|
:param etag:
|
|
If specified, instruct the Box API to update the item only if the current version's etag matches.
|
|
:returns:
|
|
Whether the operation succeeded.
|
|
"""
|
|
url = self.get_url('versions', file_version.object_id)
|
|
headers = {'If-Match': etag} if etag is not None else None
|
|
response = self._session.delete(url, expect_json_response=False, headers=headers)
|
|
return response.ok
|
|
|
|
@api_call
|
|
def get_embed_url(self) -> str:
|
|
"""
|
|
Get a URL suitable for embedding the file in an iframe in a web application.
|
|
|
|
:returns:
|
|
The embed URL.
|
|
"""
|
|
url = self.get_url()
|
|
params = {'fields': 'expiring_embed_link'}
|
|
response = self._session.get(url, params=params).json()
|
|
return response['expiring_embed_link']['url']
|
|
|
|
@api_call
|
|
def get_representation_info(self, rep_hints: Optional[str] = None) -> List[dict]:
|
|
"""
|
|
Get information about the representations available for a file.
|
|
|
|
:param rep_hints:
|
|
A formatted string describing which representations are desired.
|
|
:returns:
|
|
The representation information
|
|
"""
|
|
url = self.get_url()
|
|
params = {'fields': 'representations'}
|
|
headers = {'X-Rep-Hints': rep_hints} if rep_hints is not None else None
|
|
response = self._session.get(url, params=params, headers=headers).json()
|
|
return response['representations']['entries']
|
|
|
|
@deprecated('Use get_thumbnail_representation')
|
|
@api_call
|
|
def get_thumbnail(
|
|
self,
|
|
extension: str = 'png',
|
|
min_width: Optional[int] = None,
|
|
min_height: Optional[int] = None,
|
|
max_width: Optional[int] = None,
|
|
max_height: Optional[int] = None
|
|
) -> bytes:
|
|
"""
|
|
Retrieve a thumbnail image for the file.
|
|
|
|
:param extension:
|
|
The file extension for the thumbnail, e.g. 'png' or 'jpg'
|
|
:param min_width:
|
|
The minimum width required for the thumbnail image
|
|
:param min_height:
|
|
The minimum height required for the thumbnail image
|
|
:param max_width:
|
|
The maximum width required for the thumbnail image
|
|
:param max_height:
|
|
The maximum height required for the thumbnail image
|
|
:returns:
|
|
The file contents of the thumbnail image
|
|
"""
|
|
url = self.get_url('thumbnail.' + extension)
|
|
params = {}
|
|
if min_width is not None:
|
|
params['min_width'] = min_width
|
|
if min_height is not None:
|
|
params['min_height'] = min_height
|
|
if max_width is not None:
|
|
params['max_width'] = max_width
|
|
if max_height is not None:
|
|
params['max_height'] = max_height
|
|
|
|
response = self._session.get(url, params=params, expect_json_response=False)
|
|
return response.content
|
|
|
|
@api_call
|
|
def get_thumbnail_representation(self, dimensions: str, extension: str = 'png') -> bytes:
|
|
"""
|
|
Retrieve a thumbnail image for the file.
|
|
|
|
:param dimensions:
|
|
The width by height size of this representation in pixels (e.g. '92x92')
|
|
:param extension:
|
|
The file extension for the thumbnail, e.g. 'png' or 'jpg'
|
|
:returns:
|
|
The file contents of the thumbnail image
|
|
"""
|
|
rep_hints = f'[{extension}?dimensions={dimensions}]'
|
|
representations = self.get_representation_info(rep_hints)
|
|
if representations:
|
|
representation = representations[0]
|
|
if representation['status'].get('code') in ('error_conversion_failed', 'error_password_protected'):
|
|
return b''
|
|
url = representation['content']['url_template']
|
|
url = url.replace('{+asset_path}', '')
|
|
response = self._session.get(url, expect_json_response=False)
|
|
return response.content
|
|
return b''
|
|
|
|
@api_call
|
|
def copy(
|
|
self,
|
|
*,
|
|
parent_folder: 'Folder',
|
|
name: Optional[str] = None,
|
|
file_version: 'FileVersion' = None,
|
|
**_kwargs
|
|
) -> 'File':
|
|
# pylint: disable=arguments-differ
|
|
"""Copy the item to the given folder.
|
|
|
|
:param parent_folder:
|
|
The folder to which the item should be copied.
|
|
:param name:
|
|
A new name for the item, in case there is already another item in the new parent folder with the same name.
|
|
:param file_version:
|
|
A specific version of the file to copy
|
|
:returns:
|
|
The copy of the file
|
|
"""
|
|
# pylint: disable=arguments-differ
|
|
url = self.get_url('copy')
|
|
data = {
|
|
'parent': {'id': parent_folder.object_id}
|
|
}
|
|
if name is not None:
|
|
data['name'] = name
|
|
if file_version is not None:
|
|
data['version'] = file_version.object_id
|
|
box_response = self._session.post(url, data=json.dumps(data))
|
|
response = box_response.json()
|
|
return self.translator.translate(
|
|
session=self._session,
|
|
response_object=response,
|
|
)
|
|
|
|
@api_call
|
|
def set_disposition_at(self, date_time: Union[datetime, str]) -> 'File':
|
|
"""
|
|
Modifies the retention expiration timestamp for the given file. This date can't be shortened once set on a file.
|
|
|
|
:param date_time:
|
|
A datetime string in a format supported by the dateutil library or a datetime.datetime object.
|
|
If no timezone info provided, local timezone will be applied.
|
|
:return:
|
|
Updated 'File' object
|
|
"""
|
|
data = {'disposition_at': normalize_date_to_rfc3339_format(date_time)}
|
|
return self.update_info(data=data)
|