Skip to content

Commit

Permalink
Fix empty file uploads (#78)
Browse files Browse the repository at this point in the history
* Fix empty file uploads

tus-js-client and tusd support uploads of empty files. In
these cases, only the initial POST request needs to be performed:

tus/tus-js-client#106

This was supported by 0.2.5, but the refactoring inadvertently broke
this by skipping by all HTTP requests entirely.

* Add tests for empty file uploads

* Refactor "empty file" tests

It's more reliable and concise to create an uploader from the existing
client fixture instead of modifying an existing uploader to resemble
what we want.
  • Loading branch information
Matoking committed Jun 20, 2023
1 parent 4eb426c commit 3bc58e5
Show file tree
Hide file tree
Showing 3 changed files with 67 additions and 6 deletions.
27 changes: 27 additions & 0 deletions tests/test_async_uploader.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import io
import unittest
from unittest import mock
import asyncio
Expand Down Expand Up @@ -54,6 +55,32 @@ def test_upload(self):
self.assertEqual(self.async_uploader.offset,
self.async_uploader.get_file_size())

def test_upload_empty(self):
with aioresponses() as resps:
resps.post(
self.client.url, status=200,
headers={
"upload-offset": "0",
"location": f"{self.client.url}this-is-not-used"
}
)
resps.patch(
f"{self.client.url}this-is-not-used",
exception=ValueError(
"PATCH request not allowed for empty file"
)
)

# Upload an empty file
async_uploader = self.client.async_uploader(
file_stream=io.BytesIO(b"")
)
self.loop.run_until_complete(async_uploader.upload())

# Upload URL being set means the POST request was sent and the empty
# file was uploaded without a single PATCH request.
self.assertTrue(async_uploader.url)

def test_upload_retry(self):
num_of_retries = 3
self.async_uploader.retries = num_of_retries
Expand Down
26 changes: 26 additions & 0 deletions tests/test_uploader.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os
import io
from base64 import b64encode
from unittest import mock

Expand Down Expand Up @@ -136,6 +137,31 @@ def test_upload_retry(self, request_mock):
with pytest.raises(exceptions.TusCommunicationError):
self.uploader.upload_chunk()
self.assertEqual(self.uploader._retried, num_of_retries)

@responses.activate
def test_upload_empty(self):
responses.add(
responses.POST, self.client.url,
adding_headers={
"upload-offset": "0",
"location": f"{self.client.url}this-is-not-used"
}
)
responses.add(
responses.PATCH,
f"{self.client.url}this-is-not-used",
body=ValueError("PATCH request not allowed for empty file")
)

# Upload an empty file
uploader = self.client.uploader(
file_stream=io.BytesIO(b"")
)
uploader.upload()

# Upload URL being set means the POST request was sent and the empty
# file was uploaded without a single PATCH request.
self.assertTrue(uploader.url)

@mock.patch('tusclient.uploader.uploader.TusRequest')
def test_upload_checksum(self, request_mock):
Expand Down
20 changes: 14 additions & 6 deletions tusclient/uploader/uploader.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,13 @@ def upload(self, stop_at: Optional[int] = None):
"""
self.stop_at = stop_at or self.get_file_size()

if not self.url:
# Ensure the POST request is performed even for empty files.
# This ensures even empty files can be uploaded; in this case
# only the POST request needs to be performed.
self.set_url(self.create_url())
self.offset = 0

while self.offset < self.stop_at:
self.upload_chunk()

Expand All @@ -43,9 +50,7 @@ def upload_chunk(self):
Upload chunk of file.
"""
self._retried = 0
if not self.url:
self.set_url(self.create_url())
self.offset = 0

self._do_request()
self.offset = int(self.request.response_headers.get('upload-offset'))

Expand Down Expand Up @@ -106,6 +111,11 @@ async def upload(self, stop_at: Optional[int] = None):
defaults to the file size.
"""
self.stop_at = stop_at or self.get_file_size()

if not self.url:
self.set_url(await self.create_url())
self.offset = 0

while self.offset < self.stop_at:
await self.upload_chunk()

Expand All @@ -114,9 +124,7 @@ async def upload_chunk(self):
Upload chunk of file.
"""
self._retried = 0
if not self.url:
self.set_url(await self.create_url())
self.offset = 0

await self._do_request()
self.offset = int(self.request.response_headers.get('upload-offset'))

Expand Down

0 comments on commit 3bc58e5

Please sign in to comment.