diff --git a/qutebrowser/browser/downloads.py b/qutebrowser/browser/downloads.py
index 4ab271423..5a2daae7e 100644
--- a/qutebrowser/browser/downloads.py
+++ b/qutebrowser/browser/downloads.py
@@ -137,7 +137,8 @@ def create_full_filename(basename, filename):
encoding = sys.getfilesystemencoding()
filename = utils.force_encoding(filename, encoding)
basename = utils.force_encoding(basename, encoding)
- if os.path.isabs(filename) and os.path.isdir(filename):
+ if os.path.isabs(filename) and (os.path.isdir(filename) or
+ filename.endswith(os.sep)):
# We got an absolute directory from the user, so we save it under
# the default filename in that directory.
return os.path.join(filename, basename)
@@ -604,6 +605,11 @@ class AbstractDownloadItem(QObject):
"""Ask a confirmation question for the download."""
raise NotImplementedError
+ def _ask_create_parent_question(self, title, msg,
+ force_overwrite, remember_directory):
+ """Ask a confirmation question for the parent directory."""
+ raise NotImplementedError
+
def _set_fileobj(self, fileobj, *, autoclose=True):
"""Set a file object to save the download to.
@@ -630,7 +636,6 @@ class AbstractDownloadItem(QObject):
remember_directory: If True, remember the directory for future
downloads.
"""
- global last_used_directory
filename = os.path.expanduser(filename)
self._ensure_can_set_filename(filename)
@@ -657,11 +662,41 @@ class AbstractDownloadItem(QObject):
self._filename = create_full_filename(self.basename,
os.path.expanduser('~'))
+ dirname = os.path.dirname(self._filename)
+ if not os.path.exists(dirname):
+ txt = ("{} does not exist. Create it?".
+ format(html.escape(
+ os.path.join(dirname, ""))))
+ self._ask_create_parent_question("Create directory?", txt,
+ force_overwrite,
+ remember_directory)
+ else:
+ self._after_create_parent_question(force_overwrite,
+ remember_directory)
+
+ def _after_create_parent_question(self,
+ force_overwrite, remember_directory):
+ """After asking about parent directory.
+
+ Args:
+ force_overwrite: Force overwriting existing files.
+ remember_directory: If True, remember the directory for future
+ downloads.
+ """
+ global last_used_directory
+
+ try:
+ os.makedirs(os.path.dirname(self._filename))
+ except FileExistsError:
+ pass
+ except OSError as e:
+ self._die(e.strerror)
+
self.basename = os.path.basename(self._filename)
if remember_directory:
last_used_directory = os.path.dirname(self._filename)
- log.downloads.debug("Setting filename to {}".format(filename))
+ log.downloads.debug("Setting filename to {}".format(self._filename))
if force_overwrite:
self._after_set_filename()
elif os.path.isfile(self._filename):
diff --git a/qutebrowser/browser/qtnetworkdownloads.py b/qutebrowser/browser/qtnetworkdownloads.py
index 921fee54d..709c8207b 100644
--- a/qutebrowser/browser/qtnetworkdownloads.py
+++ b/qutebrowser/browser/qtnetworkdownloads.py
@@ -203,6 +203,17 @@ class DownloadItem(downloads.AbstractDownloadItem):
no_action=no_action, cancel_action=no_action,
abort_on=[self.cancelled, self.error])
+ def _ask_create_parent_question(self, title, msg,
+ force_overwrite, remember_directory):
+ no_action = functools.partial(self.cancel, remove_data=False)
+ message.confirm_async(title=title, text=msg,
+ yes_action=(lambda:
+ self._after_create_parent_question(
+ force_overwrite,
+ remember_directory)),
+ no_action=no_action, cancel_action=no_action,
+ abort_on=[self.cancelled, self.error])
+
def _set_fileobj(self, fileobj, *, autoclose=True):
""""Set the file object to write the download to.
diff --git a/qutebrowser/browser/webengine/webenginedownloads.py b/qutebrowser/browser/webengine/webenginedownloads.py
index ebb03828b..d467724d5 100644
--- a/qutebrowser/browser/webengine/webenginedownloads.py
+++ b/qutebrowser/browser/webengine/webenginedownloads.py
@@ -133,6 +133,22 @@ class DownloadItem(downloads.AbstractDownloadItem):
self.error.connect(question.abort)
message.global_bridge.ask(question, blocking=True)
+ def _ask_create_parent_question(self, title, msg,
+ force_overwrite, remember_directory):
+ no_action = functools.partial(self.cancel, remove_data=False)
+ question = usertypes.Question()
+ question.title = title
+ question.text = msg
+ question.mode = usertypes.PromptMode.yesno
+ question.answered_yes.connect(lambda:
+ self._after_create_parent_question(
+ force_overwrite, remember_directory))
+ question.answered_no.connect(no_action)
+ question.cancelled.connect(no_action)
+ self.cancelled.connect(question.abort)
+ self.error.connect(question.abort)
+ message.global_bridge.ask(question, blocking=True)
+
def _after_set_filename(self):
self._qt_item.setPath(self._filename)
self._qt_item.accept()
diff --git a/tests/end2end/features/downloads.feature b/tests/end2end/features/downloads.feature
index 103ad0123..69f47603b 100644
--- a/tests/end2end/features/downloads.feature
+++ b/tests/end2end/features/downloads.feature
@@ -255,9 +255,19 @@ Feature: Downloading things from a website.
When I run :download --mhtml http://foobar/
Then the error "Can only download the current page as mhtml." should be shown
+ Scenario: :download with a filename and directory which doesn't exist
+ When I run :download --dest (tmpdir)(dirsep)downloads(dirsep)somedir(dirsep)file http://localhost:(port)/data/downloads/download.bin
+ And I wait for "Asking question text='* does not exist. Create it?' title='Create directory?'>, *" in the log
+ And I run :prompt-accept yes
+ And I wait until the download is finished
+ Then the downloaded file somedir/file should exist
+
Scenario: :download with a directory which doesn't exist
- When I run :download --dest (tmpdir)/downloads/somedir/filename http://localhost:(port)/
- Then the error "Download error: No such file or directory" should be shown
+ When I run :download --dest (tmpdir)(dirsep)downloads(dirsep)somedir(dirsep) http://localhost:(port)/data/downloads/download.bin
+ And I wait for "Asking question text='* does not exist. Create it?' title='Create directory?'>, *" in the log
+ And I run :prompt-accept yes
+ And I wait until the download is finished
+ Then the downloaded file somedir/download.bin should exist
## mhtml downloads