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