Repository: qutebrowser/qutebrowser

Issue:
History backup fails when history.bak already exists
When there's already a `history.bak` around, this happens:

```
08:30:34 INFO     message    message:info:86 History import complete. Moving C:\Users\Florian\AppData\Local\qutebrowser\data\history to C:\Users\Florian\AppData\Local\qutebrowser\data\history.bak
08:30:34 DEBUG    misc       mainwindow:_update_overlay_geometry:285 new geometry for <qutebrowser.mainwindow.messageview.MessageView object at 0x000002666B1B2798>: PyQt5.QtCore.QRect(0, 20, 643, -1)
08:30:34 DEBUG    init       debug:__exit__:264 Import old history file to sqlite took 0.039671 seconds.
08:30:34 ERROR    misc       crashsignal:exception_hook:210 Uncaught exception
Traceback (most recent call last):
  File "C:\Users\Florian\qutebrowser\qutebrowser\browser\history.py", line 253, in action
    os.rename(path, bakpath)
FileExistsError: [WinError 183] Cannot create a file when that file already exists: 'C:\\Users\\Florian\\AppData\\Local\\qutebrowser\\data\\history' -> 'C:\\Users\\Florian\\AppData\\Local\\qutebrowser\\data\\history.bak'
```

cc @rcorre 


Discussion:

Resolving patch:
diff --git a/qutebrowser/browser/history.py b/qutebrowser/browser/history.py
--- a/qutebrowser/browser/history.py
+++ b/qutebrowser/browser/history.py
@@ -252,10 +252,7 @@ def action():
                 except ValueError as ex:
                     message.error('Failed to import history: {}'.format(ex))
                 else:
-                    bakpath = path + '.bak'
-                    message.info('History import complete. Moving {} to {}'
-                                 .format(path, bakpath))
-                    os.rename(path, bakpath)
+                    self._write_backup(path)
 
         # delay to give message time to appear before locking down for import
         message.info('Converting {} to sqlite...'.format(path))
@@ -287,6 +284,16 @@ def _read(self, path):
         self.insert_batch(data)
         self.completion.insert_batch(completion_data, replace=True)
 
+    def _write_backup(self, path):
+        bak = path + '.bak'
+        message.info('History import complete. Appending {} to {}'
+                     .format(path, bak))
+        with open(path, 'r', encoding='utf-8') as infile:
+            with open(bak, 'a', encoding='utf-8') as outfile:
+                for line in infile:
+                    outfile.write('\n' + line)
+        os.remove(path)
+
     def _format_url(self, url):
         return url.toString(QUrl.RemovePassword | QUrl.FullyEncoded)
 


Test patch:
diff --git a/tests/unit/browser/test_history.py b/tests/unit/browser/test_history.py
--- a/tests/unit/browser/test_history.py
+++ b/tests/unit/browser/test_history.py
@@ -284,6 +284,22 @@ def test_import_txt(hist, data_tmpdir, monkeypatch, stubs):
     assert (data_tmpdir / 'history.bak').exists()
 
 
+def test_import_txt_existing_backup(hist, data_tmpdir, monkeypatch, stubs):
+    monkeypatch.setattr(history, 'QTimer', stubs.InstaTimer)
+    histfile = data_tmpdir / 'history'
+    bakfile = data_tmpdir / 'history.bak'
+    histfile.write('12345 http://example.com/ title')
+    bakfile.write('12346 http://qutebrowser.org/')
+
+    hist.import_txt()
+
+    assert list(hist) == [('http://example.com/', 'title', 12345, False)]
+
+    assert not histfile.exists()
+    assert bakfile.read().split('\n') == ['12346 http://qutebrowser.org/',
+                                          '12345 http://example.com/ title']
+
+
 @pytest.mark.parametrize('line', [
     '',
     '#12345 http://example.com/commented',