diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 39da3253..03cab7be 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -9,7 +9,9 @@ This project adheres to `Semantic Versioning `_.
`1.0.3-dev`_ (unreleased)
-------------------------
-* No changes yet.
+* Changed the way the output filename is generated for ``--download`` requests
+ without ``--output`` and with a redirect — now only the initial URL is
+ considered, not the final one.
`1.0.2`_ (2018-11-14)
diff --git a/README.rst b/README.rst
index a210d1dd..a18a385e 100644
--- a/README.rst
+++ b/README.rst
@@ -1316,10 +1316,22 @@ is being saved to a file.
Downloaded file name
--------------------
-If not provided via ``--output, -o``, the output filename will be determined
-from ``Content-Disposition`` (if available), or from the URL and
-``Content-Type``. If the guessed filename already exists, HTTPie adds a unique
-suffix to it.
+There are three mutually exclusive ways through which HTTPie determines
+the output file name (with decreasing priority):
+
+1. You can explicitly provide the exact output file name via ``--output, -o``.
+ The file gets overwritten if it already exists
+ (or appended to with ``--continue, -c``).
+2. The server may specify the file name in the optional ``Content-Disposition``
+ response header. Any leading dots are stripped from a server-provided filename.
+3. The last resort HTTPie uses is to generate the filename from a combination
+ of the request URL and the response ``Content-Type``.
+ The initial URL is always used as the basis for
+ the generated filename — even if there has been one or more redirects.
+
+
+To prevent data loss, HTTPie adds a unique numerical suffix to the
+filename, unless the name has been explicitly provided via ``--output, -o``.
Piping while downloading
diff --git a/httpie/downloads.py b/httpie/downloads.py
index 8c714c54..200dfb84 100644
--- a/httpie/downloads.py
+++ b/httpie/downloads.py
@@ -224,13 +224,13 @@ class Downloader(object):
request_headers['Range'] = 'bytes=%d-' % bytes_have
self._resumed_from = bytes_have
- def start(self, response):
+ def start(self, final_response):
"""
Initiate and return a stream for `response` body with progress
callback attached. Can be called only once.
- :param response: Initiated response object with headers already fetched
- :type response: requests.models.Response
+ :param final_response: Initiated response object with headers already fetched
+ :type final_response: requests.models.Response
:return: RawStream, output_file
@@ -240,14 +240,18 @@ class Downloader(object):
# FIXME: some servers still might sent Content-Encoding: gzip
#
try:
- total_size = int(response.headers['Content-Length'])
+ total_size = int(final_response.headers['Content-Length'])
except (KeyError, ValueError, TypeError):
total_size = None
- if self._output_file:
- if self._resume and response.status_code == PARTIAL_CONTENT:
+ if not self._output_file:
+ self._output_file = self._get_output_file_from_response(
+ final_response)
+ else:
+ # `--continue, -c` provided
+ if self._resume and final_response.status_code == PARTIAL_CONTENT:
total_size = parse_content_range(
- response.headers.get('Content-Range'),
+ final_response.headers.get('Content-Range'),
self._resumed_from
)
@@ -258,19 +262,6 @@ class Downloader(object):
self._output_file.truncate()
except IOError:
pass # stdout
- else:
- # TODO: Should the filename be taken from response.history[0].url?
- # Output file not specified. Pick a name that doesn't exist yet.
- filename = None
- if 'Content-Disposition' in response.headers:
- filename = filename_from_content_disposition(
- response.headers['Content-Disposition'])
- if not filename:
- filename = filename_from_url(
- url=response.url,
- content_type=response.headers.get('Content-Type'),
- )
- self._output_file = open(get_unique_filename(filename), mode='a+b')
self.status.started(
resumed_from=self._resumed_from,
@@ -278,7 +269,7 @@ class Downloader(object):
)
stream = RawStream(
- msg=HTTPResponse(response),
+ msg=HTTPResponse(final_response),
with_headers=False,
with_body=True,
on_body_chunk_downloaded=self.chunk_downloaded,
@@ -324,6 +315,25 @@ class Downloader(object):
"""
self.status.chunk_downloaded(len(chunk))
+ @staticmethod
+ def _get_output_file_from_response(final_response):
+ # Output file not specified. Pick a name that doesn't exist yet.
+ filename = None
+ if 'Content-Disposition' in final_response.headers:
+ filename = filename_from_content_disposition(
+ final_response.headers['Content-Disposition'])
+ if not filename:
+ initial_response = (
+ final_response.history[0] if final_response.history
+ else final_response
+ )
+ filename = filename_from_url(
+ url=initial_response.url,
+ content_type=final_response.headers.get('Content-Type'),
+ )
+ unique_filename = get_unique_filename(filename)
+ return open(unique_filename, mode='a+b')
+
class Status(object):
"""Holds details about the downland status."""
diff --git a/tests/test_downloads.py b/tests/test_downloads.py
index 5356721f..e14fcef3 100644
--- a/tests/test_downloads.py
+++ b/tests/test_downloads.py
@@ -1,4 +1,5 @@
import os
+import tempfile
import time
import pytest
@@ -22,6 +23,7 @@ class Response(object):
class TestDownloadUtils:
+
def test_Content_Range_parsing(self):
parse = parse_content_range
@@ -166,3 +168,15 @@ class TestDownloads:
downloader.finish()
assert downloader.interrupted
downloader._progress_reporter.join()
+
+ def test_download_with_redirect_original_url_used_for_filename(self, httpbin):
+ # Redirect from `/redirect/1` to `/get`.
+ expected_filename = '1.json'
+ orig_cwd = os.getcwd()
+ os.chdir(tempfile.mkdtemp(prefix='httpie_download_test_'))
+ try:
+ assert os.listdir('.') == []
+ http('--download', httpbin.url + '/redirect/1')
+ assert os.listdir('.') == [expected_filename]
+ finally:
+ os.chdir(orig_cwd)