Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add zip functionality #539

Merged
merged 15 commits into from
Oct 1, 2020
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
sujaygarlanka marked this conversation as resolved.
Show resolved Hide 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'