From 7c68d4d2a29a93ad34e7ee912dcdecbff1279a58 Mon Sep 17 00:00:00 2001 From: Christian Moser Date: Tue, 11 Feb 2025 10:35:29 +0100 Subject: [PATCH] 2025.02.11 10:35:29 (desktop) --- ...install-requirements.sh => msys-install.sh | 6 + sgbackup/_logging.py | 1 + sgbackup/archiver/_archiver.py | 99 +++++- sgbackup/gui/_settingsdialog.py | 187 ++++++++++- sgbackup/gui/_steam.py | 19 +- sgbackup/settings.py | 314 ++++++++++++++---- 6 files changed, 542 insertions(+), 84 deletions(-) rename msys-install-requirements.sh => msys-install.sh (74%) mode change 100755 => 100644 diff --git a/msys-install-requirements.sh b/msys-install.sh old mode 100755 new mode 100644 similarity index 74% rename from msys-install-requirements.sh rename to msys-install.sh index 344fe06..8db445b --- a/msys-install-requirements.sh +++ b/msys-install.sh @@ -1,6 +1,9 @@ #!/bin/bash # vim: syn=sh ts=4 sts=4 sw=4 smartindent expandtab ff=unix +SELF="$( realpath "$0" )" +PROJECT_DIR="$( dirname "$SELF")" + PACKAGES="gtk4 gobject-introspection python-gobject python-rapidfuzz" _install_pkg="" @@ -11,3 +14,6 @@ done pacman -Sy pacman -S --noconfirm $_install_pkg +cd $PROJECT_DIR +pip install --user . + diff --git a/sgbackup/_logging.py b/sgbackup/_logging.py index 0cabc60..52c9fc3 100644 --- a/sgbackup/_logging.py +++ b/sgbackup/_logging.py @@ -26,3 +26,4 @@ if os.path.isfile(settings.logger_conf): logging.config.fileConfig(settings.logger_conf) else: logging.config.fileConfig(os.path.join(os.path.dirname(__file__),"logger.conf")) + diff --git a/sgbackup/archiver/_archiver.py b/sgbackup/archiver/_archiver.py index 7e9194b..9b0f9ac 100644 --- a/sgbackup/archiver/_archiver.py +++ b/sgbackup/archiver/_archiver.py @@ -31,12 +31,15 @@ import time from ..game import Game from ..settings import settings +import logging +logger = logging.getLogger(__name__) class Archiver(GObject): def __init__(self,key:str,name:str,extensions:list[str],description:str|None=None): GObject.__init__(self) self.__key = key self.__name = name + self._logger = logger.getChild("Archiver") if description: self.__description = description else: @@ -67,13 +70,18 @@ class Archiver(GObject): return False def backup(self,game:Game)->bool: + self._logger.info("Backing up {game}".format(game=game.key)) if not game.get_backup_files(): - return + self._logger.warning("No files SaveGame files for game {game}!".format(game=game.key)) + return False + filename = self.generate_new_backup_filename(game) dirname = os.path.dirname(filename) if not os.path.isdir(dirname): os.makedirs(dirname) + self._logger.info("Backing up {game} -> {filename}".format( + game=game.key,filename=filename)) return self.emit('backup',game,filename) def generate_new_backup_filename(self,game:Game)->str: @@ -82,7 +90,7 @@ class Archiver(GObject): game.savegame_subdir, dt.strftime("%Y%m%d-%H%M%S"), "sgbackup", - self.extensions[0][1:])) + self.extensions[0][1:] if self.extensions[0].startswith('.') else self.extensions[0])) return os.path.join(settings.backup_dir,game.savegame_name,game.subdir,basename) def _backup_progress(self,game:Game,fraction:float,message:str|None): @@ -147,11 +155,11 @@ class ArchiverManager(GObject): def archivers(self): return self.__archivers - def _on_archiver_backup_progress_single(self,archiver,game,fraction,message): - pass + def _on_archiver_backup_progress_single(self,game:Game,fraction:float,message:str): + self.emit('backup-game-progress',game,fraction,str) - def _on_archiver_backup_progress_multi(self,archiver,game,fraction,message): - pass + def _on_archiver_backup_progress_multi(self,fraction): + self.emit('backup-progress',fraction) @Signal(name="backup-game-progress",return_type=None,arg_types=(Game,float,str),flags=SignalFlags.RUN_FIRST) @@ -170,10 +178,23 @@ class ArchiverManager(GObject): def do_backup_finished(self): pass - def backup(self,game:Game): + @Signal(name="remove-backup",return_type=None,arg_types=(Game,str),flags=SignalFlags.RUN_FIRST) + def do_remove_backup(self,game,filename): + logger.info("Removing backup \"{filename}\" for {game}".format( + filename=os.path.basename(filename), + game=game.key)) + + if os.path.isfile(filename): + os.unlink(filename) + + def remove_backup(self,game,filename): + self.emit("remove-backup",game,filename) + + def backup(self,game:Game,multi_backups:bool=False): def on_progress(archiver,game,fraction,message): self.emit("backup-game-progress",game,fraction,message) - self.emit("backup-progress",fraction) + if not multi_backups: + self.emit("backup-progress",fraction) if self.backup_in_progress: raise RuntimeError("A backup is already in progress!!!") @@ -184,19 +205,41 @@ class ArchiverManager(GObject): backup_sc = archiver.connect('backup-progress',on_progress) archiver.backup(game) archiver.disconnect(backup_sc) + if game.is_live and settings.backup_versions > 0: + backups = sorted(self.get_live_backups(game)) + if backups and len(backups) > settings.backup_versions: + for filename in backups[settings.backup_versions:]: + self.remove_backup(game,filename) + + self.emit("backup-game-finished",game) - self.emit("backup-finished") + if not multi_backups: + self.emit("backup-finished") self.backup_in_progress = False def backup_many(self,games:list[Game]): + def on_game_progress(game,fraction,message,game_progress): + with game_progress._mutex: + game_progress[game.key] = fraction + sum_fractions = 0.0 + for f in game_progress.values(): + sum_fractions += f + n = len(game_progress) + + self._on_archiver_backup_progress_multi(sum_fractions/n if n > 0 else 0.0) + + def thread_function(game): archiver = self.standard_archiver - self._on_archiver_backup_progress_multi(archiver,game,1.0,"Finished ...") + self.backup(game,True) if self.backup_in_progress: raise RuntimeError("A backup is already in progress!!!") self.backup_in_progress = True game_list = list(games) + game_progress = dict(((game.key,0.0) for game in game_list)) + game_progress._mutex = threading.RLock() + game_progress._game_progress_connection = self.connect('backup-game-progress',game_progress) threadpool = {} if len(game_list) > 8: @@ -226,7 +269,18 @@ class ArchiverManager(GObject): thread = threading.Thread(thread_function,args=game,daemon=True) threadpool.append(thread) thread.start() + + self.disconnect(game_progress._game_progress_connection) + self.emit("backup-finished") self.backup_in_progress = False + + def _on_archiver_backup(self,archiver:Archiver,game:Game,filename:str)->bool: + return self.emit('backup',archiver,game,filename) + + @Signal(name="backup",return_type=bool,arg_types=(Archiver,Game,str), + flags=SignalFlags.RUN_FIRST,accumulator=signal_accumulator_true_handled) + def backup(self,archiver,game,filename): + return True def is_archive(self,filename)->bool: if self.standard_archiver.is_archive(filename): @@ -236,6 +290,28 @@ class ArchiverManager(GObject): return True return False + def get_live_backups(self,game:Game): + ret = [] + backupdir = os.path.join(settings.backup_dir,game.savegame_name,'live') + + if os.path.isdir(backupdir): + for basename in os.listdir(backupdir): + filename = os.path.join(backupdir,basename) + if (self.is_archive(filename)): + ret.append(filename) + return ret + + def get_finished_backups(self,game:Game): + ret=[] + backupdir = os.path.join(settings.backup_dir,game.savegame_name,'finished') + + if os.path.isdir(backupdir): + for basename in os.listdir(backupdir): + filename = os.path.join(backupdir,basename) + if (self.is_archive(filename)): + ret.append(filename) + return ret + def get_backups(self,game:Game): ret = [] for backupdir in [os.path.join(settings.backup_dir,game.savegame_name,i) for i in ('live','finished')]: @@ -246,5 +322,4 @@ class ArchiverManager(GObject): ret.append(filename) return ret - - \ No newline at end of file + \ No newline at end of file diff --git a/sgbackup/gui/_settingsdialog.py b/sgbackup/gui/_settingsdialog.py index ffd1ede..4a55e66 100644 --- a/sgbackup/gui/_settingsdialog.py +++ b/sgbackup/gui/_settingsdialog.py @@ -20,6 +20,50 @@ from gi.repository import Gtk,GLib,Gio from gi.repository.GObject import GObject,Signal,Property,SignalFlags from ..settings import settings +from ..archiver import ArchiverManager,Archiver +import zipfile + + +class ArchiverSorter(Gtk.Sorter): + def do_compare(self,item1,item2): + c1 = item1.name.upper() + c2 = item2.name.upper() + + if (c1 > c2): + return Gtk.Ordering.LARGER + elif (c1 < c2): + return Gtk.Ordering.SMALLER + else: + return Gtk.Ordering.EQUAL + +class ZipfileCompressorData(GObject): + def __init__(self,compressor,name,is_standard): + GObject.__init__(self) + self.__compressor = compressor + self.__name = name + self.__is_standard = is_standard + + @Property(type=int) + def compressor(self)->int: + return self.__compressor + + @Property(type=str) + def name(self)->str: + return self.__name + + @Property(type=bool,default=False) + def is_standard(self)->bool: + return self.__is_standard + +class ZipfileCompressorDataSorter(Gtk.Sorter): + def do_compare(self,item1:ZipfileCompressorData,item2:ZipfileCompressorData): + if (item1.name > item2.name): + return Gtk.Ordering.LARGER + elif (item1.name < item2.name): + return Gtk.Ordering.SMALLER + else: + return Gtk.Ordering.EQUAL + class SettingsDialog(Gtk.Dialog): def __init__(self,parent=None): @@ -28,33 +72,48 @@ class SettingsDialog(Gtk.Dialog): self.set_transient_for(parent) self.set_default_size(800,600) vbox = self.get_content_area() + self._widget_set_margin(vbox,4) paned = Gtk.Paned(orientation=Gtk.Orientation.HORIZONTAL) paned.set_position(250) self.__stack = Gtk.Stack() self.__stack_sidebar = Gtk.StackSidebar.new() - self.__add_general_settings_page() - - paned.set_start_child(self.__stack_sidebar) + self.__general_page = self.__add_general_settings_page() + self.__archiver_page = self.__add_archiver_settings_page() + + sidebar_scrolled=Gtk.ScrolledWindow() + sidebar_scrolled.set_child(self.__stack_sidebar) + sidebar_scrolled.set_hexpand(True) + sidebar_scrolled.set_vexpand(True) + paned.set_start_child(sidebar_scrolled) paned.set_end_child(self.__stack) paned.set_vexpand(True) self.__stack_sidebar.set_stack(self.__stack) + vbox.append(paned) self.add_button("Apply",Gtk.ResponseType.APPLY) self.add_button("Cancel",Gtk.ResponseType.CANCEL) + @property + def general_page(self): + return self.__general_page + + @property + def archiver_page(self): + return self.__archiver_page + def __add_general_settings_page(self): page = Gtk.ScrolledWindow() - vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + vbox = Gtk.Box.new(Gtk.Orientation.VERTICAL,4) grid = Gtk.Grid() - label = Gtk.Label.new('Backup directory: ') + label = Gtk.Label.new('Backup directory:') grid.attach(label,0,0,1,1) - self.__backupdir_label = Gtk.Label.new(settings.backup_dir) - self.__backupdir_label.set_hexpand(True) - grid.attach(self.__backupdir_label,1,0,1,1) + page.backupdir_label = Gtk.Label.new(settings.backup_dir) + page.backupdir_label.set_hexpand(True) + grid.attach(page.backupdir_label,1,0,1,1) img = Gtk.Image.new_from_icon_name('document-open-symbolic') img.set_pixel_size(16) backupdir_button = Gtk.Button() @@ -62,10 +121,96 @@ class SettingsDialog(Gtk.Dialog): backupdir_button.connect('clicked',self._on_backupdir_button_clicked) grid.attach(backupdir_button,2,0,1,1) + label = Gtk.Label.new('Backup versions:') + grid.attach(label,0,1,1,1) + page.backup_versions_spinbutton = Gtk.SpinButton.new_with_range(0,1000,1) + page.backup_versions_spinbutton.set_hexpand(True) + grid.attach(page.backup_versions_spinbutton,1,1,2,1) + + label = Gtk.Label.new("Archiver:") + archiver_model = Gio.ListStore.new(Archiver) + for archiver in ArchiverManager.get_global().archivers.values(): + archiver_model.append(archiver) + archiver_sort_model = Gtk.SortListModel.new(archiver_model,ArchiverSorter()) + + archiver_factory = Gtk.SignalListItemFactory() + archiver_factory.connect('setup',self._on_archiver_factory_setup) + archiver_factory.connect('bind',self._on_archiver_factory_bind) + page.archiver_dropdown = Gtk.DropDown(model=archiver_sort_model,factory=archiver_factory) + page.archiver_dropdown.set_hexpand(True) + archiver_key = settings.archiver if settings.archiver in ArchiverManager.get_global().archivers else "zipfile" + + for i in range(archiver_model.get_n_items()): + archiver = archiver_model.get_item(i) + if archiver_key == archiver.key: + page.archiver_dropdown.set_selected(i) + break + grid.attach(label,0,2,1,1) + grid.attach(page.archiver_dropdown,1,2,2,1) + vbox.append(grid) page.set_child(vbox) self.add_page(page,"general","Generic settings") + return page + + def __add_archiver_settings_page(self): + page = Gtk.ScrolledWindow() + page.vbox = Gtk.Box.new(Gtk.Orientation.VERTICAL,4) + self._widget_set_margin(page.vbox,4) + + grid = Gtk.Grid() + self._widget_set_margin(grid,4) + + zf_compressors = [ + (zipfile.ZIP_STORED,"Stored",True), + (zipfile.ZIP_DEFLATED,"Deflated",True), + (zipfile.ZIP_BZIP2,"BZip2",False), + (zipfile.ZIP_LZMA,"LZMA",False), + ] + + zipfile_frame = Gtk.Frame.new("ZipFile Archiver") + label = Gtk.Label.new("Compressor:") + zf_compressor_model = Gio.ListStore.new(ZipfileCompressorData) + for i in zf_compressors: + zf_compressor_model.append(ZipfileCompressorData(*i)) + zf_compressor_sort_model = Gtk.SortListModel.new(zf_compressor_model,ZipfileCompressorDataSorter()) + zf_compressor_factory = Gtk.SignalListItemFactory() + zf_compressor_factory.connect('setup',self._on_zipfile_compressor_setup) + zf_compressor_factory.connect('bind',self._on_zipfile_compressor_bind) + page.zf_compressor_dropdown = Gtk.DropDown(model=zf_compressor_sort_model,factory=zf_compressor_factory) + page.zf_compressor_dropdown.set_hexpand(True) + c = settings.zipfile_compression + for i in range(zf_compressor_model.get_n_items()): + if (c == zf_compressor_model.get_item(i).compressor): + page.zf_compressor_dropdown.set_selected(i) + break + grid.attach(label,0,0,1,1) + grid.attach(page.zf_compressor_dropdown,1,0,1,1) + + label = Gtk.Label.new("Compression Level:") + page.zf_compresslevel_spinbutton = Gtk.SpinButton.new_with_range(0.0,9.0,1.0) + page.zf_compresslevel_spinbutton.set_value(settings.zipfile_compresslevel) + page.zf_compresslevel_spinbutton.set_hexpand(True) + grid.attach(label,0,1,1,1) + grid.attach(page.zf_compresslevel_spinbutton,1,1,1,1) + + zipfile_frame.set_child(grid) + page.vbox.append(zipfile_frame) + + page.set_child(page.vbox) + self.add_page(page,"zipfile","Archiver Settings") + return page + + def _on_archiver_factory_setup(self,factory,item): + label = Gtk.Label() + item.set_child(label) + + def _on_archiver_factory_bind(self,factory,item): + label = item.get_child() + archiver = item.get_item() + label.set_text(archiver.name) + def _on_backupdir_dialog_select_folder(self,dialog,result,*data): try: @@ -81,6 +226,25 @@ class SettingsDialog(Gtk.Dialog): dialog.select_folder(self,None,self._on_backupdir_dialog_select_folder) + def _on_zipfile_compressor_setup(self,factory,item): + label = Gtk.Label() + item.set_child(label) + + def _on_zipfile_compressor_bind(self,factory,item): + label = item.get_child() + data = item.get_item() + + if (not data.is_standard): + label.set_markup("{name}".format(name=GLib.markup_escape_text(data.name))) + else: + label.set_text(data.name) + + def _widget_set_margin(self,widget:Gtk.Widget,margin:int): + widget.set_margin_top(margin) + widget.set_margin_bottom(margin) + widget.set_margin_start(margin) + widget.set_margin_end(margin) + def add_page(self,page,name,title): self.__stack.add_titled(page,name,title) @@ -95,5 +259,10 @@ class SettingsDialog(Gtk.Dialog): return_type=None, arg_types=()) def do_save(self): - settings.backup_dir = self.__backupdir_label.get_text() + settings.backup_dir = self.general_page.backupdir_label.get_text() + settings.backup_versions = self.general_page.backup_versions_spinbutton.get_value_as_int() + settings.archiver = self.general_page.archiver_dropdown.get_selected_item().key + settings.zipfile_compression = self.archiver_page.zf_compressor_dropdown.get_selected_item().compressor + settings.zipfile_compresslevel = self.archiver_page.zf_compresslevel_spinbutton.get_value_as_int() + \ No newline at end of file diff --git a/sgbackup/gui/_steam.py b/sgbackup/gui/_steam.py index 8094b29..56aa3bf 100644 --- a/sgbackup/gui/_steam.py +++ b/sgbackup/gui/_steam.py @@ -230,6 +230,8 @@ class NewSteamAppsDialog(Gtk.Dialog): self.get_content_area().append(scrolled) + self.__gamedialog = None + self.add_button("OK",Gtk.ResponseType.OK) def _on_listitem_setup(self,factory,item): @@ -289,12 +291,16 @@ class NewSteamAppsDialog(Gtk.Dialog): def _on_add_steamapp_button_clicked(self,button,data:SteamApp,*args): def on_dialog_response(dialog,response): + self.__gamedialog = None if response == Gtk.ResponseType.APPLY: for i in range(self.__listmodel.get_n_items()): if data.appid == self.__listmodel.get_item(i).appid: self.__listmodel.remove(i) break + if self.__gamedialog is not None: + return + game = Game("Enter key",data.name,"") if PLATFORM_WINDOWS: game.steam_windows = SteamWindowsGame(data.appid,"","",installdir=data.installdir) @@ -311,12 +317,13 @@ class NewSteamAppsDialog(Gtk.Dialog): game.steam_windows = SteamWindowsGame(data.appid,"","") game.steam_macos = SteamMacOSGame(data.appid,"","") game.savegame_type = SavegameType.STEAM_MACOS - - dialog = GameDialog(self,game) - dialog.set_title("sgbackup: Add Steam Game") - dialog.set_modal(False) - dialog.connect('response',on_dialog_response) - dialog.present() + + if self.__gamedialog is None: + self.__gamedialog = GameDialog(self,game) + self.__gamedialog.set_title("sgbackup: Add Steam Game") + self.__gamedialog.set_modal(False) + self.__gamedialog.connect('response',on_dialog_response) + self.__gamedialog.present() def _on_ignore_steamapp_button_clicked(self,button,data,*args): def on_dialog_response(dialog,response,data): diff --git a/sgbackup/settings.py b/sgbackup/settings.py index f0afe6c..8ec5f43 100644 --- a/sgbackup/settings.py +++ b/sgbackup/settings.py @@ -22,6 +22,7 @@ import sys from gi.repository import GLib,GObject import zipfile +from threading import RLock ZIPFILE_COMPRESSION_STR = { zipfile.ZIP_STORED: "stored", @@ -55,27 +56,219 @@ class Settings(GObject.GObject): def __init__(self): super().__init__() + self.__mutex = RLock() - self.__configparser = ConfigParser() + self.__keyfile = GLib.KeyFile.new() self.__config_dir = os.path.join(GLib.get_user_config_dir(),'sgbackup') self.__gameconf_dir = os.path.join(self.__config_dir,'games') self.__logger_conf = os.path.join(self.__config_dir,'logger.conf') + self.__backup_versions = 0 self.__config_file = os.path.join(self.__config_dir,'sgbackup.conf') if (os.path.isfile(self.__config_file)): - with open(self.__config_file,'r') as conf: - self.__configparser.read_file(conf) + self.__keyfile.load_from_file(self.__config_file, + (GLib.KeyFileFlags.KEEP_COMMENTS | GLib.KeyFileFlags.KEEP_TRANSLATIONS)) if not os.path.isdir(self.config_dir): os.makedirs(self.config_dir) if not os.path.isdir(self.gameconf_dir): os.makedirs(self.gameconf_dir) + + def has_group(self,group:str)->bool: + with self.__mutex: + return self.keyfile.has_group(group) + + def has_section(self,section:str)->bool: + with self.__mutex: + return self.keyfile.has_group(section) + + def has_option(self,section:str,option:str): + if self.has_section(section): + with self.__mutex: + keys,length = self.keyfile.get_keys(section) + if (keys and option in keys): + return True + return False + + def has_key(self,group:str,key:str): + if self.has_group(group): + with self.__mutex: + keys,length = self.keyfile.get_keys(group) + return (keys and key in keys) + return False + + def get_groups(self): + with self.__mutex: + return self.keyfile.get_groups()[0] + + def get_sections(self): + with self.__mutex: + return self.keyfile.get_groups()[0] + + def get_keys(self,group:str): + with self.__mutex: + return self.keyfile.get_keys(group)[0] + + def get_options(self,section:str): + with self.__mutex: + return self.keyfile.get_keys(section)[0] + + def get(self,group:str,key:str,default=None)->str|None: + if (self.has_key(group,key)): + with self.__mutex: + self.keyfile.get_value(group,key) + return default + + def set(self,group:str,key:str,value:str): + with self.__mutex: + self.keyfile.set_key(group,key,value) + + def get_boolean(self,group:str,key:str,default:bool|None=None)->bool|None: + if self.has_key(group,key): + with self.__mutex: + return self.keyfile.get_boolean(group,key) + return default + + def set_boolean(self,group:str,key:str,value:bool): + with self.__mutex: + self.keyfile.set_boolean(group,key,value) + + def get_boolean_list(self,group:str,key:str,default:list[bool]|None=None)->list[bool]|None: + if self.has_key(group,key): + with self.__mutex: + return self.keyfile.get_boolean_list(group,key) + return default + + def set_boolean_list(self,group:str,key:str,value:list[bool]): + with self.__mutex: + self.keyfile.set_boolean_list(group,key,value) + + def get_double(self,group:str,key:str,default:float|None=None)->float|None: + if self.has_key(group,key): + with self.__mutex: + return self.keyfile.get_double(group,key) + return default + + + def set_double(self,group:str,key:str,value:float): + with self.__mutex: + self.keyfile.set_double(group,key,value) + + def get_double_list(self,group:str,key:str,default:list[float]|None=None)->list[float]|None: + if self.has_key(group,key): + with self.__mutex: + return self.keyfile.get_double_list(group,key) + return default + + def set_double_list(self,group:str,key:str,value:list[float]): + with self.__mutex: + self.keyfile.set_double_list(group,key,value) + def get_integer(self,group:str,key:str,default:None|int=None)->int|None: + if self.has_key(group,key): + with self.__mutex: + return self.keyfile.get_integer(group,key) + return default + + def set_integer(self,group:str,key:str,value:int): + with self.__mutex: + self.keyfile.set_integer(group,key,value) + + def get_integer_list(self,group:str,key:str,default:list[int]|None=None)->list[int]|None: + if self.has_key(group,key): + with self.__mutex: + return self.keyfile.get_integer_list(group,key) + return default + + def set_integer_list(self,group:str,key:str,value:list[int]): + with self.__mutex: + self.keyfile.set_integer_list(group,key,value) + + def get_locale_for_key(self,group:str,key:str,locale:str|None=None)->str|None: + if self.has_key(group,key): + with self.__mutex: + return self.keyfile.get_locale_for_key(group,key,locale) + return None + + def get_locale_string(self,group:str,key:str,locale:str|None=None,default:str|None=None)->str|None: + if self.has_key(group,key): + with self.__mutex: + ret = self.keyfile.get_locale_string(group,key,locale) + if ret is not None: + return ret + return default + + def set_locale_string(self,group:str,key:str,locale:str,value:str): + with self.__mutex: + self.set_locale_string(group,key,locale,value) + + def get_locale_string_list(self,group:str,key:str,locale:str|None=None,default:list[str]|None=None)->list[str]|None: + if self.has_key(group,key): + with self.__mutex: + ret = self.keyfile.get_locale_string_list(group,key,locale) + if ret is not None: + return ret + return default + + def set_locale_string_list(self,group:str,key:str,locale:str,value:list[str]): + with self.__mutex: + self.keyfile.set_locale_string_list(group,key,locale,value) + + def get_string(self,group:str,key:str,default:str|None=None)->str|None: + if self.has_key(group,key): + with self.__mutex: + return self.keyfile.get_string(group,key) + return default + + def set_string(self,group:str,key:str,value:str): + with self.__mutex: + self.keyfile.set_string(group,key,value) + + def get_string_list(self,group:str,key:str,default:list[str]|None=None)->list[str]|None: + if self.has_key(group,key): + with self.__mutex: + return self.keyfile.get_string_list(group,key) + return default + + def set_string_list(self,group:str,key:str,value:list[str]): + with self.__mutex: + self.keyfile.set_string_list(group,key,value) + + def remove_key(self,group:str,key:str): + if self.has_key(group,key): + with self.__mutex: + self.keyfile.remove_key(group,key) + + def remove_group(self,group): + if self.has_group(group): + keys = self.get_keys(group) + with self.__mutex: + for key in keys: + self.keyfile.remove_key(group,key) + self.keyfile.remove_group(group) + + def remove_comment(self,group:str|None=None,key:str|None=None): + with self.__mutex: + try: + self.keyfile.remove_comment(group,key) + except: + pass + + def set_comment(self,comment:str,group:str|None=None,key:str|None=None): + with self.__mutex: + try: + self.keyfile.set_comment(group,key,comment) + except: + pass @GObject.Property(nick="parser") - def parser(self)->ConfigParser: - return self.__configparser + def parser(self)->GLib.KeyFile: + return self.__keyfile + + @GObject.Property(nick="keyfile") + def keyfile(self)->GLib.KeyFile: + return self.__keyfile @GObject.Property(type=str,nick="config-dir") def config_dir(self)->str: @@ -93,53 +286,46 @@ class Settings(GObject.GObject): def logger_conf(self)->str: return self.__logger_conf - @GObject.Property(type=str,nick="backup-dir") def backup_dir(self)->str: - if self.parser.has_option('sgbackup','backupDirectory'): - return self.parser.get('sgbackup','backupDirectory') - return os.path.join(GLib.get_home_dir(),'SavagameBackups') + return self.get_string('sgbackup','backupDirectory', + os.path.join(GLib.get_home_dir(),'SavagameBackups')) + @backup_dir.setter def backup_dir(self,directory:str): if not os.path.isabs(directory): raise ValueError("\"backup_dir\" needs to be an absolute path!") - self.ensure_section('sgbackup') - return self.parser.set('sgbackup','backupDirectory',directory) + return self.set_string('sgbackup','backupDirectory',directory) @GObject.Property(type=str) def loglevel(self)->str: - if self.parser.has_option('sgbackup','logLevel'): - return self.parser.get('sgbackup','logLevel') - return "INFO" + return self.get_string('sgbackup','logLevel',"INFO") @GObject.Property def variables(self)->dict[str:str]: ret = {} - if self.parser.has_section('variables'): - for k,v in self.parser.items('variables'): - ret[k] = v + if self.keyfile.has_group('variables'): + for key in self.get_keys('variables'): + ret[key] = self.get_string('variables',key,"") return ret @variables.setter def variables(self,vars:dict|list|tuple): - if self.parser.has_section('variables'): - for opt in self.parser['variables'].keys(): - self.parser.remove_option('variables',opt) - + self.remove_group("variables") if not vars: return if isinstance(vars,dict): for k,v in vars.items(): - self.parser.set('variables',k,v) + self.set_string('variables',k,v) else: - for v in vars: - self.parser.set('variables',v[0],v[1]) + for k,v in dict(vars).items(): + self.set_string('variables',v[0],v[1]) @GObject.Property(type=str) def steam_installpath(self): - if self.parser.has_section('steam') and self.parser.has_option('installpath'): - return self.parser.get('steam','installdir') + if self.has_key('steam','installpath'): + return self.get_string('steam','installpath') if PLATFORM_WINDOWS: for i in ('SOFTWARE\\WOW6432Node\\Valve\\Steam','SOFTWARE\\Valve\\Steam'): @@ -148,7 +334,7 @@ class Settings(GObject.GObject): skey = winreg.OpenKeyEx(winreg.HKEY_LOCAL_MACHINE,i) svalue = winreg.QueryValueEx(skey,'InstallPath')[0] if svalue: - self.parser.set('steam','installpath',svalue) + self.set_string('steam','installpath',svalue) return svalue except: continue @@ -157,20 +343,18 @@ class Settings(GObject.GObject): skey.Close() return "" + @steam_installpath.setter + def steam_installpath(self,path:str): + self.set_string('steam','installpath',path) + def add_variable(self,name:str,value:str): - self.parser.set('variables',name,value) + self.set_string('variables',name,value) def remove_variable(self,name:str): - try: - self.parser.remove_option('variables',name) - except: - pass + self.remove_key('variables',name) def get_variable(self,name:str)->str: - try: - return self.parser.get('variables',name) - except: - return "" + return self.get_string('variables',name,"") def get_variables(self)->dict[str:str]: ret = dict(os.environ) @@ -178,48 +362,64 @@ class Settings(GObject.GObject): "DOCUMENTS": GLib.get_user_special_dir(GLib.UserDirectory.DIRECTORY_DOCUMENTS), "DOCUMENTS_DIR": GLib.get_user_special_dir(GLib.UserDirectory.DIRECTORY_DOCUMENTS), "DATADIR": GLib.get_user_data_dir(), + "DATA_DIR": GLib.get_user_data_dir(), + "CONFIGDIR": GLib.get_user_config_dir(), + "CONFIG_DIR": GLib.get_user_config_dir(), "STEAM_INSTALLPATH": self.steam_installpath, }) ret.update(self.variables) return ret @GObject.Property(type=str) - def zipfile_compression(self)->str: - if self.parser.has_option('zipfile','compression'): - try: - ZIPFILE_STR_COMPRESSION[self.parser.get('zipfile','compression')] - except: - pass - return ZIPFILE_STR_COMPRESSION["deflated"] + def archiver(self)->str: + return self.get_string('sgbackup','archiver',"zipfile") + + @archiver.setter + def archiver(self,archiver_key:str): + self.set_string('sgbackup','archiver',archiver_key) + + @GObject.Property(type=int) + def backup_versions(self)->int: + return self.get_integer('sgbackup','backupVersions',0) + + @backup_versions.setter + def backup_versions(self,versions:int): + self.set_integer('sgbackup','backupVersions',versions) + + + @GObject.Property(type=int) + def zipfile_compression(self)->int: + comp = self.parser.has_option('zipfile','compression','deflated') + try: + return ZIPFILE_STR_COMPRESSION[comp] + except: + pass + return zipfile.ZIP_DEFLATED @zipfile_compression.setter - def zipfile_compression(self,compression): + def zipfile_compression(self,compression:int): try: - self.parser.set('zipfile','compression',ZIPFILE_COMPRESSION_STR[compression]) + self.set_string('zipfile','compression',ZIPFILE_COMPRESSION_STR[compression]) except: - self.parser.set('zipfile','compression',ZIPFILE_STR_COMPRESSION[zipfile.ZIP_DEFLATED]) + self.set_string('zipfile','compression',ZIPFILE_COMPRESSION_STR[zipfile.ZIP_DEFLATED]) @GObject.Property(type=int) def zipfile_compresslevel(self)->int: - if self.parser.has_option('zipfile','compressLevel'): - cl = self.parser.getint('zipfile','compressLevel') - return cl if cl <= ZIPFILE_COMPRESSLEVEL_MAX[self.zipfile_compression] else ZIPFILE_COMPRESSLEVEL_MAX[self.zipfile_compression] - return ZIPFILE_COMPRESSLEVEL_MAX[self.zipfile_compression] + cl = self.get_integer('zipfile','compresslevel',9) + if cl < 0: + cl = 9 + return cl if cl <= ZIPFILE_COMPRESSLEVEL_MAX[self.zipfile_compression] else ZIPFILE_COMPRESSLEVEL_MAX[self.zipfile_compression] @zipfile_compresslevel.setter def zipfile_compresslevel(self,cl:int): - self.parser.set('zipfile','compressLevel',cl) + self.set_integer('zipfile','compressLevel',cl) def save(self): self.emit('save') - def ensure_section(self,section:str): - if not self.parser.has_section(section): - self.parser.add_section(section) - @GObject.Signal(name='save',flags=GObject.SIGNAL_RUN_LAST,return_type=None,arg_types=()) def do_save(self): - with open(self.config_file,'w') as ofile: - self.__configparser.write(ofile) + with self.__mutex: + self.keyfile.save_to_file(self.config_file) settings = Settings()