2025.02.23 13:23:07 (desktop)

This commit is contained in:
Christian Moser 2025-02-23 13:23:07 +01:00
parent 2fa39385c0
commit 6aac8282ef
Failed to extract signature
10 changed files with 238 additions and 70 deletions

View File

@ -24,6 +24,7 @@ if [ ! -d "$bindir" ]; then
fi 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 cat > "${bindir}/sgbackup" << EOF
#!/bin/bash #!/bin/bash
@ -52,7 +53,7 @@ install_ps1="${PRJECT_DIR}/install.ps1"
wproject_dir="$( cygpath -w "${PROJECT_DIR}" )" wproject_dir="$( cygpath -w "${PROJECT_DIR}" )"
cat > "$install_ps1" << EOF cat > "$install_ps1" << EOF
[Environment]::SetEnvironemtnVariable("Path","\$env:PATH;$wbindir","User") [Environment]::SetEnvironmentVariable("Path","\$env:PATH;$wbindir","User")
\$desktop_dir=[Environment]::getFolderPath("Desktop") \$desktop_dir=[Environment]::getFolderPath("Desktop")
\$startmenu_dir=[Environment]::getFolderPath("StartMenu") \$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) { foreach (\$dir in \$desktop_dir,\$startmenu_dir) {
\$shell=New-Object -ComObject WScript.Shell \$shell=New-Object -ComObject WScript.Shell
\$shortcut=\$shell.CreateShortcut('\$dir\\sgbackup.lnk') \$shortcut=\$shell.CreateShortcut("\$dir\\sgbackup.lnk")
\$shortcut.TargetPath='$wbindir\\gsgbackup.cmd' \$shortcut.TargetPath='$pythonwpath'
\$shortcut.Arguments='-m sgbackup.gui'
\$shortcut.IconLocation="\$picture_dir\\sgbackup.ico" \$shortcut.IconLocation="\$picture_dir\\sgbackup.ico"
\$shortcut.Save() \$shortcut.Save()
} }

View File

@ -31,6 +31,7 @@ setup(
], ],
package_data={ package_data={
'sgbackup':[ 'sgbackup':[
'logger.conf',
'icons/sgbackup.ico', 'icons/sgbackup.ico',
'icons/hicolor/symbolic/*/*.svg' 'icons/hicolor/symbolic/*/*.svg'
], ],

View File

@ -29,7 +29,7 @@ import os
import threading import threading
import time import time
from ..game import Game from ..game import Game,SavegameType,VALID_SAVEGAME_TYPES,SAVEGAME_TYPE_ICONS
from ..settings import settings from ..settings import settings
from ..utility import sanitize_path,sanitize_windows_path from ..utility import sanitize_path,sanitize_windows_path
@ -72,9 +72,8 @@ class Archiver(GObject):
return False return False
def backup(self,game:Game)->bool: def backup(self,game:Game)->bool:
self._logger.info("Backing up {game}".format(game=game.key))
if not game.get_backup_files(): 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 return False
filename = self.generate_new_backup_filename(game) filename = self.generate_new_backup_filename(game)
@ -82,18 +81,24 @@ class Archiver(GObject):
if not os.path.isdir(dirname): if not os.path.isdir(dirname):
os.makedirs(dirname) os.makedirs(dirname)
self._logger.info("Backing up {game} -> {filename}".format( self._logger.info("[backup] {game} -> {filename}".format(
game=game.key,filename=filename)) game=game.key,filename=filename))
return self.emit('backup',game,filename) return self.emit('backup',game,filename)
def generate_new_backup_filename(self,game:Game)->str: def generate_new_backup_filename(self,game:Game)->str:
dt = datetime.datetime.now() 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"), dt.strftime("%Y%m%d-%H%M%S"),
game.savegame_type.value,
game.savegame_subdir,
"sgbackup", "sgbackup",
self.extensions[0][1:] if self.extensions[0].startswith('.') else self.extensions[0])) 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): def _backup_progress(self,game:Game,fraction:float,message:str|None):
if fraction > 1.0: if fraction > 1.0:
@ -201,12 +206,13 @@ class ArchiverManager(GObject):
archiver.backup(game) archiver.backup(game)
archiver.disconnect(backup_sc) archiver.disconnect(backup_sc)
if game.is_live and settings.backup_versions > 0: 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: if backups and len(backups) > settings.backup_versions:
for filename in backups[settings.backup_versions:]: for filename in backups[settings.backup_versions:]:
self.remove_backup(game,filename) self.remove_backup(game,filename)
self.emit("backup-game-finished",game) self.emit("backup-game-finished",game)
if not multi_backups: if not multi_backups:
self.emit("backup-finished") self.emit("backup-finished")
@ -253,8 +259,6 @@ class ArchiverManager(GObject):
else: else:
n = len(games) n = len(games)
print("Starting backup with {n} threads".format(n=n))
for i in range(n): for i in range(n):
game=game_list[0] game=game_list[0]
del game_list[0] del game_list[0]
@ -307,34 +311,48 @@ class ArchiverManager(GObject):
def get_live_backups(self,game:Game): def get_live_backups(self,game:Game):
ret = [] 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): if os.path.isdir(backupdir):
for basename in os.listdir(backupdir): for basename in os.listdir(backupdir):
filename = os.path.join(backupdir,basename) filename = os.path.join(backupdir,basename)
if (self.is_archive(filename)): if (self.is_archive(filename)):
ret.append(filename) 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 return ret
def get_finished_backups(self,game:Game): def get_finished_backups(self,game:Game):
ret=[] ret=[]
backupdir = os.path.join(settings.backup_dir,game.savegame_name,'finished') for sgtype in VALID_SAVEGAME_TYPES:
backupdir = os.path.join(settings.backup_dir,game.savegame_name,sgtype.value,'finished')
if os.path.isdir(backupdir): if os.path.isdir(backupdir):
for basename in os.listdir(backupdir): for basename in os.listdir(backupdir):
filename = os.path.join(backupdir,basename) filename = os.path.join(backupdir,basename)
if (self.is_archive(filename)): if (self.is_archive(filename)):
ret.append(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)
if (self.is_archive(filename)):
ret.append(filename)
return ret 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

View File

@ -20,7 +20,7 @@ import os
COMMANDS = {} COMMANDS = {}
_mods = ['commandbase'] _mods = []
for _f in os.listdir(os.path.dirname(__file__)): for _f in os.listdir(os.path.dirname(__file__)):
if _f.startswith('_'): if _f.startswith('_'):
@ -35,14 +35,11 @@ for _f in os.listdir(os.path.dirname(__file__)):
if _m not in _mods: if _m not in _mods:
exec("\n".join([ exec("\n".join([
"from . import " + _m, "from . import " + _m,
"_mods += _m", "_mods.append(_m)",
"_mod = " + _m])) "_mod = " + _m]))
if hasattr(_mod,"COMMANDS") and len(_mod.COMMANDS) > 0: if hasattr(_mod,"COMMANDS"): #and _mod.COMMANDS:
for _cmd in _mod.COMMANDS: COMMANDS.update(_mod.COMMANDS)
COMMANDS[_cmd.get_id()] = _cmd
del _cmd
del _mods del _mods
del _f del _f
del _m del _m
del _mod

View File

@ -16,8 +16,11 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>. # # along with this program. If not, see <https://www.gnu.org/licenses/>. #
############################################################################### ###############################################################################
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): class VersionCommand(Command):
def __init__(self): def __init__(self):
super().__init__('version', 'Version', 'Show version information.') super().__init__('version', 'Version', 'Show version information.')
@ -33,6 +36,39 @@ class VersionCommand(Command):
return 0 return 0
# VersionCommand class # VersionCommand class
COMMANDS = [ class SynopsisCommand(Command):
VersionCommand(), 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
}

View File

@ -144,6 +144,19 @@ class SavegameType(StrEnum):
return st.UNSET 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 = { SAVEGAME_TYPE_ICONS = {
SavegameType.UNSET : None, SavegameType.UNSET : None,
SavegameType.WINDOWS: 'windows-svgrepo-com-symbolic', SavegameType.WINDOWS: 'windows-svgrepo-com-symbolic',

View File

@ -28,7 +28,7 @@ from pathlib import Path
from ..settings import settings from ..settings import settings
from ._settingsdialog import SettingsDialog from ._settingsdialog import SettingsDialog
from ._gamedialog import GameDialog 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 SteamLibrariesDialog,NewSteamAppsDialog,NoNewSteamAppsDialog
from ..steam import Steam from ..steam import Steam
from ._backupdialog import BackupSingleDialog,BackupManyDialog from ._backupdialog import BackupSingleDialog,BackupManyDialog
@ -532,12 +532,42 @@ class BackupViewData(GObject):
self.__filename = filename self.__filename = filename
basename = os.path.basename(filename) basename = os.path.basename(filename)
self.__is_live = (os.path.basename(os.path.dirname(filename)) == 'live')
parts = basename.split('.') parts = basename.split('.')
self.__savegame_name = parts[0]
self.__timestamp = DateTime.strptime(parts[2],"%Y%m%d-%H%M%S")
self.__extension = '.' + '.'.join(parts[3:]) self.__savegame_name = parts[0]
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])
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 @property
def game(self)->Game: def game(self)->Game:
@ -548,6 +578,24 @@ class BackupViewData(GObject):
""" """
return self.__game 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) @Property(type=str)
def savegame_name(self)->str: def savegame_name(self)->str:
""" """
@ -618,6 +666,16 @@ class BackupView(Gtk.Box):
self.__liststore = Gio.ListStore() self.__liststore = Gio.ListStore()
selection = Gtk.SingleSelection.new(self.__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 = Gtk.SignalListItemFactory()
live_factory.connect('setup',self._on_live_column_setup) live_factory.connect('setup',self._on_live_column_setup)
live_factory.connect('bind',self._on_live_column_bind) 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) size_column = Gtk.ColumnViewColumn.new("Size",size_factory)
self.__columnview = Gtk.ColumnView.new(selection) 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(live_column)
self.__columnview.append_column(sgname_column) self.__columnview.append_column(sgname_column)
self.__columnview.append_column(timestamp_column) self.__columnview.append_column(timestamp_column)
@ -662,6 +722,27 @@ class BackupView(Gtk.Box):
""" """
return self.__gameview 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): def _on_live_column_setup(self,factory,item):
checkbutton = Gtk.CheckButton() checkbutton = Gtk.CheckButton()
checkbutton.set_sensitive(False) checkbutton.set_sensitive(False)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

View File

@ -1,5 +1,5 @@
[loggers] [loggers]
keys=root,console,file keys=root
[handlers] [handlers]
keys=consoleHandler,fileHandler keys=consoleHandler,fileHandler
@ -8,30 +8,19 @@ keys=consoleHandler,fileHandler
keys=consoleFormatter,fileFormatter keys=consoleFormatter,fileFormatter
[logger_root] [logger_root]
level=INFO level=DEBUG
handlers=consoleHandler,fileHandler 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] [handler_consoleHandler]
class=StreamHandler class=StreamHandler
formatter=consoleFormatter formatter=consoleFormatter
level=DEBUG level=INFO
[handler_fileHandler] [handler_fileHandler]
class=handlers.RotatingFileHandler class=handlers.RotatingFileHandler
args=('sgbackup.log','a',10485760,10,'UTF-8',False,) args=('sgbackup.log','a',10485760,10,'UTF-8',False,)
formatter=fileFormatter formatter=fileFormatter
level=DEBUG
[formatter_consoleFormatter] [formatter_consoleFormatter]
format=[%(levelname)s:%(name)s] %(message)s format=[%(levelname)s:%(name)s] %(message)s

View File

@ -21,17 +21,48 @@ import logging
from . import gui from . import gui
from .gui import Application from .gui import Application
from .steam import SteamLibrary from .steam import SteamLibrary
import sys
from . import commands
logger=logging.getLogger(__name__) logger=logging.getLogger(__name__)
def cli_main(): def cli_main():
logger.debug("Running cli_main()") logger.debug("Running cli_main()")
return 0 argc = len(sys.argv)
if argc < 2:
return commands.COMMANDS['synopsis'].execute([])
#def curses_main(): commands_to_execute = []
# logger.debug("Running curses_main()") last = 0
# return 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 gui_main(): def gui_main():
logger.debug("Running gui_main()") logger.debug("Running gui_main()")