mirror of
https://github.com/c9moser/sgbackup.git
synced 2026-01-19 11:30:13 +00:00
2025.01.13 02:08:06
This commit is contained in:
parent
538f70c8b8
commit
a63453fd87
28
sgbackup/archiver/__init__.py
Normal file
28
sgbackup/archiver/__init__.py
Normal 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/>. #
|
||||
###############################################################################
|
||||
|
||||
from ._archiver import Archiver,ArchiverManager
|
||||
import importlib
|
||||
|
||||
archiver = ArchiverManager()
|
||||
|
||||
__ALL__ = [
|
||||
"Archiver",
|
||||
"AchiverManager",
|
||||
"archiver",
|
||||
]
|
||||
@ -24,7 +24,11 @@ from gi.repository.GObject import (
|
||||
signal_accumulator_true_handled,
|
||||
)
|
||||
|
||||
from .game import Game
|
||||
import datetime
|
||||
import os
|
||||
|
||||
from ..game import Game
|
||||
from ..settings import settings
|
||||
|
||||
class Archiver:
|
||||
def __init__(self,key:str,name:str,extensions:list[str],decription:str|None=None):
|
||||
@ -35,6 +39,8 @@ class Archiver:
|
||||
else:
|
||||
self.__description = ""
|
||||
|
||||
self.__extensions = list(extensions)
|
||||
|
||||
@Property(type=str)
|
||||
def name(self)->str:
|
||||
return self.__name
|
||||
@ -47,20 +53,68 @@ class Archiver:
|
||||
def description(self)->str:
|
||||
return self.__description
|
||||
|
||||
def backup(self,game)->bool:
|
||||
pass
|
||||
@Property
|
||||
def extensions(self)->list[str]:
|
||||
return self.__extensions
|
||||
|
||||
def backup(self,game:Game)->bool:
|
||||
if not game.get_backup_files():
|
||||
return
|
||||
filename = self.generate_new_backup_filename()
|
||||
dirname = os.path.dirname(filename)
|
||||
if not os.path.isdir(dirname):
|
||||
os.makedirs(dirname)
|
||||
|
||||
self.emit('backup',game,filename)
|
||||
|
||||
def restore(self,game,file)->bool:
|
||||
pass
|
||||
|
||||
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"),
|
||||
"sgbackup",
|
||||
self.extensions[0])
|
||||
return os.path.join(settings.backup_dir,game.savegame_name,game.subdir,basename)
|
||||
|
||||
|
||||
|
||||
|
||||
@Signal(name="backup",flags=SignalFlags.RUN_FIRST,
|
||||
return_type=bool, arg_types=(GObject,str),
|
||||
accumulator=signal_accumulator_true_handled)
|
||||
def do_backup(self,game:Game,filename:str):
|
||||
pass
|
||||
raise NotImplementedError("{_class}.{function}() is not implemented!",_class=__class__,function="do_backup")
|
||||
|
||||
@Signal(name="restore",flags=SignalFlags.RUN_FIRST,
|
||||
return_type=bool,arg_types=(GObject,str),
|
||||
return_type=bool,arg_types=(str,),
|
||||
accumulator=signal_accumulator_true_handled)
|
||||
def do_backup(self,game:Game,filanme:str):
|
||||
pass
|
||||
def do_restore(self,filanme:str):
|
||||
raise NotImplementedError("{_class}.{function}() is not implemented!",_class=__class__,function="do_restore")
|
||||
|
||||
class ArchiverManager(GObject):
|
||||
__global_archiver_manager = None
|
||||
|
||||
def __init__(self):
|
||||
GObject.__init__(self)
|
||||
self.__archivers = {}
|
||||
|
||||
|
||||
@staticmethod
|
||||
def get_global():
|
||||
if ArchiverManager.__global_archiver_manager is None:
|
||||
ArchiverManager.__global_archiver_manager = ArchiverManager()
|
||||
|
||||
return ArchiverManager.__global_archiver_manager
|
||||
|
||||
@property
|
||||
def standard_archiver(self)->Archiver:
|
||||
try:
|
||||
return self.__archivers[settings.archiver]
|
||||
except:
|
||||
return self.__archivers["zipfile"]
|
||||
|
||||
|
||||
|
||||
71
sgbackup/archiver/zipfilearchiver.py
Normal file
71
sgbackup/archiver/zipfilearchiver.py
Normal file
@ -0,0 +1,71 @@
|
||||
###############################################################################
|
||||
# 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 ._archiver import Archiver
|
||||
import zipfile
|
||||
import json
|
||||
import os
|
||||
from ..game import Game,GameManager
|
||||
from settings import settings
|
||||
|
||||
class ZipfileArchiver(Archiver):
|
||||
def __init__(self):
|
||||
Archiver.__init__(self,"zipfile","ZipFile",[".zip"],"Archiver for .zip files.")
|
||||
|
||||
def do_backup(self, game:Game, filename:str):
|
||||
files = game.get_backup_files()
|
||||
game_data = json.dumps(game.serialize(),ensure_ascii=False,indent=4)
|
||||
with zipfile.ZipFile(filename,mode="w",
|
||||
compression=settings.zipfile_compression,
|
||||
compresslevel=settings.zipfile_compresslevel) as zf:
|
||||
zf.writestr("game.conf",game_data)
|
||||
for path,arcname in files.items():
|
||||
zf.write(path,arcname)
|
||||
|
||||
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():
|
||||
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!")
|
||||
|
||||
with zipfile.ZipFile(filename,"r") as zf:
|
||||
zip_game = Game.new_from_dict(json.loads(zf.read('game.conf').decode("utf-8")))
|
||||
try:
|
||||
game = GameManager.get_global().games[zip_game.key]
|
||||
except:
|
||||
game = zip_game
|
||||
|
||||
if not game.savegame_root:
|
||||
os.makedirs(game.savegame_root)
|
||||
|
||||
extract_files = [i for i in zf.filelist if i.startswith(zip_game.savegame_dir + "/")]
|
||||
for file in extract_files:
|
||||
zf.extract(file,game.savegame_root)
|
||||
|
||||
return True
|
||||
|
||||
ARCHIVERS = [
|
||||
ZipfileArchiver(),
|
||||
]
|
||||
@ -19,6 +19,7 @@
|
||||
from gi.repository.GObject import Property,GObject,Signal,SignalFlags
|
||||
from gi.repository import GLib
|
||||
|
||||
|
||||
import os
|
||||
import json
|
||||
import re
|
||||
@ -28,6 +29,7 @@ from enum import StrEnum
|
||||
import sys
|
||||
import logging
|
||||
import pathlib
|
||||
import datetime
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -1303,6 +1305,19 @@ class Game(GObject):
|
||||
|
||||
backup_files = get_backup_files_recursive(sgroot,sgdir)
|
||||
|
||||
@Property(type=str)
|
||||
def savegame_subdir(self)->str:
|
||||
"""
|
||||
savegame_subdir The subdir for the savegame backup.
|
||||
|
||||
If `is_live` results to `True`, *"live"* is returned. Else *"finished"* is returned.
|
||||
|
||||
:type: str
|
||||
"""
|
||||
if (self.is_live):
|
||||
return "live"
|
||||
return "finished"
|
||||
|
||||
|
||||
class GameManager(GObject):
|
||||
__global_gamemanager = None
|
||||
@ -1379,3 +1394,4 @@ class GameManager(GObject):
|
||||
if (game.steam_windows):
|
||||
self.__steam_games[game.steam_windows.appid] = game
|
||||
self.__steam_windows_games[game.steam_windows.appid] = game
|
||||
|
||||
|
||||
@ -200,6 +200,10 @@ class GameView(Gtk.ScrolledWindow):
|
||||
# GameView class
|
||||
|
||||
class BackupViewData(GObject):
|
||||
"""
|
||||
BackupViewData The data class for BackupView
|
||||
"""
|
||||
|
||||
def __init__(self,_game:Game,filename:str):
|
||||
GObject.GObject.__init__(self)
|
||||
self.__game = _game
|
||||
@ -215,34 +219,75 @@ class BackupViewData(GObject):
|
||||
|
||||
@property
|
||||
def game(self)->Game:
|
||||
"""
|
||||
game The `Game` the data belong to
|
||||
|
||||
:type: Game
|
||||
"""
|
||||
return self.__game
|
||||
|
||||
@Property
|
||||
def savegame_name(self):
|
||||
@Property(type=str)
|
||||
def savegame_name(self)->str:
|
||||
"""
|
||||
savegame_name The savegame_name of the file.
|
||||
|
||||
:type: str
|
||||
"""
|
||||
return self.__savegame_name
|
||||
|
||||
@Property(type=str)
|
||||
def filename(self)->str:
|
||||
"""
|
||||
filename The full filename of the savegame backup.
|
||||
|
||||
:type: str
|
||||
"""
|
||||
return self.__filename
|
||||
|
||||
@Property(type=bool,default=False)
|
||||
def is_live(self)->bool:
|
||||
"""
|
||||
is_live `True` if the savegame backup is from a live game.
|
||||
|
||||
:type: bool
|
||||
"""
|
||||
pass
|
||||
|
||||
@Property
|
||||
def extension(self):
|
||||
@Property(type=str)
|
||||
def extension(self)->str:
|
||||
"""
|
||||
extension The extension of the file.
|
||||
|
||||
:type: str
|
||||
"""
|
||||
return self.__extension
|
||||
|
||||
@Property
|
||||
def timestamp(self):
|
||||
def timestamp(self)->DateTime:
|
||||
"""
|
||||
timestamp The timestamp of the file.
|
||||
|
||||
DateTime is the alias for `datetime.datetime`.
|
||||
|
||||
:type: DateTime
|
||||
"""
|
||||
return self.__timestamp
|
||||
|
||||
def _on_selection_changed(self,selection):
|
||||
pass
|
||||
|
||||
class BackupView(Gtk.ScrolledWindow):
|
||||
"""
|
||||
BackupView This view displays the backup for the selected `Game`.
|
||||
"""
|
||||
__gtype_name__ = "BackupView"
|
||||
def __init__(self,gameview:GameView):
|
||||
"""
|
||||
BackupView
|
||||
|
||||
:param gameview: The `GameView` to connect this class to.
|
||||
:type gameview: GameView
|
||||
"""
|
||||
Gtk.ScrolledWindow.__init__(self)
|
||||
self.__gameview = gameview
|
||||
|
||||
@ -277,6 +322,11 @@ class BackupView(Gtk.ScrolledWindow):
|
||||
|
||||
@property
|
||||
def gameview(self)->GameView:
|
||||
"""
|
||||
gameview The GameView this class is connected to.
|
||||
|
||||
:type: GameView
|
||||
"""
|
||||
return self.__gameview
|
||||
|
||||
def _on_live_column_setup(self,factory,item):
|
||||
@ -338,8 +388,17 @@ class BackupView(Gtk.ScrolledWindow):
|
||||
|
||||
|
||||
class AppWindow(Gtk.ApplicationWindow):
|
||||
"""
|
||||
AppWindow The applications main window.
|
||||
"""
|
||||
__gtype_name__ = "AppWindow"
|
||||
def __init__(self,application=None,**kwargs):
|
||||
"""
|
||||
AppWindow
|
||||
|
||||
:param application: The `Application` this window belongs to, defaults to `None`.
|
||||
:type application: Application, optional
|
||||
"""
|
||||
kwargs['title'] = "SGBackup"
|
||||
|
||||
if (application is not None):
|
||||
@ -389,18 +448,39 @@ class AppWindow(Gtk.ApplicationWindow):
|
||||
self.set_child(vbox)
|
||||
|
||||
@property
|
||||
def builder(self):
|
||||
def builder(self)->Gtk.Builder:
|
||||
"""
|
||||
builder The Builder for this Window.
|
||||
|
||||
If application is set and it has an attriubte *builder*, The applications builder
|
||||
is used else a new `Gtk.Builder` instance is created.
|
||||
|
||||
:type: Gtk.Builder
|
||||
"""
|
||||
return self.__builder
|
||||
|
||||
@property
|
||||
def backupview(self):
|
||||
def backupview(self)->BackupView:
|
||||
"""
|
||||
backupview The `BackupView` of this window.
|
||||
|
||||
:type: BackupView
|
||||
"""
|
||||
return self.__backupview
|
||||
|
||||
@property
|
||||
def gameview(self):
|
||||
def gameview(self)->GameView:
|
||||
"""
|
||||
gameview The `GameView` for this window.
|
||||
|
||||
:type: GameView
|
||||
"""
|
||||
return self.__gameview
|
||||
|
||||
def refresh(self):
|
||||
"""
|
||||
refresh Refresh the views of this window.
|
||||
"""
|
||||
self.gameview.refresh()
|
||||
#self.backupview.refresh()
|
||||
|
||||
@ -411,7 +491,7 @@ class Application(Gtk.Application):
|
||||
Signals
|
||||
_______
|
||||
|
||||
+ `settings-dialog-init`
|
||||
+ **settings-dialog-init** - Called when the application creates a new `SettingsDialog`.
|
||||
"""
|
||||
__gtype_name__ = "Application"
|
||||
|
||||
@ -433,10 +513,18 @@ class Application(Gtk.Application):
|
||||
return self.__logger
|
||||
|
||||
@property
|
||||
def appwindow(self):
|
||||
def appwindow(self)->AppWindow:
|
||||
"""
|
||||
appwindow The main `AppWindow` of this app.
|
||||
|
||||
:type: AppWindow
|
||||
"""
|
||||
return self.__appwindow
|
||||
|
||||
def do_startup(self):
|
||||
"""
|
||||
do_startup The startup method for this application.
|
||||
"""
|
||||
self._logger.debug('do_startup()')
|
||||
if not self.__builder:
|
||||
self.__builder = Gtk.Builder.new()
|
||||
@ -451,29 +539,37 @@ class Application(Gtk.Application):
|
||||
theme.add_search_path(str(icons_path))
|
||||
|
||||
action_about = Gio.SimpleAction.new('about',None)
|
||||
action_about.connect('activate',self.on_action_about)
|
||||
action_about.connect('activate',self._on_action_about)
|
||||
self.add_action(action_about)
|
||||
|
||||
action_new_game = Gio.SimpleAction.new('new-game',None)
|
||||
action_new_game.connect('activate',self.on_action_new_game)
|
||||
action_new_game.connect('activate',self._on_action_new_game)
|
||||
self.add_action(action_new_game)
|
||||
|
||||
action_quit = Gio.SimpleAction.new('quit',None)
|
||||
action_quit.connect('activate',self.on_action_quit)
|
||||
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)
|
||||
action_settings.connect('activate',self._on_action_settings)
|
||||
self.add_action(action_settings)
|
||||
|
||||
# add accels
|
||||
self.set_accels_for_action('app.quit',["<Primary>q"])
|
||||
|
||||
@Property
|
||||
def builder(self):
|
||||
@property
|
||||
def builder(self)->Gtk.Builder:
|
||||
"""
|
||||
builder Get the builder for the application.
|
||||
|
||||
:type: Gtk.Builder
|
||||
"""
|
||||
return self.__builder
|
||||
|
||||
def do_activate(self):
|
||||
"""
|
||||
do_activate This method is called, when the application is activated.
|
||||
"""
|
||||
self._logger.debug('do_activate()')
|
||||
if not (self.__appwindow):
|
||||
self.__appwindow = AppWindow(application=self)
|
||||
@ -481,28 +577,34 @@ class Application(Gtk.Application):
|
||||
|
||||
self.appwindow.present()
|
||||
|
||||
def on_action_about(self,action,param):
|
||||
def _on_action_about(self,action,param):
|
||||
pass
|
||||
|
||||
def on_action_settings(self,action,param):
|
||||
def _on_action_settings(self,action,param):
|
||||
dialog = self.new_settings_dialog()
|
||||
dialog.present()
|
||||
|
||||
def on_action_quit(self,action,param):
|
||||
def _on_action_quit(self,action,param):
|
||||
self.quit()
|
||||
|
||||
def _on_dialog_response_refresh(self,dialog,response,check_response):
|
||||
if response == check_response:
|
||||
self.appwindow.refresh()
|
||||
|
||||
def on_action_new_game(self,action,param):
|
||||
def _on_action_new_game(self,action,param):
|
||||
dialog = GameDialog(self.appwindow)
|
||||
dialog.connect('response',
|
||||
self._on_dialog_response_refresh,
|
||||
Gtk.ResponseType.APPLY)
|
||||
dialog.present()
|
||||
|
||||
def new_settings_dialog(self):
|
||||
def new_settings_dialog(self)->SettingsDialog:
|
||||
"""
|
||||
new_settings_dialog Create a new `SettingsDialog`.
|
||||
|
||||
:return: The new dialog.
|
||||
:rtype: `SettingsDialog`
|
||||
"""
|
||||
dialog = SettingsDialog(self.appwindow)
|
||||
self.emit('settings-dialog-init',dialog)
|
||||
return dialog
|
||||
@ -511,6 +613,14 @@ class Application(Gtk.Application):
|
||||
flags=SignalFlags.RUN_LAST,
|
||||
return_type=None,
|
||||
arg_types=(SettingsDialog,))
|
||||
def do_settings_dialog_init(self,dialog):
|
||||
def do_settings_dialog_init(self,dialog:SettingsDialog):
|
||||
"""
|
||||
do_settings_dialog_init The **settings-dialog-init** signal callback for initializing the `SettingsDialog`.
|
||||
|
||||
This signal is ment to add pages to the `SettingsDialog`.
|
||||
|
||||
:param dialog: The dialog to initialize.
|
||||
:type dialog: SettingsDialog
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
@ -17,7 +17,7 @@
|
||||
###############################################################################
|
||||
|
||||
from gi.repository import Gtk,GLib,Gio
|
||||
from gi.repository.GObject import GObject,Signal,Property
|
||||
from gi.repository.GObject import GObject,Signal,Property,SignalFlags
|
||||
|
||||
from ..settings import settings
|
||||
|
||||
@ -81,7 +81,6 @@ class SettingsDialog(Gtk.Dialog):
|
||||
dialog.select_folder(self,None,self._on_backupdir_dialog_select_folder)
|
||||
|
||||
|
||||
|
||||
def add_page(self,page,name,title):
|
||||
self.__stack.add_titled(page,name,title)
|
||||
|
||||
@ -91,7 +90,10 @@ class SettingsDialog(Gtk.Dialog):
|
||||
settings.save()
|
||||
self.destroy()
|
||||
|
||||
@Signal(name='save')
|
||||
@Signal(name='save',
|
||||
flags=SignalFlags.RUN_FIRST,
|
||||
return_type=None,
|
||||
arg_types=())
|
||||
def do_save(self):
|
||||
settings.backup_dir = self.__backupdir_label.get_text()
|
||||
|
||||
@ -21,6 +21,27 @@ import os
|
||||
import sys
|
||||
|
||||
from gi.repository import GLib,GObject
|
||||
import zipfile
|
||||
|
||||
ZIPFILE_COMPRESSION_STR = {
|
||||
zipfile.ZIP_STORED: "stored",
|
||||
zipfile.ZIP_DEFLATED: "deflated",
|
||||
zipfile.ZIP_BZIP2: "bzip2",
|
||||
zipfile.ZIP_LZMA: "lzma",
|
||||
}
|
||||
|
||||
ZIPFILE_COMPRESSLEVEL_MAX = {
|
||||
zipfile.ZIP_STORED: 0,
|
||||
zipfile.ZIP_DEFLATED: 9,
|
||||
zipfile.ZIP_BZIP2: 9,
|
||||
zipfile.ZIP_LZMA: 0,
|
||||
}
|
||||
|
||||
ZIPFILE_STR_COMPRESSION = {}
|
||||
for _zc,_zs in ZIPFILE_COMPRESSION_STR.items():
|
||||
ZIPFILE_STR_COMPRESSION[_zs] = _zc
|
||||
del _zc
|
||||
del _zs
|
||||
|
||||
class Settings(GObject.GObject):
|
||||
__gtype_name__ = "Settings"
|
||||
@ -84,6 +105,33 @@ class Settings(GObject.GObject):
|
||||
return self.parser.get('sgbackup','logLevel')
|
||||
return "INFO"
|
||||
|
||||
@GObject.Property(type=str)
|
||||
def zipfile_compression(self)->str:
|
||||
if self.parser.has_option('zipfile','compression'):
|
||||
try:
|
||||
ZIPFILE_STR_COMPRESSION[self.parser.get('zipfile','compression')]
|
||||
except:
|
||||
pass
|
||||
return ZIPFILE_STR_COMPRESSION["deflated"]
|
||||
|
||||
@zipfile_compression.setter
|
||||
def zipfile_compression(self,compression):
|
||||
try:
|
||||
self.parser.set('zipfile','compression',ZIPFILE_COMPRESSION_STR[compression])
|
||||
except:
|
||||
self.parser.set('zipfile','compression',ZIPFILE_STR_COMPRESSION[zipfile.ZIP_DEFLATED])
|
||||
|
||||
@GObject.Property(type=int)
|
||||
def zipfile_compresslevel(self)->int:
|
||||
if self.parser.has_option('zipfile','compressLevel'):
|
||||
cl = self.parser.getint('zipfile','compressLevel')
|
||||
return cl if cl <= ZIPFILE_COMPRESSLEVEL_MAX[self.zipfile_compression] else ZIPFILE_COMPRESSLEVEL_MAX[self.zipfile_compression]
|
||||
return ZIPFILE_COMPRESSLEVEL_MAX[self.zipfile_compression]
|
||||
|
||||
@zipfile_compresslevel.setter
|
||||
def zipfile_compresslevel(self,cl:int):
|
||||
self.parser.set('zipfile','compressLevel',cl)
|
||||
|
||||
def save(self):
|
||||
self.emit('save')
|
||||
|
||||
|
||||
@ -5,5 +5,4 @@ Applicaction
|
||||
.. autoclass:: sgbackup.gui.Application
|
||||
:members:
|
||||
:undoc-members:
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user