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 shutil
import functools
import tempfile
import collections
import sip
@ -513,7 +514,13 @@ class DownloadItem(QObject):
def open_file(self):
"""Open the downloaded file."""
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)
def set_filename(self, filename):
@ -731,6 +738,20 @@ class DownloadManager(QAbstractListModel):
self._update_timer = usertypes.Timer(self, 'download-update')
self._update_timer.timeout.connect(self.update_gui)
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):
return utils.get_repr(self, downloads=len(self.downloads))
@ -738,6 +759,9 @@ class DownloadManager(QAbstractListModel):
def _postprocess_question(self, q):
"""Postprocess a Question object that is asked."""
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)
@pyqtSlot()
@ -816,27 +840,11 @@ class DownloadManager(QAbstractListModel):
if suggested_fn is None:
suggested_fn = 'qutebrowser-download'
# We won't need a question if a filename or fileobj is already given
if fileobj is None and filename is None:
filename, q = ask_for_filename(
suggested_fn, self._win_id, parent=self,
prompt_download_directory=prompt_download_directory
)
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):
"""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
raise TypeError("Only one of fileobj/filename may be given!")
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)
elif fileobj is not None and getattr(fileobj, 'name', None):
suggested_filename = fileobj.name
@ -915,7 +924,8 @@ class DownloadManager(QAbstractListModel):
return download
if filename is not None:
download.set_filename(filename)
self.set_filename_for_download(download, suggested_filename,
filename)
return download
# 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
if filename is not None:
download.set_filename(filename)
self.set_filename_for_download(download, suggested_filename,
filename)
return download
# Ask the user for a filename
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)
download.cancelled.connect(q.abort)
download.error.connect(q.abort)
@ -939,6 +952,35 @@ class DownloadManager(QAbstractListModel):
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):
"""Raise an exception that the download doesn't exist.

View File

@ -452,6 +452,7 @@ class MainWindow(QWidget):
self._save_geometry()
log.destroy.debug("Closing window {}".format(self.win_id))
self.tabbed_browser.shutdown()
self._get_object('download-manager').cleanup()
def closeEvent(self, e):
"""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.user_pwd: usertypes.KeyMode.prompt,
usertypes.PromptMode.alert: usertypes.KeyMode.prompt,
usertypes.PromptMode.download: usertypes.KeyMode.prompt,
}
show_prompt = pyqtSignal()
@ -164,12 +165,9 @@ class Prompter(QObject):
suffix = " (no)"
prompt.txt.setText(self._question.text + suffix)
prompt.lineedit.hide()
elif self._question.mode == usertypes.PromptMode.text:
prompt.txt.setText(self._question.text)
if self._question.default:
prompt.lineedit.setText(self._question.default)
prompt.lineedit.show()
elif self._question.mode == usertypes.PromptMode.user_pwd:
elif self._question.mode in {usertypes.PromptMode.text,
usertypes.PromptMode.user_pwd,
usertypes.PromptMode.download}:
prompt.txt.setText(self._question.text)
if 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,
'prompt accept')
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:
# User wants to accept the default of a yes/no question.
self._question.answer = self._question.default
@ -287,6 +291,18 @@ class Prompter(QObject):
'prompt accept')
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)
def ask_question(self, question, blocking):
"""Display a question in the statusbar.

View File

@ -33,6 +33,7 @@ from qutebrowser.utils import log, qtutils, utils
_UNSET = object()
OPEN_DOWNLOAD = object()
def enum(name, items, start=1, is_int=False):
@ -221,7 +222,8 @@ class NeighborList(collections.abc.Sequence):
# 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.