mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-05 09:33:25 -08:00
[Installer][Client] Fix review findings: throttle backups, NSIS old dir, portable messaging
This commit is contained in:
parent
bb5f17b876
commit
f4e3d9acba
5 changed files with 255 additions and 49 deletions
|
|
@ -15,6 +15,7 @@ Var ReinstallMode
|
||||||
Var AllowForceClose
|
Var AllowForceClose
|
||||||
Var GuardDir
|
Var GuardDir
|
||||||
Var OldUninstallString
|
Var OldUninstallString
|
||||||
|
Var OldInstallDir
|
||||||
Var DataBackupDir
|
Var DataBackupDir
|
||||||
Var StaleFindHandle
|
Var StaleFindHandle
|
||||||
Var StaleFindName
|
Var StaleFindName
|
||||||
|
|
@ -129,6 +130,9 @@ StrCpy $InstallLogPath "$TEMP\Cockatrice-installer.log"
|
||||||
; No backup has been taken yet; RestoreUserDataAfterUninstall must be a no-op
|
; No backup has been taken yet; RestoreUserDataAfterUninstall must be a no-op
|
||||||
; until BackupUserDataBeforeUninstall sets this after actually copying something.
|
; until BackupUserDataBeforeUninstall sets this after actually copying something.
|
||||||
StrCpy $DataBackupDir ""
|
StrCpy $DataBackupDir ""
|
||||||
|
; No old install location resolved yet; Backup/Restore fall back to $INSTDIR
|
||||||
|
; when this is empty (the /R path where $INSTDIR already is the old dir).
|
||||||
|
StrCpy $OldInstallDir ""
|
||||||
; Start each run from a clean file so the log that gets sent back is the last
|
; Start each run from a clean file so the log that gets sent back is the last
|
||||||
; run, not every past run glued together.
|
; run, not every past run glued together.
|
||||||
FileOpen $LogHandle "$InstallLogPath" w
|
FileOpen $LogHandle "$InstallLogPath" w
|
||||||
|
|
@ -318,11 +322,14 @@ SetShellVarContext all
|
||||||
SetRegView 32
|
SetRegView 32
|
||||||
ReadRegStr $R0 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" "QuietUninstallString"
|
ReadRegStr $R0 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" "QuietUninstallString"
|
||||||
|
|
||||||
!insertmacro CockLog "uninstall (32-bit): '$R0 _?=$INSTDIR'"
|
; On the /R path the updater passes /D=<appDir>, so $INSTDIR already is the old
|
||||||
|
; install dir. Publish it for the backup/restore pair below.
|
||||||
|
StrCpy $OldInstallDir $INSTDIR
|
||||||
|
!insertmacro CockLog "uninstall (32-bit): '$R0 _?=$OldInstallDir'"
|
||||||
StrCmp $R0 "" done32
|
StrCmp $R0 "" done32
|
||||||
DetailPrint "Removing previous version (32-bit)..."
|
DetailPrint "Removing previous version (32-bit)..."
|
||||||
Call BackupUserDataBeforeUninstall
|
Call BackupUserDataBeforeUninstall
|
||||||
ExecWait '$R0 _?=$INSTDIR'
|
ExecWait '$R0 _?=$OldInstallDir'
|
||||||
Call RestoreUserDataAfterUninstall
|
Call RestoreUserDataAfterUninstall
|
||||||
|
|
||||||
done32:
|
done32:
|
||||||
|
|
@ -332,11 +339,12 @@ ${If} ${RunningX64}
|
||||||
SetRegView 64
|
SetRegView 64
|
||||||
ReadRegStr $R0 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" "QuietUninstallString"
|
ReadRegStr $R0 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" "QuietUninstallString"
|
||||||
|
|
||||||
!insertmacro CockLog "uninstall (64-bit): '$R0 _?=$INSTDIR'"
|
StrCpy $OldInstallDir $INSTDIR
|
||||||
|
!insertmacro CockLog "uninstall (64-bit): '$R0 _?=$OldInstallDir'"
|
||||||
StrCmp $R0 "" done64
|
StrCmp $R0 "" done64
|
||||||
DetailPrint "Removing previous version (64-bit)..."
|
DetailPrint "Removing previous version (64-bit)..."
|
||||||
Call BackupUserDataBeforeUninstall
|
Call BackupUserDataBeforeUninstall
|
||||||
ExecWait '$R0 _?=$INSTDIR'
|
ExecWait '$R0 _?=$OldInstallDir'
|
||||||
Call RestoreUserDataAfterUninstall
|
Call RestoreUserDataAfterUninstall
|
||||||
|
|
||||||
done64:
|
done64:
|
||||||
|
|
@ -358,59 +366,72 @@ FunctionEnd
|
||||||
; Non-portable data paths beyond loose decks are covered by the client's
|
; Non-portable data paths beyond loose decks are covered by the client's
|
||||||
; startup safety backup (warnAboutDataPathsInProgramDir).
|
; startup safety backup (warnAboutDataPathsInProgramDir).
|
||||||
Function BackupUserDataBeforeUninstall
|
Function BackupUserDataBeforeUninstall
|
||||||
; Always start from a clean slate: %TEMP% survives across installer runs, and a
|
; $OldInstallDir is the directory being uninstalled: on the /R path it equals
|
||||||
; leftover backup must never be mistaken for this run's - a fresh install would
|
; $INSTDIR (updater passes /D=<appDir>), on the interactive path componentsPagePre
|
||||||
; otherwise resurrect ancient decks via RestoreUserDataAfterUninstall.
|
; sets it from the registry InstallLocation via $GuardDir, because $INSTDIR is
|
||||||
RMDir /r "$TEMP\CockatriceDataBackup"
|
; still the default there (directory page comes later).
|
||||||
StrCpy $DataBackupDir ""
|
${If} $OldInstallDir == ""
|
||||||
${If} ${FileExists} "$INSTDIR\portable.dat"
|
StrCpy $OldInstallDir $INSTDIR
|
||||||
${AndIf} ${FileExists} "$INSTDIR\data"
|
${EndIf}
|
||||||
|
; Start from a clean slate only on the first backup of this run: %TEMP% survives
|
||||||
|
; across installer runs, so a stale backup must never be mistaken for this run's -
|
||||||
|
; but this function runs twice in sequence (32-bit then 64-bit), and an
|
||||||
|
; unconditional RMDir here would delete the first call's safety copy before the
|
||||||
|
; second uninstall runs.
|
||||||
|
${If} $DataBackupDir == ""
|
||||||
|
RMDir /r "$TEMP\CockatriceDataBackup"
|
||||||
|
${EndIf}
|
||||||
|
${If} ${FileExists} "$OldInstallDir\portable.dat"
|
||||||
|
${AndIf} ${FileExists} "$OldInstallDir\data"
|
||||||
StrCpy $DataBackupDir "$TEMP\CockatriceDataBackup"
|
StrCpy $DataBackupDir "$TEMP\CockatriceDataBackup"
|
||||||
!insertmacro CockLog "pre-uninstall backup: copying $INSTDIR\data (downloaded pics excluded)"
|
!insertmacro CockLog "pre-uninstall backup: copying $OldInstallDir\data (downloaded pics excluded)"
|
||||||
DetailPrint "Backing up portable user data before removing previous version..."
|
DetailPrint "Backing up portable user data before removing previous version..."
|
||||||
CreateDirectory "$DataBackupDir\data"
|
CreateDirectory "$DataBackupDir\data"
|
||||||
CopyFiles /SILENT "$INSTDIR\data\*.*" "$DataBackupDir\data\"
|
CopyFiles /SILENT "$OldInstallDir\data\*.*" "$DataBackupDir\data\"
|
||||||
; Downloaded pictures are large and re-downloadable, so drop them from the
|
; Downloaded pictures are large and re-downloadable, so drop them from the
|
||||||
; safety backup - but keep user-made custom pics (data\pics\CUSTOM).
|
; safety backup - but keep user-made custom pics (data\pics\CUSTOM).
|
||||||
RMDir /r "$DataBackupDir\data\pics"
|
RMDir /r "$DataBackupDir\data\pics"
|
||||||
${If} ${FileExists} "$INSTDIR\data\pics\CUSTOM"
|
${If} ${FileExists} "$OldInstallDir\data\pics\CUSTOM"
|
||||||
CreateDirectory "$DataBackupDir\data\pics\CUSTOM"
|
CreateDirectory "$DataBackupDir\data\pics\CUSTOM"
|
||||||
CopyFiles /SILENT "$INSTDIR\data\pics\CUSTOM\*.*" "$DataBackupDir\data\pics\CUSTOM\"
|
CopyFiles /SILENT "$OldInstallDir\data\pics\CUSTOM\*.*" "$DataBackupDir\data\pics\CUSTOM\"
|
||||||
${EndIf}
|
${EndIf}
|
||||||
DetailPrint "Portable user data backup complete."
|
DetailPrint "Portable user data backup complete."
|
||||||
${EndIf}
|
${EndIf}
|
||||||
${If} ${FileExists} "$INSTDIR\*.cod"
|
${If} ${FileExists} "$OldInstallDir\*.cod"
|
||||||
${OrIf} ${FileExists} "$INSTDIR\*.dec"
|
${OrIf} ${FileExists} "$OldInstallDir\*.dec"
|
||||||
${If} $DataBackupDir == ""
|
${If} $DataBackupDir == ""
|
||||||
StrCpy $DataBackupDir "$TEMP\CockatriceDataBackup"
|
StrCpy $DataBackupDir "$TEMP\CockatriceDataBackup"
|
||||||
${EndIf}
|
${EndIf}
|
||||||
!insertmacro CockLog "pre-uninstall backup: copying loose deck files"
|
!insertmacro CockLog "pre-uninstall backup: copying loose deck files"
|
||||||
DetailPrint "Backing up deck files stored in the install directory..."
|
DetailPrint "Backing up deck files stored in the install directory..."
|
||||||
CreateDirectory "$DataBackupDir\loose_decks"
|
CreateDirectory "$DataBackupDir\loose_decks"
|
||||||
CopyFiles /SILENT "$INSTDIR\*.cod" "$DataBackupDir\loose_decks\"
|
CopyFiles /SILENT "$OldInstallDir\*.cod" "$DataBackupDir\loose_decks\"
|
||||||
CopyFiles /SILENT "$INSTDIR\*.dec" "$DataBackupDir\loose_decks\"
|
CopyFiles /SILENT "$OldInstallDir\*.dec" "$DataBackupDir\loose_decks\"
|
||||||
${EndIf}
|
${EndIf}
|
||||||
FunctionEnd
|
FunctionEnd
|
||||||
|
|
||||||
Function RestoreUserDataAfterUninstall
|
Function RestoreUserDataAfterUninstall
|
||||||
StrCmp $DataBackupDir "" done
|
StrCmp $DataBackupDir "" done
|
||||||
|
${If} $OldInstallDir == ""
|
||||||
|
StrCpy $OldInstallDir $INSTDIR
|
||||||
|
${EndIf}
|
||||||
${If} ${FileExists} "$DataBackupDir\data"
|
${If} ${FileExists} "$DataBackupDir\data"
|
||||||
${If} ${FileExists} "$INSTDIR\data"
|
${If} ${FileExists} "$OldInstallDir\data"
|
||||||
!insertmacro CockLog "post-uninstall restore: user data survived, backup kept"
|
!insertmacro CockLog "post-uninstall restore: user data survived, backup kept"
|
||||||
DetailPrint "Previous user data survived the uninstall; backup kept at $DataBackupDir\data."
|
DetailPrint "Previous user data survived the uninstall; backup kept at $DataBackupDir\data."
|
||||||
${Else}
|
${Else}
|
||||||
!insertmacro CockLog "post-uninstall restore: restoring $INSTDIR\data"
|
!insertmacro CockLog "post-uninstall restore: restoring $OldInstallDir\data"
|
||||||
CreateDirectory "$INSTDIR\data"
|
CreateDirectory "$OldInstallDir\data"
|
||||||
CopyFiles /SILENT "$DataBackupDir\data\*.*" "$INSTDIR\data\"
|
CopyFiles /SILENT "$DataBackupDir\data\*.*" "$OldInstallDir\data\"
|
||||||
DetailPrint "Restored portable user data from pre-uninstall backup."
|
DetailPrint "Restored portable user data from pre-uninstall backup."
|
||||||
${EndIf}
|
${EndIf}
|
||||||
${EndIf}
|
${EndIf}
|
||||||
${If} ${FileExists} "$DataBackupDir\loose_decks\*.cod"
|
${If} ${FileExists} "$DataBackupDir\loose_decks\*.cod"
|
||||||
${OrIf} ${FileExists} "$DataBackupDir\loose_decks\*.dec"
|
${OrIf} ${FileExists} "$DataBackupDir\loose_decks\*.dec"
|
||||||
!insertmacro CockLog "post-uninstall restore: restoring loose deck files"
|
!insertmacro CockLog "post-uninstall restore: restoring loose deck files"
|
||||||
CreateDirectory "$INSTDIR"
|
CreateDirectory "$OldInstallDir"
|
||||||
CopyFiles /SILENT "$DataBackupDir\loose_decks\*.cod" "$INSTDIR\"
|
CopyFiles /SILENT "$DataBackupDir\loose_decks\*.cod" "$OldInstallDir\"
|
||||||
CopyFiles /SILENT "$DataBackupDir\loose_decks\*.dec" "$INSTDIR\"
|
CopyFiles /SILENT "$DataBackupDir\loose_decks\*.dec" "$OldInstallDir\"
|
||||||
DetailPrint "Restored deck files stored in the install directory."
|
DetailPrint "Restored deck files stored in the install directory."
|
||||||
${IfNot} ${Silent}
|
${IfNot} ${Silent}
|
||||||
MessageBox MB_OK|MB_ICONINFORMATION "Your decks were stored inside the Cockatrice install folder, which the previous version's uninstaller deletes.$\n$\nThey have been restored from a backup. Please move them out of the install folder (Settings -> General -> Paths) so a future uninstall cannot delete them again."
|
MessageBox MB_OK|MB_ICONINFORMATION "Your decks were stored inside the Cockatrice install folder, which the previous version's uninstaller deletes.$\n$\nThey have been restored from a backup. Please move them out of the install folder (Settings -> General -> Paths) so a future uninstall cannot delete them again."
|
||||||
|
|
@ -1120,12 +1141,16 @@ ${If} $PortableMode = 0
|
||||||
; instance of the application is still running out of the *old* install
|
; instance of the application is still running out of the *old* install
|
||||||
; directory before letting it delete anything. The uninstall string is
|
; directory before letting it delete anything. The uninstall string is
|
||||||
; stashed away first, the check works on $R0.
|
; stashed away first, the check works on $R0.
|
||||||
|
; $INSTDIR is still the default here (directory page comes later), so the
|
||||||
|
; backup/restore dir comes from the registry InstallLocation via $GuardDir,
|
||||||
|
; not from $INSTDIR - otherwise a custom-location install is unprotected.
|
||||||
StrCpy $OldUninstallString $R0
|
StrCpy $OldUninstallString $R0
|
||||||
!insertmacro CockLog "componentsPagePre: old 32-bit uninstall '$OldUninstallString _?=$INSTDIR'"
|
|
||||||
Call EnsureOldInstallNotRunning
|
Call EnsureOldInstallNotRunning
|
||||||
|
StrCpy $OldInstallDir $GuardDir
|
||||||
|
!insertmacro CockLog "componentsPagePre: old 32-bit uninstall '$OldUninstallString _?=$OldInstallDir' (oldDir=$OldInstallDir InstDir=$INSTDIR)"
|
||||||
ClearErrors
|
ClearErrors
|
||||||
Call BackupUserDataBeforeUninstall
|
Call BackupUserDataBeforeUninstall
|
||||||
ExecWait "$OldUninstallString _?=$INSTDIR"
|
ExecWait "$OldUninstallString _?=$OldInstallDir"
|
||||||
Call RestoreUserDataAfterUninstall
|
Call RestoreUserDataAfterUninstall
|
||||||
|
|
||||||
done32:
|
done32:
|
||||||
|
|
@ -1146,11 +1171,12 @@ ${If} $PortableMode = 0
|
||||||
|
|
||||||
uninst64:
|
uninst64:
|
||||||
StrCpy $OldUninstallString $R0
|
StrCpy $OldUninstallString $R0
|
||||||
!insertmacro CockLog "componentsPagePre: old 64-bit uninstall '$OldUninstallString _?=$INSTDIR'"
|
|
||||||
Call EnsureOldInstallNotRunning
|
Call EnsureOldInstallNotRunning
|
||||||
|
StrCpy $OldInstallDir $GuardDir
|
||||||
|
!insertmacro CockLog "componentsPagePre: old 64-bit uninstall '$OldUninstallString _?=$OldInstallDir' (oldDir=$OldInstallDir InstDir=$INSTDIR)"
|
||||||
ClearErrors
|
ClearErrors
|
||||||
Call BackupUserDataBeforeUninstall
|
Call BackupUserDataBeforeUninstall
|
||||||
ExecWait "$OldUninstallString _?=$INSTDIR"
|
ExecWait "$OldUninstallString _?=$OldInstallDir"
|
||||||
Call RestoreUserDataAfterUninstall
|
Call RestoreUserDataAfterUninstall
|
||||||
|
|
||||||
done64:
|
done64:
|
||||||
|
|
|
||||||
|
|
@ -627,22 +627,34 @@ void MainWindow::warnAboutDataPathsInProgramDir()
|
||||||
if (isPortable) {
|
if (isPortable) {
|
||||||
// Portable data lives inside the install directory by design, so there is
|
// Portable data lives inside the install directory by design, so there is
|
||||||
// nothing to reconfigure. Back up the small irreplaceable data instead.
|
// nothing to reconfigure. Back up the small irreplaceable data instead.
|
||||||
// Pictures are deliberately excluded: they are large and can always be
|
// Downloaded pictures are deliberately excluded: they are large and can
|
||||||
// re-downloaded (the installer's pre-uninstall backup keeps custom pics
|
// always be re-downloaded (the installer's pre-uninstall backup drops
|
||||||
// but drops downloaded ones for the same reason). Replays are excluded
|
// them for the same reason). Replays are excluded here for size; the
|
||||||
// here for size; the installer backup covers them.
|
// installer backup covers them during installer-driven upgrades, but a
|
||||||
|
// manual run of the old uninstall.exe is not covered - hence the note
|
||||||
|
// in the warning text below.
|
||||||
const QString dataDir = SettingsCache::instance().getDataPath();
|
const QString dataDir = SettingsCache::instance().getDataPath();
|
||||||
for (const QString &sub :
|
for (const QString &sub :
|
||||||
QStringList{QStringLiteral("decks"), QStringLiteral("customsets"), QStringLiteral("settings"),
|
QStringList{QStringLiteral("decks"), QStringLiteral("customsets"), QStringLiteral("settings"),
|
||||||
QStringLiteral("filters"), QStringLiteral("themes")}) {
|
QStringLiteral("filters"), QStringLiteral("themes"), QStringLiteral("pics/CUSTOM")}) {
|
||||||
const QString candidate = QDir(dataDir).absoluteFilePath(sub);
|
const QString candidate = QDir(dataDir).absoluteFilePath(sub);
|
||||||
if (QFileInfo::exists(candidate)) {
|
if (QFileInfo::exists(candidate)) {
|
||||||
backupSources.append(candidate);
|
backupSources.append(candidate);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// A portable user may also have pointed a custom path inside the install
|
||||||
|
// dir but outside data/ (e.g. appDir/mydecks). Those offenders would be
|
||||||
|
// wiped by the old uninstaller but are not in the hardcoded list above.
|
||||||
|
offenders = paths.pathsInsideDir(appDir);
|
||||||
|
for (const QString &offender : offenders) {
|
||||||
|
if (!PathsSettings::isInsideDir(offender, dataDir) && !backupSources.contains(offender)) {
|
||||||
|
backupSources.append(offender);
|
||||||
|
}
|
||||||
|
}
|
||||||
if (backupSources.isEmpty()) {
|
if (backupSources.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
offenders.clear();
|
||||||
} else {
|
} else {
|
||||||
offenders = paths.pathsInsideDir(appDir);
|
offenders = paths.pathsInsideDir(appDir);
|
||||||
if (offenders.isEmpty()) {
|
if (offenders.isEmpty()) {
|
||||||
|
|
@ -655,13 +667,26 @@ void MainWindow::warnAboutDataPathsInProgramDir()
|
||||||
if (PathsSettings::isInsideDir(backupRoot, appDir)) {
|
if (PathsSettings::isInsideDir(backupRoot, appDir)) {
|
||||||
backupRoot = QDir::temp().absoluteFilePath(QStringLiteral("CockatriceBackups"));
|
backupRoot = QDir::temp().absoluteFilePath(QStringLiteral("CockatriceBackups"));
|
||||||
}
|
}
|
||||||
// One timestamped session directory for all sources, so a second boundary
|
// Throttle: dismissing the dialog without "Don't show again" must not create
|
||||||
// never splits a single startup check across two backup folders and
|
// a full duplicate on every launch. Reuse a recent session instead, and keep
|
||||||
// same-named sources cannot overwrite each other.
|
// only a handful of sessions so Documents cannot grow without bound.
|
||||||
QStringList backupLocations;
|
QStringList backupLocations;
|
||||||
QString sessionDir;
|
QString sessionDir;
|
||||||
const bool backupComplete =
|
bool backupComplete = true;
|
||||||
UserDataBackup::backupAtRiskPaths(backupSources, backupRoot, &backupLocations, &sessionDir);
|
bool backupReused = false;
|
||||||
|
const QString recentSession = UserDataBackup::findRecentBackupSession(backupRoot, 24);
|
||||||
|
if (!recentSession.isEmpty()) {
|
||||||
|
sessionDir = recentSession;
|
||||||
|
backupLocations = UserDataBackup::listSessionBackupLocations(sessionDir);
|
||||||
|
backupReused = true;
|
||||||
|
qCInfo(MainWindowLog) << "Reusing recent at-risk user data backup at" << sessionDir;
|
||||||
|
} else {
|
||||||
|
// One timestamped session directory for all sources, so a second boundary
|
||||||
|
// never splits a single startup check across two backup folders and
|
||||||
|
// same-named sources cannot overwrite each other.
|
||||||
|
backupComplete = UserDataBackup::backupAtRiskPaths(backupSources, backupRoot, &backupLocations, &sessionDir);
|
||||||
|
UserDataBackup::pruneOldBackupSessions(backupRoot, 5);
|
||||||
|
}
|
||||||
for (const QString &backupLocation : backupLocations) {
|
for (const QString &backupLocation : backupLocations) {
|
||||||
qCInfo(MainWindowLog) << "Backed up at-risk user data to" << backupLocation;
|
qCInfo(MainWindowLog) << "Backed up at-risk user data to" << backupLocation;
|
||||||
}
|
}
|
||||||
|
|
@ -669,15 +694,28 @@ void MainWindow::warnAboutDataPathsInProgramDir()
|
||||||
qCWarning(MainWindowLog) << "At-risk user data backup incomplete, session dir:" << sessionDir;
|
qCWarning(MainWindowLog) << "At-risk user data backup incomplete, session dir:" << sessionDir;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const QString backupVerb = backupReused ? tr("is already available") : tr("has been created");
|
||||||
QString message;
|
QString message;
|
||||||
if (isPortable) {
|
if (isPortable) {
|
||||||
message = tr("This portable install keeps its data inside the program directory, where the "
|
if (!backupLocations.isEmpty()) {
|
||||||
"uninstallers shipped in Cockatrice 3.1.0/3.1.1 delete everything on uninstall or "
|
message = tr("This portable install keeps its data inside the program directory, where the "
|
||||||
"update.\n\nA safety backup of your decks, custom sets, settings, filters and themes has "
|
"uninstallers shipped in Cockatrice 3.1.0/3.1.1 delete everything on uninstall or "
|
||||||
"been created "
|
"update.\n\nA safety backup of your decks, custom sets, settings, filters, themes and custom "
|
||||||
"at:\n%1\n\nUninstalling with the old uninstaller still deletes the program "
|
"pictures %1 "
|
||||||
"directory, so keep your own copy of that backup until you have updated once.")
|
"at:\n%2\n\nReplays and downloaded pictures are not included in this backup. Uninstalling "
|
||||||
.arg(backupLocations.isEmpty() ? backupRoot : backupLocations.join(QLatin1Char('\n')));
|
"with the old uninstaller still deletes the program "
|
||||||
|
"directory, so keep your own copy of that backup until you have updated once.")
|
||||||
|
.arg(backupVerb, backupLocations.join(QLatin1Char('\n')));
|
||||||
|
if (!backupComplete) {
|
||||||
|
message += tr("\n\nThe backup may be incomplete; please back up these folders manually as well.");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
message = tr("This portable install keeps its data inside the program directory, where the "
|
||||||
|
"uninstallers shipped in Cockatrice 3.1.0/3.1.1 delete everything on uninstall or "
|
||||||
|
"update.\n\nThe automatic safety backup failed; please back up your data folder manually "
|
||||||
|
"before uninstalling or updating.\n\nReplays and downloaded pictures would not have been "
|
||||||
|
"included in that backup either.");
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
message = tr("These paths are configured inside the Cockatrice program directory:\n\n%1\n\n"
|
message = tr("These paths are configured inside the Cockatrice program directory:\n\n%1\n\n"
|
||||||
"Cockatrice keeps its own data outside the program directory by default. Files "
|
"Cockatrice keeps its own data outside the program directory by default. Files "
|
||||||
|
|
@ -686,7 +724,7 @@ void MainWindow::warnAboutDataPathsInProgramDir()
|
||||||
"them safe.")
|
"them safe.")
|
||||||
.arg(offenders.join(QLatin1Char('\n')));
|
.arg(offenders.join(QLatin1Char('\n')));
|
||||||
if (!backupLocations.isEmpty()) {
|
if (!backupLocations.isEmpty()) {
|
||||||
message += tr("\n\nA safety backup has been created at:\n%1").arg(backupLocations.join(QLatin1Char('\n')));
|
message += tr("\n\nA safety backup %1 at:\n%2").arg(backupVerb, backupLocations.join(QLatin1Char('\n')));
|
||||||
if (!backupComplete) {
|
if (!backupComplete) {
|
||||||
message += tr("\n\nThe backup may be incomplete; please back up these folders manually as well.");
|
message += tr("\n\nThe backup may be incomplete; please back up these folders manually as well.");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -160,9 +160,71 @@ QString defaultBackupRoot()
|
||||||
}
|
}
|
||||||
QString appData = QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation);
|
QString appData = QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation);
|
||||||
if (!appData.isEmpty()) {
|
if (!appData.isEmpty()) {
|
||||||
return QDir(appData).absoluteFilePath("../CockatriceBackups");
|
return QDir::cleanPath(QDir(appData).absoluteFilePath("../CockatriceBackups"));
|
||||||
}
|
}
|
||||||
return QDir::temp().absoluteFilePath("CockatriceBackups");
|
return QDir::temp().absoluteFilePath("CockatriceBackups");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QStringList listBackupSessions(const QString &backupRoot)
|
||||||
|
{
|
||||||
|
QDir root(backupRoot);
|
||||||
|
if (!root.exists()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
const QStringList names = root.entryList(QStringList() << QStringLiteral("CockatriceDataBackup-*"),
|
||||||
|
QDir::Dirs | QDir::NoDotAndDotDot, QDir::Name | QDir::Reversed);
|
||||||
|
QStringList sessions;
|
||||||
|
sessions.reserve(names.size());
|
||||||
|
for (const QString &name : names) {
|
||||||
|
sessions.append(root.absoluteFilePath(name));
|
||||||
|
}
|
||||||
|
return sessions;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString findRecentBackupSession(const QString &backupRoot, int maxAgeHours)
|
||||||
|
{
|
||||||
|
const QStringList sessions = listBackupSessions(backupRoot);
|
||||||
|
if (sessions.isEmpty()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
const QDateTime now = QDateTime::currentDateTime();
|
||||||
|
for (const QString &session : sessions) {
|
||||||
|
const QFileInfo info(session);
|
||||||
|
// Newest first; the first recent entry is the reuse candidate. Entries
|
||||||
|
// are timestamped, so an old newest entry means no recent backup exists.
|
||||||
|
if (info.exists() && info.lastModified().secsTo(now) < static_cast<qint64>(maxAgeHours) * 3600) {
|
||||||
|
return session;
|
||||||
|
}
|
||||||
|
// Sessions are newest-first, so stop after the newest: older ones are older.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
void pruneOldBackupSessions(const QString &backupRoot, int keepCount)
|
||||||
|
{
|
||||||
|
const QStringList sessions = listBackupSessions(backupRoot);
|
||||||
|
if (sessions.size() <= keepCount) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (int i = keepCount; i < sessions.size(); ++i) {
|
||||||
|
QDir(sessions.at(i)).removeRecursively();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QStringList listSessionBackupLocations(const QString &sessionDir)
|
||||||
|
{
|
||||||
|
const QDir session(sessionDir);
|
||||||
|
if (!session.exists()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
const QStringList entries = session.entryList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot | QDir::Hidden);
|
||||||
|
QStringList locations;
|
||||||
|
locations.reserve(entries.size());
|
||||||
|
for (const QString &entry : entries) {
|
||||||
|
locations.append(session.absoluteFilePath(entry));
|
||||||
|
}
|
||||||
|
return locations;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace UserDataBackup
|
} // namespace UserDataBackup
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,30 @@ bool backupAtRiskPaths(const QStringList &sourcePaths,
|
||||||
*/
|
*/
|
||||||
QString defaultBackupRoot();
|
QString defaultBackupRoot();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Lists existing backup session directories under @p backupRoot, newest first.
|
||||||
|
*/
|
||||||
|
QStringList listBackupSessions(const QString &backupRoot);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Returns the newest session directory younger than @p maxAgeHours, or empty.
|
||||||
|
*
|
||||||
|
* Used to throttle startup safety backups so dismissing the warning without
|
||||||
|
* "Don't show again" does not create a full duplicate on every launch.
|
||||||
|
*/
|
||||||
|
QString findRecentBackupSession(const QString &backupRoot, int maxAgeHours = 24);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Deletes oldest sessions keeping only the newest @p keepCount ones.
|
||||||
|
*/
|
||||||
|
void pruneOldBackupSessions(const QString &backupRoot, int keepCount = 5);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Lists the backup targets inside an existing session directory.
|
||||||
|
* @returns absolute paths of the entries, for reuse messages without a new copy.
|
||||||
|
*/
|
||||||
|
QStringList listSessionBackupLocations(const QString &sessionDir);
|
||||||
|
|
||||||
} // namespace UserDataBackup
|
} // namespace UserDataBackup
|
||||||
|
|
||||||
#endif // USER_DATA_BACKUP_H
|
#endif // USER_DATA_BACKUP_H
|
||||||
|
|
|
||||||
|
|
@ -212,6 +212,62 @@ TEST(UserDataBackup, DefaultBackupRootIsUsable)
|
||||||
EXPECT_TRUE(QDir(root).isAbsolute());
|
EXPECT_TRUE(QDir(root).isAbsolute());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(UserDataBackup, DefaultBackupRootHasNoDotDotSegments)
|
||||||
|
{
|
||||||
|
QString root = UserDataBackup::defaultBackupRoot();
|
||||||
|
EXPECT_FALSE(root.contains(QStringLiteral("..")));
|
||||||
|
EXPECT_EQ(root, QDir::cleanPath(root));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(UserDataBackup, ListSessionsFindsRecentAndPrunesOld)
|
||||||
|
{
|
||||||
|
QTemporaryDir sourceDir;
|
||||||
|
QTemporaryDir backupRoot;
|
||||||
|
ASSERT_TRUE(sourceDir.isValid());
|
||||||
|
ASSERT_TRUE(backupRoot.isValid());
|
||||||
|
QFile deck(QDir(sourceDir.path()).absoluteFilePath("mydeck.cod"));
|
||||||
|
ASSERT_TRUE(deck.open(QIODevice::WriteOnly | QIODevice::Text));
|
||||||
|
deck.write("<cockatrice_deck version=\"1\"/>\n");
|
||||||
|
deck.close();
|
||||||
|
|
||||||
|
QStringList locations;
|
||||||
|
QString firstSession;
|
||||||
|
QString secondSession;
|
||||||
|
ASSERT_TRUE(UserDataBackup::backupAtRiskPaths({sourceDir.path()}, backupRoot.path(), &locations, &firstSession));
|
||||||
|
ASSERT_TRUE(UserDataBackup::backupAtRiskPaths({sourceDir.path()}, backupRoot.path(), &locations, &secondSession));
|
||||||
|
|
||||||
|
const QStringList sessions = UserDataBackup::listBackupSessions(backupRoot.path());
|
||||||
|
ASSERT_EQ(sessions.size(), 2);
|
||||||
|
// Newest first
|
||||||
|
EXPECT_EQ(sessions.first(), secondSession);
|
||||||
|
|
||||||
|
const QString recent = UserDataBackup::findRecentBackupSession(backupRoot.path(), 24);
|
||||||
|
EXPECT_EQ(recent, secondSession);
|
||||||
|
|
||||||
|
const QStringList reusedLocations = UserDataBackup::listSessionBackupLocations(recent);
|
||||||
|
EXPECT_FALSE(reusedLocations.isEmpty());
|
||||||
|
for (const QString &location : reusedLocations) {
|
||||||
|
EXPECT_TRUE(location.startsWith(QDir(recent).absolutePath()));
|
||||||
|
EXPECT_TRUE(QFile::exists(location));
|
||||||
|
}
|
||||||
|
|
||||||
|
UserDataBackup::pruneOldBackupSessions(backupRoot.path(), 1);
|
||||||
|
const QStringList pruned = UserDataBackup::listBackupSessions(backupRoot.path());
|
||||||
|
ASSERT_EQ(pruned.size(), 1);
|
||||||
|
EXPECT_EQ(pruned.first(), secondSession);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(UserDataBackup, FindRecentReturnsEmptyWhenNone)
|
||||||
|
{
|
||||||
|
QTemporaryDir backupRoot;
|
||||||
|
ASSERT_TRUE(backupRoot.isValid());
|
||||||
|
EXPECT_TRUE(UserDataBackup::listBackupSessions(backupRoot.path()).isEmpty());
|
||||||
|
EXPECT_TRUE(UserDataBackup::findRecentBackupSession(backupRoot.path(), 24).isEmpty());
|
||||||
|
EXPECT_TRUE(UserDataBackup::listSessionBackupLocations(backupRoot.path() + "/missing").isEmpty());
|
||||||
|
// Pruning an empty root must not fail
|
||||||
|
UserDataBackup::pruneOldBackupSessions(backupRoot.path(), 5);
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
int main(int argc, char **argv)
|
int main(int argc, char **argv)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue