1. import io
    
  2. import itertools
    
  3. import os
    
  4. import sys
    
  5. import tempfile
    
  6. from unittest import skipIf
    
  7. 
    
  8. from django.core.files.base import ContentFile
    
  9. from django.http import FileResponse
    
  10. from django.test import SimpleTestCase
    
  11. 
    
  12. 
    
  13. class UnseekableBytesIO(io.BytesIO):
    
  14.     def seekable(self):
    
  15.         return False
    
  16. 
    
  17. 
    
  18. class FileResponseTests(SimpleTestCase):
    
  19.     def test_content_length_file(self):
    
  20.         response = FileResponse(open(__file__, "rb"))
    
  21.         response.close()
    
  22.         self.assertEqual(
    
  23.             response.headers["Content-Length"], str(os.path.getsize(__file__))
    
  24.         )
    
  25. 
    
  26.     def test_content_length_buffer(self):
    
  27.         response = FileResponse(io.BytesIO(b"binary content"))
    
  28.         self.assertEqual(response.headers["Content-Length"], "14")
    
  29. 
    
  30.     def test_content_length_nonzero_starting_position_file(self):
    
  31.         file = open(__file__, "rb")
    
  32.         file.seek(10)
    
  33.         response = FileResponse(file)
    
  34.         response.close()
    
  35.         self.assertEqual(
    
  36.             response.headers["Content-Length"], str(os.path.getsize(__file__) - 10)
    
  37.         )
    
  38. 
    
  39.     def test_content_length_nonzero_starting_position_buffer(self):
    
  40.         test_tuples = (
    
  41.             ("BytesIO", io.BytesIO),
    
  42.             ("UnseekableBytesIO", UnseekableBytesIO),
    
  43.         )
    
  44.         for buffer_class_name, BufferClass in test_tuples:
    
  45.             with self.subTest(buffer_class_name=buffer_class_name):
    
  46.                 buffer = BufferClass(b"binary content")
    
  47.                 buffer.seek(10)
    
  48.                 response = FileResponse(buffer)
    
  49.                 self.assertEqual(response.headers["Content-Length"], "4")
    
  50. 
    
  51.     def test_content_length_nonzero_starting_position_file_seekable_no_tell(self):
    
  52.         class TestFile:
    
  53.             def __init__(self, path, *args, **kwargs):
    
  54.                 self._file = open(path, *args, **kwargs)
    
  55. 
    
  56.             def read(self, n_bytes=-1):
    
  57.                 return self._file.read(n_bytes)
    
  58. 
    
  59.             def seek(self, offset, whence=io.SEEK_SET):
    
  60.                 return self._file.seek(offset, whence)
    
  61. 
    
  62.             def seekable(self):
    
  63.                 return True
    
  64. 
    
  65.             @property
    
  66.             def name(self):
    
  67.                 return self._file.name
    
  68. 
    
  69.             def close(self):
    
  70.                 if self._file:
    
  71.                     self._file.close()
    
  72.                     self._file = None
    
  73. 
    
  74.             def __enter__(self):
    
  75.                 return self
    
  76. 
    
  77.             def __exit__(self, e_type, e_val, e_tb):
    
  78.                 self.close()
    
  79. 
    
  80.         file = TestFile(__file__, "rb")
    
  81.         file.seek(10)
    
  82.         response = FileResponse(file)
    
  83.         response.close()
    
  84.         self.assertEqual(
    
  85.             response.headers["Content-Length"], str(os.path.getsize(__file__) - 10)
    
  86.         )
    
  87. 
    
  88.     def test_content_type_file(self):
    
  89.         response = FileResponse(open(__file__, "rb"))
    
  90.         response.close()
    
  91.         self.assertIn(response.headers["Content-Type"], ["text/x-python", "text/plain"])
    
  92. 
    
  93.     def test_content_type_buffer(self):
    
  94.         response = FileResponse(io.BytesIO(b"binary content"))
    
  95.         self.assertEqual(response.headers["Content-Type"], "application/octet-stream")
    
  96. 
    
  97.     def test_content_type_buffer_explicit(self):
    
  98.         response = FileResponse(
    
  99.             io.BytesIO(b"binary content"), content_type="video/webm"
    
  100.         )
    
  101.         self.assertEqual(response.headers["Content-Type"], "video/webm")
    
  102. 
    
  103.     def test_content_type_buffer_explicit_default(self):
    
  104.         response = FileResponse(
    
  105.             io.BytesIO(b"binary content"), content_type="text/html; charset=utf-8"
    
  106.         )
    
  107.         self.assertEqual(response.headers["Content-Type"], "text/html; charset=utf-8")
    
  108. 
    
  109.     def test_content_type_buffer_named(self):
    
  110.         test_tuples = (
    
  111.             (__file__, ["text/x-python", "text/plain"]),
    
  112.             (__file__ + "nosuchfile", ["application/octet-stream"]),
    
  113.             ("test_fileresponse.py", ["text/x-python", "text/plain"]),
    
  114.             ("test_fileresponse.pynosuchfile", ["application/octet-stream"]),
    
  115.         )
    
  116.         for filename, content_types in test_tuples:
    
  117.             with self.subTest(filename=filename):
    
  118.                 buffer = io.BytesIO(b"binary content")
    
  119.                 buffer.name = filename
    
  120.                 response = FileResponse(buffer)
    
  121.                 self.assertIn(response.headers["Content-Type"], content_types)
    
  122. 
    
  123.     def test_content_disposition_file(self):
    
  124.         filenames = (
    
  125.             ("", "test_fileresponse.py"),
    
  126.             ("custom_name.py", "custom_name.py"),
    
  127.         )
    
  128.         dispositions = (
    
  129.             (False, "inline"),
    
  130.             (True, "attachment"),
    
  131.         )
    
  132.         for (filename, header_filename), (
    
  133.             as_attachment,
    
  134.             header_disposition,
    
  135.         ) in itertools.product(filenames, dispositions):
    
  136.             with self.subTest(filename=filename, disposition=header_disposition):
    
  137.                 response = FileResponse(
    
  138.                     open(__file__, "rb"), filename=filename, as_attachment=as_attachment
    
  139.                 )
    
  140.                 response.close()
    
  141.                 self.assertEqual(
    
  142.                     response.headers["Content-Disposition"],
    
  143.                     '%s; filename="%s"' % (header_disposition, header_filename),
    
  144.                 )
    
  145. 
    
  146.     def test_content_disposition_escaping(self):
    
  147.         # fmt: off
    
  148.         tests = [
    
  149.             (
    
  150.                 'multi-part-one";\" dummy".txt',
    
  151.                 r"multi-part-one\";\" dummy\".txt"
    
  152.             ),
    
  153.         ]
    
  154.         # fmt: on
    
  155.         # Non-escape sequence backslashes are path segments on Windows, and are
    
  156.         # eliminated by an os.path.basename() check in FileResponse.
    
  157.         if sys.platform != "win32":
    
  158.             # fmt: off
    
  159.             tests += [
    
  160.                 (
    
  161.                     'multi-part-one\\";\" dummy".txt',
    
  162.                     r"multi-part-one\\\";\" dummy\".txt"
    
  163.                 ),
    
  164.                 (
    
  165.                     'multi-part-one\\";\\\" dummy".txt',
    
  166.                     r"multi-part-one\\\";\\\" dummy\".txt"
    
  167.                 )
    
  168.             ]
    
  169.             # fmt: on
    
  170.         for filename, escaped in tests:
    
  171.             with self.subTest(filename=filename, escaped=escaped):
    
  172.                 response = FileResponse(
    
  173.                     io.BytesIO(b"binary content"), filename=filename, as_attachment=True
    
  174.                 )
    
  175.                 response.close()
    
  176.                 self.assertEqual(
    
  177.                     response.headers["Content-Disposition"],
    
  178.                     f'attachment; filename="{escaped}"',
    
  179.                 )
    
  180. 
    
  181.     def test_content_disposition_buffer(self):
    
  182.         response = FileResponse(io.BytesIO(b"binary content"))
    
  183.         self.assertFalse(response.has_header("Content-Disposition"))
    
  184. 
    
  185.     def test_content_disposition_buffer_attachment(self):
    
  186.         response = FileResponse(io.BytesIO(b"binary content"), as_attachment=True)
    
  187.         self.assertEqual(response.headers["Content-Disposition"], "attachment")
    
  188. 
    
  189.     def test_content_disposition_buffer_explicit_filename(self):
    
  190.         dispositions = (
    
  191.             (False, "inline"),
    
  192.             (True, "attachment"),
    
  193.         )
    
  194.         for as_attachment, header_disposition in dispositions:
    
  195.             response = FileResponse(
    
  196.                 io.BytesIO(b"binary content"),
    
  197.                 as_attachment=as_attachment,
    
  198.                 filename="custom_name.py",
    
  199.             )
    
  200.             self.assertEqual(
    
  201.                 response.headers["Content-Disposition"],
    
  202.                 '%s; filename="custom_name.py"' % header_disposition,
    
  203.             )
    
  204. 
    
  205.     def test_response_buffer(self):
    
  206.         response = FileResponse(io.BytesIO(b"binary content"))
    
  207.         self.assertEqual(list(response), [b"binary content"])
    
  208. 
    
  209.     def test_response_nonzero_starting_position(self):
    
  210.         test_tuples = (
    
  211.             ("BytesIO", io.BytesIO),
    
  212.             ("UnseekableBytesIO", UnseekableBytesIO),
    
  213.         )
    
  214.         for buffer_class_name, BufferClass in test_tuples:
    
  215.             with self.subTest(buffer_class_name=buffer_class_name):
    
  216.                 buffer = BufferClass(b"binary content")
    
  217.                 buffer.seek(10)
    
  218.                 response = FileResponse(buffer)
    
  219.                 self.assertEqual(list(response), [b"tent"])
    
  220. 
    
  221.     def test_buffer_explicit_absolute_filename(self):
    
  222.         """
    
  223.         Headers are set correctly with a buffer when an absolute filename is
    
  224.         provided.
    
  225.         """
    
  226.         response = FileResponse(io.BytesIO(b"binary content"), filename=__file__)
    
  227.         self.assertEqual(response.headers["Content-Length"], "14")
    
  228.         self.assertEqual(
    
  229.             response.headers["Content-Disposition"],
    
  230.             'inline; filename="test_fileresponse.py"',
    
  231.         )
    
  232. 
    
  233.     @skipIf(sys.platform == "win32", "Named pipes are Unix-only.")
    
  234.     def test_file_from_named_pipe_response(self):
    
  235.         with tempfile.TemporaryDirectory() as temp_dir:
    
  236.             pipe_file = os.path.join(temp_dir, "named_pipe")
    
  237.             os.mkfifo(pipe_file)
    
  238.             pipe_for_read = os.open(pipe_file, os.O_RDONLY | os.O_NONBLOCK)
    
  239.             with open(pipe_file, "wb") as pipe_for_write:
    
  240.                 pipe_for_write.write(b"binary content")
    
  241. 
    
  242.             response = FileResponse(os.fdopen(pipe_for_read, mode="rb"))
    
  243.             response_content = list(response)
    
  244.             response.close()
    
  245.             self.assertEqual(response_content, [b"binary content"])
    
  246.             self.assertFalse(response.has_header("Content-Length"))
    
  247. 
    
  248.     def test_compressed_response(self):
    
  249.         """
    
  250.         If compressed responses are served with the uncompressed Content-Type
    
  251.         and a compression Content-Encoding, browsers might automatically
    
  252.         uncompress the file, which is most probably not wanted.
    
  253.         """
    
  254.         test_tuples = (
    
  255.             (".tar.gz", "application/gzip"),
    
  256.             (".tar.bz2", "application/x-bzip"),
    
  257.             (".tar.xz", "application/x-xz"),
    
  258.         )
    
  259.         for extension, mimetype in test_tuples:
    
  260.             with self.subTest(ext=extension):
    
  261.                 with tempfile.NamedTemporaryFile(suffix=extension) as tmp:
    
  262.                     response = FileResponse(tmp)
    
  263.                 self.assertEqual(response.headers["Content-Type"], mimetype)
    
  264.                 self.assertFalse(response.has_header("Content-Encoding"))
    
  265. 
    
  266.     def test_unicode_attachment(self):
    
  267.         response = FileResponse(
    
  268.             ContentFile(b"binary content", name="祝您平安.odt"),
    
  269.             as_attachment=True,
    
  270.             content_type="application/vnd.oasis.opendocument.text",
    
  271.         )
    
  272.         self.assertEqual(
    
  273.             response.headers["Content-Type"],
    
  274.             "application/vnd.oasis.opendocument.text",
    
  275.         )
    
  276.         self.assertEqual(
    
  277.             response.headers["Content-Disposition"],
    
  278.             "attachment; filename*=utf-8''%E7%A5%9D%E6%82%A8%E5%B9%B3%E5%AE%89.odt",
    
  279.         )
    
  280. 
    
  281.     def test_repr(self):
    
  282.         response = FileResponse(io.BytesIO(b"binary content"))
    
  283.         self.assertEqual(
    
  284.             repr(response),
    
  285.             '<FileResponse status_code=200, "application/octet-stream">',
    
  286.         )