[Installer][Client] Back up user data before old uninstallers can delete it

This commit is contained in:
Lukas Brübach 2026-10-13 07:13:59 +02:00
parent a712553c81
commit d33542db59
7 changed files with 367 additions and 12 deletions

View file

@ -13,6 +13,7 @@ set(UTILITY_SOURCES
libcockatrice/utility/report_categories.cpp
libcockatrice/utility/server_rate_limiter.cpp
libcockatrice/utility/translation_loader.cpp
libcockatrice/utility/user_data_backup.cpp
libcockatrice/utility/warning_categories.cpp
)
@ -35,6 +36,7 @@ set(UTILITY_HEADERS
libcockatrice/utility/days_years_between.h
libcockatrice/utility/server_rate_limiter.h
libcockatrice/utility/translation_loader.h
libcockatrice/utility/user_data_backup.h
libcockatrice/utility/warning_categories.h
)

View file

@ -0,0 +1,93 @@
#include "user_data_backup.h"
#include <QDateTime>
#include <QDir>
#include <QDirIterator>
#include <QFile>
#include <QFileInfo>
#include <QStandardPaths>
namespace UserDataBackup
{
bool copyDirectoryRecursively(const QString &sourceDir, const QString &destDir)
{
QDir source(sourceDir);
if (!source.exists()) {
return false;
}
if (!QDir().mkpath(destDir)) {
return false;
}
bool ok = true;
QDirIterator it(sourceDir, QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot | QDir::Hidden,
QDirIterator::Subdirectories);
while (it.hasNext()) {
it.next();
QString entryRel = source.relativeFilePath(it.filePath());
QString target = QDir(destDir).absoluteFilePath(entryRel);
if (it.fileInfo().isDir()) {
if (!QDir().mkpath(target)) {
ok = false;
}
} else {
if (QFile::exists(target) && !QFile::remove(target)) {
ok = false;
continue;
}
if (!QFile::copy(it.filePath(), target)) {
ok = false;
}
}
}
return ok;
}
bool backupAtRiskPath(const QString &sourcePath, const QString &backupRoot, QString *backupLocationOut)
{
QFileInfo source(sourcePath);
if (!source.exists()) {
return false;
}
QString timestamp = QDateTime::currentDateTime().toString("yyyyMMdd-hhmmss");
QString backupDir = QDir(backupRoot).absoluteFilePath("CockatriceDataBackup-" + timestamp);
if (!QDir().mkpath(backupDir)) {
return false;
}
bool ok;
QString backupLocation;
if (source.isDir()) {
backupLocation = QDir(backupDir).absoluteFilePath(source.fileName());
ok = copyDirectoryRecursively(sourcePath, backupLocation);
} else {
backupLocation = QDir(backupDir).absoluteFilePath(source.fileName());
if (QFile::exists(backupLocation)) {
QFile::remove(backupLocation);
}
ok = QFile::copy(sourcePath, backupLocation);
}
if (!ok) {
return false;
}
if (backupLocationOut) {
*backupLocationOut = backupLocation;
}
return true;
}
QString defaultBackupRoot()
{
QString documents = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation);
if (!documents.isEmpty()) {
return QDir(documents).absoluteFilePath("CockatriceBackups");
}
QString appData = QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation);
if (!appData.isEmpty()) {
return QDir(appData).absoluteFilePath("../CockatriceBackups");
}
return QDir::temp().absoluteFilePath("CockatriceBackups");
}
} // namespace UserDataBackup

View file

@ -0,0 +1,46 @@
/**
* @file user_data_backup.h
* @ingroup Utility
* @brief Best-effort safety copies of user data that lives inside the install directory.
*
* Background: Windows uninstallers shipped in 3.1.0/3.1.1 ran a blanket
* "RMDir /r $INSTDIR". For a portable install that deletes $INSTDIR\data
* (decks, settings, pictures) along with the payload, and for a normal install
* it deletes anything the user pointed into $INSTDIR. The fixed uninstaller
* preserves foreign files, but the destructive one is already on disk on
* affected machines, so the client keeps a safety copy outside the install
* directory before warning the user. Detection of at-risk paths lives in
* PathsSettings::pathsInsideDir; this helper only copies.
*/
#ifndef USER_DATA_BACKUP_H
#define USER_DATA_BACKUP_H
#include <QString>
namespace UserDataBackup
{
/**
* @brief Recursively copies @p sourceDir into @p destDir, creating it as needed.
* @returns true when every file was copied (or there was nothing to copy).
*/
bool copyDirectoryRecursively(const QString &sourceDir, const QString &destDir);
/**
* @brief Copies an at-risk path (file or directory) to a timestamped folder under @p backupRoot.
* @param sourcePath file or directory holding user data (must exist).
* @param backupRoot directory OUTSIDE the install tree that will hold timestamped backups.
* @param backupLocationOut receives the created backup file or directory on success.
* @returns true on success, false when the source is missing or nothing could be copied.
*/
bool backupAtRiskPath(const QString &sourcePath, const QString &backupRoot, QString *backupLocationOut = nullptr);
/**
* @brief Default backup root guaranteed to live outside the install directory.
*/
QString defaultBackupRoot();
} // namespace UserDataBackup
#endif // USER_DATA_BACKUP_H