Skip to content

Commit b3b41d3

Browse files
Add zip functionality (#539)
1 parent 65febeb commit b3b41d3

File tree

4 files changed

+164
-1
lines changed

4 files changed

+164
-1
lines changed

HISTORY.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ Next Release
99
- Fix bug with updating a collaboration role to owner
1010
- Allow creating tasks with the `action` and `completion_rule` parameters.
1111
- Add support for `copyInstanceOnItemCopy` field for metadata templates
12+
- Add zip functionality
1213

1314
2.9.0 (2020-06-23)
1415
++++++++

boxsdk/client/client.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1671,3 +1671,65 @@ def create_metadata_template(self, display_name, fields, template_key=None, hidd
16711671
session=self._session,
16721672
response_object=response,
16731673
)
1674+
1675+
@api_call
1676+
def __create_zip(self, name, items):
1677+
"""
1678+
Creates a zip file containing multiple files and/or folders for later download.
1679+
1680+
:param name:
1681+
The name of the zip file to be created.
1682+
:type name:
1683+
`unicode`
1684+
:param items:
1685+
List of files and/or folders to be contained in the zip file.
1686+
:type items:
1687+
`Iterable`
1688+
:returns:
1689+
A dictionary representing a created zip
1690+
:rtype:
1691+
:class:`dict`
1692+
"""
1693+
# pylint: disable=protected-access
1694+
url = self._session.get_url('zip_downloads')
1695+
zip_file_items = []
1696+
for item in items:
1697+
zip_file_items.append({'type': item._item_type, 'id': item.object_id})
1698+
data = {
1699+
'download_file_name': name,
1700+
'items': zip_file_items
1701+
}
1702+
return self._session.post(url, data=json.dumps(data)).json()
1703+
1704+
@api_call
1705+
def download_zip(self, name, items, writeable_stream):
1706+
"""
1707+
Downloads a zip file containing multiple files and/or folders.
1708+
1709+
:param name:
1710+
The name of the zip file to be created.
1711+
:type name:
1712+
`unicode`
1713+
:param items:
1714+
List of files or folders to be part of the created zip.
1715+
:type items:
1716+
`Iterable`
1717+
:param writeable_stream:
1718+
Stream to pipe the readable stream of the zip file.
1719+
:type writeable_stream:
1720+
`zip`
1721+
:returns:
1722+
A status response object
1723+
:rtype:
1724+
:class:`dict`
1725+
"""
1726+
created_zip = self.__create_zip(name, items)
1727+
response = self._session.get(created_zip['download_url'], expect_json_response=False, stream=True)
1728+
for chunk in response.network_response.response_as_stream.stream(decode_content=True):
1729+
writeable_stream.write(chunk)
1730+
status = self._session.get(created_zip['status_url']).json()
1731+
status.update(created_zip)
1732+
return self.translator.translate(
1733+
session=self._session,
1734+
response_object=status,
1735+
)

docs/usage/zip.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
Zip
2+
========
3+
4+
Allows you to create a temporary zip file on Box, containing Box files and folders, and then download it.
5+
6+
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
7+
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
8+
9+
- [Download a Zip File](#download-a-zip-file)
10+
11+
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
12+
13+
Download a Zip File
14+
-----------------------------
15+
16+
Calling [`client.download_zip(name, items, writable_stream)`][create_zip] will let you create a new zip file
17+
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.
18+
19+
```python
20+
name = 'test'
21+
file = mock_client.file('466239504569')
22+
folder = mock_client.folder('466239504580')
23+
items = [file, folder]
24+
output_file = open('test.zip', 'wb')
25+
status = client.download_zip(name, items, output_file)
26+
print('The status of the zip download is {0}'.format(status['state']))
27+
```
28+
29+
[download_zip]: https://box-python-sdk.readthedocs.io/en/latest/boxsdk.client.html#boxsdk.client.client.Client.download_zip

test/unit/client/test_client.py

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
from mock import Mock
88
import pytest
9-
from six import text_type
9+
from six import text_type, BytesIO, int2byte, PY2
1010

1111
# pylint:disable=redefined-builtin
1212
# pylint:disable=import-error
@@ -119,6 +119,14 @@ def mock_folder_response(mock_object_id, make_mock_box_request):
119119
return mock_box_response
120120

121121

122+
@pytest.fixture(scope='function')
123+
def mock_content_response(make_mock_box_request):
124+
mock_box_response, mock_network_response = make_mock_box_request(content=b'Contents of a text file.')
125+
mock_network_response.response_as_stream = raw = Mock()
126+
raw.stream.return_value = (b if PY2 else int2byte(b) for b in mock_box_response.content)
127+
return mock_box_response
128+
129+
122130
@pytest.fixture(scope='module')
123131
def marker_id():
124132
return 'marker_1'
@@ -1465,3 +1473,66 @@ def test_device_pinner(mock_client):
14651473

14661474
assert isinstance(pin, DevicePinner)
14671475
assert pin.object_id == pin_id
1476+
1477+
1478+
def test_download_zip(mock_client, mock_box_session, mock_content_response):
1479+
expected_create_url = '{0}/zip_downloads'.format(API.BASE_API_URL)
1480+
name = 'test'
1481+
file_item = mock_client.file('466239504569')
1482+
folder_item = mock_client.folder('466239504580')
1483+
items = [file_item, folder_item]
1484+
mock_writeable_stream = BytesIO()
1485+
expected_create_body = {
1486+
'download_file_name': name,
1487+
'items': [
1488+
{
1489+
'type': 'file',
1490+
'id': '466239504569'
1491+
},
1492+
{
1493+
'type': 'folder',
1494+
'id': '466239504580'
1495+
}
1496+
]
1497+
}
1498+
status_response_mock = Mock()
1499+
status_response_mock.json.return_value = {
1500+
'total_file_count': 20,
1501+
'downloaded_file_count': 10,
1502+
'skipped_file_count': 10,
1503+
'skipped_folder_count': 10,
1504+
'state': 'succeeded'
1505+
}
1506+
mock_box_session.post.return_value.json.return_value = {
1507+
'download_url': 'https://dl.boxcloud.com/2.0/zip_downloads/124hfiowk3fa8kmrwh/content',
1508+
'status_url': 'https://api.box.com/2.0/zip_downloads/124hfiowk3fa8kmrwh/status',
1509+
'expires_at': '2018-04-25T11:00:18-07:00',
1510+
'name_conflicts': [
1511+
[
1512+
{
1513+
'id': '100',
1514+
'type': 'file',
1515+
'original_name': 'salary.pdf',
1516+
'download_name': 'aqc823.pdf'
1517+
},
1518+
{
1519+
'id': '200',
1520+
'type': 'file',
1521+
'original_name': 'salary.pdf',
1522+
'download_name': 'aci23s.pdf'
1523+
}
1524+
]
1525+
]
1526+
}
1527+
1528+
mock_box_session.get.side_effect = [mock_content_response, status_response_mock]
1529+
1530+
status_returned = mock_client.download_zip(name, items, mock_writeable_stream)
1531+
mock_box_session.post.assert_called_once_with(expected_create_url, data=json.dumps(expected_create_body))
1532+
mock_box_session.get.assert_any_call('https://dl.boxcloud.com/2.0/zip_downloads/124hfiowk3fa8kmrwh/content',
1533+
expect_json_response=False, stream=True)
1534+
mock_box_session.get.assert_called_with('https://api.box.com/2.0/zip_downloads/124hfiowk3fa8kmrwh/status')
1535+
mock_writeable_stream.seek(0)
1536+
assert mock_writeable_stream.read() == mock_content_response.content
1537+
assert status_returned['total_file_count'] == 20
1538+
assert status_returned['name_conflicts'][0][0]['id'] == '100'

0 commit comments

Comments
 (0)