Skip to content

Commit

Permalink
Initial work on auto backup of files
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
cameronwhite committed Oct 30, 2024
1 parent b2d2f01 commit 4b75db8
Show file tree
Hide file tree
Showing 8 changed files with 243 additions and 4 deletions.
2 changes: 2 additions & 0 deletions source/app/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ project( pteapp )

set( srcs
appinfo.cpp
autobackup.cpp
caret.cpp
clipboard.cpp
command.cpp
Expand All @@ -18,6 +19,7 @@ set( srcs

set( headers
appinfo.h
autobackup.h
caret.h
clipboard.h
command.h
Expand Down
176 changes: 176 additions & 0 deletions source/app/autobackup.cpp
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*/

#include "autobackup.h"

#include "documentmanager.h"
#include "paths.h"

#include <actions/undomanager.h>
#include <formats/powertab/powertabexporter.h>

#include <QTimer>

#include <chrono>
#include <condition_variable>
#include <iostream>
#include <memory>
#include <mutex>
#include <thread>

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<BackupItem> theBackupItems;
static bool theFinishedFlag = false;

/// Background thread for saving backup files without blocking the UI.
static void
backupThread()
{
while (true)
{
std::vector<BackupItem> 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<QTimer>(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<BackupItem> 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<std::chrono::milliseconds>(end - start).count() << "ms"
<< std::endl;
}
Expand Down
46 changes: 46 additions & 0 deletions source/app/autobackup.h
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*/

#ifndef APP_AUTOBACKUP_H
#define APP_AUTOBACKUP_H

#include <QObject>
#include <memory>
#include <thread>

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<QTimer> myTimer;
std::thread myWorkerThread;
};

#endif
Expand Down
6 changes: 6 additions & 0 deletions source/app/paths.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ path getUserDataDir()
QStandardPaths::writableLocation(QStandardPaths::AppDataLocation));
}

path
getBackupDir()
{
return getUserDataDir() / "backup";
}

std::vector<path> getDataDirs()
{
std::vector<path> paths;
Expand Down
3 changes: 3 additions & 0 deletions source/app/paths.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<path> getDataDirs();
Expand Down
5 changes: 3 additions & 2 deletions source/app/powertabeditor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
#include <actions/volumeswell.h>

#include <app/appinfo.h>
#include <app/autobackup.h>
#include <app/caret.h>
#include <app/clipboard.h>
#include <app/command.h>
Expand Down Expand Up @@ -161,9 +162,9 @@ PowerTabEditor::PowerTabEditor()
: QMainWindow(nullptr),
mySettingsManager(std::make_unique<SettingsManager>()),
myDocumentManager(std::make_unique<DocumentManager>()),
myFileFormatManager(
std::make_unique<FileFormatManager>(*mySettingsManager)),
myFileFormatManager(std::make_unique<FileFormatManager>(*mySettingsManager)),
myUndoManager(std::make_unique<UndoManager>()),
myAutoBackup(std::make_unique<AutoBackup>(*myDocumentManager, *myUndoManager, this)),
myTuningDictionary(std::make_unique<TuningDictionary>()),
myIsPlaying(false),
myRecentFiles(nullptr),
Expand Down
2 changes: 2 additions & 0 deletions source/app/powertabeditor.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
#include <string>
#include <vector>

class AutoBackup;
class Caret;
class Command;
class DocumentManager;
Expand Down Expand Up @@ -430,6 +431,7 @@ private slots:
std::unique_ptr<DocumentManager> myDocumentManager;
std::unique_ptr<FileFormatManager> myFileFormatManager;
std::unique_ptr<UndoManager> myUndoManager;
std::unique_ptr<AutoBackup> myAutoBackup;
std::unique_ptr<QThread> myMidiThread;
MidiPlayer *myMidiPlayer = nullptr;
std::unique_ptr<TuningDictionary> myTuningDictionary;
Expand Down
7 changes: 5 additions & 2 deletions source/score/score.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 <class Archive>
Expand Down

0 comments on commit 4b75db8

Please sign in to comment.