Cockatrice/libcockatrice_utility/libcockatrice/utility/user_data_backup.h
BruebachL 6c660d6b21
[Installer][Client] Back up user data before old uninstallers can delete it (#7503)
* [Installer][Client] Back up user data before old uninstallers can delete it

* [Installer][Client] Fix stale backup resurrection, batch session backups, extend coverage

NSIS: clear stale %TEMP% backup and only arm restore when something was copied, directory-existence checks, back up loose .dec decks, drop downloaded pics but keep pics\CUSTOM.

Helper: single session dir per run (ms timestamps), same-basename disambiguation, partial copies still report their location.

* [Installer][Client] Fix review findings: throttle backups, NSIS old dir, portable messaging

* [Installer][Client] Fix review findings: backup markers, NSIS relocate, size cap

* [Installer][Client] Fix review findings: sounds, session coverage, NSIS wording

Client: back up data/sounds in portable mode and mention it in the
warning; reuse a recent session only when complete and covering the
current sources (source manifest), estimate size only for fresh copies,
normalize dedup via isInsideDir.

Helper: source manifest per session with sessionCoversSources(), clear
out-params on all failure paths.

NSIS: document defensive portable branch, merge backup over surviving
data/ to repair partial deletes, copy (not move) wording in relocate,
note .txt exclusion in deck notice.

* [Installer][Client] Fix review findings: stale backup, empty reuse, relocate, portable warning

NSIS: wipe TEMP backup once per run to avoid resurrecting deleted files,
check data dir existence in relocate so dir-only trees are moved.

Client: never reuse a recent session for an empty source set, only fall
back to a covering session on copy failure, warn fresh portable installs
instead of staying silent.

* [Installer][Client] Fix review findings: usable session reuse, loose decks, NSIS pics, symlinks

* [Installer][Client] Fix portable backup exclusions in offender merge

* [Client] Split at-risk data warning into focused helpers

Ricky's nit on the 200+ line warnAboutDataPathsInProgramDir: extract
collection (portable/installed), backup, message building and dialog
into named helpers. No behavior change, all tr() strings untouched.

* [Client] Read path settings inside isPortableBackupExcluded

* [Client] Make stateless at-risk helpers static

* [Client] Extract at-risk data warning out of MainWindow

* [Installer][Client] Fix review findings: subdir backup, portable warning, backup API

NSIS:
- Back up unknown top-level install subdirs (e.g. <installdir>\decks),
  not just loose deck files, so the 3.1.1 -> new upgrade no longer loses
  data folders the old RMDir /r deletes before the client ever runs.
  Payload dirs (plugins/translations/sounds/themes) stay in place to avoid
  restoring stale DLLs; restore + relocate handle the new backup set and
  the one-time notice covers both decks and folders.
- Restart the %TEMP% backup when the 32/64-bit passes point at different
  dirs instead of merging (which restored one dir's files into the other);
  document that only the last pass relocates and the %TEMP% copy is not
  a durable second copy.

Client:
- Drop the portable at-risk warning: portable installs never shipped an
  uninstaller and portable updates preserve data/, so every portable user
  got a spurious dialog plus daily host-machine copies.
- AtRiskDataWarning is now a plain class with Q_DECLARE_TR_FUNCTIONS
  instead of QObject+Q_OBJECT; logging category renamed to
  at_risk_data_warning; MainWindow wrapper inlined into startupConfigCheck.
- Lower the synchronous startup backup cap 500 -> 200 MB.

Utility/tests:
- Remove test-only backupAtRiskPath()/findRecentBackupSession() and the
  no-marker/no-manifest legacy paths; isBackupSessionComplete() now
  requires the complete marker; tests updated to the plural API.

* [Client] Restore collectInstalledAtRiskPaths per review

a23e02b5 removed the definition along with the portable helpers,
breaking the build (undefined reference at link time, and
isInsideAnyDir unused under -Werror on Arch/Debian 13). Restores
the 202c0d65 version (pathsInsideDir plus loose top-level deck
files) as suggested.

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-10-16 22:05:42 +02:00

132 lines
5.9 KiB
C++

/**
* @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>
#include <QStringList>
namespace UserDataBackup
{
/** @brief Above this total, the client skips the automatic startup backup and asks for a manual copy.
* Kept small on purpose: the copy runs synchronously on the UI thread at startup. */
constexpr qint64 MAX_AUTO_BACKUP_BYTES = 200LL * 1024 * 1024;
/**
* @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).
* Symlinks (file or directory) are copied best-effort via QFile::copy without
* dereferencing them; a dangling link fails that entry only.
*/
bool copyDirectoryRecursively(const QString &sourceDir, const QString &destDir);
/**
* @brief Creates a fresh timestamped session directory under @p backupRoot.
* @param backupRoot directory OUTSIDE the install tree that will hold timestamped backups.
* @param sessionDirOut receives the created directory on success.
* @returns true on success. Millisecond timestamps plus a numeric suffix keep rapid
* successive calls (e.g. one per installer phase) from sharing a directory.
*/
bool createBackupSessionDir(const QString &backupRoot, QString *sessionDirOut);
/**
* @brief Copies several at-risk paths into a single timestamped session directory.
* @param sourcePaths files or directories holding user data; missing entries are skipped.
* @param backupRoot directory OUTSIDE the install tree that will hold timestamped backups.
* @param backupLocationsOut receives one entry per source that produced a backup target
* (including partial copies).
* @param sessionDirOut optionally receives the shared session directory; set to empty
* when nothing was backed up (all sources missing, or every copy failed outright).
* @returns true when every existing source was copied completely.
* A false return with a non-empty @p backupLocationsOut means a partial backup exists.
* When nothing at all could be salvaged the session directory is removed again, so a
* failed attempt never leaves an empty session behind that would block retries.
* Completeness is recorded with a marker file; see isBackupSessionComplete().
*/
bool backupAtRiskPaths(const QStringList &sourcePaths,
const QString &backupRoot,
QStringList *backupLocationsOut,
QString *sessionDirOut = nullptr);
/**
* @brief Estimates the total bytes that backing up @p sourcePaths would copy.
* @param capBytes stop counting once the total exceeds this; the returned value is then
* at least @p capBytes (used to skip the scan early on huge trees).
* Missing entries contribute 0. Best effort only.
*/
qint64 estimateBackupSize(const QStringList &sourcePaths, qint64 capBytes = MAX_AUTO_BACKUP_BYTES);
/**
* @brief Default backup root guaranteed to live outside the install directory.
*/
QString defaultBackupRoot();
/**
* @brief Lists existing backup session directories under @p backupRoot, newest first.
*/
QStringList listBackupSessions(const QString &backupRoot);
/**
* @brief Returns the newest session that is non-empty, complete and covering @p sourcePaths, or empty.
*
* An incomplete newest session (partial failure) or one covering a different
* source set does not block reuse of an older good session.
*/
QString findRecentUsableBackupSession(const QString &backupRoot, const QStringList &sourcePaths, int maxAgeHours = 24);
/**
* @brief Returns the newest non-empty session covering @p sourcePaths, regardless of completeness.
*
* Used as a fallback pointer when a fresh copy fails entirely, so the dialog can still
* point at salvaged content. Completeness is left to the caller to report.
*/
QString
findRecentCoveringBackupSession(const QString &backupRoot, const QStringList &sourcePaths, int maxAgeHours = 24);
/**
* @brief Reports whether @p sessionDir holds a complete backup.
* @returns true when the session carries the complete marker, false otherwise
* (partial backups and empty/missing sessions).
*/
bool isBackupSessionComplete(const QString &sessionDir);
/**
* @brief Deletes oldest sessions keeping only the newest @p keepCount ones.
* Empty sessions are junk from failed attempts and are always removed, so they
* can never evict a good session from the kept set.
*/
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.
* Completeness marker and source manifest files are internal bookkeeping and are never listed.
*/
QStringList listSessionBackupLocations(const QString &sessionDir);
/**
* @brief Reports whether @p sessionDir was recorded as covering all of @p sourcePaths.
* @returns true when the session's source manifest lists every entry of @p sourcePaths
* (normalized via QDir::cleanPath, case-insensitive on Windows/macOS). Sessions
* without a readable manifest return false so a changed source set always triggers
* a fresh copy instead of reusing a backup that may not cover it.
*/
bool sessionCoversSources(const QString &sessionDir, const QStringList &sourcePaths);
} // namespace UserDataBackup
#endif // USER_DATA_BACKUP_H