feat: Add manual exclude list functionality
- Create a separate file for manual excludes (`rsync-manual-excludes.conf`) that is not cleared on reset. - Add a button to the settings frame to add files/folders to the manual exclude list. - Update the backup and calculation logic to use the manual exclude list. - Ensure the UI reflects the combined exclude lists.
This commit is contained in:
@@ -10,6 +10,8 @@ from crontab import CronTab
|
||||
import tempfile
|
||||
import stat
|
||||
|
||||
from pbp_app_config import AppConfig
|
||||
|
||||
|
||||
class BackupManager:
|
||||
"""
|
||||
@@ -272,6 +274,9 @@ set -e
|
||||
for exclude_file in exclude_files:
|
||||
command.append(f"--exclude-from={exclude_file}")
|
||||
|
||||
if AppConfig.MANUAL_EXCLUDE_LIST_PATH.exists():
|
||||
command.append(f"--exclude-from={AppConfig.MANUAL_EXCLUDE_LIST_PATH}")
|
||||
|
||||
if is_dry_run:
|
||||
command.append('--dry-run')
|
||||
|
||||
|
||||
@@ -43,6 +43,20 @@ class DataProcessing:
|
||||
except IOError as e:
|
||||
app_logger.log(f"Error loading user-defined exclusion list: {e}")
|
||||
|
||||
try:
|
||||
if AppConfig.MANUAL_EXCLUDE_LIST_PATH.exists():
|
||||
with open(AppConfig.MANUAL_EXCLUDE_LIST_PATH, 'r') as f:
|
||||
manual_patterns = [
|
||||
line.strip() for line in f if line.strip() and not line.startswith('#')]
|
||||
all_patterns.update(manual_patterns)
|
||||
app_logger.log(
|
||||
f"Loaded manual exclusion patterns: {manual_patterns}")
|
||||
except FileNotFoundError:
|
||||
app_logger.log(
|
||||
f"Manual exclusion list not found: {AppConfig.MANUAL_EXCLUDE_LIST_PATH}")
|
||||
except IOError as e:
|
||||
app_logger.log(f"Error loading manual exclusion list: {e}")
|
||||
|
||||
final_patterns = sorted(list(all_patterns))
|
||||
app_logger.log(f"Combined exclusion patterns: {final_patterns}")
|
||||
return final_patterns
|
||||
@@ -144,6 +158,9 @@ class DataProcessing:
|
||||
for exclude_file in exclude_files:
|
||||
command.append(f"--exclude-from={exclude_file}")
|
||||
|
||||
if AppConfig.MANUAL_EXCLUDE_LIST_PATH.exists():
|
||||
command.append(f"--exclude-from={AppConfig.MANUAL_EXCLUDE_LIST_PATH}")
|
||||
|
||||
# The destination for a dry run can be a dummy path, but it must exist.
|
||||
# Let's use a temporary directory.
|
||||
dummy_dest = os.path.join(parent_dest, "dry_run_dest")
|
||||
|
||||
@@ -15,6 +15,8 @@ class AppConfig:
|
||||
GENERATED_EXCLUDE_LIST_PATH: Path = CONFIG_DIR / "rsync-generated-excludes.conf"
|
||||
USER_EXCLUDE_LIST_PATH: Path = CONFIG_DIR / \
|
||||
"rsync-user-excludes.conf" # Single file
|
||||
MANUAL_EXCLUDE_LIST_PATH: Path = CONFIG_DIR / \
|
||||
"rsync-manual-excludes.conf" # Single file
|
||||
APP_ICONS_DIR: Path = Path(__file__).parent / "lx-icons"
|
||||
|
||||
# --- Application Info ---
|
||||
@@ -267,6 +269,10 @@ class Msg:
|
||||
"accurate_size_failed": _("Failed to calculate size. See log for details."),
|
||||
"please_wait": _("Please wait, calculating..."),
|
||||
"accurate_calc_cancelled": _("Calculate size cancelled."),
|
||||
"add_to_exclude_list": _("Add to exclude list"),
|
||||
"exclude_dialog_text": _("Do you want to add a folder or a file?"),
|
||||
"add_folder_button": _("Folder"),
|
||||
"add_file_button": _("File"),
|
||||
|
||||
# Menus
|
||||
"file_menu": _("File"),
|
||||
@@ -338,4 +344,4 @@ class Msg:
|
||||
"force_compression": _("Always compress backup"),
|
||||
"force_encryption": _("Always encrypt backup"),
|
||||
"encryption_note_system_backup": _("Note: For system backups, encryption only applies to files directly within the /home directory. Folders are not automatically encrypted unless explicitly included in the backup."),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,6 +157,8 @@ class Actions:
|
||||
AppConfig.GENERATED_EXCLUDE_LIST_PATH)
|
||||
if AppConfig.USER_EXCLUDE_LIST_PATH.exists():
|
||||
exclude_file_paths.append(AppConfig.USER_EXCLUDE_LIST_PATH)
|
||||
if AppConfig.MANUAL_EXCLUDE_LIST_PATH.exists():
|
||||
exclude_file_paths.append(AppConfig.MANUAL_EXCLUDE_LIST_PATH)
|
||||
|
||||
base_dest = self.app.destination_path
|
||||
correct_parent_dir = os.path.join(base_dest, "pybackup")
|
||||
@@ -668,6 +670,8 @@ class Actions:
|
||||
exclude_file_paths.append(AppConfig.GENERATED_EXCLUDE_LIST_PATH)
|
||||
if AppConfig.USER_EXCLUDE_LIST_PATH.exists():
|
||||
exclude_file_paths.append(AppConfig.USER_EXCLUDE_LIST_PATH)
|
||||
if AppConfig.MANUAL_EXCLUDE_LIST_PATH.exists():
|
||||
exclude_file_paths.append(AppConfig.MANUAL_EXCLUDE_LIST_PATH)
|
||||
|
||||
is_dry_run = self.app.testlauf_var.get()
|
||||
is_compressed = self.app.compressed_var.get()
|
||||
|
||||
@@ -303,7 +303,12 @@ class AdvancedSettingsFrame(tk.Toplevel):
|
||||
user_patterns = []
|
||||
if AppConfig.USER_EXCLUDE_LIST_PATH.exists():
|
||||
with open(AppConfig.USER_EXCLUDE_LIST_PATH, 'r') as f:
|
||||
user_patterns = [
|
||||
line.strip() for line in f if line.strip() and not line.startswith('#')]
|
||||
user_patterns.extend(
|
||||
[line.strip() for line in f if line.strip() and not line.startswith('#')])
|
||||
|
||||
if AppConfig.MANUAL_EXCLUDE_LIST_PATH.exists():
|
||||
with open(AppConfig.MANUAL_EXCLUDE_LIST_PATH, 'r') as f:
|
||||
user_patterns.extend(
|
||||
[line.strip() for line in f if line.strip() and not line.startswith('#')])
|
||||
|
||||
return generated_patterns, user_patterns
|
||||
|
||||
@@ -5,6 +5,8 @@ from pathlib import Path
|
||||
|
||||
from pbp_app_config import AppConfig, Msg
|
||||
from pyimage_ui.advanced_settings_frame import AdvancedSettingsFrame
|
||||
from shared_libs.custom_file_dialog import CustomFileDialog
|
||||
from shared_libs.message import MessageDialog
|
||||
|
||||
|
||||
class SettingsFrame(ttk.Frame):
|
||||
@@ -72,6 +74,10 @@ class SettingsFrame(ttk.Frame):
|
||||
'unhide')
|
||||
self.show_hidden_button.config(image=self.unhide_icon)
|
||||
|
||||
add_to_exclude_button = ttk.Button(
|
||||
button_frame, text=Msg.STR["add_to_exclude_list"], command=self._add_to_exclude_list)
|
||||
add_to_exclude_button.pack(side=tk.LEFT, padx=5)
|
||||
|
||||
apply_button = ttk.Button(
|
||||
button_frame, text=Msg.STR["apply"], command=self._apply_changes)
|
||||
apply_button.pack(side=tk.LEFT, padx=5)
|
||||
@@ -90,6 +96,34 @@ class SettingsFrame(ttk.Frame):
|
||||
|
||||
self.hidden_files_visible = False
|
||||
|
||||
def _add_to_exclude_list(self) -> bool:
|
||||
result = MessageDialog("ask", Msg.STR["exclude_dialog_text"], title=Msg.STR["add_to_exclude_list"], buttons=[
|
||||
Msg.STR["add_folder_button"], Msg.STR["add_file_button"]]).show()
|
||||
|
||||
path = None
|
||||
if result:
|
||||
dialog = CustomFileDialog(
|
||||
self, mode="dir", title=Msg.STR["add_to_exclude_list"])
|
||||
self.wait_window(dialog)
|
||||
path = dialog.get_result()
|
||||
dialog.destroy()
|
||||
else:
|
||||
dialog = CustomFileDialog(
|
||||
self, filetypes=[("All Files", "*.*")], title=Msg.STR["add_to_exclude_list"])
|
||||
self.wait_window(dialog)
|
||||
path = dialog.get_result()
|
||||
dialog.destroy()
|
||||
|
||||
if path:
|
||||
with open(AppConfig.MANUAL_EXCLUDE_LIST_PATH, 'a') as f:
|
||||
if os.path.isdir(path):
|
||||
f.write(f"\n{path}/*")
|
||||
else:
|
||||
f.write(f"\n{path}")
|
||||
|
||||
self.load_and_display_excludes()
|
||||
self._load_hidden_files()
|
||||
|
||||
def show(self):
|
||||
self.grid(row=2, column=0, sticky="nsew")
|
||||
self.load_and_display_excludes()
|
||||
@@ -98,27 +132,27 @@ class SettingsFrame(ttk.Frame):
|
||||
self.grid_remove()
|
||||
|
||||
def _load_exclude_patterns(self):
|
||||
|
||||
generated_patterns = []
|
||||
all_patterns = []
|
||||
if AppConfig.GENERATED_EXCLUDE_LIST_PATH.exists():
|
||||
with open(AppConfig.GENERATED_EXCLUDE_LIST_PATH, 'r') as f:
|
||||
generated_patterns = [
|
||||
line.strip() for line in f if line.strip() and not line.startswith('#')]
|
||||
all_patterns.extend([line.strip() for line in f if line.strip() and not line.startswith('#')])
|
||||
|
||||
self.user_exclude_patterns = []
|
||||
if AppConfig.USER_EXCLUDE_LIST_PATH.exists():
|
||||
with open(AppConfig.USER_EXCLUDE_LIST_PATH, 'r') as f:
|
||||
self.user_exclude_patterns = [
|
||||
line.strip() for line in f if line.strip() and not line.startswith('#')]
|
||||
all_patterns.extend([line.strip() for line in f if line.strip() and not line.startswith('#')])
|
||||
|
||||
return generated_patterns, self.user_exclude_patterns
|
||||
if AppConfig.MANUAL_EXCLUDE_LIST_PATH.exists():
|
||||
with open(AppConfig.MANUAL_EXCLUDE_LIST_PATH, 'r') as f:
|
||||
all_patterns.extend([line.strip() for line in f if line.strip() and not line.startswith('#')])
|
||||
|
||||
return all_patterns
|
||||
|
||||
def load_and_display_excludes(self):
|
||||
# Clear existing items
|
||||
for i in self.tree.get_children():
|
||||
self.tree.delete(i)
|
||||
|
||||
_, self.user_exclude_patterns = self._load_exclude_patterns()
|
||||
exclude_patterns = self._load_exclude_patterns()
|
||||
|
||||
home_dir = Path.home()
|
||||
|
||||
@@ -126,7 +160,7 @@ class SettingsFrame(ttk.Frame):
|
||||
for item in home_dir.iterdir():
|
||||
if not item.name.startswith('.'):
|
||||
item_path_str = str(item.absolute())
|
||||
is_excluded = f"{item_path_str}/*" in self.user_exclude_patterns
|
||||
is_excluded = f"{item_path_str}/*" in exclude_patterns or item_path_str in exclude_patterns
|
||||
included_text = Msg.STR["no"] if is_excluded else Msg.STR["yes"]
|
||||
items_to_display.append(
|
||||
(included_text, item.name, item_path_str))
|
||||
@@ -183,16 +217,16 @@ class SettingsFrame(ttk.Frame):
|
||||
else:
|
||||
new_excludes.append(path)
|
||||
|
||||
# Load existing patterns
|
||||
existing_patterns = []
|
||||
# Load existing user patterns
|
||||
existing_user_patterns = []
|
||||
if AppConfig.USER_EXCLUDE_LIST_PATH.exists():
|
||||
with open(AppConfig.USER_EXCLUDE_LIST_PATH, 'r') as f:
|
||||
existing_patterns = [
|
||||
existing_user_patterns = [
|
||||
line.strip() for line in f if line.strip() and not line.startswith('#')]
|
||||
|
||||
# Preserve patterns that are not managed by this view
|
||||
preserved_patterns = []
|
||||
for pattern in existing_patterns:
|
||||
for pattern in existing_user_patterns:
|
||||
clean_pattern = pattern.replace('/*', '')
|
||||
if clean_pattern not in tree_paths:
|
||||
preserved_patterns.append(pattern)
|
||||
@@ -200,12 +234,6 @@ class SettingsFrame(ttk.Frame):
|
||||
# Combine preserved patterns with new excludes from this view
|
||||
final_excludes = list(set(preserved_patterns + new_excludes))
|
||||
|
||||
# Handle backup destination separately to ensure it's always excluded
|
||||
if self.master.master.master.destination_path:
|
||||
backup_root_to_exclude = f"/{self.master.master.master.destination_path.strip('/').split('/')[0]}/*"
|
||||
if backup_root_to_exclude not in final_excludes:
|
||||
final_excludes.append(backup_root_to_exclude)
|
||||
|
||||
with open(AppConfig.USER_EXCLUDE_LIST_PATH, 'w') as f:
|
||||
for path in final_excludes:
|
||||
f.write(f"{path}\n")
|
||||
@@ -240,7 +268,7 @@ class SettingsFrame(ttk.Frame):
|
||||
for i in self.hidden_tree.get_children():
|
||||
self.hidden_tree.delete(i)
|
||||
|
||||
_, self.user_exclude_patterns = self._load_exclude_patterns()
|
||||
exclude_patterns = self._load_exclude_patterns()
|
||||
|
||||
home_dir = Path.home()
|
||||
|
||||
@@ -248,7 +276,7 @@ class SettingsFrame(ttk.Frame):
|
||||
for item in home_dir.iterdir():
|
||||
if item.name.startswith('.'):
|
||||
item_path_str = str(item.absolute())
|
||||
is_excluded = f"{item_path_str}/*" in self.user_exclude_patterns or f"{item_path_str}" in self.user_exclude_patterns
|
||||
is_excluded = f"{item_path_str}/*" in exclude_patterns or f"{item_path_str}" in exclude_patterns
|
||||
included_text = Msg.STR["no"] if is_excluded else Msg.STR["yes"]
|
||||
items_to_display.append(
|
||||
(included_text, item.name, item_path_str))
|
||||
|
||||
Reference in New Issue
Block a user