allow downloads to be openened directly

The file is saved in a temporary directory, which is cleaned up when the
window is closed.
This commit is contained in:
Daniel Schadt 2016-07-05 23:01:48 +02:00
parent 64b32ec87d
commit c060f9e5c2
4 changed files with 94 additions and 33 deletions

View File

@ -25,6 +25,7 @@ import sys
import os.path import os.path
import shutil import shutil
import functools import functools
import tempfile
import collections import collections
import sip import sip
@ -513,7 +514,13 @@ class DownloadItem(QObject):
def open_file(self): def open_file(self):
"""Open the downloaded file.""" """Open the downloaded file."""
assert self.successful assert self.successful
url = QUrl.fromLocalFile(self._filename) filename = self._filename
if filename is None:
filename = getattr(self.fileobj, 'name', None)
if filename is None:
log.downloads.error("No filename to open the download!")
return
url = QUrl.fromLocalFile(filename)
QDesktopServices.openUrl(url) QDesktopServices.openUrl(url)
def set_filename(self, filename): def set_filename(self, filename):
@ -731,6 +738,20 @@ class DownloadManager(QAbstractListModel):
self._update_timer = usertypes.Timer(self, 'download-update') self._update_timer = usertypes.Timer(self, 'download-update')
self._update_timer.timeout.connect(self.update_gui) self._update_timer.timeout.connect(self.update_gui)
self._update_timer.setInterval(REFRESH_INTERVAL) self._update_timer.setInterval(REFRESH_INTERVAL)
self._tmpdir_obj = None
def cleanup(self):
"""Clean up any temporary files from this manager."""
if self._tmpdir_obj is not None:
self._tmpdir_obj.cleanup()
@property
def tmpdir(self):
"""Lazily create a temporary directory if one is needed."""
if self._tmpdir_obj is None:
self._tmpdir_obj = tempfile.TemporaryDirectory(
prefix='qutebrowser-downloads-')
return self._tmpdir_obj
def __repr__(self): def __repr__(self):
return utils.get_repr(self, downloads=len(self.downloads)) return utils.get_repr(self, downloads=len(self.downloads))
@ -738,6 +759,9 @@ class DownloadManager(QAbstractListModel):
def _postprocess_question(self, q): def _postprocess_question(self, q):
"""Postprocess a Question object that is asked.""" """Postprocess a Question object that is asked."""
q.destroyed.connect(functools.partial(self.questions.remove, q)) q.destroyed.connect(functools.partial(self.questions.remove, q))
# We set the mode here so that other code that uses ask_for_filename
# doesn't need to handle the special download mode.
q.mode = usertypes.PromptMode.download
self.questions.append(q) self.questions.append(q)
@pyqtSlot() @pyqtSlot()
@ -816,27 +840,11 @@ class DownloadManager(QAbstractListModel):
if suggested_fn is None: if suggested_fn is None:
suggested_fn = 'qutebrowser-download' suggested_fn = 'qutebrowser-download'
# We won't need a question if a filename or fileobj is already given return self.fetch_request(request,
if fileobj is None and filename is None: fileobj=fileobj,
filename, q = ask_for_filename( filename=filename,
suggested_fn, self._win_id, parent=self, suggested_filename=suggested_fn,
prompt_download_directory=prompt_download_directory **kwargs)
)
if fileobj is not None or filename is not None:
return self.fetch_request(request,
fileobj=fileobj,
filename=filename,
suggested_filename=suggested_fn,
**kwargs)
q.answered.connect(
lambda fn: self.fetch_request(request,
filename=fn,
suggested_filename=suggested_fn,
**kwargs))
self._postprocess_question(q)
q.ask()
return None
def fetch_request(self, request, *, page=None, **kwargs): def fetch_request(self, request, *, page=None, **kwargs):
"""Download a QNetworkRequest to disk. """Download a QNetworkRequest to disk.
@ -874,7 +882,8 @@ class DownloadManager(QAbstractListModel):
if fileobj is not None and filename is not None: # pragma: no cover if fileobj is not None and filename is not None: # pragma: no cover
raise TypeError("Only one of fileobj/filename may be given!") raise TypeError("Only one of fileobj/filename may be given!")
if not suggested_filename: if not suggested_filename:
if filename is not None: if (filename is not None and
filename is not usertypes.OPEN_DOWNLOAD):
suggested_filename = os.path.basename(filename) suggested_filename = os.path.basename(filename)
elif fileobj is not None and getattr(fileobj, 'name', None): elif fileobj is not None and getattr(fileobj, 'name', None):
suggested_filename = fileobj.name suggested_filename = fileobj.name
@ -915,7 +924,8 @@ class DownloadManager(QAbstractListModel):
return download return download
if filename is not None: if filename is not None:
download.set_filename(filename) self.set_filename_for_download(download, suggested_filename,
filename)
return download return download
# Neither filename nor fileobj were given, prepare a question # Neither filename nor fileobj were given, prepare a question
@ -926,12 +936,15 @@ class DownloadManager(QAbstractListModel):
# User doesn't want to be asked, so just use the download_dir # User doesn't want to be asked, so just use the download_dir
if filename is not None: if filename is not None:
download.set_filename(filename) self.set_filename_for_download(download, suggested_filename,
filename)
return download return download
# Ask the user for a filename # Ask the user for a filename
self._postprocess_question(q) self._postprocess_question(q)
q.answered.connect(download.set_filename) q.answered.connect(
functools.partial(self.set_filename_for_download, download,
suggested_filename))
q.cancelled.connect(download.cancel) q.cancelled.connect(download.cancel)
download.cancelled.connect(q.abort) download.cancelled.connect(q.abort)
download.error.connect(q.abort) download.error.connect(q.abort)
@ -939,6 +952,35 @@ class DownloadManager(QAbstractListModel):
return download return download
def set_filename_for_download(self, download, suggested_filename,
filename):
"""Set the filename for a given download.
This correctly handles the case where filename = OPEN_DOWNLOAD.
Args:
download: The download to set the filename for.
suggested_filename: The suggested filename.
filename: The filename as string or usertypes.OPEN_DOWNLOAD.
"""
if filename is not usertypes.OPEN_DOWNLOAD:
download.set_filename(filename)
return
# Find the next free filename without causing a race condition
index = 0
while True:
basename = '{}-{}'.format(index, suggested_filename)
path = os.path.join(self.tmpdir.name, basename)
try:
fobj = open(path, 'xb')
except FileExistsError:
index += 1
else:
break
download.finished.connect(download.open_file)
download.autoclose = True
download.set_fileobj(fobj)
def raise_no_download(self, count): def raise_no_download(self, count):
"""Raise an exception that the download doesn't exist. """Raise an exception that the download doesn't exist.

View File

@ -452,6 +452,7 @@ class MainWindow(QWidget):
self._save_geometry() self._save_geometry()
log.destroy.debug("Closing window {}".format(self.win_id)) log.destroy.debug("Closing window {}".format(self.win_id))
self.tabbed_browser.shutdown() self.tabbed_browser.shutdown()
self._get_object('download-manager').cleanup()
def closeEvent(self, e): def closeEvent(self, e):
"""Override closeEvent to display a confirmation if needed.""" """Override closeEvent to display a confirmation if needed."""

View File

@ -80,6 +80,7 @@ class Prompter(QObject):
usertypes.PromptMode.text: usertypes.KeyMode.prompt, usertypes.PromptMode.text: usertypes.KeyMode.prompt,
usertypes.PromptMode.user_pwd: usertypes.KeyMode.prompt, usertypes.PromptMode.user_pwd: usertypes.KeyMode.prompt,
usertypes.PromptMode.alert: usertypes.KeyMode.prompt, usertypes.PromptMode.alert: usertypes.KeyMode.prompt,
usertypes.PromptMode.download: usertypes.KeyMode.prompt,
} }
show_prompt = pyqtSignal() show_prompt = pyqtSignal()
@ -164,12 +165,9 @@ class Prompter(QObject):
suffix = " (no)" suffix = " (no)"
prompt.txt.setText(self._question.text + suffix) prompt.txt.setText(self._question.text + suffix)
prompt.lineedit.hide() prompt.lineedit.hide()
elif self._question.mode == usertypes.PromptMode.text: elif self._question.mode in {usertypes.PromptMode.text,
prompt.txt.setText(self._question.text) usertypes.PromptMode.user_pwd,
if self._question.default: usertypes.PromptMode.download}:
prompt.lineedit.setText(self._question.default)
prompt.lineedit.show()
elif self._question.mode == usertypes.PromptMode.user_pwd:
prompt.txt.setText(self._question.text) prompt.txt.setText(self._question.text)
if self._question.default: if self._question.default:
prompt.lineedit.setText(self._question.default) prompt.lineedit.setText(self._question.default)
@ -248,6 +246,12 @@ class Prompter(QObject):
modeman.maybe_leave(self._win_id, usertypes.KeyMode.prompt, modeman.maybe_leave(self._win_id, usertypes.KeyMode.prompt,
'prompt accept') 'prompt accept')
self._question.done() self._question.done()
elif self._question.mode == usertypes.PromptMode.download:
# User just entered a path for a download.
self._question.answer = prompt.lineedit.text()
modeman.maybe_leave(self._win_id, usertypes.KeyMode.prompt,
'prompt accept')
self._question.done()
elif self._question.mode == usertypes.PromptMode.yesno: elif self._question.mode == usertypes.PromptMode.yesno:
# User wants to accept the default of a yes/no question. # User wants to accept the default of a yes/no question.
self._question.answer = self._question.default self._question.answer = self._question.default
@ -287,6 +291,18 @@ class Prompter(QObject):
'prompt accept') 'prompt accept')
self._question.done() self._question.done()
@cmdutils.register(instance='prompter', hide=True, scope='window',
modes=[usertypes.KeyMode.prompt])
def prompt_open_download(self):
"""Immediately open a download."""
if self._question.mode != usertypes.PromptMode.download:
# We just ignore this if we don't have a download question.
return
self._question.answer = usertypes.OPEN_DOWNLOAD
modeman.maybe_leave(self._win_id, usertypes.KeyMode.prompt,
'download open')
self._question.done()
@pyqtSlot(usertypes.Question, bool) @pyqtSlot(usertypes.Question, bool)
def ask_question(self, question, blocking): def ask_question(self, question, blocking):
"""Display a question in the statusbar. """Display a question in the statusbar.

View File

@ -33,6 +33,7 @@ from qutebrowser.utils import log, qtutils, utils
_UNSET = object() _UNSET = object()
OPEN_DOWNLOAD = object()
def enum(name, items, start=1, is_int=False): def enum(name, items, start=1, is_int=False):
@ -221,7 +222,8 @@ class NeighborList(collections.abc.Sequence):
# The mode of a Question. # The mode of a Question.
PromptMode = enum('PromptMode', ['yesno', 'text', 'user_pwd', 'alert']) PromptMode = enum('PromptMode', ['yesno', 'text', 'user_pwd', 'alert',
'download'])
# Where to open a clicked link. # Where to open a clicked link.