Repository: qutebrowser/qutebrowser

Issue:
Prevent downloading to existing file
We should ask before overwriting an existing file, and prevent double-downloading.



Discussion:

Resolving patch:
diff --git a/qutebrowser/browser/downloads.py b/qutebrowser/browser/downloads.py
--- a/qutebrowser/browser/downloads.py
+++ b/qutebrowser/browser/downloads.py
@@ -171,6 +171,7 @@ class DownloadItem(QObject):
         _read_timer: A QTimer which reads the QNetworkReply into self._buffer
                      periodically.
         _retry_info: A RetryInfo instance.
+        _win_id: The window ID the DownloadItem runs in.
 
     Signals:
         data_changed: The downloads metadata changed.
@@ -217,6 +218,7 @@ def __init__(self, reply, parent=None):
         self.fileobj = None
         self._filename = None
         self.init_reply(reply)
+        self._win_id = parent._win_id
 
     def __repr__(self):
         return utils.get_repr(self, basename=self.basename)
@@ -256,6 +258,27 @@ def __str__(self):
                         name=self.basename, speed=speed, remaining=remaining,
                         perc=perc, down=down, total=total, errmsg=errmsg))
 
+    def _create_fileobj(self):
+        """Creates a file object using the internal filename."""
+        try:
+            fileobj = open(self._filename, 'wb')
+        except OSError as e:
+            self._die(e.strerror)
+        else:
+            self.set_fileobj(fileobj)
+
+    def _ask_overwrite_question(self):
+        """Create a Question object to be asked."""
+        q = usertypes.Question(self)
+        q.text = self._filename + " already exists. Overwrite? (y/n)"
+        q.mode = usertypes.PromptMode.yesno
+        q.answered_yes.connect(self._create_fileobj)
+        q.answered_no.connect(functools.partial(self.cancel, False))
+        q.cancelled.connect(functools.partial(self.cancel, False))
+        message_bridge = objreg.get('message-bridge', scope='window',
+                                    window=self._win_id)
+        message_bridge.ask(q, blocking=False)
+
     def _die(self, msg):
         """Abort the download and emit an error."""
         assert not self.successful
@@ -312,8 +335,12 @@ def bg_color(self):
             return utils.interpolate_color(
                 start, stop, self.stats.percentage(), system)
 
-    def cancel(self):
-        """Cancel the download."""
+    def cancel(self, remove_data=True):
+        """Cancel the download.
+
+        Args:
+            remove_data: Whether to remove the downloaded data.
+        """
         log.downloads.debug("cancelled")
         self._read_timer.stop()
         self.cancelled.emit()
@@ -325,7 +352,8 @@ def cancel(self):
         if self.fileobj is not None:
             self.fileobj.close()
         try:
-            if self._filename is not None and os.path.exists(self._filename):
+            if (self._filename is not None and os.path.exists(self._filename)
+                    and remove_data):
                 os.remove(self._filename)
         except OSError:
             log.downloads.exception("Failed to remove partial file")
@@ -376,12 +404,12 @@ def set_filename(self, filename):
             self._filename = os.path.join(download_dir, filename)
             self.basename = filename
         log.downloads.debug("Setting filename to {}".format(filename))
-        try:
-            fileobj = open(self._filename, 'wb')
-        except OSError as e:
-            self._die(e.strerror)
+        if os.path.isfile(self._filename):
+            # The file already exists, so ask the user if it should be
+            # overwritten.
+            self._ask_overwrite_question()
         else:
-            self.set_fileobj(fileobj)
+            self._create_fileobj()
 
     def set_fileobj(self, fileobj):
         """"Set the file object to write the download to.


Test patch: