From 07e95dba4f5983afc1e673df14bdd278817288aa Mon Sep 17 00:00:00 2001 From: Maks Skorokhod Date: Mon, 9 Oct 2017 17:45:22 +0300 Subject: [PATCH 1/3] :repeat: customize filename in file response --- sanic/response.py | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/sanic/response.py b/sanic/response.py index f661758b..c05be0fd 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -292,15 +292,21 @@ def html(body, status=200, headers=None): content_type="text/html; charset=utf-8") -async def file(location, mime_type=None, headers=None, _range=None): +async def file( + location, mime_type=None, headers=None, filename=None, _range=None): """Return a response object with file data. :param location: Location of file on system. :param mime_type: Specific mime_type. :param headers: Custom Headers. + :param filename: Override filename. :param _range: """ - filename = path.split(location)[-1] + headers = headers or {} + if filename: + headers.setdefault( + 'Content-Disposition', f'attachment; filename="{filename}"') + filename = filename or path.split(location)[-1] async with open_async(location, mode='rb') as _file: if _range: @@ -312,24 +318,29 @@ async def file(location, mime_type=None, headers=None, _range=None): out_stream = await _file.read() mime_type = mime_type or guess_type(filename)[0] or 'text/plain' - return HTTPResponse(status=200, headers=headers, content_type=mime_type, body_bytes=out_stream) -async def file_stream(location, chunk_size=4096, mime_type=None, headers=None, - _range=None): +async def file_stream( + location, chunk_size=4096, mime_type=None, headers=None, + filename=None, _range=None): """Return a streaming response object with file data. :param location: Location of file on system. :param chunk_size: The size of each chunk in the stream (in bytes) :param mime_type: Specific mime_type. :param headers: Custom Headers. + :param filename: Override filename. :param _range: """ - filename = path.split(location)[-1] + headers = headers or {} + if filename: + headers.setdefault( + 'Content-Disposition', f'attachment; filename="{filename}"') + filename = filename or path.split(location)[-1] _file = await open_async(location, mode='rb') From c4e3a98ea794892bbda14d07c92ab61c868fc7cb Mon Sep 17 00:00:00 2001 From: Maks Skorokhod Date: Mon, 9 Oct 2017 17:45:42 +0300 Subject: [PATCH 2/3] :white_check_mark: add test for custom filename --- tests/test_response.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/test_response.py b/tests/test_response.py index fb213b56..910c4e80 100644 --- a/tests/test_response.py +++ b/tests/test_response.py @@ -149,7 +149,22 @@ def test_file_response(file_name, static_file_directory): request, response = app.test_client.get('/files/{}'.format(file_name)) assert response.status == 200 assert response.body == get_file_content(static_file_directory, file_name) + assert 'Content-Disposition' not in response.headers +@pytest.mark.parametrize('source,dest', [ + ('test.file', 'my_file.txt'), ('decode me.txt', 'readme.md'), ('python.png', 'logo.png')]) +def test_file_response_custom_filename(source, dest, static_file_directory): + app = Sanic('test_file_helper') + @app.route('/files/', methods=['GET']) + def file_route(request, filename): + file_path = os.path.join(static_file_directory, filename) + file_path = os.path.abspath(unquote(file_path)) + return file(file_path, filename=dest) + + request, response = app.test_client.get('/files/{}'.format(source)) + assert response.status == 200 + assert response.body == get_file_content(static_file_directory, source) + assert response.headers['Content-Disposition'] == 'attachment; filename="{}"'.format(dest) @pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt']) def test_file_head_response(file_name, static_file_directory): @@ -191,7 +206,22 @@ def test_file_stream_response(file_name, static_file_directory): request, response = app.test_client.get('/files/{}'.format(file_name)) assert response.status == 200 assert response.body == get_file_content(static_file_directory, file_name) + assert 'Content-Disposition' not in response.headers +@pytest.mark.parametrize('source,dest', [ + ('test.file', 'my_file.txt'), ('decode me.txt', 'readme.md'), ('python.png', 'logo.png')]) +def test_file_stream_response_custom_filename(source, dest, static_file_directory): + app = Sanic('test_file_helper') + @app.route('/files/', methods=['GET']) + def file_route(request, filename): + file_path = os.path.join(static_file_directory, filename) + file_path = os.path.abspath(unquote(file_path)) + return file_stream(file_path, chunk_size=32, filename=dest) + + request, response = app.test_client.get('/files/{}'.format(source)) + assert response.status == 200 + assert response.body == get_file_content(static_file_directory, source) + assert response.headers['Content-Disposition'] == 'attachment; filename="{}"'.format(dest) @pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt']) def test_file_stream_head_response(file_name, static_file_directory): From 86f87cf4ace6cd41a192a53ca4b4c47851f30c78 Mon Sep 17 00:00:00 2001 From: Maks Skorokhod Date: Mon, 9 Oct 2017 17:55:35 +0300 Subject: [PATCH 3/3] :wrench: no use f'string' --- sanic/response.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/sanic/response.py b/sanic/response.py index c05be0fd..582e11cf 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -305,7 +305,8 @@ async def file( headers = headers or {} if filename: headers.setdefault( - 'Content-Disposition', f'attachment; filename="{filename}"') + 'Content-Disposition', + 'attachment; filename="{}"'.format(filename)) filename = filename or path.split(location)[-1] async with open_async(location, mode='rb') as _file: @@ -339,7 +340,8 @@ async def file_stream( headers = headers or {} if filename: headers.setdefault( - 'Content-Disposition', f'attachment; filename="{filename}"') + 'Content-Disposition', + 'attachment; filename="{}"'.format(filename)) filename = filename or path.split(location)[-1] _file = await open_async(location, mode='rb')