From 6aac8282efa05126ac35697fecdeae9786e24473 Mon Sep 17 00:00:00 2001 From: Christian Moser Date: Sun, 23 Feb 2025 13:23:07 +0100 Subject: [PATCH] 2025.02.23 13:23:07 (desktop) --- msys-install.sh | 10 ++-- setup.py | 1 + sgbackup/archiver/_archiver.py | 80 +++++++++++++++++------------ sgbackup/commands/__init__.py | 11 ++-- sgbackup/commands/help.py | 44 ++++++++++++++-- sgbackup/game.py | 13 +++++ sgbackup/gui/_app.py | 89 +++++++++++++++++++++++++++++++-- sgbackup/icons/sgbackup.ico | Bin 16958 -> 5694 bytes sgbackup/logger.conf | 19 ++----- sgbackup/main.py | 41 +++++++++++++-- 10 files changed, 238 insertions(+), 70 deletions(-) diff --git a/msys-install.sh b/msys-install.sh index fb6aeb2..1f7f86e 100755 --- a/msys-install.sh +++ b/msys-install.sh @@ -23,7 +23,8 @@ if [ ! -d "$bindir" ]; then mkdir -p "$bindir" fi -pythonpath="$( python -c 'import sys; print(sys.executable)' )" +pythonpath="$( python -c 'import sys; print(sys.executable)' )" +pythonwpath="$( pythonw -c 'import sys; print(sys.executable)' )" cat > "${bindir}/sgbackup" << EOF #!/bin/bash @@ -52,7 +53,7 @@ install_ps1="${PRJECT_DIR}/install.ps1" wproject_dir="$( cygpath -w "${PROJECT_DIR}" )" cat > "$install_ps1" << EOF -[Environment]::SetEnvironemtnVariable("Path","\$env:PATH;$wbindir","User") +[Environment]::SetEnvironmentVariable("Path","\$env:PATH;$wbindir","User") \$desktop_dir=[Environment]::getFolderPath("Desktop") \$startmenu_dir=[Environment]::getFolderPath("StartMenu") @@ -62,8 +63,9 @@ Copy-Item -Path "$wproject_dir\\sgbackup\\icons\\sgbackup.ico" -Destination "\$p foreach (\$dir in \$desktop_dir,\$startmenu_dir) { \$shell=New-Object -ComObject WScript.Shell - \$shortcut=\$shell.CreateShortcut('\$dir\\sgbackup.lnk') - \$shortcut.TargetPath='$wbindir\\gsgbackup.cmd' + \$shortcut=\$shell.CreateShortcut("\$dir\\sgbackup.lnk") + \$shortcut.TargetPath='$pythonwpath' + \$shortcut.Arguments='-m sgbackup.gui' \$shortcut.IconLocation="\$picture_dir\\sgbackup.ico" \$shortcut.Save() } diff --git a/setup.py b/setup.py index 23cb032..eb15436 100644 --- a/setup.py +++ b/setup.py @@ -31,6 +31,7 @@ setup( ], package_data={ 'sgbackup':[ + 'logger.conf', 'icons/sgbackup.ico', 'icons/hicolor/symbolic/*/*.svg' ], diff --git a/sgbackup/archiver/_archiver.py b/sgbackup/archiver/_archiver.py index 5f8ca51..cf76f58 100644 --- a/sgbackup/archiver/_archiver.py +++ b/sgbackup/archiver/_archiver.py @@ -29,7 +29,7 @@ import os import threading import time -from ..game import Game +from ..game import Game,SavegameType,VALID_SAVEGAME_TYPES,SAVEGAME_TYPE_ICONS from ..settings import settings from ..utility import sanitize_path,sanitize_windows_path @@ -72,9 +72,8 @@ 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(): - self._logger.warning("No files SaveGame files for game {game}!".format(game=game.key)) + self._logger.warning("[backup] No files SaveGame files for game {game}!".format(game=game.key)) return False filename = self.generate_new_backup_filename(game) @@ -82,18 +81,24 @@ class Archiver(GObject): if not os.path.isdir(dirname): os.makedirs(dirname) - self._logger.info("Backing up {game} -> {filename}".format( + self._logger.info("[backup] {game} -> {filename}".format( game=game.key,filename=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, - game.savegame_subdir, dt.strftime("%Y%m%d-%H%M%S"), + game.savegame_type.value, + game.savegame_subdir, "sgbackup", self.extensions[0][1:] if self.extensions[0].startswith('.') else self.extensions[0])) - return sanitize_path(os.path.join(settings.backup_dir,game.savegame_name,game.subdir,basename)) + return sanitize_path(os.path.join(settings.backup_dir, + game.savegame_name, + game.savegame_type.value, + game.subdir, + basename)) def _backup_progress(self,game:Game,fraction:float,message:str|None): if fraction > 1.0: @@ -201,10 +206,11 @@ class ArchiverManager(GObject): archiver.backup(game) archiver.disconnect(backup_sc) if game.is_live and settings.backup_versions > 0: - backups = sorted(self.get_live_backups(game)) + backups = sorted(self.get_live_backups_for_type(game,game.savegame_type),reverse=True) 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) @@ -253,8 +259,6 @@ class ArchiverManager(GObject): else: n = len(games) - print("Starting backup with {n} threads".format(n=n)) - for i in range(n): game=game_list[0] del game_list[0] @@ -307,29 +311,9 @@ class ArchiverManager(GObject): def get_live_backups(self,game:Game): ret = [] - backupdir = os.path.join(settings.backup_dir,game.savegame_name,'live') + for sgtype in VALID_SAVEGAME_TYPES: + backupdir = os.path.join(settings.backup_dir,game.savegame_name,sgtype.value,'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')]: if os.path.isdir(backupdir): for basename in os.listdir(backupdir): filename = os.path.join(backupdir,basename) @@ -337,4 +321,38 @@ class ArchiverManager(GObject): ret.append(filename) return ret + + def get_live_backups_for_type(self,game:Game,type:SavegameType): + ret = [] + backupdir = os.path.join(settings.backup_dir,game.savegame_name,type.value,'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=[] + for sgtype in VALID_SAVEGAME_TYPES: + backupdir = os.path.join(settings.backup_dir,game.savegame_name,sgtype.value,'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 sgtype in VALID_SAVEGAME_TYPES: + for backupdir in [os.path.join(settings.backup_dir,game.savegame_name,sgtype.value,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 diff --git a/sgbackup/commands/__init__.py b/sgbackup/commands/__init__.py index bf0d13c..66d15ad 100644 --- a/sgbackup/commands/__init__.py +++ b/sgbackup/commands/__init__.py @@ -20,7 +20,7 @@ import os COMMANDS = {} -_mods = ['commandbase'] +_mods = [] for _f in os.listdir(os.path.dirname(__file__)): if _f.startswith('_'): @@ -35,14 +35,11 @@ for _f in os.listdir(os.path.dirname(__file__)): if _m not in _mods: exec("\n".join([ "from . import " + _m, - "_mods += _m", + "_mods.append(_m)", "_mod = " + _m])) - if hasattr(_mod,"COMMANDS") and len(_mod.COMMANDS) > 0: - for _cmd in _mod.COMMANDS: - COMMANDS[_cmd.get_id()] = _cmd - del _cmd + if hasattr(_mod,"COMMANDS"): #and _mod.COMMANDS: + COMMANDS.update(_mod.COMMANDS) del _mods del _f del _m -del _mod diff --git a/sgbackup/commands/help.py b/sgbackup/commands/help.py index 1d56c5c..825c996 100644 --- a/sgbackup/commands/help.py +++ b/sgbackup/commands/help.py @@ -16,8 +16,11 @@ # along with this program. If not, see . # ############################################################################### -from sgbackup import __version__ as VERSION, Command +from sgbackup import __version__ as VERSION +from ..command import Command +import logging +logger = logging.getLogger(__name__) class VersionCommand(Command): def __init__(self): super().__init__('version', 'Version', 'Show version information.') @@ -33,6 +36,39 @@ class VersionCommand(Command): return 0 # VersionCommand class -COMMANDS = [ - VersionCommand(), -] +class SynopsisCommand(Command): + def __init__(self): + super().__init__('synopsis','Synopsis', 'Show usage information.') + self.logger = logger.getChild('SynopsisCommand') + + def get_synopsis(self): + return "sgbackup synopsis [COMMAND] ..." + + def get_sgbackup_synopsis(self): + return "sgbackup COMMAND1 [OPTIONS1] [-- COMMAND2 [OPTIONS2]] ..." + + def get_help(self): + return super().get_help() + + def execute(self,argv): + error_code = 0 + if not argv: + print(self.get_sgbackup_synopsis()) + + for i in argv: + try: + print(COMMANDS[i].get_synopsis()) + except: + self.logger.error("No such command {command}".foramt(command=i)) + error_code = 4 + + return error_code + + +__synopsis = SynopsisCommand() + +COMMANDS = { + 'version':VersionCommand(), + 'synopsis': __synopsis, + 'usage': __synopsis +} diff --git a/sgbackup/game.py b/sgbackup/game.py index 2e23114..c47a635 100644 --- a/sgbackup/game.py +++ b/sgbackup/game.py @@ -144,6 +144,19 @@ class SavegameType(StrEnum): return st.UNSET +VALID_SAVEGAME_TYPES = [ + SavegameType.WINDOWS, + SavegameType.LINUX, + SavegameType.MACOS, + SavegameType.STEAM_LINUX, + SavegameType.STEAM_MACOS, + SavegameType.STEAM_WINDOWS, + #SavegameType.EPIC_LINUX, + #SavegameType.EPIC_WINDOWS, + #SavegameType.GOG_LINUX, + #SavegameType.GOG_WINDOWS, +] + SAVEGAME_TYPE_ICONS = { SavegameType.UNSET : None, SavegameType.WINDOWS: 'windows-svgrepo-com-symbolic', diff --git a/sgbackup/gui/_app.py b/sgbackup/gui/_app.py index 3e269d6..75d7d95 100644 --- a/sgbackup/gui/_app.py +++ b/sgbackup/gui/_app.py @@ -28,7 +28,7 @@ from pathlib import Path from ..settings import settings from ._settingsdialog import SettingsDialog from ._gamedialog import GameDialog -from ..game import Game,GameManager,SAVEGAME_TYPE_ICONS +from ..game import Game,GameManager,SAVEGAME_TYPE_ICONS,SavegameType from ._steam import SteamLibrariesDialog,NewSteamAppsDialog,NoNewSteamAppsDialog from ..steam import Steam from ._backupdialog import BackupSingleDialog,BackupManyDialog @@ -532,13 +532,43 @@ class BackupViewData(GObject): self.__filename = filename basename = os.path.basename(filename) - self.__is_live = (os.path.basename(os.path.dirname(filename)) == 'live') parts = basename.split('.') + self.__savegame_name = parts[0] - self.__timestamp = DateTime.strptime(parts[2],"%Y%m%d-%H%M%S") + self.__timestamp = DateTime.strptime(parts[1],"%Y%m%d-%H%M%S") + self.__is_live = parts[3] == 'live' + self.__sgtype = SavegameType.from_string(parts[2]) - self.__extension = '.' + '.'.join(parts[3:]) + + WINDOWS_TYPES = [ + SavegameType.WINDOWS, + SavegameType.STEAM_WINDOWS, + SavegameType.EPIC_WINDOWS, + SavegameType.GOG_WINDOWS, + ] + LINUX_TYPES = [ + SavegameType.LINUX, + SavegameType.STEAM_LINUX, + SavegameType.EPIC_LINUX, + SavegameType.GOG_LINUX, + ] + MACOS_TYPES = [ + SavegameType.MACOS, + SavegameType.STEAM_MACOS, + ] + if self.__sgtype in WINDOWS_TYPES: + self.__sgos = 'windows' + elif self.__sgtype in LINUX_TYPES: + self.__sgos = 'linux' + elif self.__sgtype in MACOS_TYPES: + self.__sgos = 'macos' + else: + self.__sgos = '' + + self.__extension = '.' + '.'.join(parts[5:]) + + @property def game(self)->Game: """ @@ -548,6 +578,24 @@ class BackupViewData(GObject): """ return self.__game + @Property + def savegame_type(self)->SavegameType: + return self.__sgtype + + @Property(type=str) + def savegame_type_icon_name(self)->str: + return SAVEGAME_TYPE_ICONS[self.__sgtype] + + @Property(type=str) + def savegame_os_icon_name(self)->str: + if self.__sgos: + return SAVEGAME_TYPE_ICONS[self.__sgos] + return "" + + @Property(type=str) + def ostype_icon_name(self): + pass + @Property(type=str) def savegame_name(self)->str: """ @@ -618,6 +666,16 @@ class BackupView(Gtk.Box): self.__liststore = Gio.ListStore() selection = Gtk.SingleSelection.new(self.__liststore) + sgtype_factory = Gtk.SignalListItemFactory() + sgtype_factory.connect('setup',self._on_sgtype_column_setup) + sgtype_factory.connect('bind',self._on_sgtype_column_bind) + sgtype_column = Gtk.ColumnViewColumn.new("",sgtype_factory) + + sgos_factory = Gtk.SignalListItemFactory() + sgos_factory.connect('setup',self._on_sgos_column_setup) + sgos_factory.connect('bind',self._on_sgos_column_bind) + sgos_column = Gtk.ColumnViewColumn.new("OS",sgos_factory) + live_factory = Gtk.SignalListItemFactory() live_factory.connect('setup',self._on_live_column_setup) live_factory.connect('bind',self._on_live_column_bind) @@ -640,6 +698,8 @@ class BackupView(Gtk.Box): size_column = Gtk.ColumnViewColumn.new("Size",size_factory) self.__columnview = Gtk.ColumnView.new(selection) + self.__columnview.append_column(sgtype_column) + self.__columnview.append_column(sgos_column) self.__columnview.append_column(live_column) self.__columnview.append_column(sgname_column) self.__columnview.append_column(timestamp_column) @@ -662,6 +722,27 @@ class BackupView(Gtk.Box): """ return self.__gameview + def _on_sgtype_column_setup(self,factory,item): + icon = Gtk.Image() + icon.set_pixel_size(16) + item.set_child(icon) + + def _on_sgtype_column_bind(self,factory,item): + icon = item.get_child() + data = item.get_item() + icon.set_from_icon_name(data.savegame_type_icon_name) + + def _on_sgos_column_setup(self,factory,item): + icon = Gtk.Image() + icon.set_pixel_size(16) + item.set_child(icon) + + def _on_sgos_column_bind(self,factory,item): + icon = item.get_child() + data = item.get_item() + if data.savegame_os_icon_name: + icon.set_from_icon_name(data.savegame_os_icon_name) + def _on_live_column_setup(self,factory,item): checkbutton = Gtk.CheckButton() checkbutton.set_sensitive(False) diff --git a/sgbackup/icons/sgbackup.ico b/sgbackup/icons/sgbackup.ico index 94ffc0866fc127e993bfb10266648e2f1c074845..c2eca71274719c0e59a09665bc66854ca5130da6 100644 GIT binary patch literal 5694 zcmeHK`A<_p5dOgXLPbHms(95~j#?_T0u{6yyI4f>|lW2m`z3y z_LDLEcOxP)45r25Fh@ngVlg2)+Kd=$6l^vNVq=$3-5A*IRwOy%k-TadQj!y}HqC+T z%v5aJ=)~rn4anVgsAI8$AOvo!~B{!}q8 z)s~>XwiNZ}OL3X3q3#&2T`EUoeFd7XRFYMpxuFU-t{z8AV>Mc?*PylOBgu$uz}WvJPSJVAb*ni8{l5^17@(2e51UQpuQa=@@~Oq~zpwr4t6n+( z0Fx=e>*9$Zo&O@4@6a@dJa@(BEQefpk=Vc4#i4+~@|?$kgq!$;yjkdDB)G@=7@=Rz z^#LZEmBChp7oT+Kq{Qe=`cd~d*=K0>aC&)ir=(Ul* z$Wl*~o^l=cL?0>1wei372s9WNZZNpruK2pS2PVk+Fs>)haA_(+p=DT@tY0QrpnYXa zQ&%XhonZ;GK4XB3#=aNI-g!pV4^SUKV>pX^pxfSgN?HFH`SO~!n3HZgMTDclsoEy*8;g1txjOwX~rIaRRJ9nJUfA?4& zxq5T?S!-rGek)8agFhd-&uKKh7LJ-zn10`NKKx7m_S+ t&nPw0IL$D{xWJyN<}&RIj8hELjFG7-?wk2K;OjsiI>3LIKL3Aq;0KZh2zmej literal 16958 zcmeHOOKTKO6s-^+;Km2WXF>#t4^&9f1_Mb*A_{^Fg^-1jpkP46C`cA=5|gD1b*CW# zO?)Gw2;xHBjDNwMJHed`9~+HkX4-QmQ$yR-bX9eCA~V)}-}P4Cx^?b3)pvU6>4^|E z_^+o&;NNXx*FhmR2qAXC5E2tG#>>5Y-xto@9=INGJ>Yu4^?>UE*8{ExTo0&vpw(+` zfE|MM!@6K=Re4JpBZndvaw4~eH0!q=@M?g02KFB2hb)KV7qES4)-=IF&Ar9iS@;d;N2bFLgt+Hux`v1XV@_?X;R#i^goW(C$hHK-A+dA+x{ zd(rNvdJcAx(=|&@PpQRr4^ZQXWgWQ>lU_%%zEg2+Z*S-Oa55&RBPz_O80x%TsDE0eRTYEw*(%gOtJ129LHmO$)c-)GRTZmJ z{r6Q`RWazFslxL=qtdF1LHkq{>Yq|+RmGhCSfBr&ux}TO#&Ocm>$KP3RGn*84BW?J z@Nbh?Slbq3y@}poZ-3!li1@a9JSO?+Jh0E(@;`O*Z0R1m?^HkU(@pf)F1a+80UDN zY#)9YKvzWAJT)mE$1erT2Rj7UzH!_j;hytDBcx>6>&s|z)@ ztz+Ipf!}G^j~F~ZW6g2?9YQ%i+4;h6ZjK?3xi*@aOHQYAIlKG3a$KU~sIvpc-+kp_ zI-XyUa}{R%8vy1O*Kn;4pB%_#M-=D9m_yC&uuCvLFS!fjz3@XApC6CI@Ef9*6>)2G z?E*#tiq}Mb9Fu7dcgI7>jBpTt_NHXxE^pl;CkRc>jB73@%52d zEAiC7+xPKjCwbB6`*UJEFIM~hH!+?ScnqBp-C2P@O_~thbNo6$h(>tC3Z7pP@Hi#E zsfg1lL_Ki>o^@)#qUdYhWg4?Mh6fkq6*x%`oO*Ix@OjP(>iXM53t!Z<;3wz`S{HnV e$Jm39qBbM2Pv699{LeiCH|$lOnB)FZz5W2d3WJXT diff --git a/sgbackup/logger.conf b/sgbackup/logger.conf index 6889fbb..1946320 100644 --- a/sgbackup/logger.conf +++ b/sgbackup/logger.conf @@ -1,5 +1,5 @@ [loggers] -keys=root,console,file +keys=root [handlers] keys=consoleHandler,fileHandler @@ -8,30 +8,19 @@ keys=consoleHandler,fileHandler keys=consoleFormatter,fileFormatter [logger_root] -level=INFO +level=DEBUG handlers=consoleHandler,fileHandler -[logger_console] -level=DEBUG -handlers=consoleHandler -propagate=0 -qualname=console - -[logger_file] -level=DEBUG -handlers=fileHandler -propagate=0 -qualname=file - [handler_consoleHandler] class=StreamHandler formatter=consoleFormatter -level=DEBUG +level=INFO [handler_fileHandler] class=handlers.RotatingFileHandler args=('sgbackup.log','a',10485760,10,'UTF-8',False,) formatter=fileFormatter +level=DEBUG [formatter_consoleFormatter] format=[%(levelname)s:%(name)s] %(message)s diff --git a/sgbackup/main.py b/sgbackup/main.py index d238dcc..98f6b28 100644 --- a/sgbackup/main.py +++ b/sgbackup/main.py @@ -21,18 +21,49 @@ import logging from . import gui from .gui import Application from .steam import SteamLibrary - +import sys +from . import commands logger=logging.getLogger(__name__) def cli_main(): logger.debug("Running cli_main()") + argc = len(sys.argv) + if argc < 2: + return commands.COMMANDS['synopsis'].execute([]) + + commands_to_execute = [] + last = 0 + command = None + + for i in range(1,len(sys.argv)): + if (sys.argv[i] == '--'): + if command is not None: + commands_to_execute.append((command,sys.argv[last+1:i] if last < i else [])) + command = None + continue + if command is None: + try: + command = commands.COMMANDS[sys.argv[i]] + last = i + except: + logger.error("No such command \"{command}\"!".format(command=sys.argv[i])) + return 4 + + if command is not None: + commands_to_execute.append((command,sys.argv[last+1:] if (last+1) < len(sys.argv) else [])) + command=None + elif not commands_to_execute: + return commands.COMMANDS['synopsis'].execute([]) + + for cmd,argv in commands_to_execute: + ec = cmd.execute(argv) + if ec: + logger.error('sgbackup aborted due to an error!') + return ec + return 0 -#def curses_main(): -# logger.debug("Running curses_main()") -# return 0 - def gui_main(): logger.debug("Running gui_main()") gui._app = Application()