3 Commits

Author SHA1 Message Date
forust afb089bee6 fix(search): make stop interrupt all phases
ci-release / verify (push) Successful in 33s
ci-release / publish-linux-amd64 (push) Has been skipped
2026-07-25 22:22:03 +02:00
forust 79001370b8 feat(ui): warn when search results are stale
ci-release / verify (push) Successful in 34s
ci-release / publish-linux-amd64 (push) Has been skipped
2026-07-25 22:11:30 +02:00
forust 07fc419f1e fix(duplicates): correct result visibility
ci-release / verify (push) Successful in 37s
ci-release / publish-linux-amd64 (push) Has been skipped
2026-07-25 21:51:48 +02:00
4 changed files with 168 additions and 35 deletions
+99 -23
View File
@@ -255,6 +255,12 @@ public slots:
void search(const SearchOptions &o) void search(const SearchOptions &o)
{ {
cancelled_.store(false, std::memory_order_relaxed); cancelled_.store(false, std::memory_order_relaxed);
const auto finishIfCancelled = [this] {
if (!cancelled_.load(std::memory_order_relaxed))
return false;
emit finished({}, tr("Search stopped"));
return true;
};
cancelledByLimit_ = false; cancelledByLimit_ = false;
cache_.setEnabled(o.useCache); cache_.setEnabled(o.useCache);
cacheHits_.store(0, std::memory_order_relaxed); cacheHits_.store(0, std::memory_order_relaxed);
@@ -284,7 +290,8 @@ public slots:
dirOptions |= fs::directory_options::follow_directory_symlink; dirOptions |= fs::directory_options::follow_directory_symlink;
if (!o.recursive) { if (!o.recursive) {
for (fs::directory_iterator it(root, dirOptions, ec), end; for (fs::directory_iterator it(root, dirOptions, ec), end;
it != end && !ec; it.increment(ec)) { it != end && !ec && !cancelled_.load(std::memory_order_relaxed);
it.increment(ec)) {
++totalEntries; ++totalEntries;
if (totalEntries % 2048 == 0) if (totalEntries % 2048 == 0)
emit phaseProgress(tr("Counting files"), totalEntries, 0); emit phaseProgress(tr("Counting files"), totalEntries, 0);
@@ -397,7 +404,9 @@ public slots:
if (!o.recursive) { if (!o.recursive) {
for (fs::directory_iterator it(root, dirOptions, ec), end; for (fs::directory_iterator it(root, dirOptions, ec), end;
it != end && !ec && !cancelledByLimit_; it.increment(ec)) { it != end && !ec && !cancelled_.load(std::memory_order_relaxed)
&& !cancelledByLimit_;
it.increment(ec)) {
process(*it, root); process(*it, root);
if (++scanned % 512 == 0) { if (++scanned % 512 == 0) {
if (totalEntries) if (totalEntries)
@@ -438,10 +447,8 @@ public slots:
if (totalEntries) if (totalEntries)
emit phaseProgress(tr("Scanning files"), std::min(scanned, totalEntries), totalEntries); emit phaseProgress(tr("Scanning files"), std::min(scanned, totalEntries), totalEntries);
if (cancelled_.load(std::memory_order_relaxed)) { if (finishIfCancelled())
emit finished({}, tr("Search stopped"));
return; return;
}
if (!o.contains.isEmpty() && !o.findFolders) { if (!o.contains.isEmpty() && !o.findFolders) {
emit phaseProgress(tr("Searching file contents"), 0, candidates.size()); emit phaseProgress(tr("Searching file contents"), 0, candidates.size());
@@ -449,6 +456,8 @@ public slots:
const qsizetype total = candidates.size(); const qsizetype total = candidates.size();
QFuture<FileRecord> future = QtConcurrent::filtered( QFuture<FileRecord> future = QtConcurrent::filtered(
candidates, [this, o, &checked, total](const FileRecord &r) { candidates, [this, o, &checked, total](const FileRecord &r) {
if (cancelled_.load(std::memory_order_relaxed))
return false;
const bool matched = cachedFileContains(r, o); const bool matched = cachedFileContains(r, o);
const qsizetype done = checked.fetch_add(1, std::memory_order_relaxed) + 1; const qsizetype done = checked.fetch_add(1, std::memory_order_relaxed) + 1;
if (done == total || done % 32 == 0) if (done == total || done % 32 == 0)
@@ -456,6 +465,8 @@ public slots:
return matched; return matched;
}); });
future.waitForFinished(); future.waitForFinished();
if (finishIfCancelled())
return;
candidates = future.results(); candidates = future.results();
if (o.maxResults > 0 && candidates.size() > o.maxResults) if (o.maxResults > 0 && candidates.size() > o.maxResults)
candidates.resize(o.maxResults); candidates.resize(o.maxResults);
@@ -480,6 +491,8 @@ public slots:
int group = 1; int group = 1;
QSet<QString> duplicatedPaths; QSet<QString> duplicatedPaths;
for (auto &[size, sameSize] : bySize) { for (auto &[size, sameSize] : bySize) {
if (cancelled_.load(std::memory_order_relaxed))
break;
Q_UNUSED(size); Q_UNUSED(size);
if (sameSize.size() < 2) if (sameSize.size() < 2)
continue; continue;
@@ -490,18 +503,24 @@ public slots:
emit phaseProgress(tr("Checking duplicates"), done, hashTotal); emit phaseProgress(tr("Checking duplicates"), done, hashTotal);
return result; return result;
}); });
if (cancelled_.load(std::memory_order_relaxed))
break;
QHash<QByteArray, QVector<FileRecord>> sameHash; QHash<QByteArray, QVector<FileRecord>> sameHash;
for (auto &[hash, record] : hashes) for (auto &[hash, record] : hashes)
if (!hash.isEmpty()) if (!hash.isEmpty())
sameHash[hash].push_back(record); sameHash[hash].push_back(record);
for (const auto &hashCandidates : sameHash) { for (const auto &hashCandidates : sameHash) {
if (cancelled_.load(std::memory_order_relaxed))
break;
if (hashCandidates.size() < 2) if (hashCandidates.size() < 2)
continue; continue;
QVector<QVector<FileRecord>> exactGroups; QVector<QVector<FileRecord>> exactGroups;
for (const auto &record : hashCandidates) { for (const auto &record : hashCandidates) {
if (cancelled_.load(std::memory_order_relaxed))
break;
bool placed = false; bool placed = false;
for (auto &exact : exactGroups) { for (auto &exact : exactGroups) {
if (filesEqual(exact.front().path, record.path)) { if (filesEqual(exact.front().path, record.path, &cancelled_)) {
exact.push_back(record); exact.push_back(record);
placed = true; placed = true;
break; break;
@@ -511,6 +530,8 @@ public slots:
exactGroups.push_back({record}); exactGroups.push_back({record});
} }
for (auto &exact : exactGroups) { for (auto &exact : exactGroups) {
if (cancelled_.load(std::memory_order_relaxed))
break;
if (exact.size() < 2) if (exact.size() < 2)
continue; continue;
int duplicateCopy = 1; int duplicateCopy = 1;
@@ -518,21 +539,22 @@ public slots:
record.group = group; record.group = group;
record.duplicateCopy = duplicateCopy++; record.duplicateCopy = duplicateCopy++;
duplicatedPaths.insert(record.path); duplicatedPaths.insert(record.path);
if (o.mode == QStringLiteral("Duplicates search")) if (o.mode == QStringLiteral("Duplicates search")
&& shouldShowDuplicateResult(
o.showDuplicateCopiesOnly, record.duplicateCopy)) {
results.push_back(record); results.push_back(record);
}
} }
++group; ++group;
} }
} }
} }
if (finishIfCancelled())
return;
if (o.mode == QStringLiteral("Non-Duplicates search")) { if (o.mode == QStringLiteral("Non-Duplicates search")) {
for (const auto &record : candidates) for (const auto &record : candidates)
if (record.type == QStringLiteral("File") && !duplicatedPaths.contains(record.path)) if (record.type == QStringLiteral("File") && !duplicatedPaths.contains(record.path))
results.push_back(record); results.push_back(record);
} else if (!o.showOnlyDuplicateFiles) {
for (const auto &record : candidates)
if (record.type == QStringLiteral("File") && !duplicatedPaths.contains(record.path))
results.push_back(record);
} }
} else if (o.mode == QStringLiteral("Duplicate names search")) { } else if (o.mode == QStringLiteral("Duplicate names search")) {
QHash<QString, QVector<FileRecord>> byName; QHash<QString, QVector<FileRecord>> byName;
@@ -546,6 +568,8 @@ public slots:
quint64 namesDone = 0; quint64 namesDone = 0;
emit phaseProgress(tr("Comparing duplicate names"), 0, byName.size()); emit phaseProgress(tr("Comparing duplicate names"), 0, byName.size());
for (auto sameName : byName) { for (auto sameName : byName) {
if (cancelled_.load(std::memory_order_relaxed))
break;
++namesDone; ++namesDone;
if (namesDone % 32 == 0 || namesDone == quint64(byName.size())) if (namesDone % 32 == 0 || namesDone == quint64(byName.size()))
emit phaseProgress(tr("Comparing duplicate names"), namesDone, byName.size()); emit phaseProgress(tr("Comparing duplicate names"), namesDone, byName.size());
@@ -559,7 +583,8 @@ public slots:
bool identical = !firstHash.isEmpty(); bool identical = !firstHash.isEmpty();
for (qsizetype i = 1; identical && i < sameName.size(); ++i) { for (qsizetype i = 1; identical && i < sameName.size(); ++i) {
identical = cachedSha256(sameName[i]) == firstHash identical = cachedSha256(sameName[i]) == firstHash
&& filesEqual(sameName.front().path, sameName[i].path); && filesEqual(
sameName.front().path, sameName[i].path, &cancelled_);
} }
const bool wantsNonIdentical = o.duplicateNameMode.startsWith(QStringLiteral("Only non")); const bool wantsNonIdentical = o.duplicateNameMode.startsWith(QStringLiteral("Only non"));
if (identical == wantsNonIdentical) if (identical == wantsNonIdentical)
@@ -573,9 +598,13 @@ public slots:
} }
++group; ++group;
} }
if (finishIfCancelled())
return;
} else if (o.mode == QStringLiteral("Summary")) { } else if (o.mode == QStringLiteral("Summary")) {
QHash<QString, QVector<FileRecord>> byFolder; QHash<QString, QVector<FileRecord>> byFolder;
for (const auto &record : candidates) { for (const auto &record : candidates) {
if (cancelled_.load(std::memory_order_relaxed))
break;
byFolder[record.folder].push_back(record); byFolder[record.folder].push_back(record);
if (o.includeSubfoldersInSummary) { if (o.includeSubfoldersInSummary) {
QDir parent(record.folder); QDir parent(record.folder);
@@ -595,6 +624,8 @@ public slots:
} }
} }
for (auto it = byFolder.cbegin(); it != byFolder.cend(); ++it) { for (auto it = byFolder.cbegin(); it != byFolder.cend(); ++it) {
if (cancelled_.load(std::memory_order_relaxed))
break;
qint64 total = 0; qint64 total = 0;
qint64 totalOnDisk = 0; qint64 totalOnDisk = 0;
qint64 newest = 0; qint64 newest = 0;
@@ -608,10 +639,14 @@ public slots:
newest, 0, 0, 0, tr("%1 files").arg(it.value().size()), newest, 0, 0, 0, tr("%1 files").arg(it.value().size()),
{}, {}, 0, 0}); {}, {}, 0, 0});
} }
if (finishIfCancelled())
return;
} else { } else {
results = std::move(candidates); results = std::move(candidates);
} }
if (finishIfCancelled())
return;
const double seconds = std::chrono::duration<double>( const double seconds = std::chrono::duration<double>(
std::chrono::steady_clock::now() - started).count(); std::chrono::steady_clock::now() - started).count();
cache_.save(); cache_.save();
@@ -635,8 +670,9 @@ private:
return digest; return digest;
} }
cacheMisses_.fetch_add(1, std::memory_order_relaxed); cacheMisses_.fetch_add(1, std::memory_order_relaxed);
digest = sha256(record.path); digest = sha256(record.path, &cancelled_);
cache_.storeHash(record, digest); if (!cancelled_.load(std::memory_order_relaxed))
cache_.storeHash(record, digest);
return digest; return digest;
} }
@@ -651,8 +687,9 @@ private:
return *cached; return *cached;
} }
cacheMisses_.fetch_add(1, std::memory_order_relaxed); cacheMisses_.fetch_add(1, std::memory_order_relaxed);
const bool matched = fileContains(record.path, o); const bool matched = fileContains(record.path, o, &cancelled_);
cache_.storeContent(record, queryKey, matched); if (!cancelled_.load(std::memory_order_relaxed))
cache_.storeContent(record, queryKey, matched);
return matched; return matched;
} }
@@ -1221,14 +1258,14 @@ public:
action->setCheckable(true); action->setCheckable(true);
duplicateOptionsGroup->addAction(action); duplicateOptionsGroup->addAction(action);
} }
showOnlyDuplicates->setChecked(options_.showOnlyDuplicateFiles); showOnlyDuplicates->setChecked(options_.showDuplicateCopiesOnly);
showAllDuplicates->setChecked(!options_.showOnlyDuplicateFiles); showAllDuplicates->setChecked(!options_.showDuplicateCopiesOnly);
connect(showOnlyDuplicates, &QAction::triggered, this, [this] { connect(showOnlyDuplicates, &QAction::triggered, this, [this] {
options_.showOnlyDuplicateFiles = true; options_.showDuplicateCopiesOnly = true;
saveOptions(); saveOptions();
}); });
connect(showAllDuplicates, &QAction::triggered, this, [this] { connect(showAllDuplicates, &QAction::triggered, this, [this] {
options_.showOnlyDuplicateFiles = false; options_.showDuplicateCopiesOnly = false;
saveOptions(); saveOptions();
}); });
@@ -1288,6 +1325,15 @@ public:
.arg(QStringLiteral(FOLDERSCOPE_VERSION))); .arg(QStringLiteral(FOLDERSCOPE_VERSION)));
}); });
staleSearchWarning_ = new QLabel(
tr("⚠ Search settings changed — press F5 to rescan"));
staleSearchWarning_->setStyleSheet(
QStringLiteral("QLabel { color: #f0b429; font-weight: 600; padding: 0 6px; }"));
staleSearchWarning_->setToolTip(
tr("Displayed results were produced with different search settings."));
staleSearchWarning_->hide();
statusBar()->addPermanentWidget(staleSearchWarning_);
progress_ = new QProgressBar; progress_ = new QProgressBar;
progress_->setRange(0, 0); progress_->setRange(0, 0);
progress_->setMinimumWidth(280); progress_->setMinimumWidth(280);
@@ -1307,12 +1353,16 @@ public:
statusBar()->showMessage(tr("Search cache cleared"), 5000); statusBar()->showMessage(tr("Search cache cleared"), 5000);
}); });
connect(engine_, &SearchEngine::progress, this, [this](const QString &path, quint64 count) { connect(engine_, &SearchEngine::progress, this, [this](const QString &path, quint64 count) {
if (isStopping_)
return;
progress_->setRange(0, 0); progress_->setRange(0, 0);
progress_->setFormat(tr("Scanning… %1 entries").arg(count)); progress_->setFormat(tr("Scanning… %1 entries").arg(count));
statusBar()->showMessage(tr("Scanning %1 (%2 entries)").arg(path).arg(count)); statusBar()->showMessage(tr("Scanning %1 (%2 entries)").arg(path).arg(count));
}); });
connect(engine_, &SearchEngine::phaseProgress, this, connect(engine_, &SearchEngine::phaseProgress, this,
[this](const QString &phase, quint64 completed, quint64 total) { [this](const QString &phase, quint64 completed, quint64 total) {
if (isStopping_)
return;
if (total == 0) { if (total == 0) {
progress_->setRange(0, 0); progress_->setRange(0, 0);
progress_->setFormat(completed progress_->setFormat(completed
@@ -1334,7 +1384,14 @@ public:
workerThread_.start(); workerThread_.start();
connect(searchAction_, &QAction::triggered, this, &MainWindow::showOptions); connect(searchAction_, &QAction::triggered, this, &MainWindow::showOptions);
connect(stopAction_, &QAction::triggered, this, [this] { emit stopRequested(); }); connect(stopAction_, &QAction::triggered, this, [this] {
isStopping_ = true;
stopAction_->setEnabled(false);
progress_->setRange(0, 0);
progress_->setFormat(tr("Stopping…"));
statusBar()->showMessage(tr("Stopping search…"));
emit stopRequested();
});
connect(refreshAction_, &QAction::triggered, this, &MainWindow::refreshSearch); connect(refreshAction_, &QAction::triggered, this, &MainWindow::refreshSearch);
connect(exportAction, &QAction::triggered, this, &MainWindow::exportResults); connect(exportAction, &QAction::triggered, this, &MainWindow::exportResults);
connect(openAction, &QAction::triggered, this, &MainWindow::openSelected); connect(openAction, &QAction::triggered, this, &MainWindow::openSelected);
@@ -1516,6 +1573,9 @@ private slots:
if (isSearching_ || options_.roots.trimmed().isEmpty()) if (isSearching_ || options_.roots.trimmed().isEmpty())
return; return;
isSearching_ = true; isSearching_ = true;
isStopping_ = false;
lastSearchOptions_ = options_;
updateStaleSearchWarning();
applyDisplayOptions(); applyDisplayOptions();
model_->setRows({}); model_->setRows({});
searchAction_->setEnabled(false); searchAction_->setEnabled(false);
@@ -1534,12 +1594,14 @@ private slots:
void searchFinished(const QVector<FileRecord> &results, const QString &message) void searchFinished(const QVector<FileRecord> &results, const QString &message)
{ {
isSearching_ = false; isSearching_ = false;
isStopping_ = false;
model_->setRows(results); model_->setRows(results);
searchAction_->setEnabled(true); searchAction_->setEnabled(true);
stopAction_->setEnabled(false); stopAction_->setEnabled(false);
refreshAction_->setEnabled(true); refreshAction_->setEnabled(true);
progress_->hide(); progress_->hide();
statusBar()->showMessage(message); statusBar()->showMessage(message);
updateStaleSearchWarning();
if (autoSizeOnSearchEnd_) { if (autoSizeOnSearchEnd_) {
table_->horizontalHeader()->setSectionResizeMode(QHeaderView::Interactive); table_->horizontalHeader()->setSectionResizeMode(QHeaderView::Interactive);
table_->resizeColumnsToContents(); table_->resizeColumnsToContents();
@@ -1991,11 +2053,20 @@ private:
settings.setValue(QStringLiteral("mode"), options_.mode); settings.setValue(QStringLiteral("mode"), options_.mode);
settings.setValue(QStringLiteral("duplicateNameMode"), options_.duplicateNameMode); settings.setValue(QStringLiteral("duplicateNameMode"), options_.duplicateNameMode);
settings.setValue(QStringLiteral("duplicateNameWithoutExtension"), options_.duplicateNameWithoutExtension); settings.setValue(QStringLiteral("duplicateNameWithoutExtension"), options_.duplicateNameWithoutExtension);
settings.setValue(QStringLiteral("showOnlyDuplicateFiles"), options_.showOnlyDuplicateFiles); settings.setValue(QStringLiteral("showDuplicateCopiesOnly"), options_.showDuplicateCopiesOnly);
settings.setValue(QStringLiteral("includeSubfoldersInSummary"), options_.includeSubfoldersInSummary); settings.setValue(QStringLiteral("includeSubfoldersInSummary"), options_.includeSubfoldersInSummary);
settings.setValue(QStringLiteral("hidden"), options_.hidden); settings.setValue(QStringLiteral("hidden"), options_.hidden);
settings.setValue(QStringLiteral("readonly"), options_.readonly); settings.setValue(QStringLiteral("readonly"), options_.readonly);
settings.setValue(QStringLiteral("executable"), options_.executable); settings.setValue(QStringLiteral("executable"), options_.executable);
updateStaleSearchWarning();
}
void updateStaleSearchWarning()
{
if (!staleSearchWarning_)
return;
staleSearchWarning_->setVisible(
lastSearchOptions_.has_value() && options_ != *lastSearchOptions_);
} }
void loadOptions() void loadOptions()
@@ -2028,7 +2099,9 @@ private:
options_.mode = s.value(QStringLiteral("mode"), options_.mode).toString(); options_.mode = s.value(QStringLiteral("mode"), options_.mode).toString();
options_.duplicateNameMode = s.value(QStringLiteral("duplicateNameMode"), options_.duplicateNameMode).toString(); options_.duplicateNameMode = s.value(QStringLiteral("duplicateNameMode"), options_.duplicateNameMode).toString();
options_.duplicateNameWithoutExtension = s.value(QStringLiteral("duplicateNameWithoutExtension"), false).toBool(); options_.duplicateNameWithoutExtension = s.value(QStringLiteral("duplicateNameWithoutExtension"), false).toBool();
options_.showOnlyDuplicateFiles = s.value(QStringLiteral("showOnlyDuplicateFiles"), true).toBool(); options_.showDuplicateCopiesOnly =
s.value(QStringLiteral("showDuplicateCopiesOnly"),
s.value(QStringLiteral("showOnlyDuplicateFiles"), true)).toBool();
options_.includeSubfoldersInSummary = s.value(QStringLiteral("includeSubfoldersInSummary"), false).toBool(); options_.includeSubfoldersInSummary = s.value(QStringLiteral("includeSubfoldersInSummary"), false).toBool();
options_.hidden = s.value(QStringLiteral("hidden"), options_.hidden).toString(); options_.hidden = s.value(QStringLiteral("hidden"), options_.hidden).toString();
options_.readonly = s.value(QStringLiteral("readonly"), options_.readonly).toString(); options_.readonly = s.value(QStringLiteral("readonly"), options_.readonly).toString();
@@ -2057,9 +2130,11 @@ private:
QAction *searchAction_{}; QAction *searchAction_{};
QAction *stopAction_{}; QAction *stopAction_{};
QAction *refreshAction_{}; QAction *refreshAction_{};
QLabel *staleSearchWarning_{};
QProgressBar *progress_{}; QProgressBar *progress_{};
QThread workerThread_; QThread workerThread_;
SearchEngine *engine_{}; SearchEngine *engine_{};
std::optional<SearchOptions> lastSearchOptions_;
QString findQuery_; QString findQuery_;
QString summarySizeUnitName_ = QStringLiteral("Automatic"); QString summarySizeUnitName_ = QStringLiteral("Automatic");
QString doubleClickAction_ = QStringLiteral("Open Properties Window"); QString doubleClickAction_ = QStringLiteral("Open Properties Window");
@@ -2075,6 +2150,7 @@ private:
bool showGrid_ = true; bool showGrid_ = true;
bool showTooltips_ = true; bool showTooltips_ = true;
bool isSearching_ = false; bool isSearching_ = false;
bool isStopping_ = false;
}; };
int main(int argc, char **argv) int main(int argc, char **argv)
+30 -8
View File
@@ -54,6 +54,13 @@ bool wildcardMatch(const QString &value, const QStringList &items, Qt::CaseSensi
return false; return false;
} }
bool shouldShowDuplicateResult(bool copiesOnly, int keeperPriority)
{
if (keeperPriority < 1)
return false;
return !copiesOnly || keeperPriority > 1;
}
QString humanSize(qint64 bytes) QString humanSize(qint64 bytes)
{ {
static const QStringList units{QStringLiteral("B"), QStringLiteral("KB"), static const QStringList units{QStringLiteral("B"), QStringLiteral("KB"),
@@ -87,8 +94,18 @@ std::optional<QByteArray> hexNeedle(QString text)
return QByteArray::fromHex(text.toLatin1()); return QByteArray::fromHex(text.toLatin1());
} }
bool fileContains(const QString &path, const SearchOptions &options) namespace {
bool isCancelled(const std::atomic_bool *cancelled)
{ {
return cancelled && cancelled->load(std::memory_order_relaxed);
}
}
bool fileContains(const QString &path, const SearchOptions &options,
const std::atomic_bool *cancelled)
{
if (isCancelled(cancelled))
return false;
if (options.contains.isEmpty()) if (options.contains.isEmpty())
return true; return true;
@@ -117,7 +134,7 @@ bool fileContains(const QString &path, const SearchOptions &options)
for (const QByteArray &needle : needles) for (const QByteArray &needle : needles)
overlap = std::max(overlap, needle.size()); overlap = std::max(overlap, needle.size());
QByteArray tail; QByteArray tail;
while (!file.atEnd()) { while (!file.atEnd() && !isCancelled(cancelled)) {
QByteArray data = tail + file.read(1024 * 1024); QByteArray data = tail + file.read(1024 * 1024);
if (!options.caseSensitive && !options.binary) if (!options.caseSensitive && !options.binary)
data = data.toLower(); data = data.toLower();
@@ -138,31 +155,36 @@ bool fileContains(const QString &path, const SearchOptions &options)
return false; return false;
} }
bool filesEqual(const QString &leftPath, const QString &rightPath) bool filesEqual(const QString &leftPath, const QString &rightPath,
const std::atomic_bool *cancelled)
{ {
if (isCancelled(cancelled))
return false;
QFile left(leftPath); QFile left(leftPath);
QFile right(rightPath); QFile right(rightPath);
if (!left.open(QIODevice::ReadOnly) || !right.open(QIODevice::ReadOnly) if (!left.open(QIODevice::ReadOnly) || !right.open(QIODevice::ReadOnly)
|| left.size() != right.size()) || left.size() != right.size())
return false; return false;
while (!left.atEnd()) { while (!left.atEnd() && !isCancelled(cancelled)) {
const QByteArray leftData = left.read(1024 * 1024); const QByteArray leftData = left.read(1024 * 1024);
const QByteArray rightData = right.read(leftData.size()); const QByteArray rightData = right.read(leftData.size());
if (leftData != rightData) if (leftData != rightData)
return false; return false;
} }
return right.atEnd(); return !isCancelled(cancelled) && right.atEnd();
} }
QByteArray sha256(const QString &path) QByteArray sha256(const QString &path, const std::atomic_bool *cancelled)
{ {
if (isCancelled(cancelled))
return {};
QFile file(path); QFile file(path);
if (!file.open(QIODevice::ReadOnly)) if (!file.open(QIODevice::ReadOnly))
return {}; return {};
QCryptographicHash hash(QCryptographicHash::Sha256); QCryptographicHash hash(QCryptographicHash::Sha256);
while (!file.atEnd()) while (!file.atEnd() && !isCancelled(cancelled))
hash.addData(file.read(1024 * 1024)); hash.addData(file.read(1024 * 1024));
return hash.result(); return isCancelled(cancelled) ? QByteArray{} : hash.result();
} }
bool parseOptionalIsoDate(const QString &text, qint64 &seconds) bool parseOptionalIsoDate(const QString &text, qint64 &seconds)
+10 -4
View File
@@ -7,6 +7,7 @@
#include <QStringList> #include <QStringList>
#include <QVector> #include <QVector>
#include <atomic>
#include <optional> #include <optional>
#include <sys/types.h> #include <sys/types.h>
@@ -40,11 +41,13 @@ struct SearchOptions {
QString mode = QStringLiteral("Standard search"); QString mode = QStringLiteral("Standard search");
QString duplicateNameMode = QStringLiteral("All files and folders"); QString duplicateNameMode = QStringLiteral("All files and folders");
bool duplicateNameWithoutExtension = false; bool duplicateNameWithoutExtension = false;
bool showOnlyDuplicateFiles = true; bool showDuplicateCopiesOnly = true;
bool includeSubfoldersInSummary = false; bool includeSubfoldersInSummary = false;
QString hidden = QStringLiteral("Any"); QString hidden = QStringLiteral("Any");
QString readonly = QStringLiteral("Any"); QString readonly = QStringLiteral("Any");
QString executable = QStringLiteral("Any"); QString executable = QStringLiteral("Any");
bool operator==(const SearchOptions &) const = default;
}; };
struct FileRecord { struct FileRecord {
@@ -74,7 +77,10 @@ bool wildcardMatch(const QString &value, const QStringList &items, Qt::CaseSensi
QString humanSize(qint64 bytes); QString humanSize(qint64 bytes);
QString ownerName(uid_t uid); QString ownerName(uid_t uid);
std::optional<QByteArray> hexNeedle(QString text); std::optional<QByteArray> hexNeedle(QString text);
bool fileContains(const QString &path, const SearchOptions &options); bool fileContains(const QString &path, const SearchOptions &options,
bool filesEqual(const QString &leftPath, const QString &rightPath); const std::atomic_bool *cancelled = nullptr);
QByteArray sha256(const QString &path); bool filesEqual(const QString &leftPath, const QString &rightPath,
const std::atomic_bool *cancelled = nullptr);
QByteArray sha256(const QString &path, const std::atomic_bool *cancelled = nullptr);
bool parseOptionalIsoDate(const QString &text, qint64 &seconds); bool parseOptionalIsoDate(const QString &text, qint64 &seconds);
bool shouldShowDuplicateResult(bool copiesOnly, int keeperPriority);
+29
View File
@@ -33,6 +33,17 @@ private slots:
QVERIFY(!parseOptionalIsoDate(QStringLiteral("2026-not-a-date"), seconds)); QVERIFY(!parseOptionalIsoDate(QStringLiteral("2026-not-a-date"), seconds));
} }
void selectsDuplicateRowsForDisplay()
{
QVERIFY(!shouldShowDuplicateResult(true, 0));
QVERIFY(!shouldShowDuplicateResult(true, 1));
QVERIFY(shouldShowDuplicateResult(true, 2));
QVERIFY(!shouldShowDuplicateResult(false, 0));
QVERIFY(shouldShowDuplicateResult(false, 1));
QVERIFY(shouldShowDuplicateResult(false, 2));
}
void searchesContentsAndHashes() void searchesContentsAndHashes()
{ {
QTemporaryDir directory; QTemporaryDir directory;
@@ -79,6 +90,24 @@ private slots:
QVERIFY(!filesEqual(firstPath, differentPath)); QVERIFY(!filesEqual(firstPath, differentPath));
QCOMPARE(sha256(firstPath), QCryptographicHash::hash(content, QCryptographicHash::Sha256)); QCOMPARE(sha256(firstPath), QCryptographicHash::hash(content, QCryptographicHash::Sha256));
} }
void cancelsFileOperations()
{
QTemporaryDir directory;
QVERIFY(directory.isValid());
const QString path = directory.filePath(QStringLiteral("file.bin"));
QFile file(path);
QVERIFY(file.open(QIODevice::WriteOnly));
QCOMPARE(file.write("needle"), qint64(6));
file.close();
std::atomic_bool cancelled = true;
SearchOptions options;
options.contains = QStringLiteral("needle");
QVERIFY(!fileContains(path, options, &cancelled));
QVERIFY(!filesEqual(path, path, &cancelled));
QVERIFY(sha256(path, &cancelled).isEmpty());
}
}; };
QTEST_GUILESS_MAIN(SearchCoreTest) QTEST_GUILESS_MAIN(SearchCoreTest)