mirror of
https://github.com/borgbase/vorta
synced 2024-12-21 23:33:13 +00:00
Add back Exclude If Present. By @shivansh02 (#2016)
This commit is contained in:
parent
a1cbca2a5f
commit
7ab769e117
5 changed files with 208 additions and 31 deletions
|
@ -96,7 +96,47 @@
|
||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
<widget class="QWidget" name="tab_3">
|
<widget class="QWidget" name="tab_5">
|
||||||
|
<attribute name="title">
|
||||||
|
<string>Exclude If Present</string>
|
||||||
|
</attribute>
|
||||||
|
<layout class="QVBoxLayout" name="verticalLayout_5">
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="excludeIfPresentHelpText">
|
||||||
|
<property name="wordWrap">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QListView" name="excludeIfPresentList"/>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_5">
|
||||||
|
<item>
|
||||||
|
<spacer name="horizontalSpacer_5">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>40</width>
|
||||||
|
<height>20</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QToolButton" name="bAddPatternExcludeIfPresent"/>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QToolButton" name="bRemovePatternExcludeIfPresent"/>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
<widget class="QWidget" name="tab_3">
|
||||||
<attribute name="title">
|
<attribute name="title">
|
||||||
<string>Preview</string>
|
<string>Preview</string>
|
||||||
</attribute>
|
</attribute>
|
||||||
|
|
|
@ -162,6 +162,17 @@ def prepare(cls, profile):
|
||||||
]
|
]
|
||||||
cmd += extra_cmd_options
|
cmd += extra_cmd_options
|
||||||
|
|
||||||
|
# Function to extend command with exclude-if-present patterns
|
||||||
|
if profile.exclude_if_present is not None:
|
||||||
|
patterns = []
|
||||||
|
for f in profile.exclude_if_present.split('\n'):
|
||||||
|
f = f.strip()
|
||||||
|
if f.startswith('[x]'):
|
||||||
|
patterns.append(f[3:].strip()) # Remove the '[x]' prefix
|
||||||
|
|
||||||
|
for pattern in patterns:
|
||||||
|
cmd.extend(['--exclude-if-present', pattern])
|
||||||
|
|
||||||
# Add excludes
|
# Add excludes
|
||||||
# Partly inspired by borgmatic/borgmatic/borg/create.py
|
# Partly inspired by borgmatic/borgmatic/borg/create.py
|
||||||
exclude_dirs = []
|
exclude_dirs = []
|
||||||
|
|
|
@ -79,7 +79,7 @@ class BackupProfileModel(BaseModel):
|
||||||
ssh_key = pw.CharField(default=None, null=True)
|
ssh_key = pw.CharField(default=None, null=True)
|
||||||
compression = pw.CharField(default='lz4')
|
compression = pw.CharField(default='lz4')
|
||||||
exclude_patterns = pw.TextField(null=True)
|
exclude_patterns = pw.TextField(null=True)
|
||||||
exclude_if_present = pw.TextField(null=True)
|
exclude_if_present = pw.TextField(null=True, default="[] .nobackup\n[] CACHEDIR.TAG")
|
||||||
schedule_mode = pw.CharField(default='off')
|
schedule_mode = pw.CharField(default='off')
|
||||||
schedule_interval_count = pw.IntegerField(default=3)
|
schedule_interval_count = pw.IntegerField(default=3)
|
||||||
schedule_interval_unit = pw.CharField(default='hours')
|
schedule_interval_unit = pw.CharField(default='hours')
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
from PyQt6 import uic
|
from PyQt6 import uic
|
||||||
from PyQt6.QtCore import QModelIndex, QObject, Qt
|
from PyQt6.QtCore import QModelIndex, QObject, Qt
|
||||||
from PyQt6.QtGui import QStandardItem, QStandardItemModel
|
from PyQt6.QtGui import QCursor, QStandardItem, QStandardItemModel
|
||||||
from PyQt6.QtWidgets import (
|
from PyQt6.QtWidgets import (
|
||||||
QAbstractItemView,
|
QAbstractItemView,
|
||||||
QApplication,
|
QApplication,
|
||||||
|
@ -85,8 +85,12 @@ def __init__(self, profile, parent=None):
|
||||||
self.bRemovePattern.setIcon(get_colored_icon('minus'))
|
self.bRemovePattern.setIcon(get_colored_icon('minus'))
|
||||||
self.bPreviewCopy.clicked.connect(self.copy_preview_to_clipboard)
|
self.bPreviewCopy.clicked.connect(self.copy_preview_to_clipboard)
|
||||||
self.bPreviewCopy.setIcon(get_colored_icon('copy'))
|
self.bPreviewCopy.setIcon(get_colored_icon('copy'))
|
||||||
self.bAddPattern.clicked.connect(self.add_pattern)
|
self.bAddPattern.clicked.connect(self.add_pattern_custom_exclusion)
|
||||||
self.bAddPattern.setIcon(get_colored_icon('plus'))
|
self.bAddPattern.setIcon(get_colored_icon('plus'))
|
||||||
|
self.bAddPatternExcludeIfPresent.setIcon(get_colored_icon('plus'))
|
||||||
|
self.bAddPatternExcludeIfPresent.clicked.connect(self.add_pattern_exclude_if_present)
|
||||||
|
self.bRemovePatternExcludeIfPresent.setIcon(get_colored_icon('minus'))
|
||||||
|
self.bRemovePatternExcludeIfPresent.clicked.connect(self.remove_pattern_exclude_if_present)
|
||||||
|
|
||||||
# help text
|
# help text
|
||||||
self.customPresetsHelpText.setOpenExternalLinks(True)
|
self.customPresetsHelpText.setOpenExternalLinks(True)
|
||||||
|
@ -114,11 +118,33 @@ def __init__(self, profile, parent=None):
|
||||||
"This is a preview of the patterns that will be used to exclude files and folders from the backup.",
|
"This is a preview of the patterns that will be used to exclude files and folders from the backup.",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
self.excludeIfPresentHelpText.setText(
|
||||||
|
translate(
|
||||||
|
"ExcludeIfPresentHelp",
|
||||||
|
"Folders that contain the following files will be excluded from backups.",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add the Exclude If Present tab
|
||||||
|
self.excludeIfPresentModel = MandatoryInputItemModel(profile=profile)
|
||||||
|
self.excludeIfPresentList.setModel(self.excludeIfPresentModel)
|
||||||
|
self.excludeIfPresentList.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
|
||||||
|
self.excludeIfPresentList.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
|
||||||
|
self.excludeIfPresentList.setFocusPolicy(Qt.FocusPolicy.NoFocus)
|
||||||
|
self.excludeIfPresentList.setAlternatingRowColors(True)
|
||||||
|
self.excludeIfPresentListDelegate = QStyledItemDelegate()
|
||||||
|
self.excludeIfPresentList.setItemDelegate(self.excludeIfPresentListDelegate)
|
||||||
|
self.excludeIfPresentListDelegate.closeEditor.connect(self.exclude_if_present_pattern_editing_finished)
|
||||||
|
self.excludeIfPresentList.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
|
||||||
|
self.excludeIfPresentList.customContextMenuRequested.connect(self.exclude_if_present_context_menu)
|
||||||
|
|
||||||
|
self.excludeIfPresentModel.itemChanged.connect(self.exclude_if_present_item_changed)
|
||||||
|
|
||||||
self.populate_custom_exclusions_list()
|
self.populate_custom_exclusions_list()
|
||||||
self.populate_presets_list()
|
self.populate_presets_list()
|
||||||
self.populate_raw_exclusions_text()
|
self.populate_raw_exclusions_text()
|
||||||
self.populate_preview_tab()
|
self.populate_preview_tab()
|
||||||
|
self.populate_exclude_if_present_patterns()
|
||||||
|
|
||||||
def populate_custom_exclusions_list(self):
|
def populate_custom_exclusions_list(self):
|
||||||
user_excluded_patterns = {
|
user_excluded_patterns = {
|
||||||
|
@ -134,37 +160,41 @@ def populate_custom_exclusions_list(self):
|
||||||
item.setCheckState(Qt.CheckState.Checked if enabled else Qt.CheckState.Unchecked)
|
item.setCheckState(Qt.CheckState.Checked if enabled else Qt.CheckState.Unchecked)
|
||||||
self.customExclusionsModel.appendRow(item)
|
self.customExclusionsModel.appendRow(item)
|
||||||
|
|
||||||
def custom_exclusions_context_menu(self, pos):
|
def create_context_menu(self, list_widget, remove_method, toggle_method=None):
|
||||||
# index under cursor
|
pos = list_widget.viewport().mapFromGlobal(QCursor.pos())
|
||||||
index = self.customExclusionsList.indexAt(pos)
|
index = list_widget.indexAt(pos)
|
||||||
if not index.isValid():
|
if not index.isValid():
|
||||||
return
|
return
|
||||||
|
|
||||||
selected_rows = self.customExclusionsList.selectedIndexes()
|
selected_rows = list_widget.selectedIndexes()
|
||||||
|
|
||||||
if selected_rows and index not in selected_rows:
|
if selected_rows and index not in selected_rows:
|
||||||
return # popup only for selected items
|
return
|
||||||
|
|
||||||
menu = QMenu(self.customExclusionsList)
|
menu = QMenu(list_widget)
|
||||||
menu.addAction(
|
menu.addAction(
|
||||||
get_colored_icon('copy'),
|
get_colored_icon('copy'),
|
||||||
self.tr('Copy'),
|
self.tr('Copy'),
|
||||||
lambda: QApplication.clipboard().setText(index.data()),
|
lambda: QApplication.clipboard().setText(index.data()),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Remove and Toggle can work with multiple items selected
|
|
||||||
menu.addAction(
|
menu.addAction(
|
||||||
get_colored_icon('minus'),
|
get_colored_icon('minus'),
|
||||||
self.tr('Remove'),
|
self.tr('Remove'),
|
||||||
lambda: self.remove_pattern(index if not selected_rows else None),
|
lambda: remove_method(index if not selected_rows else None),
|
||||||
)
|
|
||||||
menu.addAction(
|
|
||||||
get_colored_icon('check-circle'),
|
|
||||||
self.tr('Toggle'),
|
|
||||||
lambda: self.toggle_custom_pattern(index if not selected_rows else None),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
menu.popup(self.customExclusionsList.viewport().mapToGlobal(pos))
|
if toggle_method:
|
||||||
|
menu.addAction(
|
||||||
|
get_colored_icon('check-circle'),
|
||||||
|
self.tr('Toggle'),
|
||||||
|
lambda: toggle_method(index if not selected_rows else None),
|
||||||
|
)
|
||||||
|
|
||||||
|
menu.popup(list_widget.viewport().mapToGlobal(pos))
|
||||||
|
|
||||||
|
def custom_exclusions_context_menu(self, pos):
|
||||||
|
self.create_context_menu(self.customExclusionsList, self.remove_pattern, self.toggle_custom_pattern)
|
||||||
|
|
||||||
def populate_presets_list(self):
|
def populate_presets_list(self):
|
||||||
for preset_slug in self.allPresets.keys():
|
for preset_slug in self.allPresets.keys():
|
||||||
|
@ -190,8 +220,14 @@ def populate_raw_exclusions_text(self):
|
||||||
self.rawExclusionsText.setPlainText(raw_excludes)
|
self.rawExclusionsText.setPlainText(raw_excludes)
|
||||||
|
|
||||||
def populate_preview_tab(self):
|
def populate_preview_tab(self):
|
||||||
excludes = self.profile.get_combined_exclusion_string()
|
preview = self.profile.get_combined_exclusion_string()
|
||||||
self.exclusionsPreviewText.setPlainText(excludes)
|
if self.profile.exclude_if_present:
|
||||||
|
preview += '\n# Exclude if present'
|
||||||
|
for f in self.profile.exclude_if_present.split('\n'):
|
||||||
|
f = f.strip()
|
||||||
|
if f.startswith('[x]'):
|
||||||
|
preview += '\n' + f[3:].strip()
|
||||||
|
self.exclusionsPreviewText.setPlainText(preview)
|
||||||
|
|
||||||
def copy_preview_to_clipboard(self):
|
def copy_preview_to_clipboard(self):
|
||||||
cb = QApplication.clipboard()
|
cb = QApplication.clipboard()
|
||||||
|
@ -242,29 +278,35 @@ def toggle_custom_pattern(self, index=None):
|
||||||
else:
|
else:
|
||||||
item.setCheckState(Qt.CheckState.Checked)
|
item.setCheckState(Qt.CheckState.Checked)
|
||||||
|
|
||||||
def add_pattern(self):
|
def add_pattern(self, list_widget, model):
|
||||||
'''
|
'''
|
||||||
Add an empty item to the list in editable mode.
|
Add an empty item to the list in editable mode.
|
||||||
Don't add an item if the user is already editing an item.
|
Don't add an item if the user is already editing an item.
|
||||||
'''
|
'''
|
||||||
if self.customExclusionsList.state() == QAbstractItemView.State.EditingState:
|
if list_widget.state() == QAbstractItemView.State.EditingState:
|
||||||
return
|
return
|
||||||
item = QStandardItem('')
|
item = QStandardItem('')
|
||||||
item.setCheckable(True)
|
item.setCheckable(True)
|
||||||
item.setCheckState(Qt.CheckState.Checked)
|
item.setCheckState(Qt.CheckState.Checked)
|
||||||
self.customExclusionsList.model().appendRow(item)
|
model.appendRow(item)
|
||||||
self.customExclusionsList.edit(item.index())
|
list_widget.edit(item.index())
|
||||||
self.customExclusionsList.scrollToBottom()
|
list_widget.scrollToBottom()
|
||||||
|
|
||||||
def custom_pattern_editing_finished(self, editor):
|
def add_pattern_custom_exclusion(self):
|
||||||
|
self.add_pattern(self.customExclusionsList, self.customExclusionsModel)
|
||||||
|
|
||||||
|
def text_editing_finished(self, model):
|
||||||
'''
|
'''
|
||||||
Go through all items in the list and if any of them are empty, remove them.
|
Go through all items in the list and if any of them are empty, remove them.
|
||||||
Handles the case where the user presses the escape key to cancel editing.
|
Handles the case where the user presses the escape key to cancel editing.
|
||||||
'''
|
'''
|
||||||
for row in range(self.customExclusionsModel.rowCount()):
|
for row in range(model.rowCount()):
|
||||||
item = self.customExclusionsModel.item(row)
|
item = model.item(row)
|
||||||
if item.text() == '':
|
if item.text() == '':
|
||||||
self.customExclusionsModel.removeRow(row)
|
model.removeRow(row)
|
||||||
|
|
||||||
|
def custom_pattern_editing_finished(self, editor):
|
||||||
|
self.text_editing_finished(self.customExclusionsModel)
|
||||||
|
|
||||||
def custom_item_changed(self, item):
|
def custom_item_changed(self, item):
|
||||||
'''
|
'''
|
||||||
|
@ -310,6 +352,83 @@ def preset_item_changed(self, item):
|
||||||
|
|
||||||
self.populate_preview_tab()
|
self.populate_preview_tab()
|
||||||
|
|
||||||
|
def exclude_if_present_item_changed(self, item):
|
||||||
|
self.save_exclude_if_present_patterns()
|
||||||
|
|
||||||
|
def exclude_if_present_context_menu(self, pos):
|
||||||
|
self.create_context_menu(self.excludeIfPresentList, self.remove_pattern_exclude_if_present)
|
||||||
|
|
||||||
|
def remove_pattern_exclude_if_present(self, index=None):
|
||||||
|
if not index:
|
||||||
|
indexes = self.excludeIfPresentList.selectedIndexes()
|
||||||
|
for index in reversed(sorted(indexes)):
|
||||||
|
self.excludeIfPresentModel.removeRow(index.row())
|
||||||
|
else:
|
||||||
|
self.excludeIfPresentModel.removeRow(index.row())
|
||||||
|
self.save_exclude_if_present_patterns()
|
||||||
|
|
||||||
|
def add_pattern_exclude_if_present(self):
|
||||||
|
self.add_pattern(self.excludeIfPresentList, self.excludeIfPresentModel)
|
||||||
|
self.save_exclude_if_present_patterns()
|
||||||
|
|
||||||
|
def exclude_if_present_pattern_editing_finished(self, editor):
|
||||||
|
self.text_editing_finished(self.customExclusionsModel)
|
||||||
|
self.save_exclude_if_present_patterns()
|
||||||
|
|
||||||
|
def save_exclude_if_present_patterns(self):
|
||||||
|
'''
|
||||||
|
Save Exclude If Present files to profile.
|
||||||
|
'''
|
||||||
|
patterns = []
|
||||||
|
for row in range(self.excludeIfPresentModel.rowCount()):
|
||||||
|
item = self.excludeIfPresentModel.item(row)
|
||||||
|
text = item.text().strip()
|
||||||
|
|
||||||
|
# Remove any existing prefix
|
||||||
|
if text.startswith('[x] '):
|
||||||
|
text = text[4:]
|
||||||
|
elif text.startswith('[] '):
|
||||||
|
text = text[3:]
|
||||||
|
|
||||||
|
# Add the correct prefix based on the check state
|
||||||
|
prefix = '[x] ' if item.checkState() == Qt.CheckState.Checked else '[] '
|
||||||
|
patterns.append(prefix + text)
|
||||||
|
|
||||||
|
self.profile.exclude_if_present = '\n'.join(patterns)
|
||||||
|
self.profile.save()
|
||||||
|
self.populate_preview_tab()
|
||||||
|
|
||||||
|
def normalize_exclude_if_present_patterns(self):
|
||||||
|
'''
|
||||||
|
Ensure all patterns in exclude_if_present start with either [x] or [].
|
||||||
|
'''
|
||||||
|
patterns = self.profile.exclude_if_present.split('\n') if self.profile.exclude_if_present else []
|
||||||
|
normalized_patterns = []
|
||||||
|
for pattern in patterns:
|
||||||
|
if not pattern.startswith('[x]') and not pattern.startswith('[]'):
|
||||||
|
pattern = '[x] ' + pattern
|
||||||
|
normalized_patterns.append(pattern)
|
||||||
|
self.profile.exclude_if_present = '\n'.join(normalized_patterns)
|
||||||
|
self.profile.save()
|
||||||
|
|
||||||
|
def populate_exclude_if_present_patterns(self):
|
||||||
|
'''
|
||||||
|
Populate the 'Exclude If Present' list from the profile.
|
||||||
|
'''
|
||||||
|
self.normalize_exclude_if_present_patterns()
|
||||||
|
patterns = self.profile.exclude_if_present.split('\n') if self.profile.exclude_if_present else []
|
||||||
|
for pattern in patterns:
|
||||||
|
item = QStandardItem()
|
||||||
|
if pattern.startswith('[x]'):
|
||||||
|
item.setText(pattern[4:])
|
||||||
|
item.setCheckable(True)
|
||||||
|
item.setCheckState(Qt.CheckState.Checked)
|
||||||
|
elif pattern.startswith('[]'):
|
||||||
|
item.setText(pattern[3:])
|
||||||
|
item.setCheckable(True)
|
||||||
|
item.setCheckState(Qt.CheckState.Unchecked)
|
||||||
|
self.excludeIfPresentModel.appendRow(item)
|
||||||
|
|
||||||
def raw_exclusions_saved(self):
|
def raw_exclusions_saved(self):
|
||||||
'''
|
'''
|
||||||
When the user saves changes in the raw exclusions text box, add it to the database.
|
When the user saves changes in the raw exclusions text box, add it to the database.
|
||||||
|
|
|
@ -11,16 +11,23 @@ def test_exclusion_preview_populated(qapp, qtbot):
|
||||||
|
|
||||||
qtbot.keyClicks(tab._window.customExclusionsList.viewport().focusWidget(), "custom pattern")
|
qtbot.keyClicks(tab._window.customExclusionsList.viewport().focusWidget(), "custom pattern")
|
||||||
qtbot.keyClick(tab._window.customExclusionsList.viewport().focusWidget(), QtCore.Qt.Key.Key_Enter)
|
qtbot.keyClick(tab._window.customExclusionsList.viewport().focusWidget(), QtCore.Qt.Key.Key_Enter)
|
||||||
qtbot.waitUntil(lambda: tab._window.exclusionsPreviewText.toPlainText() == "# custom added rules\ncustom pattern\n")
|
qtbot.waitUntil(lambda: "custom pattern" in tab._window.exclusionsPreviewText.toPlainText())
|
||||||
|
|
||||||
tab._window.tabWidget.setCurrentIndex(1)
|
tab._window.tabWidget.setCurrentIndex(1)
|
||||||
|
|
||||||
tab._window.exclusionPresetsModel.itemFromIndex(tab._window.exclusionPresetsModel.index(0, 0)).setCheckState(
|
tab._window.exclusionPresetsModel.itemFromIndex(tab._window.exclusionPresetsModel.index(0, 0)).setCheckState(
|
||||||
QtCore.Qt.CheckState.Checked
|
QtCore.Qt.CheckState.Checked
|
||||||
)
|
)
|
||||||
qtbot.waitUntil(lambda: "# Chromium cache and config files" in tab._window.exclusionsPreviewText.toPlainText())
|
|
||||||
|
|
||||||
|
qtbot.waitUntil(lambda: "# chromium-cache" in tab._window.exclusionsPreviewText.toPlainText())
|
||||||
tab._window.tabWidget.setCurrentIndex(2)
|
tab._window.tabWidget.setCurrentIndex(2)
|
||||||
|
|
||||||
qtbot.keyClicks(tab._window.rawExclusionsText, "test raw pattern 1")
|
qtbot.keyClicks(tab._window.rawExclusionsText, "test raw pattern 1")
|
||||||
qtbot.waitUntil(lambda: "test raw pattern 1\n" in tab._window.exclusionsPreviewText.toPlainText())
|
qtbot.waitUntil(lambda: "test raw pattern 1\n" in tab._window.exclusionsPreviewText.toPlainText())
|
||||||
|
|
||||||
|
qtbot.mouseClick(tab.bExclude, QtCore.Qt.MouseButton.LeftButton)
|
||||||
|
qtbot.mouseClick(tab._window.bAddPatternExcludeIfPresent, QtCore.Qt.MouseButton.LeftButton)
|
||||||
|
|
||||||
|
qtbot.keyClicks(tab._window.excludeIfPresentList.viewport().focusWidget(), "exclude_if_present_file")
|
||||||
|
qtbot.keyClick(tab._window.excludeIfPresentList.viewport().focusWidget(), QtCore.Qt.Key.Key_Enter)
|
||||||
|
qtbot.waitUntil(lambda: "exclude_if_present_file" in tab._window.exclusionsPreviewText.toPlainText())
|
||||||
|
|
Loading…
Reference in a new issue