mirror of
https://github.com/c9moser/sgbackup.git
synced 2026-01-19 19:40:13 +00:00
2025.02.11 10:35:29 (desktop)
This commit is contained in:
parent
f9779ea8a1
commit
7c68d4d2a2
6
msys-install-requirements.sh → msys-install.sh
Executable file → Normal file
6
msys-install-requirements.sh → msys-install.sh
Executable file → Normal file
@ -1,6 +1,9 @@
|
||||
#!/bin/bash
|
||||
# vim: syn=sh ts=4 sts=4 sw=4 smartindent expandtab ff=unix
|
||||
|
||||
SELF="$( realpath "$0" )"
|
||||
PROJECT_DIR="$( dirname "$SELF")"
|
||||
|
||||
PACKAGES="gtk4 gobject-introspection python-gobject python-rapidfuzz"
|
||||
|
||||
_install_pkg=""
|
||||
@ -11,3 +14,6 @@ done
|
||||
pacman -Sy
|
||||
pacman -S --noconfirm $_install_pkg
|
||||
|
||||
cd $PROJECT_DIR
|
||||
pip install --user .
|
||||
|
||||
@ -26,3 +26,4 @@ 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"))
|
||||
|
||||
|
||||
@ -31,12 +31,15 @@ import time
|
||||
|
||||
from ..game import Game
|
||||
from ..settings import settings
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class Archiver(GObject):
|
||||
def __init__(self,key:str,name:str,extensions:list[str],description:str|None=None):
|
||||
GObject.__init__(self)
|
||||
self.__key = key
|
||||
self.__name = name
|
||||
self._logger = logger.getChild("Archiver")
|
||||
if description:
|
||||
self.__description = description
|
||||
else:
|
||||
@ -67,13 +70,18 @@ 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():
|
||||
return
|
||||
self._logger.warning("No files SaveGame files for game {game}!".format(game=game.key))
|
||||
return False
|
||||
|
||||
filename = self.generate_new_backup_filename(game)
|
||||
dirname = os.path.dirname(filename)
|
||||
if not os.path.isdir(dirname):
|
||||
os.makedirs(dirname)
|
||||
|
||||
self._logger.info("Backing up {game} -> {filename}".format(
|
||||
game=game.key,filename=filename))
|
||||
return self.emit('backup',game,filename)
|
||||
|
||||
def generate_new_backup_filename(self,game:Game)->str:
|
||||
@ -82,7 +90,7 @@ class Archiver(GObject):
|
||||
game.savegame_subdir,
|
||||
dt.strftime("%Y%m%d-%H%M%S"),
|
||||
"sgbackup",
|
||||
self.extensions[0][1:]))
|
||||
self.extensions[0][1:] if self.extensions[0].startswith('.') else self.extensions[0]))
|
||||
return os.path.join(settings.backup_dir,game.savegame_name,game.subdir,basename)
|
||||
|
||||
def _backup_progress(self,game:Game,fraction:float,message:str|None):
|
||||
@ -147,11 +155,11 @@ class ArchiverManager(GObject):
|
||||
def archivers(self):
|
||||
return self.__archivers
|
||||
|
||||
def _on_archiver_backup_progress_single(self,archiver,game,fraction,message):
|
||||
pass
|
||||
def _on_archiver_backup_progress_single(self,game:Game,fraction:float,message:str):
|
||||
self.emit('backup-game-progress',game,fraction,str)
|
||||
|
||||
def _on_archiver_backup_progress_multi(self,archiver,game,fraction,message):
|
||||
pass
|
||||
def _on_archiver_backup_progress_multi(self,fraction):
|
||||
self.emit('backup-progress',fraction)
|
||||
|
||||
|
||||
@Signal(name="backup-game-progress",return_type=None,arg_types=(Game,float,str),flags=SignalFlags.RUN_FIRST)
|
||||
@ -170,10 +178,23 @@ class ArchiverManager(GObject):
|
||||
def do_backup_finished(self):
|
||||
pass
|
||||
|
||||
def backup(self,game:Game):
|
||||
@Signal(name="remove-backup",return_type=None,arg_types=(Game,str),flags=SignalFlags.RUN_FIRST)
|
||||
def do_remove_backup(self,game,filename):
|
||||
logger.info("Removing backup \"{filename}\" for {game}".format(
|
||||
filename=os.path.basename(filename),
|
||||
game=game.key))
|
||||
|
||||
if os.path.isfile(filename):
|
||||
os.unlink(filename)
|
||||
|
||||
def remove_backup(self,game,filename):
|
||||
self.emit("remove-backup",game,filename)
|
||||
|
||||
def backup(self,game:Game,multi_backups:bool=False):
|
||||
def on_progress(archiver,game,fraction,message):
|
||||
self.emit("backup-game-progress",game,fraction,message)
|
||||
self.emit("backup-progress",fraction)
|
||||
if not multi_backups:
|
||||
self.emit("backup-progress",fraction)
|
||||
|
||||
if self.backup_in_progress:
|
||||
raise RuntimeError("A backup is already in progress!!!")
|
||||
@ -184,19 +205,41 @@ class ArchiverManager(GObject):
|
||||
backup_sc = archiver.connect('backup-progress',on_progress)
|
||||
archiver.backup(game)
|
||||
archiver.disconnect(backup_sc)
|
||||
if game.is_live and settings.backup_versions > 0:
|
||||
backups = sorted(self.get_live_backups(game))
|
||||
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)
|
||||
self.emit("backup-finished")
|
||||
if not multi_backups:
|
||||
self.emit("backup-finished")
|
||||
self.backup_in_progress = False
|
||||
|
||||
def backup_many(self,games:list[Game]):
|
||||
def on_game_progress(game,fraction,message,game_progress):
|
||||
with game_progress._mutex:
|
||||
game_progress[game.key] = fraction
|
||||
sum_fractions = 0.0
|
||||
for f in game_progress.values():
|
||||
sum_fractions += f
|
||||
n = len(game_progress)
|
||||
|
||||
self._on_archiver_backup_progress_multi(sum_fractions/n if n > 0 else 0.0)
|
||||
|
||||
|
||||
def thread_function(game):
|
||||
archiver = self.standard_archiver
|
||||
self._on_archiver_backup_progress_multi(archiver,game,1.0,"Finished ...")
|
||||
self.backup(game,True)
|
||||
|
||||
if self.backup_in_progress:
|
||||
raise RuntimeError("A backup is already in progress!!!")
|
||||
self.backup_in_progress = True
|
||||
game_list = list(games)
|
||||
game_progress = dict(((game.key,0.0) for game in game_list))
|
||||
game_progress._mutex = threading.RLock()
|
||||
game_progress._game_progress_connection = self.connect('backup-game-progress',game_progress)
|
||||
threadpool = {}
|
||||
|
||||
if len(game_list) > 8:
|
||||
@ -226,7 +269,18 @@ class ArchiverManager(GObject):
|
||||
thread = threading.Thread(thread_function,args=game,daemon=True)
|
||||
threadpool.append(thread)
|
||||
thread.start()
|
||||
|
||||
self.disconnect(game_progress._game_progress_connection)
|
||||
self.emit("backup-finished")
|
||||
self.backup_in_progress = False
|
||||
|
||||
def _on_archiver_backup(self,archiver:Archiver,game:Game,filename:str)->bool:
|
||||
return self.emit('backup',archiver,game,filename)
|
||||
|
||||
@Signal(name="backup",return_type=bool,arg_types=(Archiver,Game,str),
|
||||
flags=SignalFlags.RUN_FIRST,accumulator=signal_accumulator_true_handled)
|
||||
def backup(self,archiver,game,filename):
|
||||
return True
|
||||
|
||||
def is_archive(self,filename)->bool:
|
||||
if self.standard_archiver.is_archive(filename):
|
||||
@ -236,6 +290,28 @@ class ArchiverManager(GObject):
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_live_backups(self,game:Game):
|
||||
ret = []
|
||||
backupdir = os.path.join(settings.backup_dir,game.savegame_name,'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')]:
|
||||
@ -246,5 +322,4 @@ class ArchiverManager(GObject):
|
||||
ret.append(filename)
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
|
||||
@ -20,6 +20,50 @@ from gi.repository import Gtk,GLib,Gio
|
||||
from gi.repository.GObject import GObject,Signal,Property,SignalFlags
|
||||
|
||||
from ..settings import settings
|
||||
from ..archiver import ArchiverManager,Archiver
|
||||
import zipfile
|
||||
|
||||
|
||||
class ArchiverSorter(Gtk.Sorter):
|
||||
def do_compare(self,item1,item2):
|
||||
c1 = item1.name.upper()
|
||||
c2 = item2.name.upper()
|
||||
|
||||
if (c1 > c2):
|
||||
return Gtk.Ordering.LARGER
|
||||
elif (c1 < c2):
|
||||
return Gtk.Ordering.SMALLER
|
||||
else:
|
||||
return Gtk.Ordering.EQUAL
|
||||
|
||||
class ZipfileCompressorData(GObject):
|
||||
def __init__(self,compressor,name,is_standard):
|
||||
GObject.__init__(self)
|
||||
self.__compressor = compressor
|
||||
self.__name = name
|
||||
self.__is_standard = is_standard
|
||||
|
||||
@Property(type=int)
|
||||
def compressor(self)->int:
|
||||
return self.__compressor
|
||||
|
||||
@Property(type=str)
|
||||
def name(self)->str:
|
||||
return self.__name
|
||||
|
||||
@Property(type=bool,default=False)
|
||||
def is_standard(self)->bool:
|
||||
return self.__is_standard
|
||||
|
||||
class ZipfileCompressorDataSorter(Gtk.Sorter):
|
||||
def do_compare(self,item1:ZipfileCompressorData,item2:ZipfileCompressorData):
|
||||
if (item1.name > item2.name):
|
||||
return Gtk.Ordering.LARGER
|
||||
elif (item1.name < item2.name):
|
||||
return Gtk.Ordering.SMALLER
|
||||
else:
|
||||
return Gtk.Ordering.EQUAL
|
||||
|
||||
|
||||
class SettingsDialog(Gtk.Dialog):
|
||||
def __init__(self,parent=None):
|
||||
@ -28,33 +72,48 @@ class SettingsDialog(Gtk.Dialog):
|
||||
self.set_transient_for(parent)
|
||||
self.set_default_size(800,600)
|
||||
vbox = self.get_content_area()
|
||||
self._widget_set_margin(vbox,4)
|
||||
paned = Gtk.Paned(orientation=Gtk.Orientation.HORIZONTAL)
|
||||
paned.set_position(250)
|
||||
|
||||
self.__stack = Gtk.Stack()
|
||||
self.__stack_sidebar = Gtk.StackSidebar.new()
|
||||
self.__add_general_settings_page()
|
||||
|
||||
paned.set_start_child(self.__stack_sidebar)
|
||||
self.__general_page = self.__add_general_settings_page()
|
||||
self.__archiver_page = self.__add_archiver_settings_page()
|
||||
|
||||
sidebar_scrolled=Gtk.ScrolledWindow()
|
||||
sidebar_scrolled.set_child(self.__stack_sidebar)
|
||||
sidebar_scrolled.set_hexpand(True)
|
||||
sidebar_scrolled.set_vexpand(True)
|
||||
paned.set_start_child(sidebar_scrolled)
|
||||
paned.set_end_child(self.__stack)
|
||||
paned.set_vexpand(True)
|
||||
self.__stack_sidebar.set_stack(self.__stack)
|
||||
|
||||
|
||||
vbox.append(paned)
|
||||
|
||||
self.add_button("Apply",Gtk.ResponseType.APPLY)
|
||||
self.add_button("Cancel",Gtk.ResponseType.CANCEL)
|
||||
|
||||
@property
|
||||
def general_page(self):
|
||||
return self.__general_page
|
||||
|
||||
@property
|
||||
def archiver_page(self):
|
||||
return self.__archiver_page
|
||||
|
||||
def __add_general_settings_page(self):
|
||||
page = Gtk.ScrolledWindow()
|
||||
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
||||
vbox = Gtk.Box.new(Gtk.Orientation.VERTICAL,4)
|
||||
grid = Gtk.Grid()
|
||||
|
||||
label = Gtk.Label.new('Backup directory: ')
|
||||
label = Gtk.Label.new('Backup directory:')
|
||||
grid.attach(label,0,0,1,1)
|
||||
self.__backupdir_label = Gtk.Label.new(settings.backup_dir)
|
||||
self.__backupdir_label.set_hexpand(True)
|
||||
grid.attach(self.__backupdir_label,1,0,1,1)
|
||||
page.backupdir_label = Gtk.Label.new(settings.backup_dir)
|
||||
page.backupdir_label.set_hexpand(True)
|
||||
grid.attach(page.backupdir_label,1,0,1,1)
|
||||
img = Gtk.Image.new_from_icon_name('document-open-symbolic')
|
||||
img.set_pixel_size(16)
|
||||
backupdir_button = Gtk.Button()
|
||||
@ -62,10 +121,96 @@ class SettingsDialog(Gtk.Dialog):
|
||||
backupdir_button.connect('clicked',self._on_backupdir_button_clicked)
|
||||
grid.attach(backupdir_button,2,0,1,1)
|
||||
|
||||
label = Gtk.Label.new('Backup versions:')
|
||||
grid.attach(label,0,1,1,1)
|
||||
page.backup_versions_spinbutton = Gtk.SpinButton.new_with_range(0,1000,1)
|
||||
page.backup_versions_spinbutton.set_hexpand(True)
|
||||
grid.attach(page.backup_versions_spinbutton,1,1,2,1)
|
||||
|
||||
label = Gtk.Label.new("Archiver:")
|
||||
archiver_model = Gio.ListStore.new(Archiver)
|
||||
for archiver in ArchiverManager.get_global().archivers.values():
|
||||
archiver_model.append(archiver)
|
||||
archiver_sort_model = Gtk.SortListModel.new(archiver_model,ArchiverSorter())
|
||||
|
||||
archiver_factory = Gtk.SignalListItemFactory()
|
||||
archiver_factory.connect('setup',self._on_archiver_factory_setup)
|
||||
archiver_factory.connect('bind',self._on_archiver_factory_bind)
|
||||
page.archiver_dropdown = Gtk.DropDown(model=archiver_sort_model,factory=archiver_factory)
|
||||
page.archiver_dropdown.set_hexpand(True)
|
||||
archiver_key = settings.archiver if settings.archiver in ArchiverManager.get_global().archivers else "zipfile"
|
||||
|
||||
for i in range(archiver_model.get_n_items()):
|
||||
archiver = archiver_model.get_item(i)
|
||||
if archiver_key == archiver.key:
|
||||
page.archiver_dropdown.set_selected(i)
|
||||
break
|
||||
grid.attach(label,0,2,1,1)
|
||||
grid.attach(page.archiver_dropdown,1,2,2,1)
|
||||
|
||||
vbox.append(grid)
|
||||
page.set_child(vbox)
|
||||
|
||||
self.add_page(page,"general","Generic settings")
|
||||
return page
|
||||
|
||||
def __add_archiver_settings_page(self):
|
||||
page = Gtk.ScrolledWindow()
|
||||
page.vbox = Gtk.Box.new(Gtk.Orientation.VERTICAL,4)
|
||||
self._widget_set_margin(page.vbox,4)
|
||||
|
||||
grid = Gtk.Grid()
|
||||
self._widget_set_margin(grid,4)
|
||||
|
||||
zf_compressors = [
|
||||
(zipfile.ZIP_STORED,"Stored",True),
|
||||
(zipfile.ZIP_DEFLATED,"Deflated",True),
|
||||
(zipfile.ZIP_BZIP2,"BZip2",False),
|
||||
(zipfile.ZIP_LZMA,"LZMA",False),
|
||||
]
|
||||
|
||||
zipfile_frame = Gtk.Frame.new("ZipFile Archiver")
|
||||
label = Gtk.Label.new("Compressor:")
|
||||
zf_compressor_model = Gio.ListStore.new(ZipfileCompressorData)
|
||||
for i in zf_compressors:
|
||||
zf_compressor_model.append(ZipfileCompressorData(*i))
|
||||
zf_compressor_sort_model = Gtk.SortListModel.new(zf_compressor_model,ZipfileCompressorDataSorter())
|
||||
zf_compressor_factory = Gtk.SignalListItemFactory()
|
||||
zf_compressor_factory.connect('setup',self._on_zipfile_compressor_setup)
|
||||
zf_compressor_factory.connect('bind',self._on_zipfile_compressor_bind)
|
||||
page.zf_compressor_dropdown = Gtk.DropDown(model=zf_compressor_sort_model,factory=zf_compressor_factory)
|
||||
page.zf_compressor_dropdown.set_hexpand(True)
|
||||
c = settings.zipfile_compression
|
||||
for i in range(zf_compressor_model.get_n_items()):
|
||||
if (c == zf_compressor_model.get_item(i).compressor):
|
||||
page.zf_compressor_dropdown.set_selected(i)
|
||||
break
|
||||
grid.attach(label,0,0,1,1)
|
||||
grid.attach(page.zf_compressor_dropdown,1,0,1,1)
|
||||
|
||||
label = Gtk.Label.new("Compression Level:")
|
||||
page.zf_compresslevel_spinbutton = Gtk.SpinButton.new_with_range(0.0,9.0,1.0)
|
||||
page.zf_compresslevel_spinbutton.set_value(settings.zipfile_compresslevel)
|
||||
page.zf_compresslevel_spinbutton.set_hexpand(True)
|
||||
grid.attach(label,0,1,1,1)
|
||||
grid.attach(page.zf_compresslevel_spinbutton,1,1,1,1)
|
||||
|
||||
zipfile_frame.set_child(grid)
|
||||
page.vbox.append(zipfile_frame)
|
||||
|
||||
page.set_child(page.vbox)
|
||||
self.add_page(page,"zipfile","Archiver Settings")
|
||||
return page
|
||||
|
||||
def _on_archiver_factory_setup(self,factory,item):
|
||||
label = Gtk.Label()
|
||||
item.set_child(label)
|
||||
|
||||
def _on_archiver_factory_bind(self,factory,item):
|
||||
label = item.get_child()
|
||||
archiver = item.get_item()
|
||||
label.set_text(archiver.name)
|
||||
|
||||
|
||||
def _on_backupdir_dialog_select_folder(self,dialog,result,*data):
|
||||
try:
|
||||
@ -81,6 +226,25 @@ class SettingsDialog(Gtk.Dialog):
|
||||
dialog.select_folder(self,None,self._on_backupdir_dialog_select_folder)
|
||||
|
||||
|
||||
def _on_zipfile_compressor_setup(self,factory,item):
|
||||
label = Gtk.Label()
|
||||
item.set_child(label)
|
||||
|
||||
def _on_zipfile_compressor_bind(self,factory,item):
|
||||
label = item.get_child()
|
||||
data = item.get_item()
|
||||
|
||||
if (not data.is_standard):
|
||||
label.set_markup("<span foreground=\"red\">{name}</span>".format(name=GLib.markup_escape_text(data.name)))
|
||||
else:
|
||||
label.set_text(data.name)
|
||||
|
||||
def _widget_set_margin(self,widget:Gtk.Widget,margin:int):
|
||||
widget.set_margin_top(margin)
|
||||
widget.set_margin_bottom(margin)
|
||||
widget.set_margin_start(margin)
|
||||
widget.set_margin_end(margin)
|
||||
|
||||
def add_page(self,page,name,title):
|
||||
self.__stack.add_titled(page,name,title)
|
||||
|
||||
@ -95,5 +259,10 @@ class SettingsDialog(Gtk.Dialog):
|
||||
return_type=None,
|
||||
arg_types=())
|
||||
def do_save(self):
|
||||
settings.backup_dir = self.__backupdir_label.get_text()
|
||||
settings.backup_dir = self.general_page.backupdir_label.get_text()
|
||||
settings.backup_versions = self.general_page.backup_versions_spinbutton.get_value_as_int()
|
||||
settings.archiver = self.general_page.archiver_dropdown.get_selected_item().key
|
||||
settings.zipfile_compression = self.archiver_page.zf_compressor_dropdown.get_selected_item().compressor
|
||||
settings.zipfile_compresslevel = self.archiver_page.zf_compresslevel_spinbutton.get_value_as_int()
|
||||
|
||||
|
||||
@ -230,6 +230,8 @@ class NewSteamAppsDialog(Gtk.Dialog):
|
||||
|
||||
self.get_content_area().append(scrolled)
|
||||
|
||||
self.__gamedialog = None
|
||||
|
||||
self.add_button("OK",Gtk.ResponseType.OK)
|
||||
|
||||
def _on_listitem_setup(self,factory,item):
|
||||
@ -289,12 +291,16 @@ class NewSteamAppsDialog(Gtk.Dialog):
|
||||
|
||||
def _on_add_steamapp_button_clicked(self,button,data:SteamApp,*args):
|
||||
def on_dialog_response(dialog,response):
|
||||
self.__gamedialog = None
|
||||
if response == Gtk.ResponseType.APPLY:
|
||||
for i in range(self.__listmodel.get_n_items()):
|
||||
if data.appid == self.__listmodel.get_item(i).appid:
|
||||
self.__listmodel.remove(i)
|
||||
break
|
||||
|
||||
if self.__gamedialog is not None:
|
||||
return
|
||||
|
||||
game = Game("Enter key",data.name,"")
|
||||
if PLATFORM_WINDOWS:
|
||||
game.steam_windows = SteamWindowsGame(data.appid,"","",installdir=data.installdir)
|
||||
@ -311,12 +317,13 @@ class NewSteamAppsDialog(Gtk.Dialog):
|
||||
game.steam_windows = SteamWindowsGame(data.appid,"","")
|
||||
game.steam_macos = SteamMacOSGame(data.appid,"","")
|
||||
game.savegame_type = SavegameType.STEAM_MACOS
|
||||
|
||||
dialog = GameDialog(self,game)
|
||||
dialog.set_title("sgbackup: Add Steam Game")
|
||||
dialog.set_modal(False)
|
||||
dialog.connect('response',on_dialog_response)
|
||||
dialog.present()
|
||||
|
||||
if self.__gamedialog is None:
|
||||
self.__gamedialog = GameDialog(self,game)
|
||||
self.__gamedialog.set_title("sgbackup: Add Steam Game")
|
||||
self.__gamedialog.set_modal(False)
|
||||
self.__gamedialog.connect('response',on_dialog_response)
|
||||
self.__gamedialog.present()
|
||||
|
||||
def _on_ignore_steamapp_button_clicked(self,button,data,*args):
|
||||
def on_dialog_response(dialog,response,data):
|
||||
|
||||
@ -22,6 +22,7 @@ import sys
|
||||
|
||||
from gi.repository import GLib,GObject
|
||||
import zipfile
|
||||
from threading import RLock
|
||||
|
||||
ZIPFILE_COMPRESSION_STR = {
|
||||
zipfile.ZIP_STORED: "stored",
|
||||
@ -55,27 +56,219 @@ class Settings(GObject.GObject):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.__mutex = RLock()
|
||||
|
||||
self.__configparser = ConfigParser()
|
||||
self.__keyfile = GLib.KeyFile.new()
|
||||
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.__backup_versions = 0
|
||||
|
||||
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)
|
||||
self.__keyfile.load_from_file(self.__config_file,
|
||||
(GLib.KeyFileFlags.KEEP_COMMENTS | GLib.KeyFileFlags.KEEP_TRANSLATIONS))
|
||||
|
||||
if not os.path.isdir(self.config_dir):
|
||||
os.makedirs(self.config_dir)
|
||||
|
||||
if not os.path.isdir(self.gameconf_dir):
|
||||
os.makedirs(self.gameconf_dir)
|
||||
|
||||
def has_group(self,group:str)->bool:
|
||||
with self.__mutex:
|
||||
return self.keyfile.has_group(group)
|
||||
|
||||
def has_section(self,section:str)->bool:
|
||||
with self.__mutex:
|
||||
return self.keyfile.has_group(section)
|
||||
|
||||
def has_option(self,section:str,option:str):
|
||||
if self.has_section(section):
|
||||
with self.__mutex:
|
||||
keys,length = self.keyfile.get_keys(section)
|
||||
if (keys and option in keys):
|
||||
return True
|
||||
return False
|
||||
|
||||
def has_key(self,group:str,key:str):
|
||||
if self.has_group(group):
|
||||
with self.__mutex:
|
||||
keys,length = self.keyfile.get_keys(group)
|
||||
return (keys and key in keys)
|
||||
return False
|
||||
|
||||
def get_groups(self):
|
||||
with self.__mutex:
|
||||
return self.keyfile.get_groups()[0]
|
||||
|
||||
def get_sections(self):
|
||||
with self.__mutex:
|
||||
return self.keyfile.get_groups()[0]
|
||||
|
||||
def get_keys(self,group:str):
|
||||
with self.__mutex:
|
||||
return self.keyfile.get_keys(group)[0]
|
||||
|
||||
def get_options(self,section:str):
|
||||
with self.__mutex:
|
||||
return self.keyfile.get_keys(section)[0]
|
||||
|
||||
def get(self,group:str,key:str,default=None)->str|None:
|
||||
if (self.has_key(group,key)):
|
||||
with self.__mutex:
|
||||
self.keyfile.get_value(group,key)
|
||||
return default
|
||||
|
||||
def set(self,group:str,key:str,value:str):
|
||||
with self.__mutex:
|
||||
self.keyfile.set_key(group,key,value)
|
||||
|
||||
def get_boolean(self,group:str,key:str,default:bool|None=None)->bool|None:
|
||||
if self.has_key(group,key):
|
||||
with self.__mutex:
|
||||
return self.keyfile.get_boolean(group,key)
|
||||
return default
|
||||
|
||||
def set_boolean(self,group:str,key:str,value:bool):
|
||||
with self.__mutex:
|
||||
self.keyfile.set_boolean(group,key,value)
|
||||
|
||||
def get_boolean_list(self,group:str,key:str,default:list[bool]|None=None)->list[bool]|None:
|
||||
if self.has_key(group,key):
|
||||
with self.__mutex:
|
||||
return self.keyfile.get_boolean_list(group,key)
|
||||
return default
|
||||
|
||||
def set_boolean_list(self,group:str,key:str,value:list[bool]):
|
||||
with self.__mutex:
|
||||
self.keyfile.set_boolean_list(group,key,value)
|
||||
|
||||
def get_double(self,group:str,key:str,default:float|None=None)->float|None:
|
||||
if self.has_key(group,key):
|
||||
with self.__mutex:
|
||||
return self.keyfile.get_double(group,key)
|
||||
return default
|
||||
|
||||
|
||||
def set_double(self,group:str,key:str,value:float):
|
||||
with self.__mutex:
|
||||
self.keyfile.set_double(group,key,value)
|
||||
|
||||
def get_double_list(self,group:str,key:str,default:list[float]|None=None)->list[float]|None:
|
||||
if self.has_key(group,key):
|
||||
with self.__mutex:
|
||||
return self.keyfile.get_double_list(group,key)
|
||||
return default
|
||||
|
||||
def set_double_list(self,group:str,key:str,value:list[float]):
|
||||
with self.__mutex:
|
||||
self.keyfile.set_double_list(group,key,value)
|
||||
|
||||
def get_integer(self,group:str,key:str,default:None|int=None)->int|None:
|
||||
if self.has_key(group,key):
|
||||
with self.__mutex:
|
||||
return self.keyfile.get_integer(group,key)
|
||||
return default
|
||||
|
||||
def set_integer(self,group:str,key:str,value:int):
|
||||
with self.__mutex:
|
||||
self.keyfile.set_integer(group,key,value)
|
||||
|
||||
def get_integer_list(self,group:str,key:str,default:list[int]|None=None)->list[int]|None:
|
||||
if self.has_key(group,key):
|
||||
with self.__mutex:
|
||||
return self.keyfile.get_integer_list(group,key)
|
||||
return default
|
||||
|
||||
def set_integer_list(self,group:str,key:str,value:list[int]):
|
||||
with self.__mutex:
|
||||
self.keyfile.set_integer_list(group,key,value)
|
||||
|
||||
def get_locale_for_key(self,group:str,key:str,locale:str|None=None)->str|None:
|
||||
if self.has_key(group,key):
|
||||
with self.__mutex:
|
||||
return self.keyfile.get_locale_for_key(group,key,locale)
|
||||
return None
|
||||
|
||||
def get_locale_string(self,group:str,key:str,locale:str|None=None,default:str|None=None)->str|None:
|
||||
if self.has_key(group,key):
|
||||
with self.__mutex:
|
||||
ret = self.keyfile.get_locale_string(group,key,locale)
|
||||
if ret is not None:
|
||||
return ret
|
||||
return default
|
||||
|
||||
def set_locale_string(self,group:str,key:str,locale:str,value:str):
|
||||
with self.__mutex:
|
||||
self.set_locale_string(group,key,locale,value)
|
||||
|
||||
def get_locale_string_list(self,group:str,key:str,locale:str|None=None,default:list[str]|None=None)->list[str]|None:
|
||||
if self.has_key(group,key):
|
||||
with self.__mutex:
|
||||
ret = self.keyfile.get_locale_string_list(group,key,locale)
|
||||
if ret is not None:
|
||||
return ret
|
||||
return default
|
||||
|
||||
def set_locale_string_list(self,group:str,key:str,locale:str,value:list[str]):
|
||||
with self.__mutex:
|
||||
self.keyfile.set_locale_string_list(group,key,locale,value)
|
||||
|
||||
def get_string(self,group:str,key:str,default:str|None=None)->str|None:
|
||||
if self.has_key(group,key):
|
||||
with self.__mutex:
|
||||
return self.keyfile.get_string(group,key)
|
||||
return default
|
||||
|
||||
def set_string(self,group:str,key:str,value:str):
|
||||
with self.__mutex:
|
||||
self.keyfile.set_string(group,key,value)
|
||||
|
||||
def get_string_list(self,group:str,key:str,default:list[str]|None=None)->list[str]|None:
|
||||
if self.has_key(group,key):
|
||||
with self.__mutex:
|
||||
return self.keyfile.get_string_list(group,key)
|
||||
return default
|
||||
|
||||
def set_string_list(self,group:str,key:str,value:list[str]):
|
||||
with self.__mutex:
|
||||
self.keyfile.set_string_list(group,key,value)
|
||||
|
||||
def remove_key(self,group:str,key:str):
|
||||
if self.has_key(group,key):
|
||||
with self.__mutex:
|
||||
self.keyfile.remove_key(group,key)
|
||||
|
||||
def remove_group(self,group):
|
||||
if self.has_group(group):
|
||||
keys = self.get_keys(group)
|
||||
with self.__mutex:
|
||||
for key in keys:
|
||||
self.keyfile.remove_key(group,key)
|
||||
self.keyfile.remove_group(group)
|
||||
|
||||
def remove_comment(self,group:str|None=None,key:str|None=None):
|
||||
with self.__mutex:
|
||||
try:
|
||||
self.keyfile.remove_comment(group,key)
|
||||
except:
|
||||
pass
|
||||
|
||||
def set_comment(self,comment:str,group:str|None=None,key:str|None=None):
|
||||
with self.__mutex:
|
||||
try:
|
||||
self.keyfile.set_comment(group,key,comment)
|
||||
except:
|
||||
pass
|
||||
|
||||
@GObject.Property(nick="parser")
|
||||
def parser(self)->ConfigParser:
|
||||
return self.__configparser
|
||||
def parser(self)->GLib.KeyFile:
|
||||
return self.__keyfile
|
||||
|
||||
@GObject.Property(nick="keyfile")
|
||||
def keyfile(self)->GLib.KeyFile:
|
||||
return self.__keyfile
|
||||
|
||||
@GObject.Property(type=str,nick="config-dir")
|
||||
def config_dir(self)->str:
|
||||
@ -93,53 +286,46 @@ class Settings(GObject.GObject):
|
||||
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 os.path.join(GLib.get_home_dir(),'SavagameBackups')
|
||||
return self.get_string('sgbackup','backupDirectory',
|
||||
os.path.join(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!")
|
||||
self.ensure_section('sgbackup')
|
||||
return self.parser.set('sgbackup','backupDirectory',directory)
|
||||
return self.set_string('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"
|
||||
return self.get_string('sgbackup','logLevel',"INFO")
|
||||
|
||||
@GObject.Property
|
||||
def variables(self)->dict[str:str]:
|
||||
ret = {}
|
||||
if self.parser.has_section('variables'):
|
||||
for k,v in self.parser.items('variables'):
|
||||
ret[k] = v
|
||||
if self.keyfile.has_group('variables'):
|
||||
for key in self.get_keys('variables'):
|
||||
ret[key] = self.get_string('variables',key,"")
|
||||
return ret
|
||||
@variables.setter
|
||||
def variables(self,vars:dict|list|tuple):
|
||||
if self.parser.has_section('variables'):
|
||||
for opt in self.parser['variables'].keys():
|
||||
self.parser.remove_option('variables',opt)
|
||||
|
||||
self.remove_group("variables")
|
||||
if not vars:
|
||||
return
|
||||
|
||||
if isinstance(vars,dict):
|
||||
for k,v in vars.items():
|
||||
self.parser.set('variables',k,v)
|
||||
self.set_string('variables',k,v)
|
||||
else:
|
||||
for v in vars:
|
||||
self.parser.set('variables',v[0],v[1])
|
||||
for k,v in dict(vars).items():
|
||||
self.set_string('variables',v[0],v[1])
|
||||
|
||||
@GObject.Property(type=str)
|
||||
def steam_installpath(self):
|
||||
if self.parser.has_section('steam') and self.parser.has_option('installpath'):
|
||||
return self.parser.get('steam','installdir')
|
||||
if self.has_key('steam','installpath'):
|
||||
return self.get_string('steam','installpath')
|
||||
|
||||
if PLATFORM_WINDOWS:
|
||||
for i in ('SOFTWARE\\WOW6432Node\\Valve\\Steam','SOFTWARE\\Valve\\Steam'):
|
||||
@ -148,7 +334,7 @@ class Settings(GObject.GObject):
|
||||
skey = winreg.OpenKeyEx(winreg.HKEY_LOCAL_MACHINE,i)
|
||||
svalue = winreg.QueryValueEx(skey,'InstallPath')[0]
|
||||
if svalue:
|
||||
self.parser.set('steam','installpath',svalue)
|
||||
self.set_string('steam','installpath',svalue)
|
||||
return svalue
|
||||
except:
|
||||
continue
|
||||
@ -157,20 +343,18 @@ class Settings(GObject.GObject):
|
||||
skey.Close()
|
||||
return ""
|
||||
|
||||
@steam_installpath.setter
|
||||
def steam_installpath(self,path:str):
|
||||
self.set_string('steam','installpath',path)
|
||||
|
||||
def add_variable(self,name:str,value:str):
|
||||
self.parser.set('variables',name,value)
|
||||
self.set_string('variables',name,value)
|
||||
|
||||
def remove_variable(self,name:str):
|
||||
try:
|
||||
self.parser.remove_option('variables',name)
|
||||
except:
|
||||
pass
|
||||
self.remove_key('variables',name)
|
||||
|
||||
def get_variable(self,name:str)->str:
|
||||
try:
|
||||
return self.parser.get('variables',name)
|
||||
except:
|
||||
return ""
|
||||
return self.get_string('variables',name,"")
|
||||
|
||||
def get_variables(self)->dict[str:str]:
|
||||
ret = dict(os.environ)
|
||||
@ -178,48 +362,64 @@ class Settings(GObject.GObject):
|
||||
"DOCUMENTS": GLib.get_user_special_dir(GLib.UserDirectory.DIRECTORY_DOCUMENTS),
|
||||
"DOCUMENTS_DIR": GLib.get_user_special_dir(GLib.UserDirectory.DIRECTORY_DOCUMENTS),
|
||||
"DATADIR": GLib.get_user_data_dir(),
|
||||
"DATA_DIR": GLib.get_user_data_dir(),
|
||||
"CONFIGDIR": GLib.get_user_config_dir(),
|
||||
"CONFIG_DIR": GLib.get_user_config_dir(),
|
||||
"STEAM_INSTALLPATH": self.steam_installpath,
|
||||
})
|
||||
ret.update(self.variables)
|
||||
return ret
|
||||
|
||||
@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"]
|
||||
def archiver(self)->str:
|
||||
return self.get_string('sgbackup','archiver',"zipfile")
|
||||
|
||||
@archiver.setter
|
||||
def archiver(self,archiver_key:str):
|
||||
self.set_string('sgbackup','archiver',archiver_key)
|
||||
|
||||
@GObject.Property(type=int)
|
||||
def backup_versions(self)->int:
|
||||
return self.get_integer('sgbackup','backupVersions',0)
|
||||
|
||||
@backup_versions.setter
|
||||
def backup_versions(self,versions:int):
|
||||
self.set_integer('sgbackup','backupVersions',versions)
|
||||
|
||||
|
||||
@GObject.Property(type=int)
|
||||
def zipfile_compression(self)->int:
|
||||
comp = self.parser.has_option('zipfile','compression','deflated')
|
||||
try:
|
||||
return ZIPFILE_STR_COMPRESSION[comp]
|
||||
except:
|
||||
pass
|
||||
return zipfile.ZIP_DEFLATED
|
||||
|
||||
@zipfile_compression.setter
|
||||
def zipfile_compression(self,compression):
|
||||
def zipfile_compression(self,compression:int):
|
||||
try:
|
||||
self.parser.set('zipfile','compression',ZIPFILE_COMPRESSION_STR[compression])
|
||||
self.set_string('zipfile','compression',ZIPFILE_COMPRESSION_STR[compression])
|
||||
except:
|
||||
self.parser.set('zipfile','compression',ZIPFILE_STR_COMPRESSION[zipfile.ZIP_DEFLATED])
|
||||
self.set_string('zipfile','compression',ZIPFILE_COMPRESSION_STR[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]
|
||||
cl = self.get_integer('zipfile','compresslevel',9)
|
||||
if cl < 0:
|
||||
cl = 9
|
||||
return cl if cl <= ZIPFILE_COMPRESSLEVEL_MAX[self.zipfile_compression] else ZIPFILE_COMPRESSLEVEL_MAX[self.zipfile_compression]
|
||||
|
||||
@zipfile_compresslevel.setter
|
||||
def zipfile_compresslevel(self,cl:int):
|
||||
self.parser.set('zipfile','compressLevel',cl)
|
||||
self.set_integer('zipfile','compressLevel',cl)
|
||||
|
||||
def save(self):
|
||||
self.emit('save')
|
||||
|
||||
def ensure_section(self,section:str):
|
||||
if not self.parser.has_section(section):
|
||||
self.parser.add_section(section)
|
||||
|
||||
@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)
|
||||
with self.__mutex:
|
||||
self.keyfile.save_to_file(self.config_file)
|
||||
|
||||
settings = Settings()
|
||||
|
||||
Loading…
Reference in New Issue
Block a user