diff --git a/meson.build b/meson.build index ab36c0a9..5b33babc 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('savedesktop', - version: '3.8', + version: '3.8.1', meson_version: '>= 1.0.0', default_options: [ 'warning_level=2', 'werror=false', ], ) diff --git a/po/savedesktop.pot b/po/savedesktop.pot index 29446f84..f9fc823d 100644 --- a/po/savedesktop.pot +++ b/po/savedesktop.pot @@ -255,7 +255,7 @@ msgstr "" #: src/gui/window.py msgid "" -"{}\nYou can log out of the system for the changes to take effect, or go back to the previous page and log out later.\n" +"{}\nYou can log out of the system for the changes to take effect, or go back to the previous page and log out later.\n\nIf your archive contains Flatpak apps, they will start installing after the next login." msgstr "" #: src/gui/window.py diff --git a/src/core/archive.py b/src/core/archive.py index 43834461..91b8ad90 100644 --- a/src/core/archive.py +++ b/src/core/archive.py @@ -50,9 +50,6 @@ def start_saving(self): else: self._create_archive() - print("Moving the configuration archive to the user-defined directory") - shutil.copyfile('cfg.sd.zip', self.path_with_filename) - print("Configuration saved successfully.") remove_temp_file() @@ -65,7 +62,7 @@ def _copy_config_to_folder(self): # Create a new ZIP archive with 7-Zip def _create_archive(self): - cmd = ['7z', 'a', '-tzip', '-mx=3', '-x!*.zip', '-x!saving_status', 'cfg.sd.zip', '.'] + cmd = ['7z', 'a', '-snL', '-mx=3', '-x!*.zip', '-x!saving_status', 'cfg.sd.zip', "."] if settings["enable-encryption"] or os.path.exists(f"{CACHE}/pb"): if password: cmd.insert(4, "-mem=AES256") @@ -80,6 +77,9 @@ def _create_archive(self): else: print("7z finished with warnings:", proc.stderr) + print("Moving the configuration archive to the user-defined directory") + shutil.copyfile('cfg.sd.zip', self.path_with_filename) + class Unpack: def __init__(self, dir_path): self.path_with_filename = dir_path @@ -141,11 +141,24 @@ def _unpack_zip_archive(self): raise ValueError(first_error or "Wrong password") print("Checking password is completed.") - cmd = subprocess.run( - ['7z', 'x', '-y', f'-p{password}', self.path_with_filename, f'-o{TEMP_CACHE}'], - capture_output=True, text=True, check=True + cmd = ['7z', 'x', '-y', '-snL'] + + if password: + cmd.append(f'-p{password}') + + cmd.extend([ + self.path_with_filename, + f'-o{TEMP_CACHE}' + ]) + + proc = subprocess.run( + cmd, + capture_output=True, + text=True ) - print(cmd.stdout) + + if proc.returncode >= 7: + raise OSError(proc.stderr) def __handle_sync_error(self): # If 7-Zip returns an error regarding an incorrect password and the {CACHE}/sync file diff --git a/src/core/de_config.py b/src/core/de_config.py index f653dfa3..bb535f35 100644 --- a/src/core/de_config.py +++ b/src/core/de_config.py @@ -1,6 +1,7 @@ -import os, shutil, argparse +import os, shutil, argparse, subprocess from gi.repository import GLib from savedesktop.globals import * +from pathlib import Path # Helping functions def safe_copy(src: str, dst: str): @@ -17,7 +18,7 @@ def safe_copy(src: str, dst: str): def safe_copytree(src: str, dst: str, ignore=None): if os.path.isdir(src): try: - shutil.copytree(src, dst, dirs_exist_ok=True, ignore=ignore) + shutil.copytree(src, dst, dirs_exist_ok=True, symlinks=True, ignore=ignore) print(f"[OK] Copied dir: {src} → {dst}") except Exception as e: print(f"[ERR] Dir copy failed {src} → {dst}: {e}") @@ -45,6 +46,9 @@ def safe_copytree(src: str, dst: str, ignore=None): ("xdg-data", f"{home}/.local/share"), ] +# The ~/Desktop folder definition +desktop_dir = Path(GLib.get_user_special_dir(GLib.UserDirectory.DIRECTORY_DESKTOP)) + class Save: def __init__(self): @@ -71,24 +75,27 @@ def __init__(self): safe_copytree(f"{home}/.local/share/backgrounds", "backgrounds") safe_copytree(f"{home}/.local/share/wallpapers", "xdg-data/wallpapers") if settings["save-icons"]: - safe_copytree(f"{home}/.icons", ".icons") - safe_copytree(f"{home}/.local/share/icons", "icons") + xdg_path = f"{home}/.local/share" + legacy_path = f"{home}" + if os.path.isdir(f"{xdg_path}/icons") and os.listdir(f"{xdg_path}/icons"): + subprocess.run(["tar", "-czf", "icon-themes.tgz", "-C", xdg_path, "icons"]) + if os.path.isdir(f"{legacy_path}/.icons") and os.listdir(f"{legacy_path}/.icons"): + subprocess.run(["tar", "-czf", "icon-themes-legacy.tgz", "-C", legacy_path, ".icons"]) + print("[OK] Saving icons") # Desktop folder if settings["save-desktop-folder"]: - desktop_dir = GLib.get_user_special_dir(GLib.UserDirectory.DIRECTORY_DESKTOP) - if desktop_dir: - safe_copytree(desktop_dir, "Desktop") + subprocess.run(["tar", "-czf", "desktop-folder.tgz", "-C", str(desktop_dir.parent), str(desktop_dir.name)]) safe_copytree(f"{home}/.local/share/gvfs-metadata", "gvfs-metadata") + print("[OK] Saving ~/Desktop folder") # Flatpak apps and their data if flatpak: if settings["save-installed-flatpaks"]: - print("saving list of installed Flatpak apps") os.system("ls /var/lib/flatpak/app/ | awk '{print \"flatpak install --system \" $1 \" -y\"}' > ./installed_flatpaks.sh") os.system("ls ~/.local/share/flatpak/app | awk '{print \"flatpak install --user \" $1 \" -y\"}' > ./installed_user_flatpaks.sh") + print("[OK] Saving list of installed Flatpak apps") if settings["save-flatpak-data"]: - print("saving user data of installed Flatpak apps") self.save_flatpak_data() # Environment specific @@ -116,24 +123,15 @@ def save_flatpak_data(self): gsettings = settings["disabled-flatpak-apps-data"] black_list = gsettings # convert GSettings property to a list - os.makedirs(f"{CACHE}/workspace/app", exist_ok=True) - destdir = f"{CACHE}/workspace/app" - - # copy Flatpak apps data - for item in os.listdir(f"{home}/.var/app"): - if item not in black_list and item != "cache": - source_path = os.path.join(f"{home}/.var/app", item) - destination_path = os.path.join(destdir, item) - if os.path.isdir(source_path): - try: - shutil.copytree(source_path, destination_path, ignore=shutil.ignore_patterns('cache')) - except Exception as e: - print(f"Error copying directory {source_path}: {e}") - else: - try: - shutil.copy2(source_path, destination_path) - except Exception as e: - print(f"Error copying file {source_path}: {e}") + cmd = ["tar", "-czf", "flatpak-apps-data.tgz", "--exclude=*/cache"] + + for apps in black_list: + cmd.append(f"--exclude={apps}") + + cmd.extend(["-C", f"{home}/.var", "app"]) + + subprocess.run(cmd) + print("[OK] Saving Flatpak apps' data") class Import: def __init__(self): @@ -156,12 +154,11 @@ def __init__(self): safe_copytree("gnome-shell", f"{home}/.local/share/gnome-shell") safe_copytree("cinnamon", f"{home}/.local/share/cinnamon") safe_copytree("plasma", f"{home}/.local/share/plasma") - safe_copytree("Desktop", GLib.get_user_special_dir(GLib.UserDirectory.DIRECTORY_DESKTOP)) safe_copytree("gvfs-metadata", f"{home}/.local/share/gvfs-metadata") safe_copytree("backgrounds", f"{home}/.local/share/backgrounds") safe_copytree("wallpapers", f"{home}/.local/share/wallpapers") - safe_copytree("icons", f"{home}/.local/share/icons") - safe_copytree(".icons", f"{home}/.icons") + self.import_desktop_folder() + self.import_icons() # Environment specific if environment: @@ -177,9 +174,29 @@ def __init__(self): print(f"[WARN] Unknown DE: {environment_key}") if flatpak: - if any(os.path.exists(path) for path in ["app", "installed_flatpaks.sh", "installed_user_flatpaks.sh"]): + if any(os.path.exists(path) for path in ["app", "flatpak-apps-data.tgz", "installed_flatpaks.sh", "installed_user_flatpaks.sh"]): self.create_flatpak_autostart() + # Extract an archive with the Desktop folder + def import_desktop_folder(self): + if os.path.exists("desktop-folder.tgz"): + subprocess.run(["tar", "-xzvf", "desktop-folder.tgz", "-C", str(desktop_dir.parent)]) + print("[OK] Extracting a Desktop archive") + else: + safe_copytree("Desktop", GLib.get_user_special_dir(GLib.UserDirectory.DIRECTORY_DESKTOP)) + + # Extract an archive with icon themes + def import_icons(self): + if os.path.exists(f"icon-themes.tgz"): + subprocess.run(["tar", "-xzvf", "icon-themes.tgz", "-C", f"{home}/.local/share/"]) + print("[OK] Extracting a XDG icons archive") + if os.path.exists(f"icon-themes-legacy.tgz"): + print(f"[OK] Extracting a legacy icons archive") + subprocess.run(["tar", "-xzvf", "icon-themes-legacy.tgz", "-C", f"{home}/"]) + else: + safe_copytree("icons", f"{home}/.local/share/icons") + safe_copytree(".icons", f"{home}/.icons") + # Create an autostart file to install Flatpaks from a list after the next login def create_flatpak_autostart(self): os.system(f"cp /app/share/savedesktop/savedesktop/core/flatpaks_installer.py {CACHE}/workspace") diff --git a/src/core/flatpaks_installer.py b/src/core/flatpaks_installer.py index 096d928d..674261bf 100644 --- a/src/core/flatpaks_installer.py +++ b/src/core/flatpaks_installer.py @@ -23,12 +23,16 @@ dest_dir = None # If the destination directory variable is not 'None', continue in installing Flatpak apps -if not dest_dir == None: +if dest_dir: # If the destination directory has a directory with installed Flatpak apps user data, install them if os.path.exists(f"{dest_dir}/app"): print("copying the Flatpak apps' user data to the ~/.var/app directory") os.system(f"cp -au {dest_dir}/app/ ~/.var/") + # If the flatpak-apps-data.tgz archive exists, unpack it to the ~/.var/app directory + if os.path.exists(f"{dest_dir}/flatpak-apps-data.tgz"): + subprocess.run(["tar", "-xzvf", "flatpak-apps-data.tgz", "-C", f"{Path.home()}/.var"]) + # If the Bash scripts for installing Flatpak apps to the system exist, install them if os.path.exists(f"{dest_dir}/installed_flatpaks.sh") or os.path.exists(f"{dest_dir}/installed_user_flatpaks.sh"): print("installing the Flatpak apps on the system") @@ -78,4 +82,4 @@ os.remove(autostart_file) # Remove the cache dir after finishing the operations -shutil.rmtree(f"{CACHE}/workspace") +shutil.rmtree(f"{CACHE_FLATPAK}/workspace") diff --git a/src/gui/window.py b/src/gui/window.py index dea00dbb..48894646 100644 --- a/src/gui/window.py +++ b/src/gui/window.py @@ -49,10 +49,18 @@ def __init__(self, *args, **kwargs): self.headerbar.pack_end(child=self.menu_button) # primary layout + self.scrolled = Gtk.ScrolledWindow() + self.scrolled.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) + self.scrolled.set_vexpand(True) + self.scrolled.set_hexpand(True) + self.toolbarview.set_content(self.scrolled) + self.headapp = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10) - self.headapp.set_valign(Gtk.Align.CENTER) - self.headapp.set_halign(Gtk.Align.CENTER) - self.toolbarview.set_content(self.headapp) + self.headapp.set_valign(Gtk.Align.FILL) + self.headapp.set_halign(Gtk.Align.FILL) + self.headapp.set_vexpand(True) + self.headapp.set_hexpand(True) + self.scrolled.set_child(self.headapp) # A view container for the menu switcher self.stack = Adw.ViewStack(vexpand=True) @@ -512,7 +520,7 @@ def _checkDialog_closed(self, w, response): # Save configuration def save_config(self): - print("Saving the configuration is in progress…\nFull output will be available after finishing this operation.") + print("Saving the configuration is in progress…") self.archive_mode = "--create" self.archive_name = f"{self.folder}/{self.filename_text}" @@ -535,7 +543,7 @@ def _set_status_desc_save(self): # Import configuration def import_config(self): - print("Importing the configuration is in progress…\nFull output will be available after finishing this operation.") + print("Importing the configuration is in progress…") self.toast_overlay.dismiss_all() self.archive_name = self.path_to_import @@ -543,7 +551,7 @@ def import_config(self): self.status_title = _("Importing configuration …\nImporting configuration from:\n{}\n").split('')[0].split('')[-1] self.status_desc = _("Importing configuration …\nImporting configuration from:\n{}\n").format(self.archive_name) self.done_title = _("The configuration has been applied!") - self.done_desc = _("{}\nYou can log out of the system for the changes to take effect, or go back to the previous page and log out later.\n").format(_("The configuration has been applied!")) + self.done_desc = _("{}\nYou can log out of the system for the changes to take effect, or go back to the previous page and log out later.\n\nIf your archive contains Flatpak apps, they will start installing after the next login.").format(_("The configuration has been applied!")) self.please_wait() import_thread = Thread(target=self._call_archive_command) @@ -552,7 +560,7 @@ def import_config(self): def _call_archive_command(self): self.archive_proc = subprocess.Popen( [sys.executable, "-m", "savedesktop.core.archive", self.archive_mode, self.archive_name], - stdout=subprocess.PIPE, + stdout=None, stderr=subprocess.PIPE, text=True, env={**os.environ, "PYTHONPATH": f"{app_prefix}"} @@ -576,10 +584,10 @@ def please_wait(self): # Create box widget for this page self.status_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10) - self.status_box.set_halign(Gtk.Align.CENTER) - self.status_box.set_valign(Gtk.Align.CENTER) + self.status_box.set_vexpand(True) + self.status_box.set_hexpand(True) self.status_box.set_size_request(350, 100) - self.toolbarview.set_content(self.status_box) + self.scrolled.set_child(self.status_box) # Set bold title self.set_title(self.status_title) @@ -684,7 +692,7 @@ def _back_to_main(self, w): self.status_box.remove(widget) def _set_default_widgets_state(self): - self.toolbarview.set_content(self.headapp) + self.scrolled.set_child(self.headapp) self.headerbar.set_title_widget(self.switcher_title) self.set_title("Save Desktop") self.apply_handler = self.break_point.connect("apply", self.__on_break_point_apply) @@ -741,6 +749,8 @@ def on_close(self, w): # Remove this folder to cleanup unnecessary content created during saving, # importing, or syncing the configuration only if these files are not present def remove_cache(self): - files_to_check = [f"{CACHE}/pb", f"{CACHE}/sync", f"{CACHE}/workspace/flatpaks_installer.py"] - if not any(os.path.exists(path) for path in files_to_check): - shutil.rmtree(f"{CACHE}/workspace") + if os.path.exists(f"{CACHE}/workspace"): + files_to_check = [f"{CACHE}/pb", f"{CACHE}/sync", f"{CACHE}/workspace/flatpaks_installer.py"] + if not any(os.path.exists(path) for path in files_to_check): + shutil.rmtree(f"{CACHE}/workspace") +