diff options
author | David Lord <davidism@gmail.com> | 2020-07-12 19:39:59 -0700 |
---|---|---|
committer | David Lord <davidism@gmail.com> | 2020-07-12 19:39:59 -0700 |
commit | b01ff62f2867957ec0df983aec6609061e0551dd (patch) | |
tree | 15b9f9db14453c586314a7d1b2354676188f183c | |
parent | 6c11af5ab0b06eb2fa57b37d3d639c6259601c17 (diff) | |
download | werkzeug-send_file-download_name.tar.gz |
replace attachment_filename with download_namesend_file-download_name
Always send download_name, using Content-Disposition: inline if
as_attachment=False.
-rw-r--r-- | CHANGES.rst | 6 | ||||
-rw-r--r-- | src/werkzeug/utils.py | 56 | ||||
-rw-r--r-- | tests/test_send_file.py | 26 |
3 files changed, 55 insertions, 33 deletions
diff --git a/CHANGES.rst b/CHANGES.rst index 82590509..57e384fd 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -41,8 +41,12 @@ Unreleased ``RAW_URI``. :issue:`1781` - Switch the parameter order of ``default_stream_factory`` to match the order used when calling it. :pr:`1085` -- Add ``send_file()`` function to generate a response that serves a +- Add ``send_file`` function to generate a response that serves a file, adapted from Flask's implementation. :issue:`265`, :pr:`1850` +- ``send_file`` takes ``download_name``, which is passed even if + ``as_attachment=False`` by using ``Content-Disposition: inline``. + ``download_name`` replaces Flask's ``attachment_filename``. + :issue:`1869` Version 1.0.2 diff --git a/src/werkzeug/utils.py b/src/werkzeug/utils.py index 1cacdc89..5b5721a8 100644 --- a/src/werkzeug/utils.py +++ b/src/werkzeug/utils.py @@ -557,7 +557,7 @@ def send_file( environ=None, mimetype=None, as_attachment=False, - attachment_filename=None, + download_name=None, add_etags=True, cache_timeout=43200, conditional=False, @@ -592,8 +592,8 @@ def send_file( provided, it will try to detect it from the file name. :param as_attachment: Indicate to a browser that it should offer to save the file instead of displaying it. - :param attachment_filename: The file name browsers will use when - saving the file. Defaults to the passed file name. + :param download_name: The default name browsers will use when saving + the file. Defaults to the passed file name. :param add_etags: Calculate an ETag for the file. Requires passing a file path. :param conditional: Enable conditional and range responses based on @@ -611,6 +611,11 @@ def send_file( .. versionadded:: 2.0.0 Adapted from Flask's implementation. + + .. versionchanged:: 2.0.0 + ``download_name`` replaces Flask's ``attachment_filename`` + parameter. If ``as_attachment=False``, it is passed with + ``Content-Disposition: inline`` instead. """ if response_class is None: from .wrappers import Response as response_class @@ -627,44 +632,39 @@ def send_file( path = size = mtime = None file = path_or_file - if attachment_filename is None and path is not None: - attachment_filename = path.name + if download_name is None and path is not None: + download_name = path.name if mimetype is None: - if attachment_filename is not None: - mimetype = ( - mimetypes.guess_type(attachment_filename)[0] - or "application/octet-stream" - ) - - if mimetype is None: + if download_name is None: raise ValueError( "Unable to detect the MIME type because a file name is" - " not available. Either set 'attachment_filename', pass" - " a path instead of a file, or set 'mimetype'." + " not available. Either set 'download_name', pass a" + " path instead of a file, or set 'mimetype'." ) - headers = Headers() + mimetype = mimetypes.guess_type(download_name)[0] or "application/octet-stream" - if as_attachment: - if attachment_filename is None: - raise TypeError( - "No name provided for attachment. Either set" - " 'attachment_filename' or pass a path instead of a" - " file." - ) + headers = Headers() + if download_name is not None: try: - attachment_filename = attachment_filename.encode("ascii") + download_name = download_name.encode("ascii") except UnicodeEncodeError: - simple = unicodedata.normalize("NFKD", attachment_filename) + simple = unicodedata.normalize("NFKD", download_name) simple = simple.encode("ascii", "ignore") - quoted = url_quote(attachment_filename, safe="") - filenames = {"filename": simple, "filename*": f"UTF-8''{quoted}"} + quoted = url_quote(download_name, safe="") + names = {"filename": simple, "filename*": f"UTF-8''{quoted}"} else: - filenames = {"filename": attachment_filename} + names = {"filename": download_name} - headers.add("Content-Disposition", "attachment", **filenames) + value = "attachment" if as_attachment else "inline" + headers.set("Content-Disposition", value, **names) + elif as_attachment: + raise TypeError( + "No name provided for attachment. Either set" + " 'download_name' or pass a path instead of a file." + ) if use_x_sendfile and path: headers["X-Sendfile"] = str(path) diff --git a/tests/test_send_file.py b/tests/test_send_file.py index 8a44f739..d51b034e 100644 --- a/tests/test_send_file.py +++ b/tests/test_send_file.py @@ -55,8 +55,8 @@ def test_object_without_mimetype(): send_file(io.BytesIO(b"test")) -def test_object_mimetype_from_attachment(): - rv = send_file(io.BytesIO(b"test"), attachment_filename="test.txt") +def test_object_mimetype_from_name(): + rv = send_file(io.BytesIO(b"test"), download_name="test.txt") assert rv.mimetype == "text/plain" rv.close() @@ -70,6 +70,24 @@ def test_text_mode_fails(file_factory): @pytest.mark.parametrize( + ("as_attachment", "value"), [(False, "inline"), (True, "attachment")] +) +def test_disposition_name(as_attachment, value): + rv = send_file(txt_path, as_attachment=as_attachment) + assert rv.headers["Content-Disposition"] == f"{value}; filename=test.txt" + rv.close() + + +def test_object_attachment_requires_name(): + with pytest.raises(TypeError, match="attachment"): + send_file(io.BytesIO(b"test"), mimetype="text/plain", as_attachment=True) + + rv = send_file(io.BytesIO(b"test"), as_attachment=True, download_name="test.txt") + assert rv.headers["Content-Disposition"] == f"attachment; filename=test.txt" + rv.close() + + +@pytest.mark.parametrize( ("name", "ascii", "utf8"), ( ("index.html", "index.html", None), @@ -84,8 +102,8 @@ def test_text_mode_fails(file_factory): ("ัะต:/ัั", '":/"', "%D1%82%D0%B5%3A%2F%D1%81%D1%82"), ), ) -def test_non_ascii_filename(name, ascii, utf8): - rv = send_file(html_path, as_attachment=True, attachment_filename=name) +def test_non_ascii_name(name, ascii, utf8): + rv = send_file(html_path, as_attachment=True, download_name=name) rv.close() content_disposition = rv.headers["Content-Disposition"] assert f"filename={ascii}" in content_disposition |