diff --git a/beeref/fileio/__init__.py b/beeref/fileio/__init__.py index 5834d97..5de2878 100644 --- a/beeref/fileio/__init__.py +++ b/beeref/fileio/__init__.py @@ -13,24 +13,27 @@ # You should have received a copy of the GNU General Public License # along with BeeRef. If not, see . +import datetime import logging +import os from PyQt6 import QtCore from beeref import commands from beeref.fileio.errors import BeeFileIOError from beeref.fileio.image import load_image -from beeref.fileio.sql import SQLiteIO, is_bee_file +from beeref.fileio.sql import SQLiteIO, is_bee_file, read_uppdate_from_file from beeref.items import BeePixmapItem __all__ = [ 'is_bee_file', 'load_bee', - 'save_bee', 'load_images', - 'ThreadedLoader', + 'read_uppdate_from_file' + 'save_bee', 'BeeFileIOError', + 'ThreadedLoader', ] logger = logging.getLogger(__name__) @@ -52,6 +55,31 @@ def save_bee(filename, scene, create_new=False, worker=None): logger.info('End save') +def save_backup(filename, backup_filename, scene, worker=None): + """Save backup bee file.""" + logger.info(f'Saving backup to file {filename}...') + copy_to_backup = False + past = datetime(2000, 1, 1, tzinfo=datetime.timezone.utc) + + if os.path.exists(filename): + upddate_original = read_uppdate_from_file(filename) or past + if os.path.exists(backup_filename): + upddate_backup = read_uppdate_from_file(backup_filename) or past + if upddate_original > upddate_backup: + copy_to_backup = True + logger.debug('Original file newer than backup') + else:#tbd + copy_to_backup = True + logger.debug('Backup file does\'t exist yet') + + if copy_to_backup: + logger.debug('Copying original file to backup file...') + #tbd + + #tbd backup + + worker.finished.emit(filename, []) + def load_images(filenames, pos, scene, worker): """Add images to existing scene.""" diff --git a/beeref/fileio/schema.py b/beeref/fileio/schema.py index 7214305..a986604 100644 --- a/beeref/fileio/schema.py +++ b/beeref/fileio/schema.py @@ -30,7 +30,8 @@ SCHEMA = [ ON UPDATE NO ACTION ) """, - """CREATE TABLE info ( + """ + CREATE TABLE info ( key TEXT PRIMARY KEY, value TEXT ) diff --git a/beeref/fileio/sql.py b/beeref/fileio/sql.py index a5bbb17..50d9b5b 100644 --- a/beeref/fileio/sql.py +++ b/beeref/fileio/sql.py @@ -49,6 +49,13 @@ def is_bee_file(path): return os.path.splitext(path)[1] == '.bee' +def read_uppdate_from_file(filename): + io = SQLiteIO(filename, None, create_new=False, readonly=True) + datestring = io.read_info_value('upddate') + if datestring: + return datetime.datetime.fromisoformat(datestring) + + def handle_sqlite_errors(func): def wrapper(self, *args, **kwargs): try: diff --git a/beeref/view.py b/beeref/view.py index 9e40532..f18eebd 100644 --- a/beeref/view.py +++ b/beeref/view.py @@ -17,6 +17,7 @@ from functools import partial import logging import os import os.path +import tempfile from PyQt6 import QtCore, QtGui, QtWidgets from PyQt6.QtCore import Qt @@ -67,6 +68,7 @@ class BeeGraphicsView(MainControlsMixin, self.undo_stack.cleanChanged.connect(self.on_undo_clean_changed) self.filename = None + self.backup_filename = None self.previous_transform = None self.active_mode = None @@ -92,6 +94,11 @@ class BeeGraphicsView(MainControlsMixin, self.update_window_title() + # Initiate back + timer = QtCore.QTimer(parent=self) + timer.timeout.connect(self.save_backup) + timer.start(1000 * 5)# tbd configurable + @property def filename(self): return self._filename @@ -104,6 +111,42 @@ class BeeGraphicsView(MainControlsMixin, self.settings.update_recent_files(value) self.update_menu_and_actions() + @property + def backup_filename(self): + if self.filename: + return f'{self.filename}~' + + if not self._backup_filename: + self._backup_filename = tempfile.NamedTemporaryFile( + prefix=constants.APPNAME, + suffix='.bee~', + delete=False, + delete_on_close=False + ).name + return self._backup_filename + + @backup_filename.setter + def backup_filename(self, value): + self._backup_filename = value + + def save_backup(self): + if self.undo_stack.isClean(): + logger.debug('No unsaved changes to back up') + return + + if not hasattr(self, 'backup_worker'): + self.backup_worker = fileio.ThreadedIO( + fileio.save_backup, + self.filename, self.backup_filename, self.scene) + self.backup_worker.finished.connect(self.on_save_backup_finished) + self.backup_worker.start() + else: + logger.debug('Backup still ongoing, skipping') + + def on_save_backup_finished(self): + del self.backup_worker + logger.debug('Saving backup finished') + def cancel_active_modes(self): self.scene.cancel_active_modes() self.cancel_sample_color_mode() @@ -174,6 +217,7 @@ class BeeGraphicsView(MainControlsMixin, self.scene.clear() self.undo_stack.clear() self.filename = None + self.backup_filename = None self.setTransform(QtGui.QTransform()) def reset_previous_transform(self, toggle_item=None): diff --git a/tests/fileio/test_sql.py b/tests/fileio/test_sql.py index 0dc0ee8..fb563e9 100644 --- a/tests/fileio/test_sql.py +++ b/tests/fileio/test_sql.py @@ -10,7 +10,7 @@ import pytest from beeref.fileio import schema, is_bee_file from beeref.fileio.errors import BeeFileIOError -from beeref.fileio.sql import SQLiteIO +from beeref.fileio.sql import SQLiteIO, read_uppdate_from_file from beeref.items import BeePixmapItem, BeeTextItem, BeeErrorItem @@ -22,6 +22,24 @@ def test_is_bee_file(filename, expected): assert is_bee_file(filename) is expected +def test_read_upddate_from_file(tmpfile): + io = SQLiteIO(tmpfile, MagicMock(), create_new=True) + io.create_schema_on_new() + dt = datetime.datetime(2024, 5, 10, 12, 45, tzinfo=datetime.timezone.utc) + io.write_info_value('upddate', dt.isoformat()) + io.connection.commit() + del io + assert read_uppdate_from_file(tmpfile) == dt + + +def test_read_upddate_from_file_when_not_set(tmpfile): + io = SQLiteIO(tmpfile, MagicMock(), create_new=True) + io.create_schema_on_new() + io.connection.commit() + del io + assert read_uppdate_from_file(tmpfile) is None + + def test_sqliteio_migrate_does_nothing_when_version_ok(tmpfile): io = SQLiteIO(tmpfile, MagicMock(), create_new=True) io.ex('PRAGMA user_version=%s' % schema.USER_VERSION) @@ -214,8 +232,9 @@ def test_sqliteio_write_calls_create_schema_on_new(tmpfile, view): with patch.object(io, 'create_schema_on_new') as crmock: with patch.object(io, 'fetchall'): with patch.object(io, 'exmany'): - io.write() - crmock.assert_called_once() + with patch.object(io, 'ex'): + io.write() + crmock.assert_called_once() def test_sqliteio_write_calls_write_meta(tmpfile, view):