From 2daf302e54b53bf362fb889d0d08d064a388bd49 Mon Sep 17 00:00:00 2001 From: Christian Moser Date: Sat, 25 Jan 2025 14:05:44 +0100 Subject: [PATCH] 2025.01.25 14:05:44 --- sgbackup/archiver/__init__.py | 17 ++++++- sgbackup/archiver/_archiver.py | 74 ++++++++++++++++++++++------ sgbackup/archiver/zipfilearchiver.py | 9 ++-- sgbackup/game.py | 12 ++++- sgbackup/gui/_app.py | 42 +++++++++++----- sgbackup/gui/_backupdialog.py | 63 ++++++++++++++++++++++- sgbackup/settings.py | 3 +- 7 files changed, 185 insertions(+), 35 deletions(-) diff --git a/sgbackup/archiver/__init__.py b/sgbackup/archiver/__init__.py index 6196f40..bd8f518 100644 --- a/sgbackup/archiver/__init__.py +++ b/sgbackup/archiver/__init__.py @@ -17,9 +17,22 @@ ############################################################################### from ._archiver import Archiver,ArchiverManager -import importlib +#import importlib +import os -archiver = ArchiverManager() +_archiver = ArchiverManager.get_global() +_archiver_path= os.path.dirname(__file__) +for dirent in os.listdir(_archiver_path): + if dirent.startswith('.') or dirent.startswith('_'): + continue + if dirent.endswith('.py'): + module = dirent[0:-3] + exec(""" +from . import {module} +if hasattr({module},"ARCHIVERS"): + for a in {module}.ARCHIVERS: + _archiver.archivers[a.key] = a +""".format(module=module)) __ALL__ = [ "Archiver", diff --git a/sgbackup/archiver/_archiver.py b/sgbackup/archiver/_archiver.py index c18f2c2..17958e7 100644 --- a/sgbackup/archiver/_archiver.py +++ b/sgbackup/archiver/_archiver.py @@ -32,8 +32,9 @@ import time from ..game import Game from ..settings import settings -class Archiver: +class Archiver(GObject): def __init__(self,key:str,name:str,extensions:list[str],decription:str|None=None): + GObject.__init__(self) self.__key = key self.__name = name if decription: @@ -59,23 +60,29 @@ class Archiver: def extensions(self)->list[str]: return self.__extensions + def is_archive(self,filename): + for ext in self.extensions: + if filename.endswith(ext): + return True + return False + def backup(self,game:Game)->bool: if not game.get_backup_files(): return - filename = self.generate_new_backup_filename() + filename = self.generate_new_backup_filename(game) dirname = os.path.dirname(filename) if not os.path.isdir(dirname): os.makedirs(dirname) - self.emit('backup',game,filename) + return self.emit('backup',game,filename) def generate_new_backup_filename(self,game:Game)->str: dt = datetime.datetime.now() - basename = '.'.join(game.savegame_name, + basename = '.'.join((game.savegame_name, game.savegame_subdir, dt.strftime("%Y%m%d-%H%M%S"), "sgbackup", - self.extensions[0]) + self.extensions[0][1:])) return os.path.join(settings.backup_dir,game.savegame_name,game.subdir,basename) def _backup_progress(self,game:Game,fraction:float,message:str|None): @@ -84,7 +91,7 @@ class Archiver: elif fraction < 0.0: fraction = 0.0 - self.emit("progress",game,fraction,message) + self.emit("backup-progress",game,fraction,message) @Signal(name="backup",flags=SignalFlags.RUN_FIRST, @@ -112,7 +119,8 @@ class ArchiverManager(GObject): def __init__(self): GObject.__init__(self) self.__archivers = {} - + self.__backup_in_progress = False + @staticmethod def get_global(): @@ -126,7 +134,18 @@ class ArchiverManager(GObject): try: return self.__archivers[settings.archiver] except: - return self.__archivers["zipfile"] + return self.__archivers["zipfile"] + + @Property(type=bool,nick='backup-in-progress',default=False) + def backup_in_progress(self)->bool: + return self.__backup_in_progress + @backup_in_progress.setter + def backup_in_progress(self,b:bool): + self.__backup_in_progress = b + + @property + def archivers(self): + return self.__archivers def _on_archiver_backup_progress_single(self,archiver,game,fraction,message): pass @@ -134,11 +153,12 @@ class ArchiverManager(GObject): def _on_archiver_backup_progress_multi(self,archiver,game,fraction,message): pass + @Signal(name="backup-game-progress",return_type=None,arg_types=(Game,float,str),flags=SignalFlags.RUN_FIRST) def do_backup_game_progress(self,game,fraction,message): pass - @Signal(name="backup-game-finished",return_type=None,arg_types=(Game,float,str),flags=SignalFlags.RUN_FIRST) + @Signal(name="backup-game-finished",return_type=None,arg_types=(Game,),flags=SignalFlags.RUN_FIRST) def do_backup_game_finished(self,game:Game): pass @@ -155,21 +175,27 @@ class ArchiverManager(GObject): self.emit("backup-game-progress",game,fraction,message) self.emit("backup-progress",fraction) + if self.backup_in_progress: + raise RuntimeError("A backup is already in progress!!!") + + self.backup_in_progress = True + archiver = self.standard_archiver backup_sc = archiver.connect('backup-progress',on_progress) archiver.backup(game) archiver.disconnect(backup_sc) self.emit("backup-game-finished",game) - self.emit("backup-finsihed") + self.emit("backup-finished") + self.backup_in_progress = False - - - def backup_many(self,games:list[Game]): def thread_function(game): archiver = self.standard_archiver self._on_archiver_backup_progress_multi(archiver,game,1.0,"Finished ...") + if self.backup_in_progress: + raise RuntimeError("A backup is already in progress!!!") + self.backup_in_progress = True game_list = list(games) threadpool = {} @@ -200,5 +226,25 @@ class ArchiverManager(GObject): thread = threading.Thread(thread_function,args=game,daemon=True) threadpool.append(thread) thread.start() + self.backup_in_progress = False + + def is_archive(self,filename)->bool: + if self.standard_archiver.is_archive(filename): + return True + for i in self.archivers.values(): + if i.is_archive(filename): + return True + return False + + 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')]: + 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 - \ No newline at end of file + \ No newline at end of file diff --git a/sgbackup/archiver/zipfilearchiver.py b/sgbackup/archiver/zipfilearchiver.py index 48f84d4..bf19ad1 100644 --- a/sgbackup/archiver/zipfilearchiver.py +++ b/sgbackup/archiver/zipfilearchiver.py @@ -21,7 +21,7 @@ import zipfile import json import os from ..game import Game,GameManager -from settings import settings +from ..settings import settings class ZipfileArchiver(Archiver): def __init__(self): @@ -32,7 +32,7 @@ class ZipfileArchiver(Archiver): self._backup_progress(game,0.0,"Starting {game} ...".format(game=game.name)) files = game.get_backup_files() - div = len(files + 2) + div = len(files) + 2 cnt=1 game_data = json.dumps(game.serialize(),ensure_ascii=False,indent=4) with zipfile.ZipFile(filename,mode="w", @@ -45,20 +45,19 @@ class ZipfileArchiver(Archiver): self._backup_progress(game,_calc_fraction(div,cnt),"{} -> {}".format(game.name,arcname)) zf.write(path,arcname) - self._backup_progress("{game} ... FINISHED".format(game=game.name)) + self._backup_progress(game,1.0,"{game} ... FINISHED".format(game=game.name)) def is_archive(self,filename:str)->bool: if zipfile.is_zipfile(filename): with zipfile.ZipFile(filename,"r") as zf: - if 'game.conf' in zf.filelist(): + if 'gameconf.json' in [i.filename for i in zf.filelist]: return True return False def do_restore(self,filename:str): # TODO: convert savegame dir if not the same SvaegameType!!! - if not zipfile.is_zipfile(filename): raise RuntimeError("\"{filename}\" is not a valid sgbackup zipfile archive!") diff --git a/sgbackup/game.py b/sgbackup/game.py index e75c1ff..48369ad 100644 --- a/sgbackup/game.py +++ b/sgbackup/game.py @@ -1151,6 +1151,13 @@ class Game(GObject): else: self.__variables = dict(vars) + @Property(type=str) + def subdir(self): + if self.is_live: + return "live" + else: + return "finished" + @Property def game_data(self): sgtype = self.savegame_type @@ -1367,7 +1374,10 @@ class Game(GObject): if self.game_data.match(fname): ret[str(path)] = os.path.join(sgdir,fname) elif file_path.is_dir(): - ret.update(get_backup_files_recursive(sgroot,sgdir,os.path.join(subdir,dirent))) + if subdir: + ret.update(get_backup_files_recursive(sgroot,sgdir,os.path.join(subdir,dirent))) + else: + ret.update(get_backup_files_recursive(sgroot,sgdir,dirent)) return ret diff --git a/sgbackup/gui/_app.py b/sgbackup/gui/_app.py index aa75c25..b7a2924 100644 --- a/sgbackup/gui/_app.py +++ b/sgbackup/gui/_app.py @@ -30,6 +30,9 @@ from ._settingsdialog import SettingsDialog from ._gamedialog import GameDialog from ..game import Game,GameManager,SAVEGAME_TYPE_ICONS from ._steam import SteamLibrariesDialog,NewSteamAppsDialog +from ._backupdialog import BackupSingleDialog +from ..archiver import ArchiverManager + __gtype_name__ = __name__ @@ -236,18 +239,21 @@ class GameView(Gtk.ScrolledWindow): def _on_actions_column_bind(self,action,item): child = item.get_child() game = item.get_item() - + archiver_manager = ArchiverManager.get_global() child.backup_button.connect('clicked',self._on_columnview_backup_button_clicked,item) child.edit_button.connect('clicked',self._on_columnview_edit_button_clicked,item) child.remove_button.connect('clicked',self._on_columnview_remove_button_clicked,item) + archiver_manager.bind_property('backup-in-progress',child.backup_button,'sensitive', + BindingFlags.SYNC_CREATE,lambda binding,x: False if x else True) + archiver_manager.bind_property('backup-in-progress',child.edit_button,'sensitive', + BindingFlags.SYNC_CREATE,lambda binding,x: False if x else True) + archiver_manager.bind_property('backup-in-progress',child.remove_button,'sensitive', + BindingFlags.SYNC_CREATE,lambda binding,x: False if x else True) def _on_columnview_backup_button_clicked(self,button,item): - def on_dialog_response(dialog,response): - dialog.hide() - dialog.destroy() - game = item.get_item() - print('{}.{}._on_columnview_backup_button_clicked() -> {}'.format(__name__,__class__,game.name)) + dialog = BackupSingleDialog(self.get_root(),game) + dialog.run() def _on_columnview_edit_button_clicked(self,button,item): def on_dialog_response(dialog,response): @@ -303,17 +309,17 @@ class BackupViewData(GObject): """ def __init__(self,_game:Game,filename:str): - GObject.GObject.__init__(self) + GObject.__init__(self) self.__game = _game self.__filename = filename basename = os.path.basename(filename) self.__is_live = (os.path.basename(os.path.dirname(filename)) == 'live') - parts = filename.split('.') + parts = basename.split('.') self.__savegame_name = parts[0] - self.__timestamp = DateTime.strptime(parts[1],"%Y%m%d-%H%M%S") + self.__timestamp = DateTime.strptime(parts[2],"%Y%m%d-%H%M%S") - self.__extension = '.' + parts[3:] + self.__extension = '.' + '.'.join(parts[3:]) @property def game(self)->Game: @@ -410,10 +416,16 @@ class BackupView(Gtk.Box): timestamp_factory.connect('bind',self._on_timestamp_column_bind) timestamp_column = Gtk.ColumnViewColumn.new("Timestamp",timestamp_factory) + size_factory = Gtk.SignalListItemFactory() + size_factory.connect('setup',self._on_size_column_setup) + size_factory.connect('bind',self._on_size_column_bind) + size_column = Gtk.ColumnViewColumn.new("Size",size_factory) + self.__columnview = Gtk.ColumnView.new(selection) self.__columnview.append_column(live_column) self.__columnview.append_column(sgname_column) self.__columnview.append_column(timestamp_column) + self.__columnview.append_column(size_column) self.__columnview.set_vexpand(True) self.gameview.columnview.connect('activate',self._on_gameview_columnview_activate) @@ -435,6 +447,7 @@ class BackupView(Gtk.Box): def _on_live_column_setup(self,factory,item): checkbutton = Gtk.CheckButton() checkbutton.set_sensitive(False) + item.set_child(checkbutton) def _on_live_column_bind(self,factory,item): checkbutton = item.get_child() @@ -444,7 +457,7 @@ class BackupView(Gtk.Box): def _on_savegamename_column_setup(self,factory,item): label = Gtk.Label() - self.set_child(label) + item.set_child(label) def _on_savegamename_column_bind(self,factory,item): label = item.get_child() @@ -490,6 +503,13 @@ class BackupView(Gtk.Box): self._title_label.set_markup("{}".format(GLib.markup_escape_text(game.name))) + self.__liststore.remove_all() + for bf in ArchiverManager.get_global().get_backups(game): + try: + self.__liststore.append(BackupViewData(game,bf)) + except: + pass + class AppWindow(Gtk.ApplicationWindow): diff --git a/sgbackup/gui/_backupdialog.py b/sgbackup/gui/_backupdialog.py index 12c66a2..2a9fcce 100644 --- a/sgbackup/gui/_backupdialog.py +++ b/sgbackup/gui/_backupdialog.py @@ -16,7 +16,68 @@ # along with this program. If not, see . # ############################################################################### +from gi.repository import Gtk,GLib from ..game import GameManager,Game from ..archiver import ArchiverManager +from threading import Thread,ThreadError - +class BackupSingleDialog(Gtk.Dialog): + def __init__(self,parent:Gtk.Window,game:Game): + Gtk.Dialog.__init__(self) + self.set_title("sgbackup: Backup -> {game}".format(game=game.name)) + self.set_decorated(False) + self.__game = game + + self.set_transient_for(parent) + + self.__progressbar = Gtk.ProgressBar() + self.__progressbar.set_text("Starting savegame backup ...") + self.__progressbar.set_fraction(0.0) + + self.get_content_area().append(self.__progressbar) + self.set_modal(False) + + self.__am_signal_progress = None + self.__am_signal_finished = None + + + def _on_propgress(self,fraction,message): + self.__progressbar.set_text(message if message else "Working ...") + self.__progressbar.set_fraction(fraction) + return False + + def _on_finished(self): + self.__progressbar.set_text("Finished ...") + self.__progressbar.set_fraction(1.0) + am = ArchiverManager.get_global() + if self.__am_signal_finished is not None: + am.disconnect(self.__am_signal_finished) + self.__am_signal_finished = None + + if self.__am_signal_progress is not None: + am.disconnect(self.__am_signal_progress) + self.__am_signal_progress = None + + self.hide() + self.destroy() + + def _on_am_backup_game_progress(self,am,game,fraction,message): + if self.__game.key == game.key: + GLib.idle_add(self._on_propgress,fraction,message) + + def _on_am_backup_game_finished(self,am,game): + if self.__game.key == game.key: + GLib.idle_add(self._on_finished) + + def run(self): + def _thread_func(archiver_manager,game): + am.backup(game) + + self.present() + + am = ArchiverManager.get_global() + self.__am_signal_progress = am.connect('backup-game-progress',self._on_am_backup_game_progress) + self.__am_signal_finished = am.connect('backup-game-finished',self._on_am_backup_game_finished) + thread = Thread(target=_thread_func,args=(am,self.__game),daemon=True) + thread.start() + diff --git a/sgbackup/settings.py b/sgbackup/settings.py index 2ae46dc..6d7a217 100644 --- a/sgbackup/settings.py +++ b/sgbackup/settings.py @@ -85,7 +85,8 @@ class Settings(GObject.GObject): @GObject.Property(type=str,nick="logger-conf") def logger_conf(self)->str: return self.__logger_conf - + + @GObject.Property(type=str,nick="backup-dir") def backup_dir(self)->str: