devel-commit

This commit is contained in:
Christian Moser 2024-12-31 08:18:35 +01:00
parent aca16784e7
commit 7f96010822
32 changed files with 2247 additions and 0 deletions

16
.githooks/pre-commit Executable file
View File

@ -0,0 +1,16 @@
#!/bin/sh
# vim: syn=sh ts=4 sts=4 sw=4 smartindent expandtab ff=unix
SELF="$(realpath $0)"
GITHOOKS_DIR="$(dirname "$SELF")" ; export GITHOOKS_DIR
PROJECT_ROOT="$(dirname "$(dirname "$SELF")")" ; export PROJECT_ROOT
pre_commit_d="${GITHOOKS_DIR}/pre-commit-d"
for i in $(ls "$pre_commit_d"); do
script="${pre_commit_d}/$i"
if [ -x "$script" ]; then
"$script" "$@"
fi
done

View File

@ -0,0 +1,48 @@
#!/usr/bin/env bash
# vim: syn=sh ts=4 sts=4 sw=4 smartindent expandtab ff=unix
dos2unix=$(which dos2unix)
unix2dos=$(which unix2dos)
githooks="pre-commit prepare-commit-msg commit-msg post-commit applypatch-msg pre-applypatch post-applypatch pre-rebase post-rewrite post-checkout post-merge"
dos2unix_used=NO
__IFS="$IFS"
IFS=$'\n'
if [ -n "$dos2unix" -a "$unix2dos" ]; then
for line in $(git status -s); do
if [[ line == A* || $line == M* ]]; then
file="${line:3}"
abspath="${PROJECT_ROOT}/$file"
if [[ $file == *.py || $file == *.sh || file == *.rst ]]; then
$dos2unix "$abspath"
git add "$file"
dos2unix_used=YES
continue
fi
#check if we are updating a githook
for githook in $githooks; do
if [ "$file" = ".githooks/$githook" ]; then
$dos2unix "$abspath"
git add "$file"
dos2unix_used=YES
break
fi
done
fi
if [ "$dos2unix_used" = "YES" ]; then
continue
fi
if [[ "$file" == *.txt ]]; then
$unix2dos "$abspath"
git add "$file"
fi
done
else
echo "\"dos2unix\" and/or \"unix2dos\" not found!" >&2
fi
IFS="$__IFS"

16
.gitignore vendored
View File

@ -169,3 +169,19 @@ cython_debug/
# PyPI configuration file
.pypirc
.vscode/
.vsccode/*
# logfiles
*.log
*.LOG
*.[Ll][Oo][Gg]
# editor files
*~
*.swp
*.tmp
*.temp
apidoc/

24
pyproject.toml Normal file
View File

@ -0,0 +1,24 @@
[build-system]
builbackend = 'setuptools.build_meta'
requires = ['setuptools >= 61.0']
[project]
dynamic = ["version"]
name = 'sgbackup'
version = '0.0.0'
requires_python = '>= 3.11'
description = 'Savegame Backup Tool'
readme = 'README.md'
license = {file = 'LICENSE'}
authors = [
{name = 'Christian Moser', email = 'christian@mydevel.at'},
]
dependencies = ['gi','yaml']
[project.scripts]
sgbackup = 'sgbackup:cli_main'
csgbackup = 'sgbackup:curses_main'
[project.gui-scripts]
gsgbackup = 'sgbackup:gui_main'

40
sgbackup/__init__.py Normal file
View File

@ -0,0 +1,40 @@
###############################################################################
# sgbackup - The SaveGame Backup tool #
# Copyright (C) 2024 Christian Moser #
# #
# This program is free software: you can redistribute it and/or modify #
# it under the terms of the GNU General Public License as published by #
# the Free Software Foundation, either version 3 of the License, or #
# (at your option) any later version. #
# #
# This program is distributed in the hope that it will be useful, #
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
# GNU General Public License for more details. #
# #
# You should have received a copy of the GNU General Public License #
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
###############################################################################
import gi
gi.require_version('Gtk','4.0')
__version__ = "0.0.1"
from .settings import settings
from . import _logging
from .main import cli_main,curses_main,gui_main
from . import game
from .command import Command
from . import commands
from . import archiver
__ALL__ = [
"settings"
"cli_main",
"gui_main",
"curses_main",
'game',
"Command",
"commands",
"archiver",
]

23
sgbackup/__main__.py Normal file
View File

@ -0,0 +1,23 @@
###############################################################################
# sgbackup - The SaveGame Backup tool #
# Copyright (C) 2024 Christian Moser #
# #
# This program is free software: you can redistribute it and/or modify #
# it under the terms of the GNU General Public License as published by #
# the Free Software Foundation, either version 3 of the License, or #
# (at your option) any later version. #
# #
# This program is distributed in the hope that it will be useful, #
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
# GNU General Public License for more details. #
# #
# You should have received a copy of the GNU General Public License #
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
###############################################################################
from .main import cli_main
import sys
if __name__ == '__main__':
sys.exit(cli_main())

28
sgbackup/_logging.py Normal file
View File

@ -0,0 +1,28 @@
###############################################################################
# sgbackup - The SaveGame Backup tool #
# Copyright (C) 2024 Christian Moser #
# #
# This program is free software: you can redistribute it and/or modify #
# it under the terms of the GNU General Public License as published by #
# the Free Software Foundation, either version 3 of the License, or #
# (at your option) any later version. #
# #
# This program is distributed in the hope that it will be useful, #
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
# GNU General Public License for more details. #
# #
# You should have received a copy of the GNU General Public License #
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
###############################################################################
import os
import logging
import logging.config
from .settings import settings
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"))

20
sgbackup/archiver.py Normal file
View File

@ -0,0 +1,20 @@
###############################################################################
# sgbackup - The SaveGame Backup tool #
# Copyright (C) 2024 Christian Moser #
# #
# This program is free software: you can redistribute it and/or modify #
# it under the terms of the GNU General Public License as published by #
# the Free Software Foundation, either version 3 of the License, or #
# (at your option) any later version. #
# #
# This program is distributed in the hope that it will be useful, #
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
# GNU General Public License for more details. #
# #
# You should have received a copy of the GNU General Public License #
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
###############################################################################
class Archiver:
pass

43
sgbackup/command.py Normal file
View File

@ -0,0 +1,43 @@
###############################################################################
# sgbackup - The SaveGame Backup tool #
# Copyright (C) 2024 Christian Moser #
# #
# This program is free software: you can redistribute it and/or modify #
# it under the terms of the GNU General Public License as published by #
# the Free Software Foundation, either version 3 of the License, or #
# (at your option) any later version. #
# #
# This program is distributed in the hope that it will be useful, #
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
# GNU General Public License for more details. #
# #
# You should have received a copy of the GNU General Public License #
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
###############################################################################
class Command:
def __init__(self,id:str,name:str,description:str):
self.__id = id
self.__name = name
self.__description = description
def get_name(self):
return self.__name
def get_id(self):
return self.__id
def get_description(self):
return self.__description
def get_help(self):
raise NotImplementedError("Command.get_help() is not implemented!")
def get_synopsis(self):
raise NotImplementedError("Command.get_synopsis() is not implemented!")
def execute(self,argv:list):
raise NotImplementedError("Command.execute is not implemented!")

View File

@ -0,0 +1,48 @@
###############################################################################
# sgbackup - The SaveGame Backup tool #
# Copyright (C) 2024 Christian Moser #
# #
# This program is free software: you can redistribute it and/or modify #
# it under the terms of the GNU General Public License as published by #
# the Free Software Foundation, either version 3 of the License, or #
# (at your option) any later version. #
# #
# This program is distributed in the hope that it will be useful, #
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
# GNU General Public License for more details. #
# #
# You should have received a copy of the GNU General Public License #
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
###############################################################################
import os
COMMANDS = {}
_mods = ['commandbase']
for _f in os.listdir(os.path.dirname(__file__)):
if _f.startswith('_'):
continue
if _f.endswith('.py') or _f.endswith('.pyc'):
if (_f.endswith('py')):
_m = _f[0:-3]
else:
_m = _f[0:-4]
if _m not in _mods:
exec("\n".join([
"from . import " + _m,
"_mods += _m",
"_mod = " + _m]))
if hasattr(_mod,"COMMANDS") and len(_mod.COMMANDS) > 0:
for _cmd in _mod.COMMANDS:
COMMANDS[_cmd.get_id()] = _cmd
del _cmd
del _mods
del _f
del _m
del _mod

38
sgbackup/commands/help.py Normal file
View File

@ -0,0 +1,38 @@
###############################################################################
# sgbackup - The SaveGame Backup tool #
# Copyright (C) 2024 Christian Moser #
# #
# This program is free software: you can redistribute it and/or modify #
# it under the terms of the GNU General Public License as published by #
# the Free Software Foundation, either version 3 of the License, or #
# (at your option) any later version. #
# #
# This program is distributed in the hope that it will be useful, #
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
# GNU General Public License for more details. #
# #
# You should have received a copy of the GNU General Public License #
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
###############################################################################
from sgbackup import __version__ as VERSION, Command
class VersionCommand(Command):
def __init__(self):
super().__init__('version', 'Version', 'Show version information.')
def get_synopsis(self):
return 'sgbackup version'
def get_help(self):
return super().get_help()
def execute(self, argv):
print("sgbackup - {}".format(VERSION))
return 0
# VersionCommand class
COMMANDS = [
VersionCommand(),
]

View File

View File

1015
sgbackup/game.py Normal file

File diff suppressed because it is too large Load Diff

18
sgbackup/gui/__init__.py Normal file
View File

@ -0,0 +1,18 @@
###############################################################################
# sgbackup - The SaveGame Backup tool #
# Copyright (C) 2024 Christian Moser #
# #
# This program is free software: you can redistribute it and/or modify #
# it under the terms of the GNU General Public License as published by #
# the Free Software Foundation, either version 3 of the License, or #
# (at your option) any later version. #
# #
# This program is distributed in the hope that it will be useful, #
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
# GNU General Public License for more details. #
# #
# You should have received a copy of the GNU General Public License #
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
###############################################################################
from .application import Application

24
sgbackup/gui/__main__.py Normal file
View File

@ -0,0 +1,24 @@
###############################################################################
# sgbackup - The SaveGame Backup tool #
# Copyright (C) 2024 Christian Moser #
# #
# This program is free software: you can redistribute it and/or modify #
# it under the terms of the GNU General Public License as published by #
# the Free Software Foundation, either version 3 of the License, or #
# (at your option) any later version. #
# #
# This program is distributed in the hope that it will be useful, #
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
# GNU General Public License for more details. #
# #
# You should have received a copy of the GNU General Public License #
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
###############################################################################
import sys
from ..main import gui_main
if __name__ == '__main__':
sys.exit(gui_main())

View File

@ -0,0 +1,83 @@
###############################################################################
# sgbackup - The SaveGame Backup tool #
# Copyright (C) 2024 Christian Moser #
# #
# This program is free software: you can redistribute it and/or modify #
# it under the terms of the GNU General Public License as published by #
# the Free Software Foundation, either version 3 of the License, or #
# (at your option) any later version. #
# #
# This program is distributed in the hope that it will be useful, #
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
# GNU General Public License for more details. #
# #
# You should have received a copy of the GNU General Public License #
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
###############################################################################
from gi.repository import Gtk,GObject,Gio
from .appwindow import AppWindow
import logging; logger=logging.getLogger(__name__)
class Application(Gtk.Application):
def __init__(self,*args,**kwargs):
AppFlags = Gio.ApplicationFlags
kwargs['application_id'] = 'org.sgbackup.sgbackup'
kwargs['flags'] = AppFlags.FLAGS_NONE
Gtk.Application.__init__(self,*args,**kwargs)
self.__logger = logger.getChild('Application')
self.__builder = None
self.__appwindow = None
@property
def _logger(self):
return self.__logger
@GObject.Property
def appwindow(self):
return self.__appwindow
def do_startup(self):
self._logger.debug('do_startup()')
if not self.__builder:
self.__builder = Gtk.Builder.new()
Gtk.Application.do_startup(self)
action_about = Gio.SimpleAction.new('about',None)
action_about.connect('activate',self.on_action_about)
self.add_action(action_about)
action_quit = Gio.SimpleAction.new('quit',None)
action_quit.connect('activate',self.on_action_quit)
self.add_action(action_quit)
action_settings = Gio.SimpleAction.new('settings',None)
action_settings.connect('activate',self.on_action_settings)
self.add_action(action_settings)
# add accels
self.set_accels_for_action('app.quit',["<Primary>q"])
@GObject.Property
def builder(self):
return self.__builder
def do_activate(self):
self._logger.debug('do_activate()')
if not (self.__appwindow):
self.__appwindow = AppWindow(application=self)
self.appwindow.present()
def on_action_about(self,action,param):
pass
def on_action_settings(self,action,param):
pass
def on_action_quit(self,action,param):
self.quit()

32
sgbackup/gui/appmenu.ui Normal file
View File

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8" ?>
<interface>
<menu id='appmenu'>
<section>
<item>
<attribute name='label' translatable='true'>_Settings</attribute>
<attribute name='action'>app.settings</attribute>
</item>
</section>
<section>
<submenu>
<attribute name='label' translatable='true'>_Help</attribute>
<section id='appmenu.help.help'></section>
<section id='appmenu.help.about'>
<item>
<attribute name='label' translatable='true'>_About SGBackup</attribute>
<attribute name='action' translatable='true'>app.about</attribute>
</item>
</section>
</submenu>
</section>
<section>
<item>
<attribute name='label' translatable='true'>_Quit</attribute>
<attribute name='action'>app.quit</attribute>
</item>
</section>
</menu>
</interface>

86
sgbackup/gui/appwindow.py Normal file
View File

@ -0,0 +1,86 @@
###############################################################################
# sgbackup - The SaveGame Backup tool #
# Copyright (C) 2024 Christian Moser #
# #
# This program is free software: you can redistribute it and/or modify #
# it under the terms of the GNU General Public License as published by #
# the Free Software Foundation, either version 3 of the License, or #
# (at your option) any later version. #
# #
# This program is distributed in the hope that it will be useful, #
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
# GNU General Public License for more details. #
# #
# You should have received a copy of the GNU General Public License #
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
###############################################################################
from gi.repository import Gtk,Gio,GObject
import os
from .gameview import GameView
from .backupview import BackupView
class AppWindow(Gtk.ApplicationWindow):
def __init__(self,application=None,**kwargs):
kwargs['title'] = "SGBackup"
if (application is not None):
kwargs['application']=application
if (hasattr(application,'builder')):
builder = application.builder
else:
builder = Gtk.Builder.new()
Gtk.ApplicationWindow.__init__(self,**kwargs)
self.set_default_size(800,600)
self.__builder = builder
self.builder.add_from_file(os.path.join(os.path.dirname(__file__),'appmenu.ui'))
gmenu = self.builder.get_object('appmenu')
appmenu_popover = Gtk.PopoverMenu.new_from_model(gmenu)
image = Gtk.Image.new_from_icon_name('open-menu-symbolic')
menubutton = Gtk.MenuButton.new()
menubutton.set_popover(appmenu_popover)
menubutton.set_child(image)
headerbar = Gtk.HeaderBar.new()
headerbar.pack_start(menubutton)
self.set_titlebar(headerbar)
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
self.__vpaned = Gtk.Paned(orientation=Gtk.Orientation.VERTICAL)
self.__vpaned.set_hexpand(True)
self.__vpaned.set_vexpand(True)
self.__vpaned.set_wide_handle(True)
self.__gameview = GameView()
self.__vpaned.set_start_child(self.gameview)
self.__backupview = BackupView(self.gameview)
self.__vpaned.set_end_child(self.backupview)
self.__vpaned.set_resize_start_child(True)
self.__vpaned.set_resize_end_child(True)
vbox.append(self.__vpaned)
statusbar = Gtk.Statusbar()
statusbar.set_hexpand(True)
statusbar.set_vexpand(False)
statusbar.push(0,'Running ...')
vbox.append(statusbar)
self.set_child(vbox)
@GObject.Property
def builder(self):
return self.__builder
@GObject.Property
def backupview(self):
return self.__backupview
@GObject.Property
def gameview(self):
return self.__gameview

View File

@ -0,0 +1,30 @@
###############################################################################
# sgbackup - The SaveGame Backup tool #
# Copyright (C) 2024 Christian Moser #
# #
# This program is free software: you can redistribute it and/or modify #
# it under the terms of the GNU General Public License as published by #
# the Free Software Foundation, either version 3 of the License, or #
# (at your option) any later version. #
# #
# This program is distributed in the hope that it will be useful, #
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
# GNU General Public License for more details. #
# #
# You should have received a copy of the GNU General Public License #
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
###############################################################################
from gi.repository import Gtk,Gio,GObject
from .gameview import GameView
class BackupView(Gtk.ScrolledWindow):
def __init__(self,gameview:GameView):
Gtk.ScrolledWindow.__init__(self)
self.__gameview = GameView
@GObject.Property
def gameview(self):
return self.__gameview

24
sgbackup/gui/gameview.py Normal file
View File

@ -0,0 +1,24 @@
###############################################################################
# sgbackup - The SaveGame Backup tool #
# Copyright (C) 2024 Christian Moser #
# #
# This program is free software: you can redistribute it and/or modify #
# it under the terms of the GNU General Public License as published by #
# the Free Software Foundation, either version 3 of the License, or #
# (at your option) any later version. #
# #
# This program is distributed in the hope that it will be useful, #
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
# GNU General Public License for more details. #
# #
# You should have received a copy of the GNU General Public License #
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
###############################################################################
from gi.repository import Gtk,Gio,GObject
class GameView(Gtk.ScrolledWindow):
def __init__(self):
Gtk.ScrolledWindow.__init__(self)

View File

@ -0,0 +1,17 @@
###############################################################################
# sgbackup - The SaveGame Backup tool #
# Copyright (C) 2024 Christian Moser #
# #
# This program is free software: you can redistribute it and/or modify #
# it under the terms of the GNU General Public License as published by #
# the Free Software Foundation, either version 3 of the License, or #
# (at your option) any later version. #
# #
# This program is distributed in the hope that it will be useful, #
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
# GNU General Public License for more details. #
# #
# You should have received a copy of the GNU General Public License #
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
###############################################################################

40
sgbackup/logger.conf Normal file
View File

@ -0,0 +1,40 @@
[loggers]
keys=root,console,file
[handlers]
keys=consoleHandler,fileHandler
[formatters]
keys=consoleFormatter,fileFormatter
[logger_root]
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
[handler_fileHandler]
class=handlers.RotatingFileHandler
args=('sgbackup.log','a',10485760,10,'UTF-8',False,)
formatter=fileFormatter
[formatter_consoleFormatter]
format=[%(levelname)s:%(name)s] %(message)s
[formatter_fileFormatter]
format=[%(asctime)s-%(levelname)s:%(name)s] %(message)s

20
sgbackup/logger.yaml Normal file
View File

@ -0,0 +1,20 @@
version: 1
formatters:
console:
format: '[%(levelname)s:%(name)s] %(message)s'
file:
format: '[%(asctime)s - %(levelname)s:%(name)s] %(message)'
handlers:
console:
class: logging.StreamHandler
level: DEBUG
formatter: console
stream: 'ext://sys.stdout'
loggers:
console:
handlers: [console]
level: DEBUG
propagate: no
root:
level: DEBUG
handlers: [console]

42
sgbackup/main.py Normal file
View File

@ -0,0 +1,42 @@
#enconding: utf-8
###############################################################################
# sgbackup - The SaveGame Backup tool #
# Copyright (C) 2024 Christian Moser #
# #
# This program is free software: you can redistribute it and/or modify #
# it under the terms of the GNU General Public License as published by #
# the Free Software Foundation, either version 3 of the License, or #
# (at your option) any later version. #
# #
# This program is distributed in the hope that it will be useful, #
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
# GNU General Public License for more details. #
# #
# You should have received a copy of the GNU General Public License #
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
###############################################################################
import logging
from . import gui
from .gui.application import Application
from .steam import SteamLibrary
import sys
logger=logging.getLogger(__name__)
def cli_main():
logger.debug("Running cli_main()")
return 0
def curses_main():
logger.debug("Running curses_main()")
return 0
def gui_main():
logger.debug("Running gui_main()")
gui.app = Application()
gui.app.run()
return 0

85
sgbackup/settings.py Normal file
View File

@ -0,0 +1,85 @@
###############################################################################
# sgbackup - The SaveGame Backup tool #
# Copyright (C) 2024 Christian Moser #
# #
# This program is free software: you can redistribute it and/or modify #
# it under the terms of the GNU General Public License as published by #
# the Free Software Foundation, either version 3 of the License, or #
# (at your option) any later version. #
# #
# This program is distributed in the hope that it will be useful, #
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
# GNU General Public License for more details. #
# #
# You should have received a copy of the GNU General Public License #
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
###############################################################################
from configparser import ConfigParser
import os
import sys
from gi.repository import GLib,GObject
class Settings(GObject.GObject):
def __init__(self):
super().__init__()
self.__configparser = ConfigParser()
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.__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)
@GObject.Property(nick="parser")
def parser(self)->ConfigParser:
return self.__configparser
@GObject.Property(type=str,nick="config-dir")
def config_dir(self)->str:
return self.__config_dir
@GObject.Property(type=str,nick="config-file")
def config_file(self)->str:
return self.__config_file
@GObject.Property(type=str,nick="gameconf-dir")
def gameconf_dir(self)->str:
return self.__gameconf_dir
@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:
if self.parser.has_option('sgbackup','backupDirectory'):
return self.parser.get('sgbackup','backupDirectory')
return GLib.build_filename(GLib.build_filename(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!")
return self.parser.set('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"
def save(self):
self.emit('save')
@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)
settings = Settings()

296
sgbackup/steam.py Normal file
View File

@ -0,0 +1,296 @@
###############################################################################
# sgbackup - The SaveGame Backup tool #
# Copyright (C) 2024 Christian Moser #
# #
# This program is free software: you can redistribute it and/or modify #
# it under the terms of the GNU General Public License as published by #
# the Free Software Foundation, either version 3 of the License, or #
# (at your option) any later version. #
# #
# This program is distributed in the hope that it will be useful, #
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
# GNU General Public License for more details. #
# #
# You should have received a copy of the GNU General Public License #
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
###############################################################################
import os
import re
from pathlib import Path
import sys
import json
from .settings import settings
PLATFORM_WINDOWS = (sys.platform.lower() == 'win32')
from gi.repository.GObject import GObject,Property,Signal
class AcfFileParser(object):
"""
Parses steam acf files to a dict.
"""
def __init__(self):
pass
def __parse_section(self,lines:list):
option_pattern = re.compile("^\"(.+)\"([ \\t]+)\"(.*)\"$")
section_pattern = re.compile("^\"(.+)\"")
line_count = 0
ret = {}
line_count=0
while line_count < len(lines):
line = lines[line_count]
line_count+=1
if line == '}':
break
s=line.strip()
match = option_pattern.fullmatch(line)
if match:
name=line[match.start(1):match.end(1)]
value=line[match.start(3):match.end(3)]
ret[name] = value
else:
match2 = section_pattern.fullmatch(line)
if match2:
name=line[match2.start(1):match2.end(1)]
if lines[line_count] == '{':
line_count += 1
n_lines,sect = self.__parse_section(lines[line_count:])
line_count += n_lines
ret[name]=sect
return line_count,ret
def parse_file(self,acf_file)->dict:
if not os.path.isfile(acf_file):
raise FileNotFoundError("File \"{s}\" does not exist!")
with open(acf_file,'r') as ifile:
buffer = ifile.read()
lines = [l.strip() for l in buffer.split('\n')]
if lines[0] == "\"AppState\"" and lines[1] == "{":
n_lines,sect = self.__parse_section(lines[2:])
return sect
raise RuntimeError("Not a acf file!")
class IgnoreSteamApp(GObject):
@staticmethod
def new_from_dict(conf:dict):
if ('appid' in conf and 'name' in conf):
appid = conf['appid']
name = conf['name']
reason = conf['reason'] if 'reason' in conf else ""
return SteamIgnoreApp(appid,name,reason)
return None
def __init__(self,appid:int,name:str,reason:str):
GObject.__init__(self)
self.__appid = int(appid)
self.__name = name
self.__reason = reason
@Property(type=int)
def appid(self)->str:
return self.__appid
@Property(type=str)
def name(self)->str:
return self.__name
@name.setter
def name(self,name:str):
self.__name = name
@Property(type=str)
def reason(self)->str:
return self.__reason
@reason.setter
def reason(self,reason:str):
self.__reason = reason
def serialize(self):
return {
'appid': self.appid,
'name': self.name,
'reason': self.reason,
}
class SteamApp(GObject):
def __init__(self,appid:int,name:str,installdir:str):
GObject.__init__(self)
self.__appid = int(appid)
self.__name = name
self.__installdir = installdir
@Property(type=int)
def appid(self):
return self.__appid
@Property
def name(self):
return self.__name
@Property
def installdir(self):
return self.__installdir
def __str__(self):
return '{}: {}'.format(self.appid,self.name)
def __gt__(self,other):
return self.appid > other.appid
def __lt__(self,other):
return self.appid < other.appid
def __eq__(self,other):
return self.appid == other.appid
class SteamLibrary(GObject):
def __init__(self,library_path:str):
GObject.__init__(self)
self.directory = library_path
@Property(type=str)
def directory(self):
return self.__directory
@directory.setter
def directory(self,directory:str):
if not os.path.isabs(directory):
raise ValueError("\"directory\" is not an absolute path!")
if not os.path.isdir(directory):
raise NotADirectoryError("\"{}\" is not a directory or does not exist!".format(directory))
self.__directory = directory
@Property
def path(self)->Path:
return Path(self.directory).resolve()
@Property
def steam_apps(self)->list:
parser = AcfFileParser()
appdir = self.path / "steamapps"
commondir = appdir / "common"
ret = []
for acf_file in appdir.glob('appmanifest_*.acf'):
if not acf_file.is_file():
continue
try:
data = parser.parse_file(str(acf_file))
app = SteamApp(data['appid'],data['name'],str(commondir/data['installdir']))
ret.append(app)
except:
pass
return sorted(ret)
class Steam(GObject):
def __init__(self):
GObject.__init__(self)
self.__libraries = []
self.__ignore_apps = []
if not self.steamlib_list_file.is_file():
if (PLATFORM_WINDOWS):
libdirs=[
"C:\\Program Files (x86)\\steam",
"C:\\Program Files\\steam",
]
for i in libdirs:
if (os.path.isdir(i)):
self.__libraries.append(SteamLibrary(i))
break
else:
with open(str(self.steamlib_list_file),'r',encoding="utf-8") as ifile:
for line in (i.strip() for i in ifile.readlines()):
if not line or line.startswith('#'):
continue
libdir = Path(line).resolve()
if libdir.is_dir():
try:
self.add_library(str(libdir))
except:
pass
ignore_apps = []
if self.ignore_apps_file.is_file():
with open(str(self.ignore_apps_file),'r',encoding="utf-8") as ifile:
ignore_list = json.loads(ifile.read())
for ignore in ignore_list:
try:
ignore_app = IgnoreSteamApp.new_from_dict(ignore)
except:
continue
if ignore_app:
self.__ignore_apps.append(ignore_app)
self.__ignore_apps = sorted(ignore_apps)
#__init__()
@Property
def steamlib_list_file(self)->Path:
return Path(settings.config_dir).resolve() / 'steamlib.lst'
@Property
def ignore_apps_file(self)->Path:
return Path(settings.config_dir).resolve / 'ignore_steamapps.json'
@Property
def libraries(self):
return self.__libraries
@Property
def ignore_apps(self):
return self.__ignore_apps
def __write_steamlib_list_file(self):
with open(self.steamlib_list_file,'w',encoding='utf-8') as ofile:
ofile.write('\n'.join(str(sl.directory) for sl in self.libraries))
def add_library(self,steamlib:SteamLibrary|str):
if isinstance(steamlib,SteamLibrary):
lib = steamlib
else:
lib = SteamLibrary(steamlib)
lib_exists = False
for i in self.libraries:
if i.derctory == lib.directory:
lib_exists = True
break
if not lib_exists:
self.__libraries.append(lib)
self.__write_steamlib_list_file()
def remove_library(self,steamlib:SteamLibrary|str):
if isinstance(steamlib,SteamLibrary):
libdir = steamlib.directory
else:
libdir = str(steamlib)
delete_libs=[]
for i in range(len(self.__libraries)):
if self.__libraries[i].directory == libdir:
delete_libs.append(i)
if delete_libs:
for i in sorted(delete_libs,reverse=True):
del self.__libraries[i]
self.__write_steamlib_list_file()

44
sphinx/conf.py Normal file
View File

@ -0,0 +1,44 @@
# vim: syn=python ts=4 sts=4 sw=4 smartindent autoindent expandtab
import sys,os
PROJECT_ROOT = os.path.dirname(os.path.dirname(__file__))
sys.path.insert(0,PROJECT_ROOT)
import sgbackup
project = 'sgbackup'
copyright = '2024-2025, Christian Moser'
author = 'Christian Moser'
version = sgbackup.__version__
exclude_patterns = [
'_build',
'Thumbs.db',
'.DS_Store',
'*~',
'*.swp',
'*.tmp',
'*.temp',
'*.log',
]
extensions = [
'sphinx.ext.autodoc'
]
language = 'en'
master_doc = 'index'
source_suffix = '.rst'
templates_path = ['templates']
html_theme = 'sphinx_rtd_theme'
html_show_sourcelink = False
#html_static_path = ['_static']
autoclass_content='both'
autodoc_class_signature='mixed'
autodoc_default_options={
'member_order':'alphabetical',
'undoc_members':'true',
'exclude_memebers':'__weakref__',
}

29
sphinx/index.rst Normal file
View File

@ -0,0 +1,29 @@
==========================
sgbackup API documentation
==========================
.. title:: sgbackup API
Descritpion
-----------
Sgbackup is a savegame backup tool written in Python. It has a commandline and a graphical
interface using *Gtk4*.
The minimum Python version is **3.11**.
It has\ *Gtk4*\ and\ *GObject Introspection*\ as a dependency.
Table of Contents
-----------------
.. toctree::
:maxdepth: 2
install.rst
modules/sgbackup.rst
modules/sgbackup.settings.rst
modules/sgbackup.game.rst
modules/sgbackup.archiver.rst

3
sphinx/install.rst Normal file
View File

@ -0,0 +1,3 @@
========================
Installing sgbackup
========================

5
sphinx/modules.rst Normal file
View File

@ -0,0 +1,5 @@
.. title:: Modules
.. toctree:: 2
modules/sgbackup
modules/game

View File

@ -0,0 +1,10 @@
============
sgbackup API
============
.. title:: sgbackup API
.. automodule:: sgbackup
:imported-mebers:
:undoc-members: