issue126-backup #198

Merged
dominik merged 7 commits from issue126-backup into master 2024-01-03 12:30:05 +00:00
4 changed files with 162 additions and 63 deletions
Showing only changes of commit cbcc9308ad - Show all commits

View File

@ -34,8 +34,6 @@ def do_autosave():
backuping = BackupManager() backuping = BackupManager()
files = backuping.search_unsaved() files = backuping.search_unsaved()
print(files)
print(sys.executable, sys.argv)
pid = os.getpid() pid = os.getpid()
bck_name = backuping.create_entry(pid) bck_name = backuping.create_entry(pid)
@ -43,12 +41,17 @@ mplQt = NMRMainWindow(bck_file=bck_name)
do_autosave() do_autosave()
for f in files:
mplQt.management.load_files(f)
f.unlink()
timer = QtCore.QTimer() timer = QtCore.QTimer()
timer.timeout.connect(do_autosave) timer.timeout.connect(do_autosave)
timer.start(3 * 1000) timer.start(3 * 60 * 1000)
app.aboutToQuit.connect(backuping.close) app.aboutToQuit.connect(backuping.close)
mplQt.show() mplQt.show()
sys.exit(app.exec()) sys.exit(app.exec())

View File

@ -19,11 +19,11 @@ class BackupManager(QtCore.QObject):
con = sqlite3.connect(DB_FILE) con = sqlite3.connect(DB_FILE)
con.execute( con.execute(
"CREATE TABLE IF NOT EXISTS sessions " "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): 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 = sqlite3.connect(DB_FILE)
con.execute('INSERT INTO sessions VALUES(?, ?, ?);', con.execute('INSERT INTO sessions VALUES(?, ?, ?);',
@ -34,17 +34,78 @@ class BackupManager(QtCore.QObject):
return backup_path 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: if pid is None:
pid = self._pid pid = self._pid
con = sqlite3.connect(DB_FILE) con = sqlite3.connect(DB_FILE)
cursor = con.cursor() 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() 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): def delete_db_if_empty(self):
con = sqlite3.connect(DB_FILE) con = sqlite3.connect(DB_FILE)
@ -54,59 +115,93 @@ class BackupManager(QtCore.QObject):
remaining_processes = res.fetchone() remaining_processes = res.fetchone()
con.close() con.close()
if remaining_processes is None: if remaining_processes is None:
Path(DB_FILE).unlink() Path(DB_FILE).unlink()
def close(self): def close(self):
self.remove_entry() self.remove_file()
self.delete_db_if_empty() 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: self.setWindowTitle('Adopt a file!')
try: self.resize(720, 320)
os.kill(pid, 0)
except ProcessLookupError:
missing_processes.append((pid, fname))
if missing_processes: layout = QtWidgets.QVBoxLayout(self)
msg = QtWidgets.QMessageBox() self.label = QtWidgets.QLabel(self)
msg.exec() 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): self.table = QtWidgets.QTableWidget(self)
con = sqlite3.connect(DB_FILE) self.table.setColumnCount(4)
con.execute( self.table.setHorizontalHeaderLabels(['File name', 'Last saved', 'File size', 'Action'])
'UPDATE sessions SET last_save = ? WHERE pid = ?', self.table.setGridStyle(QtCore.Qt.PenStyle.DashLine)
(datetime.now(), self._pid) # self.table.horizontalHeader().setStretchLastSection(True)
) self.table.verticalHeader().setVisible(False)
con.commit() self.table.horizontalHeader().setSectionResizeMode(0, QtWidgets.QHeaderView.Stretch)
con.close() layout.addWidget(self.table)
#
# def check_for_backup(self): self.buttons = QtWidgets.QDialogButtonBox(self)
# backup_by_date = sorted(backups, key=lambda x: x[1]) self.buttons.setOrientation(QtCore.Qt.Orientation.Horizontal)
# msg = (QtWidgets.QMessageBox() self.buttons.setStandardButtons(QtWidgets.QDialogButtonBox.Ok)
# , 'Backup found',) layout.addWidget(self.buttons)
# 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' self.buttons.accepted.connect(self.accept)
# f'Press Ok to load, Cancel to delete backup, Close to do nothing.',
# QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel | QtWidgets.QMessageBox.Close def add_files(self, entries):
# ) self.table.setRowCount(len(entries))
# for i, (pid, path, date) in enumerate(entries):
# if msg == QtWidgets.QMessageBox.Ok: path = Path(path)
# self.management.load_files([str(backup_by_date[-1][0])]) item1 = QtWidgets.QTableWidgetItem(path.name)
# backup_by_date[-1][0].unlink() item1.setFlags(QtCore.Qt.ItemFlag.ItemIsEnabled)
# elif msg == QtWidgets.QMessageBox.Cancel: item1.setData(QtCore.Qt.ItemDataRole.UserRole, pid)
# backup_by_date[-1][0].unlink() item1.setData(QtCore.Qt.ItemDataRole.UserRole+1, path)
# else: self.table.setItem(i, 0, item1)
# pass
# 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()

View File

@ -1,4 +1,3 @@
import sys
from pathlib import Path from pathlib import Path
from .codeeditor import _make_textformats from .codeeditor import _make_textformats
@ -6,12 +5,14 @@ from ..Qt import QtWidgets, QtCore, QtGui
from nmreval.configs import config_paths from nmreval.configs import config_paths
STYLES = {'INFO': _make_textformats('black'), STYLES = {
'INFO': _make_textformats('black'),
'WARNING': _make_textformats('blue'), 'WARNING': _make_textformats('blue'),
'ERROR': _make_textformats('red', 'bold'), 'ERROR': _make_textformats('red', 'bold'),
'DEBUG': _make_textformats('black', 'italic'), 'DEBUG': _make_textformats('black', 'italic'),
'file': _make_textformats('red', 'italic'), 'file': _make_textformats('red', 'italic'),
'PyError': _make_textformats('red', 'bold-italic')} 'PyError': _make_textformats('red', 'bold-italic'),
}
class LogHighlighter(QtGui.QSyntaxHighlighter): class LogHighlighter(QtGui.QSyntaxHighlighter):

View File

@ -1,9 +1,8 @@
from __future__ import annotations from __future__ import annotations
import datetime
import os import os
import pathlib
import re import re
import time
from pathlib import Path from pathlib import Path
from numpy import geomspace, linspace from numpy import geomspace, linspace
@ -34,7 +33,7 @@ from ..math.smooth import QSmooth
from ..nmr.coupling_calc import QCoupCalcDialog from ..nmr.coupling_calc import QCoupCalcDialog
from ..nmr.t1_from_tau import QRelaxCalc from ..nmr.t1_from_tau import QRelaxCalc
from .._py.basewindow import Ui_BaseWindow from .._py.basewindow import Ui_BaseWindow
from ..lib.utils import UpdateDialog, Updater from ..lib.utils import UpdateDialog
class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow): 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 # TODO better separate thread may it takes some time to save
self.status.setText('Autosave...') self.status.setText('Autosave...')
success = NMRWriter(self.management.graphs, self.management.data).export(self.__backup_path.with_suffix('.nmr.0')) success = NMRWriter(self.management.graphs, self.management.data).export(self.__backup_path.with_suffix('.nmr.0'))
if success: if success:
self.__backup_path.with_suffix('.nmr.0').rename(self.__backup_path) self.__backup_path.with_suffix('.nmr.0').rename(self.__backup_path)