From 0fed2b0f7cd34dd6cfdc0a13d2d49edaeb71ed89 Mon Sep 17 00:00:00 2001 From: Heliguy Date: Tue, 31 Oct 2023 06:22:11 -0400 Subject: [PATCH] Implement abilty to create snapshots of flatpaks --- src/clock-alt-symbolic.svg | 2 + src/common.py | 6 +- src/remotes.blp | 54 ++++++------- src/snapshots.blp | 55 ++++++++----- src/snapshots_window.py | 153 +++++++++++++++++++++++++++++++++++- src/warehouse.gresource.xml | 1 + src/window.py | 2 +- 7 files changed, 222 insertions(+), 51 deletions(-) create mode 100644 src/clock-alt-symbolic.svg diff --git a/src/clock-alt-symbolic.svg b/src/clock-alt-symbolic.svg new file mode 100644 index 0000000..2cb076c --- /dev/null +++ b/src/clock-alt-symbolic.svg @@ -0,0 +1,2 @@ + + diff --git a/src/common.py b/src/common.py index a7f591f..9ccd1d5 100644 --- a/src/common.py +++ b/src/common.py @@ -15,11 +15,13 @@ class myUtils: def trashFolder(self, path): if not os.path.exists(path): + print("error in common.trashFolder: path does not exists. path =", path) return 1 try: - subprocess.run(["flatpak-spawn", "--host", "gio", "trash", path], capture_output=True, check=True, env=self.new_env) + subprocess.run(["gio", "trash", path], capture_output=False, check=True, env=self.new_env) return 0 - except subprocess.CalledProcessError: + except subprocess.CalledProcessError as e: + print("error in common.trashFolder: CalledProcessError:", e) return 2 def getSizeWithFormat(self, path): diff --git a/src/remotes.blp b/src/remotes.blp index 6a748db..b97837c 100644 --- a/src/remotes.blp +++ b/src/remotes.blp @@ -17,37 +17,37 @@ template RemotesWindow : Adw.Window { } content: Adw.ToastOverlay toast_overlay { - Stack stack { - Overlay main_overlay { - [overlay] - ProgressBar progress_bar { - visible: false; - pulse-step: 0.7; - can-target: false; - styles ["osd"] - } - ScrolledWindow scroll { - vexpand: true; - Adw.Clamp{ - ListBox remotes_list { - margin-top: 12; - margin-bottom: 12; - margin-start: 12; - margin-end: 12; - hexpand: true; - valign: start; - selection-mode: none; - styles["boxed-list"] + Stack stack { + Overlay main_overlay { + [overlay] + ProgressBar progress_bar { + visible: false; + pulse-step: 0.7; + can-target: false; + styles ["osd"] + } + ScrolledWindow scroll { + vexpand: true; + Adw.Clamp{ + ListBox remotes_list { + margin-top: 12; + margin-bottom: 12; + margin-start: 12; + margin-end: 12; + hexpand: true; + valign: start; + selection-mode: none; + styles["boxed-list"] + } } } } + Adw.StatusPage no_remotes { + icon-name: "error-symbolic"; + title: _("No Remotes"); + description: _("Warehouse cannot see the list of remotes or the system has no remotes added"); + } } - Adw.StatusPage no_remotes { - icon-name: "error-symbolic"; - title: _("No Remotes"); - description: _("Warehouse cannot see the list of remotes or the system has no remotes added"); - } - } }; } } \ No newline at end of file diff --git a/src/snapshots.blp b/src/snapshots.blp index 935dcc7..6451c52 100644 --- a/src/snapshots.blp +++ b/src/snapshots.blp @@ -3,43 +3,58 @@ using Adw 1; template SnapshotsWindow : Adw.Window { default-width: 500; - default-height: 450; + default-height: 455; modal: true; Adw.ToolbarView main_toolbar_view { [top] HeaderBar header_bar { [start] - Button new_button { + Button new_snapshot { Adw.ButtonContent { label: _("New Snapshot"); icon-name: "plus-large-symbolic"; } } + [end] + Button oepn_folder_button { + icon-name: "document-open-symbolic"; + tooltip-text: _("Open Snapshots Folder"); + } } content: Adw.ToastOverlay toast_overlay { - Stack main_stack { - Overlay main_overlay { - [overlay] - ProgressBar progress_bar { - pulse-step: 0.7; - can-target: false; - styles["osd"] - } - - Adw.PreferencesPage outerbox { - - Adw.PreferencesGroup { - Adw.SwitchRow mask_row { - title: _("Disable Updates"); - active: true; + Overlay main_overlay { + [overlay] + ProgressBar progress_bar { + pulse-step: 0.7; + can-target: false; + visible: false; + styles["osd"] + } + Stack main_stack { + ScrolledWindow outerbox { + Adw.Clamp { + ListBox snapshots_group { + margin-top: 12; + margin-bottom: 12; + margin-start: 12; + margin-end: 12; + valign: start; + selection-mode: none; + styles["boxed-list"] } } + } + Adw.StatusPage no_snapshots { + title: _("No Snapshots"); + description: _("Snapshots are backups of the app's user data. They can be reapplied at any time."); + icon-name: "clock-alt-symbolic"; - Adw.PreferencesGroup versions_group { - title: _("Select a Release"); - description: _("This will uninstall the current release and install the chosen one instead. Note that downgrading can cause issues."); + Button new_snapshot_pill { + label: _("New Snapshot"); + halign: center; + styles["pill", "suggested-action"] } } } diff --git a/src/snapshots_window.py b/src/snapshots_window.py index 8a7c076..8a3cfe9 100644 --- a/src/snapshots_window.py +++ b/src/snapshots_window.py @@ -3,6 +3,7 @@ from .common import myUtils import subprocess import os import pathlib +import time @Gtk.Template(resource_path="/io/github/flattool/Warehouse/snapshots.ui") class SnapshotsWindow(Adw.Window): @@ -10,8 +11,158 @@ class SnapshotsWindow(Adw.Window): new_env = dict( os.environ ) new_env['LC_ALL'] = 'C' + host_home = str(pathlib.Path.home()) + user_data_path = host_home + "/.var/app/" + snapshots_path = host_home + "/.var/app/io.github.flattool.Warehouse/data/Snapshots/" + + snapshots_group = Gtk.Template.Child() + main_stack = Gtk.Template.Child() + no_snapshots = Gtk.Template.Child() + new_snapshot = Gtk.Template.Child() + new_snapshot_pill = Gtk.Template.Child() + oepn_folder_button = Gtk.Template.Child() + toast_overlay = Gtk.Template.Child() + outerbox = Gtk.Template.Child() + progress_bar = Gtk.Template.Child() + should_pulse = False + + def pulser(self): + self.progress_bar.pulse() + GLib.timeout_add(500, self.pulser) + + def generateList(self): + if not os.path.exists(self.snapshots_of_app_path): + # Show no snapshots page if the folder is not there + self.main_stack.set_visible_child(self.no_snapshots) + self.oepn_folder_button.set_sensitive(False) + return + + snapshot_files = os.listdir(self.snapshots_of_app_path) + to_trash = [] + + for i in range(len(snapshot_files)): + if not snapshot_files[i].endswith(".tar.zst"): + # Find all files that aren't snapshots + to_trash.append(snapshot_files[i]) + + for i in range(len(to_trash)): + # Delete all files that aren't snapshots + a = self.my_utils.trashFolder(f"{self.snapshots_of_app_path}{to_trash[i]}") + if a == 0: + snapshot_files.remove(to_trash[i]) + + if len(snapshot_files) == 0: + self.main_stack.set_visible_child(self.no_snapshots) + return + + for i in range(len(snapshot_files)): + self.create_row(snapshot_files[i]) + + def create_row(self, file): + size = self.my_utils.getSizeWithFormat(self.snapshots_of_app_path + file) + split_file = file.removesuffix(".tar.zst").split("_") + time = GLib.DateTime.new_from_unix_local(int(split_file[0])).format("%x %X") + row = Adw.ActionRow(title=time, subtitle=size) + + label = Gtk.Label(label=_("Version {}").format(split_file[1]), hexpand=True, wrap=True, justify=Gtk.Justification.RIGHT) + row.add_suffix(label) + + apply = Gtk.Button(icon_name="check-plain-symbolic", valign=Gtk.Align.CENTER) + apply.add_css_class("flat") + row.add_suffix(apply) + + trash = Gtk.Button(icon_name="user-trash-symbolic", valign=Gtk.Align.CENTER) + trash.connect("clicked", self.trash_snapshot, file, row) + trash.add_css_class("flat") + row.add_suffix(trash) + self.snapshots_group.insert(row, 0) + self.main_stack.set_visible_child(self.outerbox) + self.oepn_folder_button.set_sensitive(True) + + def trash_snapshot(self, button, file, row): + def on_response(dialog, response, func): + if response == "cancel": + return + a = self.my_utils.trashFolder(self.snapshots_of_app_path + file) + if a == 0: + self.snapshots_group.remove(row) + if not self.snapshots_group.get_row_at_index(0): + self.main_stack.set_visible_child(self.no_snapshots) + self.my_utils.trashFolder(self.snapshots_of_app_path) + self.oepn_folder_button.set_sensitive(False) + else: + self.toast_overlay.add_toast(Adw.Toast.new(_("Could not trash snapshot"))) + + dialog = Adw.MessageDialog.new(self, _("Trash Snapshot?"), _("This snapshot and its contents will be sent to the trash.")) + dialog.add_response("cancel", _("Cancel")) + dialog.set_close_response("cancel") + dialog.add_response("continue", _("Trash Snapshot")) + dialog.set_response_appearance("continue", Adw.ResponseAppearance.DESTRUCTIVE) + dialog.connect("response", on_response, dialog.choose_finish) + dialog.present() + + def createSnapshot(self): + epoch = int(time.time()) + + def thread(): + subprocess.run( + ['tar', 'caf', f"{self.snapshots_of_app_path}{epoch}_{self.app_version}.tar.zst", "-C", f"{self.app_user_data}", "."], + check=True, env=self.new_env + ) + + # `tar -tf filepath` to see the contents of a tar file + + def callback(): + self.create_row(f"{epoch}_{self.app_version}.tar.zst") + self.new_snapshot.set_sensitive(True) + self.new_snapshot_pill.set_sensitive(True) + self.progress_bar.set_visible(False) + + if not os.path.exists(self.snapshots_of_app_path): + file = Gio.File.new_for_path(self.snapshots_of_app_path) + file.make_directory() + + self.new_snapshot.set_sensitive(False) + self.new_snapshot_pill.set_sensitive(False) + self.progress_bar.set_visible(True) + + task = Gio.Task.new(None, None, lambda *_: callback()) + task.run_in_thread(lambda *_: thread()) + + def open_button_handler(self, widget, path): + try: + Gio.AppInfo.launch_default_for_uri(f"file://{path}", None) + except GLib.GError: + self.toast_overlay.add_toast(Adw.Toast.new(_("Could not open folder"))) def __init__(self, parent_window, flatpak_row, **kwargs): super().__init__(**kwargs) - self.present() \ No newline at end of file + # Variables + self.my_utils = myUtils(self) + self.app_name = flatpak_row[0] + self.app_id = flatpak_row[2] + self.app_version = flatpak_row[3] + self.app_ref = flatpak_row[8] + self.snapshots_of_app_path = self.snapshots_path + self.app_id + "/" + self.app_user_data = self.user_data_path + self.app_id + "/" + + if self.app_version == "" or self.app_version == "-" or self.app_version == None: + self.app_version = 0.0 + + if not os.path.exists(self.snapshots_path): + # Create snapshots folder if none exists + file = Gio.File.new_for_path(self.snapshots_path) + file.make_directory() + + # Calls + self.generateList() + self.oepn_folder_button.connect("clicked", self.open_button_handler, self.snapshots_of_app_path) + self.new_snapshot.connect("clicked", lambda *_: self.createSnapshot()) + self.new_snapshot_pill.connect("clicked", lambda *_: self.createSnapshot()) + self.pulser() + + # Window stuffs + self.set_title(_("{} Snapshots").format(self.app_name)) + self.set_transient_for(parent_window) + self.set_size_request(0, 230) \ No newline at end of file diff --git a/src/warehouse.gresource.xml b/src/warehouse.gresource.xml index 97acb5c..134896a 100644 --- a/src/warehouse.gresource.xml +++ b/src/warehouse.gresource.xml @@ -25,5 +25,6 @@ funnel-symbolic.svg right-large-symbolic.svg view-more-symbolic.svg + clock-alt-symbolic.svg diff --git a/src/window.py b/src/window.py index 4db57c8..2dd95a0 100644 --- a/src/window.py +++ b/src/window.py @@ -381,7 +381,7 @@ class WarehouseWindow(Adw.ApplicationWindow): open_data_item.set_attribute_value("hidden-when", GLib.Variant.new_string("action-disabled")) data_menu_model.append_item(open_data_item) - self.create_action(("snapshot" + str(index)), lambda *_, row=self.flatpak_rows[index]: SnapshotsWindow(self, row)) + self.create_action(("snapshot" + str(index)), lambda *_, row=self.flatpak_rows[index][6]: SnapshotsWindow(self, row).present()) snapshot_item = Gio.MenuItem.new(_("Manage Snapshots"), f"win.snapshot{index}") snapshot_item.set_attribute_value("hidden-when", GLib.Variant.new_string("action-dsiabled")) data_menu_model.append_item(snapshot_item)