From 4608f7a6af68e9125b830ef0ffc697a08f8b235e Mon Sep 17 00:00:00 2001 From: Christian Moser Date: Wed, 12 Feb 2025 04:28:33 +0100 Subject: [PATCH] fixed backup of files --- sgbackup/archiver/_archiver.py | 12 ++++++---- sgbackup/archiver/tarfilearchiver.py | 17 ++++++------- sgbackup/archiver/zipfilearchiver.py | 1 + sgbackup/game.py | 6 ++--- sgbackup/gui/_settingsdialog.py | 18 +++++++++++--- sgbackup/settings.py | 21 ++++++++++++---- sgbackup/utility.py | 36 ++++++++++++++++++++++++++++ 7 files changed, 88 insertions(+), 23 deletions(-) create mode 100644 sgbackup/utility.py diff --git a/sgbackup/archiver/_archiver.py b/sgbackup/archiver/_archiver.py index 9b0f9ac..f9d0920 100644 --- a/sgbackup/archiver/_archiver.py +++ b/sgbackup/archiver/_archiver.py @@ -31,6 +31,8 @@ import time from ..game import Game from ..settings import settings +from ..utility import sanitize_path,sanitize_windows_path + import logging logger = logging.getLogger(__name__) @@ -91,7 +93,7 @@ class Archiver(GObject): dt.strftime("%Y%m%d-%H%M%S"), "sgbackup", 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) + return sanitize_path(os.path.join(settings.backup_dir,game.savegame_name,game.subdir,basename)) def _backup_progress(self,game:Game,fraction:float,message:str|None): if fraction > 1.0: @@ -242,8 +244,8 @@ class ArchiverManager(GObject): game_progress._game_progress_connection = self.connect('backup-game-progress',game_progress) threadpool = {} - if len(game_list) > 8: - n = 8 + if len(game_list) > settings.backup_threads: + n = settings.backup_threads else: n = len(games) @@ -278,8 +280,8 @@ class ArchiverManager(GObject): 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): + flags=SignalFlags.RUN_FIRST) + def do_backup(self,archiver,game,filename): return True def is_archive(self,filename)->bool: diff --git a/sgbackup/archiver/tarfilearchiver.py b/sgbackup/archiver/tarfilearchiver.py index 8f5a63c..9353e75 100644 --- a/sgbackup/archiver/tarfilearchiver.py +++ b/sgbackup/archiver/tarfilearchiver.py @@ -20,7 +20,7 @@ from gi.repository.GObject import Property from gi.repository import GLib from ._archiver import Archiver -from tarfile import TarFile +from tarfile import open as tf_open, is_tarfile from tempfile import mkdtemp,NamedTemporaryFile import json import os @@ -46,10 +46,11 @@ class TarfileArchiver(Archiver): return self.__compression def is_archive(self, filename): - if (Archiver.is_archive()): + if (Archiver.is_archive(self,filename) and is_tarfile(filename)): try: - with TarFile(filename,"r:{}".format(self.compression)) as tf: - return ("gameconf.json" in tf.getnames()) + with tf_open(filename,"r:{}".format(self.compression)) as tf: + #return ("gameconf.json" in tf.getnames()) + return True except: pass return False @@ -57,14 +58,14 @@ class TarfileArchiver(Archiver): def do_backup(self, game, filename): _calc_fraction = lambda n,cnt: ((1.0 / n) * cnt) - self._backup_progress(game,0.0,"Starting {game} ...".format(game.name)) + self._backup_progress(game,0.0,"Starting {game} ...".format(game=game.name)) files = game.get_backup_files() n = len(files) + 2 cnt=1 data=json.dumps(game.serialize(),ensure_ascii=False,indent=4) - with TarFile(filename,'x:{}'.format(self.compression)) as tf: + with tf_open(filename,'x:{}'.format(self.compression)) as tf: self._backup_progress(game,_calc_fraction(n,cnt),"gameconf.json") gcf = os.path.join(GLib.get_tmp_dir(),"sgbackup-" + GLib.get_user_name() + "." + "backup." + game.key + ".gameconf.tmp") with open(gcf,"wt",encoding="utf-8") as gcfile: @@ -72,7 +73,7 @@ class TarfileArchiver(Archiver): tf.add(gcf,"gameconf.json") - for path,arcname in files: + for path,arcname in files.items(): cnt += 1 self._backup_progress(game,_calc_fraction(n,cnt),"arcname") tf.add(path,arcname) @@ -99,7 +100,7 @@ class TarfileArchiver(Archiver): tempdir = mkdtemp(suffix="-sgbackup") tempfile= os.path.join(tempdir,"gameconf.json") try: - with TarFile(filename,'r:{}'.format(self.compression)) as tf: + with tf_open(filename,'r:{}'.format(self.compression)) as tf: tf.extract("gameconf.json",tempdir) with open(tempfile,"r",encoding="utf-8") as ifile: game = Game.new_from_json_file(tempfile) diff --git a/sgbackup/archiver/zipfilearchiver.py b/sgbackup/archiver/zipfilearchiver.py index 0f8ae9b..7ef575c 100644 --- a/sgbackup/archiver/zipfilearchiver.py +++ b/sgbackup/archiver/zipfilearchiver.py @@ -43,6 +43,7 @@ class ZipfileArchiver(Archiver): for path,arcname in files.items(): cnt+=1 self._backup_progress(game,_calc_fraction(div,cnt),"{} -> {}".format(game.name,arcname)) + print("writing file:",path) zf.write(path,arcname) self._backup_progress(game,1.0,"{game} ... FINISHED".format(game=game.name)) diff --git a/sgbackup/game.py b/sgbackup/game.py index 48369ad..b10a7cf 100644 --- a/sgbackup/game.py +++ b/sgbackup/game.py @@ -1356,7 +1356,6 @@ class Game(GObject): def get_backup_files(self)->dict[str:str]|None: def get_backup_files_recursive(sgroot:pathlib.Path,sgdir:str,subdir:str|None=None): - ret = {} if subdir: path = sgroot / sgdir / subdir else: @@ -1366,13 +1365,14 @@ class Game(GObject): for dirent in os.listdir(path): file_path = path / dirent if file_path.is_file(): + if subdir: - fname = (os.path.join(subdir,dirent)) + fname = os.path.join(subdir,dirent).replace("\\","/") else: fname = dirent if self.game_data.match(fname): - ret[str(path)] = os.path.join(sgdir,fname) + ret[str(str(file_path))] = os.path.join(sgdir,fname) elif file_path.is_dir(): if subdir: ret.update(get_backup_files_recursive(sgroot,sgdir,os.path.join(subdir,dirent))) diff --git a/sgbackup/gui/_settingsdialog.py b/sgbackup/gui/_settingsdialog.py index 4a55e66..437d01f 100644 --- a/sgbackup/gui/_settingsdialog.py +++ b/sgbackup/gui/_settingsdialog.py @@ -125,8 +125,16 @@ class SettingsDialog(Gtk.Dialog): 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) + page.backup_versions_spinbutton.set_value(settings.backup_versions) grid.attach(page.backup_versions_spinbutton,1,1,2,1) + label = Gtk.Label.new('Max. Backup Threads:') + page.backup_threads_spinbutton = Gtk.SpinButton.new_with_range(1,256,1) + page.backup_threads_spinbutton.set_hexpand(True) + page.backup_threads_spinbutton.set_value(settings.backup_threads) + grid.attach(label,0,2,1,1) + grid.attach(page.backup_threads_spinbutton,1,2,2,1) + label = Gtk.Label.new("Archiver:") archiver_model = Gio.ListStore.new(Archiver) for archiver in ArchiverManager.get_global().archivers.values(): @@ -145,8 +153,10 @@ class SettingsDialog(Gtk.Dialog): 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) + grid.attach(label,0,3,1,1) + grid.attach(page.archiver_dropdown,1,3,2,1) + + vbox.append(grid) page.set_child(vbox) @@ -216,13 +226,14 @@ class SettingsDialog(Gtk.Dialog): try: dir = dialog.select_folder_finish(result) if dir is not None: - self.__backupdir_label.set_text(dir.get_path()) + self.general_page.backupdir_label.set_text(dir.get_path()) except: pass def _on_backupdir_button_clicked(self,button): dialog = Gtk.FileDialog.new() dialog.set_title("sgbackup: Choose backup folder") + dialog.set_initial_folder(Gio.File.new_for_path(self.general_page.backupdir_label.get_text())) dialog.select_folder(self,None,self._on_backupdir_dialog_select_folder) @@ -261,6 +272,7 @@ class SettingsDialog(Gtk.Dialog): def do_save(self): settings.backup_dir = self.general_page.backupdir_label.get_text() settings.backup_versions = self.general_page.backup_versions_spinbutton.get_value_as_int() + settings.backup_threads = self.general_page.backup_threads_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() diff --git a/sgbackup/settings.py b/sgbackup/settings.py index cf728ab..a756e3e 100644 --- a/sgbackup/settings.py +++ b/sgbackup/settings.py @@ -24,6 +24,8 @@ 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", @@ -315,19 +317,30 @@ class Settings(GObject.GObject): @GObject.Property(type=str,nick="backup-dir") def backup_dir(self)->str: - return self.get_string('sgbackup','backupDirectory', - os.path.join(GLib.get_home_dir(),'SavagameBackups')) + 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',directory) + 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 def variables(self)->dict[str:str]: ret = {} @@ -415,7 +428,7 @@ class Settings(GObject.GObject): @GObject.Property(type=int) def zipfile_compression(self)->int: - comp = self.parser.has_option('zipfile','compression','deflated') + comp = self.get_string('zipfile','compression','deflated') try: return ZIPFILE_STR_COMPRESSION[comp] except: diff --git a/sgbackup/utility.py b/sgbackup/utility.py new file mode 100644 index 0000000..cc12b92 --- /dev/null +++ b/sgbackup/utility.py @@ -0,0 +1,36 @@ +############################################################################### +# 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 . # +############################################################################### + +import sys +if sys.platform.lower() == 'win32': + PLATFORM_WINDOWS=True +else: + PLATFORM_WINDOWS=False + +if sys.platform.lower() in ['linux','freebsd','netbsd','openbsd','dragonfly','macos','cygwin']: + PLATFORM_UNIX = True +else: + PLATFORM_UNIX = False + +def sanitize_windows_path(path:str)->str: + return path.replace('/','\\') + +def sanitize_path(path:str)->str: + if (PLATFORM_WINDOWS): + return sanitize_windows_path(path) + return path