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

This commit is contained in:
Lukas Brübach 2026-10-13 11:36:19 +02:00
parent 3bde484244
commit e9b05a3d46
5 changed files with 256 additions and 33 deletions

View file

@ -80,7 +80,19 @@ bool copyDirectoryRecursively(const QString &sourceDir, const QString &destDir)
it.next();
QString entryRel = source.relativeFilePath(it.filePath());
QString target = QDir(destDir).absoluteFilePath(entryRel);
if (it.fileInfo().isDir()) {
if (it.fileInfo().isSymLink()) {
// Copy the link itself best-effort without dereferencing it. QDirIterator does not
// follow symlinks here, so a symlink-to-directory must not take the isDir() branch
// below (which would create an empty dir and silently drop the contents).
QDir().mkpath(QFileInfo(target).absolutePath());
if (QFile::exists(target) && !QFile::remove(target)) {
ok = false;
continue;
}
if (!QFile::copy(it.filePath(), target)) {
ok = false;
}
} else if (it.fileInfo().isDir()) {
if (!QDir().mkpath(target)) {
ok = false;
}
@ -284,10 +296,10 @@ QString findRecentBackupSession(const QString &backupRoot, int maxAgeHours)
if (!info.exists()) {
continue;
}
// Sessions are newest-first, so once an entry is too old every later
// one is older as well and the search can stop.
// No early break: filename order is not guaranteed to match mtime order
// (clock adjustments, externally touched dirs), and session counts are tiny.
if (info.lastModified().secsTo(now) >= static_cast<qint64>(maxAgeHours) * 3600) {
break;
continue;
}
// Skip empty sessions from failed attempts: they must never block a retry.
if (listSessionBackupLocations(session).isEmpty()) {
@ -298,6 +310,58 @@ QString findRecentBackupSession(const QString &backupRoot, int maxAgeHours)
return {};
}
QString findRecentCoveringBackupSession(const QString &backupRoot, const QStringList &sourcePaths, int maxAgeHours)
{
const QStringList sessions = listBackupSessions(backupRoot);
const QDateTime now = QDateTime::currentDateTime();
for (const QString &session : sessions) {
const QFileInfo info(session);
if (!info.exists()) {
continue;
}
if (info.lastModified().secsTo(now) >= static_cast<qint64>(maxAgeHours) * 3600) {
continue;
}
if (listSessionBackupLocations(session).isEmpty()) {
continue;
}
if (!sessionCoversSources(session, sourcePaths)) {
continue;
}
return session;
}
return {};
}
QString findRecentUsableBackupSession(const QString &backupRoot, const QStringList &sourcePaths, int maxAgeHours)
{
if (sourcePaths.isEmpty()) {
return {};
}
const QStringList sessions = listBackupSessions(backupRoot);
const QDateTime now = QDateTime::currentDateTime();
for (const QString &session : sessions) {
const QFileInfo info(session);
if (!info.exists()) {
continue;
}
if (info.lastModified().secsTo(now) >= static_cast<qint64>(maxAgeHours) * 3600) {
continue;
}
if (listSessionBackupLocations(session).isEmpty()) {
continue;
}
if (!isBackupSessionComplete(session)) {
continue;
}
if (!sessionCoversSources(session, sourcePaths)) {
continue;
}
return session;
}
return {};
}
bool isBackupSessionComplete(const QString &sessionDir)
{
const QDir session(sessionDir);

View file

@ -28,7 +28,8 @@ constexpr qint64 MAX_AUTO_BACKUP_BYTES = 500LL * 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 are copied best-effort via QFile::copy; a dangling link fails that entry only.
* 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);
@ -96,11 +97,30 @@ QStringList listBackupSessions(const QString &backupRoot);
*
* Used to throttle startup safety backups so dismissing the warning without
* "Don't show again" does not create a full duplicate on every launch.
* Empty sessions (failed attempts) are skipped so they never block a retry,
* and an expired newest entry stops the search since sessions are newest-first.
* Empty sessions (failed attempts) are skipped so they never block a retry.
* Every session is age-checked individually; no early exit assumes filename
* order matches mtime order (clock adjustments or externally touched dirs
* can break that assumption).
*/
QString findRecentBackupSession(const QString &backupRoot, int maxAgeHours = 24);
/**
* @brief Returns the newest session that is non-empty, complete and covering @p sourcePaths, or empty.
*
* Unlike findRecentBackupSession(), 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, or when it predates