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), '', )