From cbcc9308ad2f8bd7f07c924edcdd336549948f62 Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Tue, 2 Jan 2024 10:25:46 +0100 Subject: [PATCH] read old autosaves --- bin/evaluate.py | 9 +- src/gui_qt/lib/backup.py | 195 +++++++++++++++++++++++++--------- src/gui_qt/lib/logger.py | 15 +-- src/gui_qt/main/mainwindow.py | 6 +- 4 files changed, 162 insertions(+), 63 deletions(-) diff --git a/bin/evaluate.py b/bin/evaluate.py index 78093ed..cc83218 100755 --- a/bin/evaluate.py +++ b/bin/evaluate.py @@ -34,8 +34,6 @@ def do_autosave(): backuping = BackupManager() files = backuping.search_unsaved() -print(files) -print(sys.executable, sys.argv) pid = os.getpid() bck_name = backuping.create_entry(pid) @@ -43,12 +41,17 @@ mplQt = NMRMainWindow(bck_file=bck_name) do_autosave() +for f in files: + mplQt.management.load_files(f) + f.unlink() + timer = QtCore.QTimer() timer.timeout.connect(do_autosave) -timer.start(3 * 1000) +timer.start(3 * 60 * 1000) app.aboutToQuit.connect(backuping.close) mplQt.show() + sys.exit(app.exec()) diff --git a/src/gui_qt/lib/backup.py b/src/gui_qt/lib/backup.py index f80efc7..b7bb39f 100644 --- a/src/gui_qt/lib/backup.py +++ b/src/gui_qt/lib/backup.py @@ -19,11 +19,11 @@ class BackupManager(QtCore.QObject): con = sqlite3.connect(DB_FILE) con.execute( "CREATE TABLE IF NOT EXISTS sessions " - "(pid INTEGER NOT NULL, backup_file TEXT NOT NULL, last_save INTEGER);" + "(pid INTEGER NOT NULL, backup_file TEXT NOT NULL, last_save TEXT);" ) def create_entry(self, pid: int): - backup_path = config_paths() / f'{datetime.now().strftime("%Y-%m-%d_%H%M%S")}.nmr' + backup_path = config_paths() / f'autosave_{datetime.now().strftime("%Y-%m-%d_%H%M%S")}_{pid}.nmr' con = sqlite3.connect(DB_FILE) con.execute('INSERT INTO sessions VALUES(?, ?, ?);', @@ -34,17 +34,78 @@ class BackupManager(QtCore.QObject): return backup_path - def remove_entry(self, pid: int = None): + def update_last_save(self): + con = sqlite3.connect(DB_FILE) + con.execute( + 'UPDATE sessions SET last_save = ? WHERE pid = ?', + (datetime.now().strftime("%Y-%m-%d %H:%M:%S"), self._pid) + ) + con.commit() + con.close() + + def search_unsaved(self): + con = sqlite3.connect(DB_FILE) + cursor = con.cursor() + res = cursor.execute('SELECT sessions.* FROM sessions;') + con.commit() + data = res.fetchall() + con.close() + + missing_processes = [] + + for pid, fname, save_date in data: + try: + os.kill(pid, 0) + except ProcessLookupError: + if Path(fname).exists(): + missing_processes.append((pid, fname, save_date)) + else: + # remove entries without valid file + self.remove_row(pid) + + if missing_processes: + msg = QLonelyBackupWindow() + msg.add_files(missing_processes) + msg.exec() + toberead = msg.recover + + for pid in toberead[0]: + self.remove_file(pid) + + for pid in toberead[1]: + self.remove_row(pid) + + return list(toberead[1].values()) + + return [] + + def remove_row(self, pid): + con = sqlite3.connect(DB_FILE) + con.execute('DELETE FROM sessions WHERE pid = ?', (pid,)) + con.commit() + con.close() + + def remove_file(self, pid: int = None): if pid is None: pid = self._pid con = sqlite3.connect(DB_FILE) cursor = con.cursor() - res = cursor.execute('DELETE FROM sessions WHERE pid = ?;', (pid,)) + # remove backup file + res = cursor.execute('SELECT sessions.backup_file FROM sessions WHERE pid = ?', (pid,)) con.commit() - self.delete_db_if_empty() + fname = Path(res.fetchone()[0]) + con.close() + + fname.unlink(missing_ok=True) + + # because autosave backups in a *.nmr.0 file and then moves it to *.nmr we look also for this one. + fname.with_suffix('.nmr.0').unlink(missing_ok=True) + + # after removal of file also remove entry + self.remove_row(pid) def delete_db_if_empty(self): con = sqlite3.connect(DB_FILE) @@ -54,59 +115,93 @@ class BackupManager(QtCore.QObject): remaining_processes = res.fetchone() con.close() - if remaining_processes is None: Path(DB_FILE).unlink() def close(self): - self.remove_entry() + self.remove_file() self.delete_db_if_empty() - def search_unsaved(self): - con = sqlite3.connect(DB_FILE) - cursor = con.cursor() - res = cursor.execute('SELECT sessions.pid, sessions.backup_file FROM sessions;') - con.commit() - data = res.fetchall() - con.close() - missing_processes = [] +class QLonelyBackupWindow(QtWidgets.QDialog): + def __init__(self, parent=None): + super().__init__(parent=parent) + self.recover = [[], {}] # list of pid to delete, dict of files to read {pid: file} - for pid, fname in data: - try: - os.kill(pid, 0) - except ProcessLookupError: - missing_processes.append((pid, fname)) + self.setWindowTitle('Adopt a file!') + self.resize(720, 320) - if missing_processes: - msg = QtWidgets.QMessageBox() - msg.exec() + layout = QtWidgets.QVBoxLayout(self) + self.label = QtWidgets.QLabel(self) + self.label.setText('Abandoned backup file(s) looking for a loving home!\n' + '(Files will all be loaded to same instance)') + layout.addWidget(self.label) - def update_last_save(self): - con = sqlite3.connect(DB_FILE) - con.execute( - 'UPDATE sessions SET last_save = ? WHERE pid = ?', - (datetime.now(), self._pid) - ) - con.commit() - con.close() - # - # def check_for_backup(self): - # backup_by_date = sorted(backups, key=lambda x: x[1]) - # msg = (QtWidgets.QMessageBox() - # , 'Backup found',) - # f'{len(backups)} backup files in directory {backup_by_date[-1][0].parent} found.\n\n' - # f'Latest backup date: {backup_by_date[-1][1]}\n\n' - # f'Press Ok to load, Cancel to delete backup, Close to do nothing.', - # QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel | QtWidgets.QMessageBox.Close - # ) - # - # if msg == QtWidgets.QMessageBox.Ok: - # self.management.load_files([str(backup_by_date[-1][0])]) - # backup_by_date[-1][0].unlink() - # elif msg == QtWidgets.QMessageBox.Cancel: - # backup_by_date[-1][0].unlink() - # else: - # pass - # + self.table = QtWidgets.QTableWidget(self) + self.table.setColumnCount(4) + self.table.setHorizontalHeaderLabels(['File name', 'Last saved', 'File size', 'Action']) + self.table.setGridStyle(QtCore.Qt.PenStyle.DashLine) + # self.table.horizontalHeader().setStretchLastSection(True) + self.table.verticalHeader().setVisible(False) + self.table.horizontalHeader().setSectionResizeMode(0, QtWidgets.QHeaderView.Stretch) + layout.addWidget(self.table) + + self.buttons = QtWidgets.QDialogButtonBox(self) + self.buttons.setOrientation(QtCore.Qt.Orientation.Horizontal) + self.buttons.setStandardButtons(QtWidgets.QDialogButtonBox.Ok) + layout.addWidget(self.buttons) + + self.buttons.accepted.connect(self.accept) + + def add_files(self, entries): + self.table.setRowCount(len(entries)) + for i, (pid, path, date) in enumerate(entries): + path = Path(path) + item1 = QtWidgets.QTableWidgetItem(path.name) + item1.setFlags(QtCore.Qt.ItemFlag.ItemIsEnabled) + item1.setData(QtCore.Qt.ItemDataRole.UserRole, pid) + item1.setData(QtCore.Qt.ItemDataRole.UserRole+1, path) + self.table.setItem(i, 0, item1) + + item2 = QtWidgets.QTableWidgetItem(date) + item2.setFlags(QtCore.Qt.ItemFlag.ItemIsEnabled) + self.table.setItem(i, 1, item2) + + size = path.stat().st_size + size_cnt = 0 + while size > 1024: + # make file size human-readable + size /= 1024 + size_cnt += 1 + if size_cnt == 5: + break + + byte = ['bytes', 'kB', 'MiB', 'GiB', 'TiB', 'PiB'][size_cnt] + + item3 = QtWidgets.QTableWidgetItem(f'{size:.2f} {byte}') + item3.setFlags(QtCore.Qt.ItemFlag.ItemIsEnabled) + self.table.setItem(i, 2, item3) + + cw = QtWidgets.QComboBox(self) + cw.addItems(['Load', 'Delete', 'Keep for later']) + self.table.setCellWidget(i, 3, cw) + + self.table.resizeColumnsToContents() + + def accept(self): + for i in range(self.table.rowCount()): + decision = self.table.cellWidget(i, 3).currentIndex() + item = self.table.item(i, 0) + pid = item.data(QtCore.Qt.ItemDataRole.UserRole) + if decision == 0: + # load file + self.recover[1][pid] = item.data(QtCore.Qt.ItemDataRole.UserRole+1) + elif decision == 1: + # delete + self.recover[0].append(pid) + else: + # do nothing + pass + + super().accept() diff --git a/src/gui_qt/lib/logger.py b/src/gui_qt/lib/logger.py index 43b6bb1..0570d84 100644 --- a/src/gui_qt/lib/logger.py +++ b/src/gui_qt/lib/logger.py @@ -1,4 +1,3 @@ -import sys from pathlib import Path from .codeeditor import _make_textformats @@ -6,12 +5,14 @@ from ..Qt import QtWidgets, QtCore, QtGui from nmreval.configs import config_paths -STYLES = {'INFO': _make_textformats('black'), - 'WARNING': _make_textformats('blue'), - 'ERROR': _make_textformats('red', 'bold'), - 'DEBUG': _make_textformats('black', 'italic'), - 'file': _make_textformats('red', 'italic'), - 'PyError': _make_textformats('red', 'bold-italic')} +STYLES = { + 'INFO': _make_textformats('black'), + 'WARNING': _make_textformats('blue'), + 'ERROR': _make_textformats('red', 'bold'), + 'DEBUG': _make_textformats('black', 'italic'), + 'file': _make_textformats('red', 'italic'), + 'PyError': _make_textformats('red', 'bold-italic'), +} class LogHighlighter(QtGui.QSyntaxHighlighter): diff --git a/src/gui_qt/main/mainwindow.py b/src/gui_qt/main/mainwindow.py index ec0e131..487c3c4 100644 --- a/src/gui_qt/main/mainwindow.py +++ b/src/gui_qt/main/mainwindow.py @@ -1,9 +1,8 @@ from __future__ import annotations -import datetime import os -import pathlib import re +import time from pathlib import Path from numpy import geomspace, linspace @@ -34,7 +33,7 @@ from ..math.smooth import QSmooth from ..nmr.coupling_calc import QCoupCalcDialog from ..nmr.t1_from_tau import QRelaxCalc from .._py.basewindow import Ui_BaseWindow -from ..lib.utils import UpdateDialog, Updater +from ..lib.utils import UpdateDialog class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow): @@ -1110,6 +1109,7 @@ class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow): # TODO better separate thread may it takes some time to save self.status.setText('Autosave...') success = NMRWriter(self.management.graphs, self.management.data).export(self.__backup_path.with_suffix('.nmr.0')) + if success: self.__backup_path.with_suffix('.nmr.0').rename(self.__backup_path)