From 92caebe7f2b7b0fec05c9e3692e94ccf5cd41d89 Mon Sep 17 00:00:00 2001 From: MehradDraco Date: Mon, 3 Aug 2026 18:05:33 +0330 Subject: [PATCH 1/2] Add files via upload --- src/parch_driver_manager/backup_manager.py | 141 +++++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 src/parch_driver_manager/backup_manager.py diff --git a/src/parch_driver_manager/backup_manager.py b/src/parch_driver_manager/backup_manager.py new file mode 100644 index 0000000..125348c --- /dev/null +++ b/src/parch_driver_manager/backup_manager.py @@ -0,0 +1,141 @@ +import os +import json +import shutil +import datetime +from typing import List, Dict, Any, Tuple + +from .backend import BackendRunner +from .system_prober import SystemProber +from .profiles import DriverProfiles + +class BackupManager: + + BACKUP_DIR = os.path.expanduser("~/ParchDriverBackups") + + @staticmethod + def _ensure_backup_dir(): + os.makedirs(BackupManager.BACKUP_DIR, exist_ok=True) + + @staticmethod + def create_backup(backend: BackendRunner) -> Tuple[bool, str]: + try: + BackupManager._ensure_backup_dir() + timestamp = datetime.datetime.now().strftime("%Y%m%d-%H%M%S") + backup_path = os.path.join(BackupManager.BACKUP_DIR, f"parch-driver-backup-{timestamp}") + os.makedirs(backup_path, exist_ok=True) + + pkg_list = BackupManager._get_driver_packages() + with open(os.path.join(backup_path, "pkglist.txt"), "w") as f: + f.write("\n".join(pkg_list)) + + code, modules_out, _ = SystemProber.run_command(["lsmod"]) + if code == 0: + with open(os.path.join(backup_path, "modules.txt"), "w") as f: + f.write(modules_out) + + hardware = SystemProber.get_hardware_devices() + with open(os.path.join(backup_path, "hardware.json"), "w") as f: + json.dump(hardware, f, indent=2, default=str) + + sys_info = { + "kernel": SystemProber.get_kernel_info(), + "system": SystemProber.get_system_info(), + "date": timestamp, + "driver_count": len(pkg_list) + } + with open(os.path.join(backup_path, "metadata.json"), "w") as f: + json.dump(sys_info, f, indent=2) + + return True, backup_path + + except Exception as e: + return False, str(e) + + @staticmethod + def _get_driver_packages() -> List[str]: + drivers = set() + all_profiles = DriverProfiles.get_all_profiles("Unknown", "default") + for profile in all_profiles: + for pkg in profile.packages: + drivers.add(pkg) + return sorted(list(drivers)) + + @staticmethod + def restore_backup(backend: BackendRunner, backup_path: str) -> Tuple[bool, str]: + try: + # 1. خواندن لیست پکیج‌ها + pkglist_path = os.path.join(backup_path, "pkglist.txt") + if not os.path.exists(pkglist_path): + return False, "pkglist.txt Not Found." + + with open(pkglist_path, "r") as f: + packages = f.read().strip().splitlines() + + if not packages: + return False, "Packages List Is Empty." + + backend.run(["pacman", "-S", "--needed", "--noconfirm"] + packages, check=True) + + modules_path = os.path.join(backup_path, "modules.txt") + if os.path.exists(modules_path): + pass + + return True, f"{len(packages)} Package Installed." + + except Exception as e: + return False, str(e) + + @staticmethod + def list_backups() -> List[Dict[str, Any]]: + BackupManager._ensure_backup_dir() + backups = [] + + for item in os.listdir(BackupManager.BACKUP_DIR): + path = os.path.join(BackupManager.BACKUP_DIR, item) + if os.path.isdir(path) and item.startswith("parch-driver-backup-"): + metadata = {} + meta_path = os.path.join(path, "metadata.json") + if os.path.exists(meta_path): + try: + with open(meta_path, "r") as f: + metadata = json.load(f) + except: + pass + + backups.append({ + "name": item, + "path": path, + "date": metadata.get("date", item.split("-")[-1]), + "driver_count": metadata.get("driver_count", 0), + "size": BackupManager._get_dir_size(path) + }) + + return sorted(backups, key=lambda x: x["date"], reverse=True) + + @staticmethod + def _get_dir_size(path: str) -> str: + total = 0 + for dirpath, _, filenames in os.walk(path): + for f in filenames: + fp = os.path.join(dirpath, f) + if os.path.exists(fp): + total += os.path.getsize(fp) + + if total < 1024: + return f"{total} B" + elif total < 1024 * 1024: + return f"{total / 1024:.1f} KB" + elif total < 1024 * 1024 * 1024: + return f"{total / (1024 * 1024):.1f} MB" + else: + return f"{total / (1024 * 1024 * 1024):.2f} GB" + + @staticmethod + def delete_backup(backup_path: str) -> Tuple[bool, str]: + try: + if os.path.exists(backup_path): + shutil.rmtree(backup_path) + return True, "Backup Deleted" + return False, "Backup Folder Not Found" + except Exception as e: + return False, str(e) From ef286ae43dd6845e77e7970f30ac290e46423f43 Mon Sep 17 00:00:00 2001 From: MehradDraco Date: Mon, 3 Aug 2026 18:06:58 +0330 Subject: [PATCH 2/2] Update window.py --- src/parch_driver_manager/ui/window.py | 174 +++++++++++++++++++++++++- 1 file changed, 168 insertions(+), 6 deletions(-) diff --git a/src/parch_driver_manager/ui/window.py b/src/parch_driver_manager/ui/window.py index 250481c..c614477 100644 --- a/src/parch_driver_manager/ui/window.py +++ b/src/parch_driver_manager/ui/window.py @@ -14,6 +14,7 @@ from ..backend import BackendRunner from ..manager import DriverManager from ..profiles import DriverProfiles, DriverProfile +from ..backup_manager import BackupManager from .settings import _, is_rtl from .widgets import LogBuffer @@ -81,7 +82,6 @@ def _build_ui(self): self.toast_overlay = Adw.ToastOverlay() self.set_content(self.toast_overlay) - # Overlay Split View (standard GNOME collapsible sidebar) self.split_view = Adw.OverlaySplitView() self.split_view.set_min_sidebar_width(240) self.split_view.set_max_sidebar_width(300) @@ -89,12 +89,10 @@ def _build_ui(self): self.split_view.set_show_sidebar(True) self.toast_overlay.set_child(self.split_view) - # Responsive Mobile Breakpoint bp_mobile = Adw.Breakpoint.new(Adw.BreakpointCondition.parse("max-width: 720px")) bp_mobile.add_setter(self.split_view, "collapsed", True) self.add_breakpoint(bp_mobile) - # Sidebar Panel sidebar_toolbar = Adw.ToolbarView() self.split_view.set_sidebar(sidebar_toolbar) @@ -132,7 +130,6 @@ def _build_ui(self): self._populate_sidebar() - # Content Panel content_toolbar = Adw.ToolbarView() self.split_view.set_content(content_toolbar) @@ -217,6 +214,7 @@ def _populate_sidebar(self): (_("Audio"), "audio-card-symbolic", "PipeWire / ALSA", _("Audio Stack")), (_("Bluetooth"), "preferences-system-bluetooth-symbolic", bt_sub, _("Bluetooth Services")), (_("System Info"), "preferences-system-symbolic", self.system_info.get("os", "Parch GNU/Linux"), _("Hardware and OS Details")), + (_("Backup"), "drive-harddisk-symbolic", _("Driver backups"), _("Manage driver backups")), (_("Logs"), "text-x-generic-symbolic", _("Operation logs"), _("Activity and Event Logs")) ] @@ -224,7 +222,6 @@ def _populate_sidebar(self): row = Adw.ActionRow(title=cat, subtitle=sub) row.set_icon_name(icon) - # Active indicator badge if cat in (_("GPU"), _("Network"), _("Audio"), _("Bluetooth")): active_icon = Gtk.Image.new_from_icon_name("emblem-ok-symbolic") active_icon.add_css_class("success-icon") @@ -245,6 +242,7 @@ def _rebuild_all_pages(self): self._build_audio_page() self._build_bluetooth_page() self._build_info_page() + self._build_backup_page() self._build_logs_page() def _is_profile_installed(self, profile: DriverProfile) -> bool: @@ -668,6 +666,170 @@ def _build_info_page(self): self.stack.add_named(scrolled, _("System Info")) + + def _build_backup_page(self): + scrolled = Gtk.ScrolledWindow() + scrolled.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) + scrolled.set_vexpand(True) + + clamp = Adw.Clamp() + clamp.set_maximum_size(840) + clamp.set_margin_top(16) + clamp.set_margin_bottom(16) + clamp.set_margin_start(12) + clamp.set_margin_end(12) + scrolled.set_child(clamp) + + main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=20) + main_box.set_vexpand(False) + clamp.set_child(main_box) + + actions_group = Adw.PreferencesGroup( + title=_("Backup Operations"), + description=_("Create and restore driver backups") + ) + main_box.append(actions_group) + + actions_grid = Gtk.Grid() + actions_grid.set_column_spacing(12) + actions_grid.set_row_spacing(12) + actions_grid.set_margin_top(12) + actions_grid.set_margin_bottom(12) + actions_grid.set_margin_start(12) + actions_grid.set_margin_end(12) + actions_group.add(actions_grid) + + backup_btn = Gtk.Button(label=_("Create Backup")) + backup_btn.add_css_class("suggested-action") + backup_btn.connect("clicked", self._on_create_backup) + actions_grid.attach(backup_btn, 0, 0, 1, 1) + + restore_btn = Gtk.Button(label=_("Restore Backup")) + restore_btn.connect("clicked", self._on_restore_backup) + actions_grid.attach(restore_btn, 1, 0, 1, 1) + + delete_btn = Gtk.Button(label=_("Delete Backup")) + delete_btn.add_css_class("destructive-action") + delete_btn.connect("clicked", self._on_delete_backup) + actions_grid.attach(delete_btn, 2, 0, 1, 1) + + list_group = Adw.PreferencesGroup( + title=_("Existing Backups"), + description=_("List of available driver backups") + ) + main_box.append(list_group) + + self.backup_listbox = Gtk.ListBox() + self.backup_listbox.set_selection_mode(Gtk.SelectionMode.SINGLE) + self.backup_listbox.add_css_class("boxed-list") + list_group.add(self.backup_listbox) + + self._refresh_backup_list() + + self.stack.add_named(scrolled, _("Backup")) + + def _refresh_backup_list(self): + child = self.backup_listbox.get_first_child() + while child is not None: + next_child = child.get_next_sibling() + self.backup_listbox.remove(child) + child = next_child + + backups = BackupManager.list_backups() + + if not backups: + row = Adw.ActionRow( + title=_("No backups found"), + subtitle=_("Create your first backup using the button above") + ) + self.backup_listbox.append(row) + return + + for backup in backups: + row = Adw.ActionRow( + title=backup["name"], + subtitle=f"{backup['driver_count']} drivers • {backup['size']} • {backup['date']}" + ) + row.set_activatable(True) + self.backup_listbox.append(row) + + + def _on_create_backup(self, _button): + def task(): + GLib.idle_add(self._start_operation) + self.log_buffer.append("➔ Creating backup...") + success, result = BackupManager.create_backup(self.backend) + if success: + GLib.idle_add(self._refresh_backup_list) + GLib.idle_add(self._show_toast, f"✅ Backup created: {os.path.basename(result)}") + self.log_buffer.append(f"✅ Backup created: {result}") + else: + GLib.idle_add(self._show_toast, f"❌ Backup failed: {result}") + self.log_buffer.append(f"❌ Backup failed: {result}") + GLib.idle_add(self._end_operation) + + self._run_in_thread(task) + + def _on_restore_backup(self, _button): + selected = self.backup_listbox.get_selected_row() + if not selected: + self._show_toast(_("Please select a backup first")) + return + + backup_name = selected.get_title() + backup_path = os.path.join(BackupManager.BACKUP_DIR, backup_name) + + self._show_confirm_dialog( + _("Restore Backup?"), + f"{_('This will install drivers from')} {backup_name}", + lambda: self._do_restore(backup_path) + ) + + def _do_restore(self, backup_path: str): + def task(): + GLib.idle_add(self._start_operation) + self.log_buffer.append(f"➔ Restoring from: {backup_path}") + success, result = BackupManager.restore_backup(self.backend, backup_path) + if success: + GLib.idle_add(self._show_toast, f"✅ Restore completed: {result}") + self.log_buffer.append(f"✅ Restore completed: {result}") + else: + GLib.idle_add(self._show_toast, f"❌ Restore failed: {result}") + self.log_buffer.append(f"❌ Restore failed: {result}") + GLib.idle_add(self._end_operation) + + self._run_in_thread(task) + + def _on_delete_backup(self, _button): + selected = self.backup_listbox.get_selected_row() + if not selected: + self._show_toast(_("Please select a backup first")) + return + + backup_name = selected.get_title() + backup_path = os.path.join(BackupManager.BACKUP_DIR, backup_name) + + self._show_confirm_dialog( + _("Delete Backup?"), + f"{_('This will permanently delete')} {backup_name}", + lambda: self._do_delete(backup_path), + destructive=True + ) + + def _do_delete(self, backup_path: str): + def task(): + GLib.idle_add(self._start_operation) + success, result = BackupManager.delete_backup(backup_path) + if success: + GLib.idle_add(self._refresh_backup_list) + self._show_toast("✅ Backup deleted") + else: + self._show_toast(f"❌ Delete failed: {result}") + GLib.idle_add(self._end_operation) + + self._run_in_thread(task) + + def _build_logs_page(self): scrolled_outer = Gtk.ScrolledWindow() scrolled_outer.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) @@ -728,6 +890,7 @@ def on_sidebar_selected(self, listbox, row): _("Audio"): _("Audio Stack"), _("Bluetooth"): _("Bluetooth Services"), _("System Info"): _("Hardware and OS Details"), + _("Backup"): _("Manage driver backups"), _("Logs"): _("Activity and Event Logs") } subtitle = subtitles.get(title, _("Parch Driver Manager")) @@ -736,7 +899,6 @@ def on_sidebar_selected(self, listbox, row): self.stack.set_visible_child_name(title) self.current_category = title - # Close overlay sidebar automatically in mobile collapsed mode if self.split_view.get_collapsed(): self.split_view.set_show_sidebar(False)