From 7280ee78807588490902eadb3311e08d206cef3c Mon Sep 17 00:00:00 2001 From: Cameron White Date: Tue, 29 Oct 2024 20:00:14 -0400 Subject: [PATCH] Initial work on auto backup of files - At a regular time interval, check for modified files and send a copy of the scores to be saved from a background thread, to avoid blocking the UI - The files are saved under the app data directory, e.g `~/.local/share/powertabeditor/backup` - Make the Score class copyable to allow making a copy that can be safely moved to a background thread Bug: #392 --- source/app/CMakeLists.txt | 2 + source/app/autobackup.cpp | 176 ++++++++++++++++++++++++++++++++++ source/app/autobackup.h | 46 +++++++++ source/app/paths.cpp | 6 ++ source/app/paths.h | 3 + source/app/powertabeditor.cpp | 5 +- source/app/powertabeditor.h | 2 + source/score/score.h | 7 +- 8 files changed, 243 insertions(+), 4 deletions(-) create mode 100644 source/app/autobackup.cpp create mode 100644 source/app/autobackup.h diff --git a/source/app/CMakeLists.txt b/source/app/CMakeLists.txt index c4f3caaf..8c23801b 100644 --- a/source/app/CMakeLists.txt +++ b/source/app/CMakeLists.txt @@ -2,6 +2,7 @@ project( pteapp ) set( srcs appinfo.cpp + autobackup.cpp caret.cpp clipboard.cpp command.cpp @@ -18,6 +19,7 @@ set( srcs set( headers appinfo.h + autobackup.h caret.h clipboard.h command.h diff --git a/source/app/autobackup.cpp b/source/app/autobackup.cpp new file mode 100644 index 00000000..4c44f46a --- /dev/null +++ b/source/app/autobackup.cpp @@ -0,0 +1,176 @@ +/* + * Copyright (C) 2024 Cameron White + * + * 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 . +*/ + +#include "autobackup.h" + +#include "documentmanager.h" +#include "paths.h" + +#include +#include + +#include + +#include +#include +#include +#include +#include +#include + +namespace +{ +struct BackupItem +{ + /// Saves the file to the backup folder. + void save() const; + + Score myScore; + std::filesystem::path myOrigPath; +}; + +void +BackupItem::save() const +{ + PowerTabExporter exporter; + + auto backup_dir = Paths::getBackupDir(); + std::filesystem::create_directories(backup_dir); + + auto filename = myOrigPath.filename(); + filename.replace_extension(".pt2"); + + auto path = backup_dir / filename; + + try + { + exporter.save(path, myScore); + std::cerr << "Saved backup to: " << path << std::endl; + } + catch (const std::exception &e) + { + std::cerr << "Failed to save backup file: " << path << std::endl; + std::cerr << "Error: " << e.what() << std::endl; + } +} +} // namespace + +static std::mutex theLock; +static std::condition_variable theCV; +static std::vector theBackupItems; +static bool theFinishedFlag = false; + +/// Background thread for saving backup files without blocking the UI. +static void +backupThread() +{ + while (true) + { + std::vector backup_items; + + // Wait for the next set of backup files. + { + std::unique_lock lock(theLock); + theCV.wait(lock, [] { return theFinishedFlag || !theBackupItems.empty(); }); + + if (theFinishedFlag) + break; + + // Move to a local copy so we can release the lock before saving to disk. + backup_items = std::move(theBackupItems); + } + + for (const BackupItem &item : backup_items) + item.save(); + } +} + +AutoBackup::AutoBackup(const DocumentManager &document_manager, const UndoManager &undo_manager, + QObject *parent) + : QObject(parent), + myDocumentManager(document_manager), + myUndoManager(undo_manager), + myTimer(std::make_unique(this)), + myWorkerThread(backupThread) +{ + connect(myTimer.get(), &QTimer::timeout, this, &AutoBackup::startBackup); + + static constexpr int interval_ms = 5000; // TODO - configure via preferences. + myTimer->start(interval_ms); +} + +AutoBackup::~AutoBackup() +{ + // Notify the worker thread and wait for it to finish. + { + std::unique_lock lock(theLock); + theFinishedFlag = true; + } + theCV.notify_one(); + + myWorkerThread.join(); +} + +void +AutoBackup::startBackup() +{ + std::cerr << "Starting backup..." << std::endl; + auto start = std::chrono::high_resolution_clock::now(); + + std::vector items_to_backup; + + for (int i = 0, n = myDocumentManager.getNumDocuments(); i < n; ++i) + { + // Only consider files with unsaved changes. + if (myUndoManager.stacks()[i]->isClean()) + continue; + + const Document &doc = myDocumentManager.getDocument(i); + + BackupItem item; + // Note we make a copy of the score, so that it can be safely saved from the background + // thread. + item.myScore = doc.getScore(); + + if (doc.hasFilename()) + item.myOrigPath = doc.getFilename(); + else + item.myOrigPath = "Untitled_" + std::to_string(i); + + std::cerr << "Added to backup: " << item.myOrigPath << std::endl; + + items_to_backup.push_back(std::move(item)); + } + + if (items_to_backup.empty()) + { + std::cerr << "No files for backup" << std::endl; + return; + } + + // Send the documents to the worker thread to be saved to disk. + { + std::unique_lock lock(theLock); + theBackupItems = std::move(items_to_backup); + } + theCV.notify_one(); + + auto end = std::chrono::high_resolution_clock::now(); + std::cerr << "Prepared documents for backup in " + << std::chrono::duration_cast(end - start).count() << "ms" + << std::endl; +} diff --git a/source/app/autobackup.h b/source/app/autobackup.h new file mode 100644 index 00000000..eec21749 --- /dev/null +++ b/source/app/autobackup.h @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2024 Cameron White + * + * 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 . +*/ + +#ifndef APP_AUTOBACKUP_H +#define APP_AUTOBACKUP_H + +#include +#include +#include + +class DocumentManager; +class QTimer; +class UndoManager; + +class AutoBackup : public QObject +{ +public: + AutoBackup(const DocumentManager &document_manager, const UndoManager &undo_manager, + QObject *parent = nullptr); + ~AutoBackup(); + +private: + void startBackup(); + + const DocumentManager &myDocumentManager; + const UndoManager &myUndoManager; + + std::unique_ptr myTimer; + std::thread myWorkerThread; +}; + +#endif diff --git a/source/app/paths.cpp b/source/app/paths.cpp index 792a4148..f361d987 100644 --- a/source/app/paths.cpp +++ b/source/app/paths.cpp @@ -45,6 +45,12 @@ path getUserDataDir() QStandardPaths::writableLocation(QStandardPaths::AppDataLocation)); } +path +getBackupDir() +{ + return getUserDataDir() / "backup"; +} + std::vector getDataDirs() { std::vector paths; diff --git a/source/app/paths.h b/source/app/paths.h index 898a3ab4..ff2ea87c 100644 --- a/source/app/paths.h +++ b/source/app/paths.h @@ -30,6 +30,9 @@ namespace Paths { /// be written to. path getUserDataDir(); + /// Return a path to the directory where backup files are written. + path getBackupDir(); + /// Return a list of paths where persistent application data should be read /// from, ordered from highest to lowest priority. std::vector getDataDirs(); diff --git a/source/app/powertabeditor.cpp b/source/app/powertabeditor.cpp index fbf5466a..a38a2441 100644 --- a/source/app/powertabeditor.cpp +++ b/source/app/powertabeditor.cpp @@ -74,6 +74,7 @@ #include #include +#include #include #include #include @@ -161,9 +162,9 @@ PowerTabEditor::PowerTabEditor() : QMainWindow(nullptr), mySettingsManager(std::make_unique()), myDocumentManager(std::make_unique()), - myFileFormatManager( - std::make_unique(*mySettingsManager)), + myFileFormatManager(std::make_unique(*mySettingsManager)), myUndoManager(std::make_unique()), + myAutoBackup(std::make_unique(*myDocumentManager, *myUndoManager), this), myTuningDictionary(std::make_unique()), myIsPlaying(false), myRecentFiles(nullptr), diff --git a/source/app/powertabeditor.h b/source/app/powertabeditor.h index 09dde78f..4a67c857 100644 --- a/source/app/powertabeditor.h +++ b/source/app/powertabeditor.h @@ -26,6 +26,7 @@ #include #include +class AutoBackup; class Caret; class Command; class DocumentManager; @@ -430,6 +431,7 @@ private slots: std::unique_ptr myDocumentManager; std::unique_ptr myFileFormatManager; std::unique_ptr myUndoManager; + std::unique_ptr myAutoBackup; std::unique_ptr myMidiThread; MidiPlayer *myMidiPlayer = nullptr; std::unique_ptr myTuningDictionary; diff --git a/source/score/score.h b/source/score/score.h index 73cb0ed9..4212fc29 100644 --- a/source/score/score.h +++ b/source/score/score.h @@ -35,8 +35,11 @@ class Score { public: Score(); - Score(const Score &other) = delete; - Score &operator=(const Score &other) = delete; + explicit Score(const Score &other) = default; + Score &operator=(const Score &other) = default; + explicit Score(Score &&other) = default; + Score &operator=(Score &&other) = default; + bool operator==(const Score &other) const; template