287 lines
11 KiB
Python
287 lines
11 KiB
Python
import io
|
|
import itertools
|
|
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
|
|
|
|
|
|
class UnseekableBytesIO(io.BytesIO):
|
|
def seekable(self):
|
|
return False
|
|
|
|
|
|
class FileResponseTests(SimpleTestCase):
|
|
def test_content_length_file(self):
|
|
response = FileResponse(open(__file__, "rb"))
|
|
response.close()
|
|
self.assertEqual(
|
|
response.headers["Content-Length"], str(os.path.getsize(__file__))
|
|
)
|
|
|
|
def test_content_length_buffer(self):
|
|
response = FileResponse(io.BytesIO(b"binary content"))
|
|
self.assertEqual(response.headers["Content-Length"], "14")
|
|
|
|
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)
|
|
)
|
|
|
|
def test_content_type_file(self):
|
|
response = FileResponse(open(__file__, "rb"))
|
|
response.close()
|
|
self.assertIn(response.headers["Content-Type"], ["text/x-python", "text/plain"])
|
|
|
|
def test_content_type_buffer(self):
|
|
response = FileResponse(io.BytesIO(b"binary content"))
|
|
self.assertEqual(response.headers["Content-Type"], "application/octet-stream")
|
|
|
|
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")
|
|
|
|
def test_content_type_buffer_explicit_default(self):
|
|
response = FileResponse(
|
|
io.BytesIO(b"binary content"), content_type="text/html; charset=utf-8"
|
|
)
|
|
self.assertEqual(response.headers["Content-Type"], "text/html; charset=utf-8")
|
|
|
|
def test_content_type_buffer_named(self):
|
|
test_tuples = (
|
|
(__file__, ["text/x-python", "text/plain"]),
|
|
(__file__ + "nosuchfile", ["application/octet-stream"]),
|
|
("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"),
|
|
)
|
|
for (filename, header_filename), (
|
|
as_attachment,
|
|
header_disposition,
|
|
) in itertools.product(filenames, dispositions):
|
|
with self.subTest(filename=filename, disposition=header_disposition):
|
|
response = FileResponse(
|
|
open(__file__, "rb"), filename=filename, as_attachment=as_attachment
|
|
)
|
|
response.close()
|
|
self.assertEqual(
|
|
response.headers["Content-Disposition"],
|
|
'%s; filename="%s"' % (header_disposition, header_filename),
|
|
)
|
|
|
|
def test_content_disposition_escaping(self):
|
|
# fmt: off
|
|
tests = [
|
|
(
|
|
'multi-part-one";\" dummy".txt',
|
|
r"multi-part-one\";\" dummy\".txt"
|
|
),
|
|
]
|
|
# fmt: on
|
|
# Non-escape sequence backslashes are path segments on Windows, and are
|
|
# eliminated by an os.path.basename() check in FileResponse.
|
|
if sys.platform != "win32":
|
|
# fmt: off
|
|
tests += [
|
|
(
|
|
'multi-part-one\\";\" dummy".txt',
|
|
r"multi-part-one\\\";\" dummy\".txt"
|
|
),
|
|
(
|
|
'multi-part-one\\";\\\" dummy".txt',
|
|
r"multi-part-one\\\";\\\" dummy\".txt"
|
|
)
|
|
]
|
|
# fmt: on
|
|
for filename, escaped in tests:
|
|
with self.subTest(filename=filename, escaped=escaped):
|
|
response = FileResponse(
|
|
io.BytesIO(b"binary content"), filename=filename, as_attachment=True
|
|
)
|
|
response.close()
|
|
self.assertEqual(
|
|
response.headers["Content-Disposition"],
|
|
f'attachment; filename="{escaped}"',
|
|
)
|
|
|
|
def test_content_disposition_buffer(self):
|
|
response = FileResponse(io.BytesIO(b"binary content"))
|
|
self.assertFalse(response.has_header("Content-Disposition"))
|
|
|
|
def test_content_disposition_buffer_attachment(self):
|
|
response = FileResponse(io.BytesIO(b"binary content"), as_attachment=True)
|
|
self.assertEqual(response.headers["Content-Disposition"], "attachment")
|
|
|
|
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(
|
|
response.headers["Content-Disposition"],
|
|
'%s; filename="custom_name.py"' % header_disposition,
|
|
)
|
|
|
|
def test_response_buffer(self):
|
|
response = FileResponse(io.BytesIO(b"binary content"))
|
|
self.assertEqual(list(response), [b"binary content"])
|
|
|
|
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"])
|
|
|
|
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"',
|
|
)
|
|
|
|
@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"))
|
|
response_content = list(response)
|
|
response.close()
|
|
self.assertEqual(response_content, [b"binary content"])
|
|
self.assertFalse(response.has_header("Content-Length"))
|
|
|
|
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)
|
|
self.assertEqual(response.headers["Content-Type"], mimetype)
|
|
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(
|
|
response.headers["Content-Type"],
|
|
"application/vnd.oasis.opendocument.text",
|
|
)
|
|
self.assertEqual(
|
|
response.headers["Content-Disposition"],
|
|
"attachment; filename*=utf-8''%E7%A5%9D%E6%82%A8%E5%B9%B3%E5%AE%89.odt",
|
|
)
|
|
|
|
def test_repr(self):
|
|
response = FileResponse(io.BytesIO(b"binary content"))
|
|
self.assertEqual(
|
|
repr(response),
|
|
'<FileResponse status_code=200, "application/octet-stream">',
|
|
)
|