1
0
Fork 0
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:
Shivansh Singh 2024-06-28 16:13:51 +05:30 committed by GitHub
parent a1cbca2a5f
commit 7ab769e117
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 208 additions and 31 deletions

View file

@ -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>

View file

@ -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 = []

View file

@ -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')

View file

@ -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.

View file

@ -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())