2018-05-16 00:12:11 +08:00
|
|
|
import io
|
2021-10-14 17:28:03 +08:00
|
|
|
import itertools
|
2018-05-16 00:12:11 +08:00
|
|
|
import os
|
|
|
|
import sys
|
|
|
|
import tempfile
|
|
|
|
from unittest import skipIf
|
|
|
|
|
|
|
|
from django.core.files.base import ContentFile
|
|
|
|
from django.http import FileResponse
|
|
|
|
from django.test import SimpleTestCase
|
|
|
|
|
|
|
|
|
2021-10-14 17:28:03 +08:00
|
|
|
class UnseekableBytesIO(io.BytesIO):
|
|
|
|
def seekable(self):
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
2018-05-16 00:12:11 +08:00
|
|
|
class FileResponseTests(SimpleTestCase):
|
2021-10-14 17:28:03 +08:00
|
|
|
def test_content_length_file(self):
|
2018-05-16 00:12:11 +08:00
|
|
|
response = FileResponse(open(__file__, 'rb'))
|
|
|
|
response.close()
|
2021-10-14 17:41:10 +08:00
|
|
|
self.assertEqual(response.headers['Content-Length'], str(os.path.getsize(__file__)))
|
2018-05-16 00:12:11 +08:00
|
|
|
|
2021-10-14 17:28:03 +08:00
|
|
|
def test_content_length_buffer(self):
|
2018-05-16 00:12:11 +08:00
|
|
|
response = FileResponse(io.BytesIO(b'binary content'))
|
2020-07-14 19:32:24 +08:00
|
|
|
self.assertEqual(response.headers['Content-Length'], '14')
|
2021-10-14 17:28:03 +08:00
|
|
|
|
2021-10-14 17:41:10 +08:00
|
|
|
def test_content_length_nonzero_starting_position_file(self):
|
|
|
|
file = open(__file__, 'rb')
|
|
|
|
file.seek(10)
|
|
|
|
response = FileResponse(file)
|
|
|
|
response.close()
|
|
|
|
self.assertEqual(response.headers['Content-Length'], str(os.path.getsize(__file__) - 10))
|
|
|
|
|
|
|
|
def test_content_length_nonzero_starting_position_buffer(self):
|
|
|
|
test_tuples = (
|
|
|
|
('BytesIO', io.BytesIO),
|
|
|
|
('UnseekableBytesIO', UnseekableBytesIO),
|
|
|
|
)
|
|
|
|
for buffer_class_name, BufferClass in test_tuples:
|
|
|
|
with self.subTest(buffer_class_name=buffer_class_name):
|
|
|
|
buffer = BufferClass(b'binary content')
|
|
|
|
buffer.seek(10)
|
|
|
|
response = FileResponse(buffer)
|
|
|
|
self.assertEqual(response.headers['Content-Length'], '4')
|
|
|
|
|
|
|
|
def test_content_length_nonzero_starting_position_file_seekable_no_tell(self):
|
|
|
|
class TestFile:
|
|
|
|
def __init__(self, path, *args, **kwargs):
|
|
|
|
self._file = open(path, *args, **kwargs)
|
|
|
|
|
|
|
|
def read(self, n_bytes=-1):
|
|
|
|
return self._file.read(n_bytes)
|
|
|
|
|
|
|
|
def seek(self, offset, whence=io.SEEK_SET):
|
|
|
|
return self._file.seek(offset, whence)
|
|
|
|
|
|
|
|
def seekable(self):
|
|
|
|
return True
|
|
|
|
|
|
|
|
@property
|
|
|
|
def name(self):
|
|
|
|
return self._file.name
|
|
|
|
|
|
|
|
def close(self):
|
|
|
|
if self._file:
|
|
|
|
self._file.close()
|
|
|
|
self._file = None
|
|
|
|
|
|
|
|
def __enter__(self):
|
|
|
|
return self
|
|
|
|
|
|
|
|
def __exit__(self, e_type, e_val, e_tb):
|
|
|
|
self.close()
|
|
|
|
|
|
|
|
file = TestFile(__file__, 'rb')
|
|
|
|
file.seek(10)
|
|
|
|
response = FileResponse(file)
|
|
|
|
response.close()
|
|
|
|
self.assertEqual(response.headers['Content-Length'], str(os.path.getsize(__file__) - 10))
|
|
|
|
|
2021-10-14 17:28:03 +08:00
|
|
|
def test_content_type_file(self):
|
|
|
|
response = FileResponse(open(__file__, 'rb'))
|
|
|
|
response.close()
|
2021-10-14 17:41:10 +08:00
|
|
|
self.assertIn(response.headers['Content-Type'], ['text/x-python', 'text/plain'])
|
2021-10-14 17:28:03 +08:00
|
|
|
|
|
|
|
def test_content_type_buffer(self):
|
|
|
|
response = FileResponse(io.BytesIO(b'binary content'))
|
2020-07-14 19:32:24 +08:00
|
|
|
self.assertEqual(response.headers['Content-Type'], 'application/octet-stream')
|
2021-10-14 17:28:03 +08:00
|
|
|
|
|
|
|
def test_content_type_buffer_explicit(self):
|
|
|
|
response = FileResponse(io.BytesIO(b'binary content'), content_type='video/webm')
|
|
|
|
self.assertEqual(response.headers['Content-Type'], 'video/webm')
|
|
|
|
|
2021-10-14 17:41:10 +08:00
|
|
|
def test_content_type_buffer_explicit_default(self):
|
|
|
|
response = FileResponse(io.BytesIO(b'binary content'), content_type='text/html')
|
|
|
|
self.assertEqual(response.headers['Content-Type'], 'text/html')
|
|
|
|
|
2021-10-14 17:28:03 +08:00
|
|
|
def test_content_type_buffer_named(self):
|
|
|
|
test_tuples = (
|
2021-10-14 17:41:10 +08:00
|
|
|
(__file__, ['text/x-python', 'text/plain']),
|
|
|
|
(__file__ + 'nosuchfile', ['application/octet-stream']),
|
2021-10-14 17:28:03 +08:00
|
|
|
('test_fileresponse.py', ['text/x-python', 'text/plain']),
|
|
|
|
('test_fileresponse.pynosuchfile', ['application/octet-stream']),
|
|
|
|
)
|
|
|
|
for filename, content_types in test_tuples:
|
|
|
|
with self.subTest(filename=filename):
|
|
|
|
buffer = io.BytesIO(b'binary content')
|
|
|
|
buffer.name = filename
|
|
|
|
response = FileResponse(buffer)
|
|
|
|
self.assertIn(response.headers['Content-Type'], content_types)
|
|
|
|
|
|
|
|
def test_content_disposition_file(self):
|
|
|
|
filenames = (
|
|
|
|
('', 'test_fileresponse.py'),
|
|
|
|
('custom_name.py', 'custom_name.py'),
|
|
|
|
)
|
|
|
|
dispositions = (
|
|
|
|
(False, 'inline'),
|
|
|
|
(True, 'attachment'),
|
|
|
|
)
|
2021-10-14 17:41:10 +08:00
|
|
|
for (filename, header_filename), (as_attachment, header_disposition) in itertools.product(
|
|
|
|
filenames, dispositions
|
|
|
|
):
|
2021-10-14 17:28:03 +08:00
|
|
|
with self.subTest(filename=filename, disposition=header_disposition):
|
|
|
|
response = FileResponse(open(__file__, 'rb'), filename=filename, as_attachment=as_attachment)
|
2021-10-14 17:41:10 +08:00
|
|
|
response.close()
|
2021-10-14 17:28:03 +08:00
|
|
|
self.assertEqual(
|
|
|
|
response.headers['Content-Disposition'],
|
|
|
|
'%s; filename="%s"' % (header_disposition, header_filename),
|
|
|
|
)
|
|
|
|
|
|
|
|
def test_content_disposition_buffer(self):
|
|
|
|
response = FileResponse(io.BytesIO(b'binary content'))
|
2019-05-17 18:07:27 +08:00
|
|
|
self.assertFalse(response.has_header('Content-Disposition'))
|
|
|
|
|
2021-10-14 17:28:03 +08:00
|
|
|
def test_content_disposition_buffer_attachment(self):
|
2019-05-17 18:07:27 +08:00
|
|
|
response = FileResponse(io.BytesIO(b'binary content'), as_attachment=True)
|
2020-07-14 19:32:24 +08:00
|
|
|
self.assertEqual(response.headers['Content-Disposition'], 'attachment')
|
2021-10-14 17:28:03 +08:00
|
|
|
|
|
|
|
def test_content_disposition_buffer_explicit_filename(self):
|
|
|
|
dispositions = (
|
|
|
|
(False, 'inline'),
|
|
|
|
(True, 'attachment'),
|
|
|
|
)
|
|
|
|
for as_attachment, header_disposition in dispositions:
|
|
|
|
response = FileResponse(
|
|
|
|
io.BytesIO(b'binary content'),
|
|
|
|
as_attachment=as_attachment,
|
|
|
|
filename='custom_name.py',
|
|
|
|
)
|
|
|
|
self.assertEqual(
|
2021-10-14 17:41:10 +08:00
|
|
|
response.headers['Content-Disposition'], '%s; filename="custom_name.py"' % header_disposition,
|
2021-10-14 17:28:03 +08:00
|
|
|
)
|
|
|
|
|
|
|
|
def test_response_buffer(self):
|
|
|
|
response = FileResponse(io.BytesIO(b'binary content'))
|
2018-05-16 00:12:11 +08:00
|
|
|
self.assertEqual(list(response), [b'binary content'])
|
|
|
|
|
2021-10-14 17:28:03 +08:00
|
|
|
def test_response_nonzero_starting_position(self):
|
|
|
|
test_tuples = (
|
|
|
|
('BytesIO', io.BytesIO),
|
|
|
|
('UnseekableBytesIO', UnseekableBytesIO),
|
|
|
|
)
|
|
|
|
for buffer_class_name, BufferClass in test_tuples:
|
|
|
|
with self.subTest(buffer_class_name=buffer_class_name):
|
|
|
|
buffer = BufferClass(b'binary content')
|
|
|
|
buffer.seek(10)
|
|
|
|
response = FileResponse(buffer)
|
|
|
|
self.assertEqual(list(response), [b'tent'])
|
|
|
|
|
2021-10-14 17:41:10 +08:00
|
|
|
def test_buffer_explicit_absolute_filename(self):
|
|
|
|
"""
|
|
|
|
Headers are set correctly with a buffer when an absolute filename is
|
|
|
|
provided.
|
|
|
|
"""
|
|
|
|
response = FileResponse(io.BytesIO(b'binary content'), filename=__file__)
|
|
|
|
self.assertEqual(response.headers['Content-Length'], '14')
|
|
|
|
self.assertEqual(response.headers['Content-Disposition'], 'inline; filename="test_fileresponse.py"')
|
|
|
|
|
2018-05-16 00:12:11 +08:00
|
|
|
@skipIf(sys.platform == 'win32', "Named pipes are Unix-only.")
|
|
|
|
def test_file_from_named_pipe_response(self):
|
|
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
|
|
pipe_file = os.path.join(temp_dir, 'named_pipe')
|
|
|
|
os.mkfifo(pipe_file)
|
|
|
|
pipe_for_read = os.open(pipe_file, os.O_RDONLY | os.O_NONBLOCK)
|
|
|
|
with open(pipe_file, 'wb') as pipe_for_write:
|
|
|
|
pipe_for_write.write(b'binary content')
|
|
|
|
|
|
|
|
response = FileResponse(os.fdopen(pipe_for_read, mode='rb'))
|
2021-10-14 20:44:35 +08:00
|
|
|
response_content = list(response)
|
2018-05-16 00:12:11 +08:00
|
|
|
response.close()
|
2021-10-14 20:44:35 +08:00
|
|
|
self.assertEqual(response_content, [b'binary content'])
|
2018-09-01 21:14:18 +08:00
|
|
|
self.assertFalse(response.has_header('Content-Length'))
|
2018-05-16 00:12:11 +08:00
|
|
|
|
|
|
|
def test_compressed_response(self):
|
|
|
|
"""
|
|
|
|
If compressed responses are served with the uncompressed Content-Type
|
|
|
|
and a compression Content-Encoding, browsers might automatically
|
|
|
|
uncompress the file, which is most probably not wanted.
|
|
|
|
"""
|
|
|
|
test_tuples = (
|
|
|
|
('.tar.gz', 'application/gzip'),
|
|
|
|
('.tar.bz2', 'application/x-bzip'),
|
|
|
|
('.tar.xz', 'application/x-xz'),
|
|
|
|
)
|
|
|
|
for extension, mimetype in test_tuples:
|
|
|
|
with self.subTest(ext=extension):
|
|
|
|
with tempfile.NamedTemporaryFile(suffix=extension) as tmp:
|
|
|
|
response = FileResponse(tmp)
|
2020-07-14 19:32:24 +08:00
|
|
|
self.assertEqual(response.headers['Content-Type'], mimetype)
|
2018-05-16 00:12:11 +08:00
|
|
|
self.assertFalse(response.has_header('Content-Encoding'))
|
|
|
|
|
|
|
|
def test_unicode_attachment(self):
|
|
|
|
response = FileResponse(
|
|
|
|
ContentFile(b'binary content', name="祝您平安.odt"), as_attachment=True,
|
|
|
|
content_type='application/vnd.oasis.opendocument.text',
|
|
|
|
)
|
|
|
|
self.assertEqual(
|
2020-07-14 19:32:24 +08:00
|
|
|
response.headers['Content-Type'],
|
|
|
|
'application/vnd.oasis.opendocument.text',
|
|
|
|
)
|
|
|
|
self.assertEqual(
|
|
|
|
response.headers['Content-Disposition'],
|
2021-10-14 17:28:03 +08:00
|
|
|
"attachment; filename*=utf-8''%E7%A5%9D%E6%82%A8%E5%B9%B3%E5%AE%89.odt",
|
2018-05-16 00:12:11 +08:00
|
|
|
)
|
2021-06-17 19:04:04 +08:00
|
|
|
|
|
|
|
def test_repr(self):
|
|
|
|
response = FileResponse(io.BytesIO(b'binary content'))
|
|
|
|
self.assertEqual(
|
|
|
|
repr(response),
|
|
|
|
'<FileResponse status_code=200, "application/octet-stream">',
|
|
|
|
)
|