Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Next Release
- Fix bug with updating a collaboration role to owner
- Allow creating tasks with the `action` and `completion_rule` parameters.
- Add support for `copyInstanceOnItemCopy` field for metadata templates
- Add zip functionality

2.9.0 (2020-06-23)
++++++++
Expand Down
62 changes: 62 additions & 0 deletions boxsdk/client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -1671,3 +1671,65 @@ def create_metadata_template(self, display_name, fields, template_key=None, hidd
session=self._session,
response_object=response,
)

@api_call
def __create_zip(self, name, items):
"""
Creates a zip file containing multiple files and/or folders for later download.

:param name:
The name of the zip file to be created.
:type name:
`unicode`
:param items:
List of files and/or folders to be contained in the zip file.
:type items:
`Iterable`
:returns:
A dictionary representing a created zip
Comment thread
sujaygarlanka marked this conversation as resolved.
:rtype:
:class:`dict`
"""
# pylint: disable=protected-access
url = self._session.get_url('zip_downloads')
zip_file_items = []
for item in items:
zip_file_items.append({'type': item._item_type, 'id': item.object_id})
data = {
'download_file_name': name,
'items': zip_file_items
}
return self._session.post(url, data=json.dumps(data)).json()

@api_call
def download_zip(self, name, items, writeable_stream):
"""
Downloads a zip file containing multiple files and/or folders.

:param name:
The name of the zip file to be created.
:type name:
`unicode`
:param items:
List of files or folders to be part of the created zip.
:type items:
`Iterable`
:param writeable_stream:
Stream to pipe the readable stream of the zip file.
:type writeable_stream:
`zip`
:returns:
A status response object
:rtype:
:class:`dict`
"""
created_zip = self.__create_zip(name, items)
response = self._session.get(created_zip['download_url'], expect_json_response=False, stream=True)
for chunk in response.network_response.response_as_stream.stream(decode_content=True):
writeable_stream.write(chunk)
status = self._session.get(created_zip['status_url']).json()
status.update(created_zip)
return self.translator.translate(
session=self._session,
response_object=status,
)
29 changes: 29 additions & 0 deletions docs/usage/zip.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
Zip
========

Allows you to create a temporary zip file on Box, containing Box files and folders, and then download it.

<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->

- [Download a Zip File](#download-a-zip-file)

<!-- END doctoc generated TOC please keep comment here to allow auto update -->

Download a Zip File
-----------------------------

Calling [`client.download_zip(name, items, writable_stream)`][create_zip] will let you create a new zip file
with the specified name and with the specified items and download it to the stream that is passed in. The response is a status `dict` that contains information about the download, including whether it was successful. The created zip file does not show up in your Box account.

```python
name = 'test'
file = mock_client.file('466239504569')
folder = mock_client.folder('466239504580')
items = [file, folder]
output_file = open('test.zip', 'wb')
status = client.download_zip(name, items, output_file)
print('The status of the zip download is {0}'.format(status['state']))
```

[download_zip]: https://box-python-sdk.readthedocs.io/en/latest/boxsdk.client.html#boxsdk.client.client.Client.download_zip
73 changes: 72 additions & 1 deletion test/unit/client/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

from mock import Mock
import pytest
from six import text_type
from six import text_type, BytesIO, int2byte, PY2

# pylint:disable=redefined-builtin
# pylint:disable=import-error
Expand Down Expand Up @@ -119,6 +119,14 @@ def mock_folder_response(mock_object_id, make_mock_box_request):
return mock_box_response


@pytest.fixture(scope='function')
def mock_content_response(make_mock_box_request):
mock_box_response, mock_network_response = make_mock_box_request(content=b'Contents of a text file.')
mock_network_response.response_as_stream = raw = Mock()
raw.stream.return_value = (b if PY2 else int2byte(b) for b in mock_box_response.content)
return mock_box_response


@pytest.fixture(scope='module')
def marker_id():
return 'marker_1'
Expand Down Expand Up @@ -1465,3 +1473,66 @@ def test_device_pinner(mock_client):

assert isinstance(pin, DevicePinner)
assert pin.object_id == pin_id


def test_download_zip(mock_client, mock_box_session, mock_content_response):
expected_create_url = '{0}/zip_downloads'.format(API.BASE_API_URL)
name = 'test'
file_item = mock_client.file('466239504569')
folder_item = mock_client.folder('466239504580')
items = [file_item, folder_item]
mock_writeable_stream = BytesIO()
expected_create_body = {
'download_file_name': name,
'items': [
{
'type': 'file',
'id': '466239504569'
},
{
'type': 'folder',
'id': '466239504580'
}
]
}
status_response_mock = Mock()
status_response_mock.json.return_value = {
'total_file_count': 20,
'downloaded_file_count': 10,
'skipped_file_count': 10,
'skipped_folder_count': 10,
'state': 'succeeded'
}
mock_box_session.post.return_value.json.return_value = {
'download_url': 'https://dl.boxcloud.com/2.0/zip_downloads/124hfiowk3fa8kmrwh/content',
'status_url': 'https://api.box.com/2.0/zip_downloads/124hfiowk3fa8kmrwh/status',
'expires_at': '2018-04-25T11:00:18-07:00',
'name_conflicts': [
[
{
'id': '100',
'type': 'file',
'original_name': 'salary.pdf',
'download_name': 'aqc823.pdf'
},
{
'id': '200',
'type': 'file',
'original_name': 'salary.pdf',
'download_name': 'aci23s.pdf'
}
]
]
}

mock_box_session.get.side_effect = [mock_content_response, status_response_mock]

status_returned = mock_client.download_zip(name, items, mock_writeable_stream)
mock_box_session.post.assert_called_once_with(expected_create_url, data=json.dumps(expected_create_body))
mock_box_session.get.assert_any_call('https://dl.boxcloud.com/2.0/zip_downloads/124hfiowk3fa8kmrwh/content',
expect_json_response=False, stream=True)
mock_box_session.get.assert_called_with('https://api.box.com/2.0/zip_downloads/124hfiowk3fa8kmrwh/status')
mock_writeable_stream.seek(0)
assert mock_writeable_stream.read() == mock_content_response.content
assert status_returned['total_file_count'] == 20
assert status_returned['name_conflicts'][0][0]['id'] == '100'