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:
parent
64b32ec87d
commit
c060f9e5c2
@ -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
|
|
||||||
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,
|
return self.fetch_request(request,
|
||||||
fileobj=fileobj,
|
fileobj=fileobj,
|
||||||
filename=filename,
|
filename=filename,
|
||||||
suggested_filename=suggested_fn,
|
suggested_filename=suggested_fn,
|
||||||
**kwargs)
|
**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.
|
||||||
|
|
||||||
|
@ -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."""
|
||||||
|
@ -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.
|
||||||
|
@ -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.
|
||||||
|
Loading…
Reference in New Issue
Block a user