Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ endif()

include(OB/FetchQx)
ob_fetch_qx(
REF "d12b1a3dd8445ba3bae1271e4a6fc6fcb0420dfd"
REF "66ccfeff2eddd912fff7e0116539bbfe84e9503c"
COMPONENTS
${FIL_QX_COMPONENTS}
)
Expand Down
2 changes: 2 additions & 0 deletions app/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ add_custom_target(fil_copy_clifp

# ------------------ Setup FIL --------------------------
set(FIL_SOURCE
import/backup.h
import/backup.cpp
import/details.h
import/details.cpp
import/properties.h
Expand Down
181 changes: 181 additions & 0 deletions app/src/import/backup.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
// Unit Includes
#include "backup.h"

// Qt Includes
#include <QFile>
#include <QFileInfo>

namespace Import
{

//===============================================================================================================
// BackupError
//===============================================================================================================

//-Constructor-------------------------------------------------------------
//Private:
BackupError::BackupError(Type t, const QString& s) :
mType(t),
mSpecific(s)
{}

//Public:
BackupError::BackupError() :
mType(NoError)
{}

//-Instance Functions-------------------------------------------------------------
//Public:
bool BackupError::isValid() const { return mType != NoError; }
QString BackupError::specific() const { return mSpecific; }
BackupError::Type BackupError::type() const { return mType; }

//Private:
Qx::Severity BackupError::deriveSeverity() const { return Qx::Err; }
quint32 BackupError::deriveValue() const { return mType; }
QString BackupError::derivePrimary() const { return ERR_STRINGS.value(mType); }
QString BackupError::deriveSecondary() const { return mSpecific; }
QString BackupError::deriveCaption() const { return CAPTION_REVERT_ERR; }

//===============================================================================================================
// BackupManager
//===============================================================================================================

//-Constructor-------------------------------------------------------------
//Private:
BackupManager::BackupManager() {}

//-Class Functions-------------------------------------------------------------
//Private:
QString BackupManager::filePathToBackupPath(const QString& filePath)
{
return filePath + '.' + BACKUP_FILE_EXT;
}

//Public:
BackupManager* BackupManager::instance() { static BackupManager inst; return &inst; }

//-Instance Functions-------------------------------------------------------------
//Private:
BackupError BackupManager::backup(const QString& path, bool (*fn)(const QString& a, const QString& b))
{
// Prevent double+ backups
if(mRevertablePaths.contains(path))
return BackupError();

// Note revertable
mRevertablePaths.insert(path);

// Backup if exists
if(QFile::exists(path))
{
QString backupPath = filePathToBackupPath(path);

if(QFile::exists(backupPath) && QFileInfo(backupPath).isFile())
{
if(!QFile::remove(backupPath))
return BackupError(BackupError::FileWontDelete, backupPath);
}

if(!fn(path, backupPath))
return BackupError(BackupError::FileWontBackup, path);
}

return BackupError();
}

BackupError BackupManager::restore(QSet<QString>::const_iterator pathItr)
{
Q_ASSERT(pathItr != mRevertablePaths.cend());

const QString path = *pathItr;
mRevertablePaths.erase(pathItr);
QString backupPath = filePathToBackupPath(path);

if(QFile::exists(path) && !QFile::remove(path))
return BackupError(BackupError::FileWontDelete, path);

if(!QFile::exists(path) && QFile::exists(backupPath) && !QFile::rename(backupPath, path))
return BackupError(BackupError::FileWontRestore, backupPath);

return BackupError();
}

//Public:
BackupError BackupManager::backupCopy(const QString& path)
{
return backup(path, [](const QString& a, const QString& b){ return QFile::copy(a, b); });
}

BackupError BackupManager::backupRename(const QString& path)
{
return backup(path, [](const QString& a, const QString& b){ return QFile::rename(a, b); });
}

BackupError BackupManager::restore(const QString& path)
{
auto store = mRevertablePaths.constFind(path);
if(store == mRevertablePaths.cend())
return BackupError();

return restore(store);
}

BackupError BackupManager::safeReplace(const QString& src, const QString& dst, bool symlink)
{
// Maybe make sure destination folder exists here?

// Backup
QString backupPath = filePathToBackupPath(dst);
bool dstOccupied = QFile::exists(dst);
if(dstOccupied)
if(!QFile::rename(dst, backupPath)) // Temp backup
return BackupError(BackupError::FileWontBackup, dst);

// Replace
std::error_code replaceError;
if(symlink)
std::filesystem::create_symlink(src.toStdString(), dst.toStdString(), replaceError);
else
replaceError = QFile::copy(src, dst) ? std::error_code() : std::make_error_code(std::io_errc::stream);

// Restore on fail
if(replaceError)
{
if(dstOccupied)
QFile::rename(backupPath, dst);
return BackupError(BackupError::FileWontReplace, src);
}

// Remove backup immediately
if(dstOccupied)
QFile::remove(backupPath);
else // Mark new files (only) as revertible so that existing ones will remain in the event of a revert
mRevertablePaths.insert(dst);

return BackupError();
}

int BackupManager::revertQueueCount() const { return mRevertablePaths.size(); }

int BackupManager::revertNextChange(BackupError& error, bool skipOnFail)
{
// Ensure error message is null
error = BackupError();

// Delete new files and restore backups if present
if(!mRevertablePaths.isEmpty())
{
BackupError rErr = restore(mRevertablePaths.cbegin());
if(rErr && !skipOnFail)
error = rErr;

return mRevertablePaths.size();
}

// Return 0 if all empty (shouldn't be reached if function is used correctly)
qWarning("Reversion function called with no reverts left!");
return 0;
}

}
111 changes: 111 additions & 0 deletions app/src/import/backup.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
#ifndef BACKUP_H
#define BACKUP_H

// Qt Includes
#include <QString>
#include <QSet>

// Qx Includes
#include <qx/core/qx-abstracterror.h>

using namespace Qt::StringLiterals;

/* TODO: The approach, or at least the language around doing a full revert (i.e. emptying the revert
* queue) could use some touch-up.
*/

namespace Import
{

class QX_ERROR_TYPE(BackupError, "Lr::BackupError", 1301)
{
friend class BackupManager;
//-Class Enums-------------------------------------------------------------
public:
enum Type
{
NoError,
FileWontDelete,
FileWontRestore,
FileWontBackup,
FileWontReplace
};

//-Class Variables-------------------------------------------------------------
private:
static inline const QHash<Type, QString> ERR_STRINGS{
{NoError, u""_s},
{FileWontDelete, u"Cannot remove a file. It may need to be deleted manually."_s},
{FileWontRestore, u"Cannot restore a file backup. It may need to be renamed manually.."_s},
{FileWontBackup, u"Cannot backup file."_s},
{FileWontReplace, u"A file that was part of a safe replace operation could not be transfered."_s}
};

static inline const QString CAPTION_REVERT_ERR = u"Error managing backups"_s;

//-Instance Variables-------------------------------------------------------------
private:
Type mType;
QString mSpecific;

//-Constructor-------------------------------------------------------------
private:
BackupError(Type t, const QString& s);

public:
BackupError();

//-Instance Functions-------------------------------------------------------------
public:
bool isValid() const;
Type type() const;
QString specific() const;

private:
Qx::Severity deriveSeverity() const override;
quint32 deriveValue() const override;
QString derivePrimary() const override;
QString deriveSecondary() const override;
QString deriveCaption() const override;
};

class BackupManager
{
//-Class Variables-----------------------------------------------------------------------------------------------
private:
// Files
static inline const QString BACKUP_FILE_EXT = u"fbk"_s;

//-Instance Variables-------------------------------------------------------------
private:
QSet<QString> mRevertablePaths;

//-Constructor-------------------------------------------------------------
private:
BackupManager();

//-Class Functions-------------------------------------------------------------
private:
static QString filePathToBackupPath(const QString& filePath);

public:
static BackupManager* instance();

//-Instance Functions-------------------------------------------------------------
private:
BackupError backup(const QString& path, bool (*fn)(const QString& a, const QString& b));
BackupError restore(QSet<QString>::const_iterator pathItr);

public:
BackupError backupCopy(const QString& path);
BackupError backupRename(const QString& path);
BackupError restore(const QString& path);
BackupError safeReplace(const QString& src, const QString& dst, bool symlink);

int revertQueueCount() const;
int revertNextChange(BackupError& error, bool skipOnFail);
};

}

#endif // BACKUP_H
44 changes: 10 additions & 34 deletions app/src/import/worker.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
// Project Includes
#include "kernel/clifp.h"
#include "import/details.h"
#include "import/backup.h"

namespace Import
{
Expand Down Expand Up @@ -148,6 +149,7 @@ ImageTransferError Worker::transferImage(bool symlink, QString sourcePath, QStri
QString sourceChecksum;
QString destinationChecksum;

// TODO: Probably better to just byte-wise compare
if(!Qx::calculateFileChecksum(sourceChecksum, source, QCryptographicHash::Md5).isFailure() &&
!Qx::calculateFileChecksum(destinationChecksum, destination, QCryptographicHash::Md5).isFailure() &&
sourceChecksum.compare(destinationChecksum, Qt::CaseInsensitive) == 0)
Expand All @@ -160,42 +162,16 @@ ImageTransferError Worker::transferImage(bool symlink, QString sourcePath, QStri
if(!destinationDir.mkpath(u"."_s))
return ImageTransferError(ImageTransferError::CantCreateDirectory, QString(), destinationDir.absolutePath());

// Determine backup path
QString backupPath = Lr::IInstall::filePathToBackupPath(destinationInfo.absoluteFilePath());

// Temporarily backup image if it already exists (also acts as deletion marking in case images for the title were removed in an update)
if(destinationOccupied)
if(!QFile::rename(destinationPath, backupPath)) // Temp backup
return ImageTransferError(ImageTransferError::ImageWontBackup, QString(), destinationPath);

// Linking error tracker
std::error_code linkError;

// Handle transfer
if(symlink)
{
std::filesystem::create_symlink(sourcePath.toStdString(), destinationPath.toStdString(), linkError);
if(linkError)
{
QFile::rename(backupPath, destinationPath); // Restore Backup
return ImageTransferError(ImageTransferError::ImageWontLink, sourcePath, destinationPath);
}
else if(QFile::exists(backupPath))
QFile::remove(backupPath);
else
mLauncherInstall->addRevertableFile(destinationPath); // Only queue image to be removed on failure if its new, so existing images aren't deleted on revert
}
else
// Transfer image
BackupError bErr = BackupManager::instance()->safeReplace(sourcePath, destinationPath, symlink);
if(bErr)
{
if(!QFile::copy(sourcePath, destinationPath))
{
QFile::rename(backupPath, destinationPath); // Restore Backup
return ImageTransferError(ImageTransferError::ImageWontCopy, sourcePath, destinationPath);
}
else if(QFile::exists(backupPath))
QFile::remove(backupPath);
if(bErr.type() == BackupError::FileWontBackup)
return ImageTransferError(ImageTransferError::ImageWontBackup, QString(), destinationPath);
else if(bErr.type() == BackupError::FileWontReplace)
return ImageTransferError(symlink ? ImageTransferError::ImageWontLink : ImageTransferError::ImageWontCopy, QString(), destinationPath);
else
mLauncherInstall->addRevertableFile(destinationPath); // Only queue image to be removed on failure if its new, so existing images aren't deleted on revert
qFatal("Unhandled image transfer error type.");
}

// Return null error on success
Expand Down
Loading