Skip to content

Commit 87652bf

Browse files
author
Temp User
committed
feat: show busy dialog while parsing archive contents for extract
Fixes #1570 — large archives could silently process for hours with only a small status-bar line as feedback. Now shows a QProgressDialog (indeterminate) that closes once parsing is done and the extract dialog opens.
1 parent 4fcf80f commit 87652bf

9 files changed

Lines changed: 1344 additions & 1348 deletions

File tree

src/vorta/assets/UI/dialogs/repo/change_passphrase.ui

Lines changed: 0 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -55,42 +55,6 @@
5555
</property>
5656
</layout>
5757
</item>
58-
<item>
59-
<widget class="QLabel" name="errorText">
60-
<property name="sizePolicy">
61-
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
62-
<horstretch>0</horstretch>
63-
<verstretch>0</verstretch>
64-
</sizepolicy>
65-
</property>
66-
<property name="minimumSize">
67-
<size>
68-
<width>0</width>
69-
<height>20</height>
70-
</size>
71-
</property>
72-
<property name="font">
73-
<font>
74-
<pointsize>11</pointsize>
75-
</font>
76-
</property>
77-
<property name="text">
78-
<string/>
79-
</property>
80-
<property name="textFormat">
81-
<enum>Qt::PlainText</enum>
82-
</property>
83-
<property name="scaledContents">
84-
<bool>false</bool>
85-
</property>
86-
<property name="alignment">
87-
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
88-
</property>
89-
<property name="wordWrap">
90-
<bool>true</bool>
91-
</property>
92-
</widget>
93-
</item>
9458
<item>
9559
<layout class="QHBoxLayout" name="horizontalLayout_3">
9660
<item>

src/vorta/profile_export.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from vorta.store.connection import DB, SCHEMA_VERSION, init_db
99
from vorta.store.models import (
1010
BackupProfileModel,
11+
ExclusionModel,
1112
RepoModel,
1213
SchemaVersion,
1314
SettingsModel,
@@ -64,6 +65,11 @@ def from_db(cls, profile, store_password=True, include_settings=True):
6465
model_to_dict(source, recurse=False, exclude=[SourceFileModel.id])
6566
for source in SourceFileModel.select().where(SourceFileModel.profile == profile)
6667
]
68+
# Add ExclusionModel
69+
profile_dict['ExclusionModel'] = [
70+
model_to_dict(exclusion, recurse=False, exclude=[ExclusionModel.id])
71+
for exclusion in ExclusionModel.select().where(ExclusionModel.profile == profile)
72+
]
6773
# Add SchemaVersion
6874
profile_dict['SchemaVersion'] = model_to_dict(SchemaVersion.get(id=1))
6975

@@ -127,18 +133,27 @@ def to_db(self, overwrite_profile=False, overwrite_settings=True):
127133
SettingsModel.insert_many(self._profile_dict['SettingsModel']).execute()
128134
WifiSettingModel.insert_many(self._profile_dict['WifiSettingModel']).execute()
129135

130-
# Set the profile ids to be match new profile
136+
# Set the profile ids to match new profile
131137
for source in self._profile_dict['SourceFileModel']:
132138
source['profile'] = self.id
133139
# Delete existing Sources to avoid duplicates
134140
SourceFileModel.delete().where(SourceFileModel.profile == self.id).execute()
135141
SourceFileModel.insert_many(self._profile_dict['SourceFileModel']).execute()
136142

143+
# Restore ExclusionModel entries
144+
if 'ExclusionModel' in self._profile_dict:
145+
for exclusion in self._profile_dict['ExclusionModel']:
146+
exclusion['profile'] = self.id
147+
ExclusionModel.delete().where(ExclusionModel.profile == self.id).execute()
148+
ExclusionModel.insert_many(self._profile_dict['ExclusionModel']).execute()
149+
137150
# Delete added dictionaries to make it match BackupProfileModel
138151
del self._profile_dict['SettingsModel']
139152
del self._profile_dict['SourceFileModel']
140153
del self._profile_dict['WifiSettingModel']
141154
del self._profile_dict['SchemaVersion']
155+
if 'ExclusionModel' in self._profile_dict:
156+
del self._profile_dict['ExclusionModel']
142157

143158
# dict to profile
144159
new_profile = dict_to_model(BackupProfileModel, self._profile_dict)
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
from PyQt6 import QtCore
2+
from PyQt6.QtWidgets import QProgressDialog
3+
from PyQt6.QtCore import Qt
4+
5+
from vorta.borg.extract import BorgExtractJob
6+
from vorta.borg.list_archive import BorgListArchiveJob
7+
from vorta.store.models import ArchiveModel
8+
from vorta.utils import choose_file_dialog
9+
from vorta.views.dialogs.archive import extract as extract_dialog
10+
from vorta.views.dialogs.archive.extract import ExtractDialog, ExtractTree
11+
12+
13+
class ArchiveExtract:
14+
def __init__(self, tab):
15+
self.tab = tab
16+
17+
def extract_action(self):
18+
"""
19+
Open a dialog for choosing what to extract from the selected archive.
20+
"""
21+
profile = self.tab.profile()
22+
23+
row_selected = self.tab.archiveTable.selectionModel().selectedRows()
24+
if row_selected:
25+
archive_cell = self.tab.archiveTable.item(row_selected[0].row(), 4)
26+
if archive_cell:
27+
archive_name = archive_cell.text()
28+
params = BorgListArchiveJob.prepare(profile, archive_name)
29+
30+
if not params['ok']:
31+
self.tab._set_status(params['message'])
32+
return
33+
self.tab._set_status('')
34+
self.tab._toggle_all_buttons(False)
35+
36+
job = BorgListArchiveJob(params['cmd'], params, self.tab.profile().repo.id)
37+
job.updated.connect(self.tab.mountErrors.setText)
38+
job.result.connect(self.extract_list_result)
39+
self.tab.app.jobs_manager.add_job(job)
40+
return job
41+
else:
42+
self.tab._set_status(self.tab.tr('Select an archive to restore first.'))
43+
44+
def extract_list_result(self, result):
45+
"""Process the contents of the archive to extract."""
46+
self.tab._set_status('')
47+
if result['returncode'] == 0:
48+
archive = ArchiveModel.get(name=result['params']['archive_name'])
49+
model = ExtractTree()
50+
self.tab._set_status(self.tab.tr("Processing archive contents"))
51+
52+
progress = QProgressDialog(self.tab.tr("Processing archive contents…"), None, 0, 0, self.tab)
53+
progress.setWindowTitle(self.tab.tr("Please wait"))
54+
progress.setWindowModality(Qt.WindowModality.WindowModal)
55+
progress.setMinimumDuration(0)
56+
progress.setValue(0)
57+
self.tab._extract_progress = progress
58+
59+
self.tab._t = extract_dialog.ParseThread(result['data'], model)
60+
self.tab._t.finished.connect(self.tab._extract_progress.close)
61+
self.tab._t.finished.connect(lambda: self.extract_show_dialog(archive, model))
62+
self.tab._t.start()
63+
64+
def extract_show_dialog(self, archive, model):
65+
"""Show the dialog for choosing the archive contents to extract."""
66+
self.tab._set_status('')
67+
68+
def process_result():
69+
def receive():
70+
extraction_folder = dialog.selectedFiles()
71+
if extraction_folder:
72+
params = BorgExtractJob.prepare(self.tab.profile(), archive.name, model, extraction_folder[0])
73+
if params['ok']:
74+
self.tab._toggle_all_buttons(False)
75+
job = BorgExtractJob(params['cmd'], params, self.tab.profile().repo.id)
76+
job.updated.connect(self.tab.mountErrors.setText)
77+
job.result.connect(self.extract_archive_result)
78+
self.tab.app.jobs_manager.add_job(job)
79+
else:
80+
self.tab._set_status(params['message'])
81+
82+
dialog = choose_file_dialog(self.tab, self.tab.tr("Choose Extraction Point"), want_folder=True)
83+
dialog.open(receive)
84+
85+
window = ExtractDialog(archive, model)
86+
self.tab._toggle_all_buttons(True)
87+
window.setParent(self.tab, QtCore.Qt.WindowType.Sheet)
88+
self.tab._window = window # for testing
89+
window.show()
90+
window.accepted.connect(process_result)
91+
92+
def extract_archive_result(self, result):
93+
"""Finished extraction."""
94+
self.tab._toggle_all_buttons(True)

0 commit comments

Comments
 (0)