import os import sqlite3 from datetime import datetime from pathlib import Path from nmreval.configs import config_paths from ..Qt import QtCore, QtWidgets DB_FILE = '/tmp/nmreval.db' class BackupManager(QtCore.QObject): def __init__(self): super().__init__() self.create_table() self._pid = None def create_table(self): con = sqlite3.connect(DB_FILE) con.execute( "CREATE TABLE IF NOT EXISTS sessions " "(pid INTEGER NOT NULL, backup_file TEXT NOT NULL, last_save TEXT);" ) def create_entry(self, pid: int): 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(?, ?, ?);', (pid, str(backup_path), None)) con.commit() con.close() self._pid = pid return backup_path 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() # remove backup file res = cursor.execute('SELECT sessions.backup_file FROM sessions WHERE pid = ?', (pid,)) con.commit() 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) cursor = con.cursor() res = cursor.execute('SELECT COUNT(sessions.pid) FROM sessions GROUP BY sessions.pid;') con.commit() remaining_processes = res.fetchone() con.close() if remaining_processes is None: Path(DB_FILE).unlink() def close(self): self.remove_file() self.delete_db_if_empty() 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} self.setWindowTitle('Adopt a file!') self.resize(720, 320) 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) 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()