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 @@ -7,6 +7,7 @@ Next Release
++++++++
- Allow ints to be passed in as item IDs
- Fix bug with updating a collaboration role to owner
- Add zip functionality

2.9.0 (2020-06-23)
++++++++
Expand Down
65 changes: 65 additions & 0 deletions boxsdk/client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -1666,3 +1666,68 @@ 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 of multiple files and folders.
sujaygarlanka marked this conversation as resolved.
Show resolved Hide resolved

: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.
sujaygarlanka marked this conversation as resolved.
Show resolved Hide resolved
: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')
data_items = []
sujaygarlanka marked this conversation as resolved.
Show resolved Hide resolved
for item in items:
data_items.append({'type': item._item_type, 'id': item.object_id})
sujaygarlanka marked this conversation as resolved.
Show resolved Hide resolved
data = {
'download_file_name': name,
'items': data_items
sujaygarlanka marked this conversation as resolved.
Show resolved Hide resolved
}
response = self._session.post(url, data=json.dumps(data)).json()
return self.translator.translate(
session=self._session,
response_object=response,
)

@api_call
def download_zip(self, name, items, writeable_stream):
"""
Creates a zip of multiple files and folders and downloads it.
sujaygarlanka marked this conversation as resolved.
Show resolved Hide resolved

: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)
sujaygarlanka marked this conversation as resolved.
Show resolved Hide resolved
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'])
return self.translator.translate(
session=self._session,
response_object=status.json(),
)
47 changes: 47 additions & 0 deletions docs/usage/zip.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
Zip
========

Allows you to create a temporary zip file of Box files and folders and download them.
sujaygarlanka marked this conversation as resolved.
Show resolved Hide resolved

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


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

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

Create a Zip File
-----------------------------

Calling [`client.create_zip(name, items)`][create_zip] will let you create a new zip file with the specified name and with the specified items and will return a `dict` with the download and status link. This file does not show up in your Box account, but will be temporarily available for download.
sujaygarlanka marked this conversation as resolved.
Show resolved Hide resolved

```python
name = 'test'
file = mock_client.file('466239504569')
folder = mock_client.folder('466239504580')
items = [file, folder]
created_zip = client.create_zip(name, items)
print('The created zip download url is {0}'.format(created_zip['download_url']))
```

[create_zip]: https://box-python-sdk.readthedocs.io/en/latest/boxsdk.client.html#boxsdk.client.client.Client.create_zip

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
117 changes: 116 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 @@ -1464,3 +1472,110 @@ def test_device_pinner(mock_client):

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


def test_create_zip(mock_client, mock_box_session):
PJSimon marked this conversation as resolved.
Show resolved Hide resolved
expected_url = '{0}/zip_downloads'.format(API.BASE_API_URL)
name = 'test'
file = mock_client.file('466239504569')
folder = mock_client.folder('466239504580')
items = [file, folder]
expected_body = {
'download_file_name': name,
'items': [
{
'type': 'file',
'id': '466239504569'
},
{
'type': 'folder',
'id': '466239504580'
}
]
}
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'
}
]
]
}

created_zip = mock_client.create_zip(name, items)
mock_box_session.post.assert_called_once_with(expected_url, data=json.dumps(expected_body))
assert created_zip['download_url'] == 'https://dl.boxcloud.com/2.0/zip_downloads/124hfiowk3fa8kmrwh/content'


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 = mock_client.file('466239504569')
folder = mock_client.folder('466239504580')
items = [file, folder]
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