Skip to content

Commit

Permalink
Add zip functionality (#539)
Browse files Browse the repository at this point in the history
  • Loading branch information
sujaygarlanka committed Oct 1, 2020
1 parent 65febeb commit b3b41d3
Show file tree
Hide file tree
Showing 4 changed files with 164 additions and 1 deletion.
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
: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'

0 comments on commit b3b41d3

Please sign in to comment.