mirror of
https://github.com/c9moser/sgbackup.git
synced 2026-01-19 19:40:13 +00:00
499 lines
18 KiB
Python
499 lines
18 KiB
Python
###############################################################################
|
|
# sgbackup - The SaveGame Backup tool #
|
|
# Copyright (C) 2024,2025 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
|
|
import zipfile
|
|
from threading import RLock
|
|
|
|
from .utility import sanitize_path
|
|
|
|
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
|
|
|
|
if sys.platform.lower() == 'win32':
|
|
PLATFORM_WINDOWS = True
|
|
import winreg
|
|
else:
|
|
PLATFORM_WINDOWS = False
|
|
|
|
|
|
class Settings(GObject.GObject):
|
|
__gtype_name__ = "Settings"
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.__mutex = RLock()
|
|
|
|
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)):
|
|
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_nb(self,group:str)->bool:
|
|
return self.keyfile.has_group(group)
|
|
|
|
def has_group(self,group:str)->bool:
|
|
with self.__mutex:
|
|
return self._has_group_nb(group)
|
|
|
|
def has_section(self,section:str)->bool:
|
|
with self.__mutex:
|
|
return self._has_group_nb(section)
|
|
|
|
def _has_key_nb(self,group:str,key:str)->bool:
|
|
if self._has_group_nb(group):
|
|
keys,length = self.keyfile.get_keys(group)
|
|
return (key in keys)
|
|
return False
|
|
|
|
def has_option(self,section:str,option:str):
|
|
with self.__mutex:
|
|
return self._has_key_nb(section,option)
|
|
|
|
def has_key(self,group:str,key:str):
|
|
with self.__mutex:
|
|
return self._has_key_nb(group,key)
|
|
|
|
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:
|
|
if not self._has_group_nb(group):
|
|
return []
|
|
return self.keyfile.get_keys(group)[0]
|
|
|
|
def get_options(self,section:str):
|
|
with self.__mutex:
|
|
if not self._has_group_nb(section):
|
|
return []
|
|
return self.keyfile.get_keys(section)[0]
|
|
|
|
def get(self,group:str,key:str,default=None)->str|None:
|
|
with self.__mutex:
|
|
if (self._has_key_nb(group,key)):
|
|
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:
|
|
with self.__mutex:
|
|
if self._has_key_nb(group,key):
|
|
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:
|
|
with self.__mutex:
|
|
if self._has_key_nb(group,key):
|
|
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:
|
|
with self.__mutex:
|
|
if self._has_key_nb(group,key):
|
|
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:
|
|
with self.__mutex:
|
|
if self._has_key_nb(group,key):
|
|
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:
|
|
with self.__mutex:
|
|
if self._has_key_nb(group,key):
|
|
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:
|
|
with self.__mutex:
|
|
if self._has_key_nb(group,key):
|
|
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_int64(self,group:str,key:str,default:int|None=None)->int|None:
|
|
with self.__mutex:
|
|
if self._has_key_nb(group,key):
|
|
self.keyfile.get_int64(group,key)
|
|
return default
|
|
|
|
def set_int64(self,group:str,key:str,value:int):
|
|
with self.__mutex:
|
|
self.keyfile.set_int64(group,key,value)
|
|
|
|
def get_uint64(self,group:str,key:str,default:int|None=None)->int|None:
|
|
with self.__mutex:
|
|
if self._has_key_nb(group,key):
|
|
self.keyfile.get_uint64(self,group,key)
|
|
return default
|
|
|
|
def set_uint64(self,group:str,key:str,value:int):
|
|
with self.__mutex:
|
|
self.keyfile.set_uint64(group,key,value)
|
|
|
|
def get_locale_for_key(self,group:str,key:str,locale:str|None=None)->str|None:
|
|
with self.__mutex:
|
|
if self._has_key_nb(group,key):
|
|
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:
|
|
with self.__mutex:
|
|
if self._has_key_nb(group,key):
|
|
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:
|
|
with self.__mutex:
|
|
if self._has_key_nb(group,key):
|
|
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:
|
|
with self.__mutex:
|
|
if self._has_key_nb(group,key):
|
|
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:
|
|
with self.__mutex:
|
|
if self._has_key_nb(group,key):
|
|
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):
|
|
with self.__mutex:
|
|
if self._has_key_nb(group,key):
|
|
self.keyfile.remove_key(group,key)
|
|
|
|
def remove_group(self,group):
|
|
with self.__mutex:
|
|
if self._has_group_nb(group):
|
|
keys = self.keyfile.get_keys(group)[0]
|
|
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)->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:
|
|
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:
|
|
return sanitize_path(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!")
|
|
return self.set_string('sgbackup','backupDirectory',sanitize_path(directory))
|
|
|
|
@GObject.Property(type=str)
|
|
def loglevel(self)->str:
|
|
return self.get_string('sgbackup','logLevel',"INFO")
|
|
|
|
@GObject.Property(type=int)
|
|
def backup_threads(self)->int:
|
|
return self.get_integer('sgbackup','maxBackupThreads',1)
|
|
|
|
@backup_threads.setter
|
|
def backup_threads(self,max_threads:int):
|
|
if (max_threads < 1):
|
|
max_threads = 1
|
|
self.set_integer('sgbackup','maxBackupThreads',max_threads)
|
|
|
|
@GObject.Property(type=int)
|
|
def search_max_results(self)->int:
|
|
return self.get_integer('search','maxResults',10)
|
|
|
|
@search_max_results.setter
|
|
def search_max_results(self,max:int):
|
|
self.set_integer('search','maxResults',max)
|
|
|
|
@GObject.Property(type=int)
|
|
def search_min_chars(self)->int:
|
|
return self.get_integer('search','minChars',3)
|
|
|
|
@search_min_chars.setter
|
|
def search_min_chars(self,min_chars:int):
|
|
self.set_integer('search','minChars',min_chars)
|
|
|
|
@GObject.Property(type=bool,default=False)
|
|
def gui_autoclose_backup_dialog(self)->bool:
|
|
return self.get_boolean('gui','autocloseBackupDialog',False)
|
|
|
|
@gui_autoclose_backup_dialog.setter
|
|
def gui_autoclose_backup_dialog(self,autoclose:bool):
|
|
self.set_boolean('gui','autocloseBackupDialog',autoclose)
|
|
|
|
@GObject.Property(type=bool,default=False)
|
|
def gui_autoclose_restore_dialog(self)->bool:
|
|
return self.get_boolean('gui','autocloseRestoreDialog',False)
|
|
|
|
@gui_autoclose_restore_dialog.setter
|
|
def gui_autoclose_restore_dialog(self,autoclose:bool):
|
|
self.set_boolean('gui','autocloseRestoreDialog',autoclose)
|
|
|
|
@GObject.Property
|
|
def variables(self)->dict[str:str]:
|
|
ret = {}
|
|
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):
|
|
self.remove_group("variables")
|
|
if not vars:
|
|
return
|
|
|
|
if isinstance(vars,dict):
|
|
for k,v in vars.items():
|
|
self.set_string('variables',k,v)
|
|
else:
|
|
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.has_key('steam','installpath'):
|
|
return self.get_string('steam','installpath')
|
|
|
|
if PLATFORM_WINDOWS:
|
|
for i in ('SOFTWARE\\WOW6432Node\\Valve\\Steam','SOFTWARE\\Valve\\Steam'):
|
|
try:
|
|
skey = None
|
|
skey = winreg.OpenKeyEx(winreg.HKEY_LOCAL_MACHINE,i)
|
|
svalue = winreg.QueryValueEx(skey,'InstallPath')[0]
|
|
if svalue:
|
|
self.set_string('steam','installpath',svalue)
|
|
return svalue
|
|
except:
|
|
continue
|
|
finally:
|
|
if skey:
|
|
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.set_string('variables',name,value)
|
|
|
|
def remove_variable(self,name:str):
|
|
self.remove_key('variables',name)
|
|
|
|
def get_variable(self,name:str)->str:
|
|
return self.get_string('variables',name,"")
|
|
|
|
def get_variables(self)->dict[str:str]:
|
|
if PLATFORM_WINDOWS:
|
|
ret = dict(((name.upper(),value) for name,value in os.environ.items()))
|
|
else:
|
|
ret = dict(os.environ)
|
|
ret.update({
|
|
"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 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.get_string('zipfile','compression','deflated')
|
|
try:
|
|
return ZIPFILE_STR_COMPRESSION[comp]
|
|
except:
|
|
pass
|
|
return zipfile.ZIP_DEFLATED
|
|
|
|
@zipfile_compression.setter
|
|
def zipfile_compression(self,compression:int):
|
|
try:
|
|
self.set_string('zipfile','compression',ZIPFILE_COMPRESSION_STR[compression])
|
|
except:
|
|
self.set_string('zipfile','compression',ZIPFILE_COMPRESSION_STR[zipfile.ZIP_DEFLATED])
|
|
|
|
@GObject.Property(type=int)
|
|
def zipfile_compresslevel(self)->int:
|
|
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.set_integer('zipfile','compressLevel',cl)
|
|
|
|
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 self.__mutex:
|
|
self.keyfile.save_to_file(self.config_file)
|
|
|
|
settings = Settings()
|