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

@ -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()
}

View File

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

View File

@ -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

View File

@ -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

View File

@ -16,8 +16,11 @@
# 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):
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
}

View File

@ -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',

View File

@ -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)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

View File

@ -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

View File

@ -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()