refactor: split main.cpp into separate files, fix bugs 5-8, 15

This commit is contained in:
2026-07-26 01:00:13 +02:00
parent afb089bee6
commit 711536c74e
14 changed files with 2551 additions and 2154 deletions
+12
View File
@@ -26,6 +26,16 @@ qt_add_executable(searchmyfiles
src/long_long_spin_box.cpp src/long_long_spin_box.cpp
src/long_long_spin_box.h src/long_long_spin_box.h
src/main.cpp src/main.cpp
src/main_window.h
src/main_window.cpp
src/search_cache.h
src/search_cache.cpp
src/search_engine.h
src/search_engine.cpp
src/results_model.h
src/results_model.cpp
src/options_dialog.h
src/options_dialog.cpp
) )
target_link_libraries(searchmyfiles PRIVATE folderscope_core Qt6::Widgets Qt6::Concurrent Qt6::DBus) target_link_libraries(searchmyfiles PRIVATE folderscope_core Qt6::Widgets Qt6::Concurrent Qt6::DBus)
@@ -35,6 +45,8 @@ target_compile_definitions(searchmyfiles PRIVATE FOLDERSCOPE_VERSION="${PROJECT_
install(TARGETS searchmyfiles RUNTIME DESTINATION bin) install(TARGETS searchmyfiles RUNTIME DESTINATION bin)
install(FILES packaging/searchmyfiles.desktop install(FILES packaging/searchmyfiles.desktop
DESTINATION share/applications) DESTINATION share/applications)
install(FILES packaging/searchmyfiles.svg
DESTINATION share/icons/hicolor/scalable/apps)
include(CTest) include(CTest)
if (BUILD_TESTING) if (BUILD_TESTING)
+1 -2
View File
@@ -3,7 +3,6 @@ Type=Application
Name=SearchMyFiles Name=SearchMyFiles
Comment=Fast advanced file search Comment=Fast advanced file search
Exec=searchmyfiles Exec=searchmyfiles
Icon=system-search Icon=searchmyfiles
Terminal=false Terminal=false
Categories=Utility;FileTools; Categories=Utility;FileTools;
+5
View File
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64">
<rect width="64" height="64" rx="8" fill="#3584e4"/>
<circle cx="28" cy="28" r="12" fill="none" stroke="#fff" stroke-width="3.5"/>
<line x1="36" y1="36" x2="50" y2="50" stroke="#fff" stroke-width="3.5" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 324 B

+2 -2152
View File
File diff suppressed because it is too large Load Diff
+1048
View File
File diff suppressed because it is too large Load Diff
+117
View File
@@ -0,0 +1,117 @@
#pragma once
#include <QAction>
#include <QCheckBox>
#include <QClipboard>
#include <QCloseEvent>
#include <QDBusConnection>
#include <QDBusInterface>
#include <QDBusMessage>
#include <QDesktopServices>
#include <QHeaderView>
#include <QInputDialog>
#include <QKeyEvent>
#include <QLabel>
#include <QMainWindow>
#include <QMenu>
#include <QMenuBar>
#include <QMessageBox>
#include <QMimeData>
#include <QProgressBar>
#include <QProcess>
#include <QSettings>
#include <QSortFilterProxyModel>
#include <QStatusBar>
#include <QTableView>
#include <QThread>
#include <QToolBar>
#include <QUrl>
#include <optional>
#include "results_model.h"
#include "search_core.h"
class SearchEngine;
class QTextEdit;
class MainWindow final : public QMainWindow {
Q_OBJECT
public:
MainWindow();
~MainWindow() override;
protected:
void closeEvent(QCloseEvent *event) override;
void keyPressEvent(QKeyEvent *event) override;
signals:
void startRequested(const SearchOptions &options);
void stopRequested();
void clearCacheRequested();
private slots:
void showOptions();
void refreshSearch();
void findText();
void findNext();
void copyInformation();
void explorerCopy();
void explorerCut();
void startCurrentSearch();
void searchFinished(const QVector<FileRecord> &results, const QString &message);
void openSelected();
void openFolder();
void openWith();
void selectInFileManager();
void moveToTrash();
void deleteSelected();
void renameSelected();
void showProperties();
void copyPaths();
void contextMenu(const QPoint &point);
void headerContextMenu(const QPoint &point);
void exportResults();
private:
std::optional<FileRecord> currentRecord() const;
QStringList actionChoices() const;
void executeConfiguredAction(const QString &action);
QString sizeUnitName() const;
int sizeUnitFromName(const QString &name) const;
void applyDisplayOptions();
void saveUiSettings();
QStringList selectedPaths() const;
void saveOptions();
void updateStaleSearchWarning();
void loadOptions();
SearchOptions options_;
ResultsModel *model_{};
QSortFilterProxyModel *proxy_{};
QTableView *table_{};
QAction *searchAction_{};
QAction *stopAction_{};
QAction *refreshAction_{};
QLabel *staleSearchWarning_{};
QProgressBar *progress_{};
QThread workerThread_;
SearchEngine *engine_{};
std::optional<SearchOptions> lastSearchOptions_;
QString findQuery_;
QString summarySizeUnitName_ = QStringLiteral("Automatic");
QString doubleClickAction_ = QStringLiteral("Open Properties Window");
QString enterKeyAction_ = QStringLiteral("Open Selected File");
int sizeUnit_ = 0;
int duplicateColorSet_ = 1;
bool showGmt_ = false;
bool markDuplicates_ = true;
bool addExportHeader_ = true;
bool focusOnSearchStart_ = true;
bool focusOnSearchEnd_ = true;
bool autoSizeOnSearchEnd_ = false;
bool showGrid_ = true;
bool showTooltips_ = true;
bool isSearching_ = false;
bool isStopping_ = false;
};
+433
View File
@@ -0,0 +1,433 @@
#include "options_dialog.h"
#include <QDialogButtonBox>
#include <QDropEvent>
#include <QEvent>
#include <QFileDialog>
#include <QInputDialog>
#include <QMessageBox>
#include <QMimeData>
#include <QSettings>
#include "long_long_spin_box.h"
OptionsDialog::OptionsDialog(const SearchOptions &o, QWidget *parent) : QDialog(parent)
{
setWindowTitle(tr("Search Options"));
resize(760, 560);
setupUi(o);
}
void OptionsDialog::setupUi(const SearchOptions &o)
{
auto *layout = new QVBoxLayout(this);
auto *profileRow = new QWidget;
auto *profileLayout = new QHBoxLayout(profileRow);
profileLayout->setContentsMargins(0, 0, 0, 0);
profileCombo_ = new QComboBox;
profileCombo_->setEditable(false);
profileCombo_->addItem(tr("(default)"));
profileCombo_->setMinimumWidth(200);
{
QSettings settings;
settings.beginGroup(QStringLiteral("profiles"));
const QStringList profiles = settings.childGroups();
for (const QString &name : profiles)
profileCombo_->addItem(name);
settings.endGroup();
}
auto *saveBtn = new QPushButton(tr("Save Profile…"));
auto *loadBtn = new QPushButton(tr("Load Profile"));
auto *deleteBtn = new QPushButton(tr("Delete Profile"));
profileLayout->addWidget(new QLabel(tr("Profile:")));
profileLayout->addWidget(profileCombo_);
profileLayout->addWidget(saveBtn);
profileLayout->addWidget(loadBtn);
profileLayout->addWidget(deleteBtn);
layout->addWidget(profileRow);
connect(saveBtn, &QPushButton::clicked, this, &OptionsDialog::saveProfile);
connect(loadBtn, &QPushButton::clicked, this, &OptionsDialog::loadProfile);
connect(deleteBtn, &QPushButton::clicked, this, [this] {
const QString name = profileCombo_->currentText();
if (name.isEmpty() || name == tr("(default)"))
return;
if (QMessageBox::question(this, tr("Delete Profile"),
tr("Delete profile \"%1\"?").arg(name))
!= QMessageBox::Yes)
return;
QSettings settings;
settings.beginGroup(QStringLiteral("profiles"));
settings.remove(name);
settings.endGroup();
profileCombo_->removeItem(profileCombo_->currentIndex());
});
auto *modeGroup = new QGroupBox(tr("Search mode"));
auto *modeLayout = new QHBoxLayout(modeGroup);
mode_ = new QComboBox;
mode_->addItems({tr("Standard search"), tr("Duplicates search"), tr("Non-Duplicates search"),
tr("Summary"), tr("Duplicate names search")});
mode_->setCurrentText(o.mode);
duplicateMode_ = new QComboBox;
duplicateMode_->addItems({tr("All files and folders"), tr("Only files"),
tr("Only identical content"), tr("Only non-identical content")});
duplicateMode_->setCurrentText(o.duplicateNameMode);
duplicateWithoutExtension_ = new QCheckBox(tr("Compare names without extension"));
duplicateWithoutExtension_->setChecked(o.duplicateNameWithoutExtension);
modeLayout->addWidget(mode_);
modeLayout->addWidget(new QLabel(tr("Duplicate-name mode:")));
modeLayout->addWidget(duplicateMode_);
modeLayout->addWidget(duplicateWithoutExtension_);
layout->addWidget(modeGroup);
auto *tabs = new QTabWidget;
layout->addWidget(tabs);
auto *filesPage = new QWidget;
auto *filesForm = new QFormLayout(filesPage);
roots_ = new QLineEdit(o.roots);
roots_->setAcceptDrops(true);
roots_->setToolTip(tr("Drop folders here from your file manager"));
roots_->installEventFilter(this);
auto *rootRow = new QWidget;
auto *rootLayout = new QHBoxLayout(rootRow);
rootLayout->setContentsMargins(0, 0, 0, 0);
rootLayout->addWidget(roots_);
auto *browse = new QPushButton(tr("Browse…"));
rootLayout->addWidget(browse);
filesForm->addRow(tr("Base folders (separate with ;):"), rootRow);
fileMasks_ = addLine(filesForm, tr("Files wildcard:"), o.fileWildcards);
subfolderMasks_ = addLine(filesForm, tr("Subfolders wildcard:"), o.subfolderWildcards);
excludeFiles_ = addLine(filesForm, tr("Exclude files:"), o.excludeFiles);
excludeFolders_ = addLine(filesForm, tr("Exclude folders:"), o.excludeFolders);
includeFolders_ = addLine(filesForm, tr("Include only folders:"), o.includeFolders);
recursive_ = new QCheckBox(tr("Search subfolders")); recursive_->setChecked(o.recursive);
findFiles_ = new QCheckBox(tr("Find files")); findFiles_->setChecked(o.findFiles);
findFolders_ = new QCheckBox(tr("Find folders")); findFolders_->setChecked(o.findFolders);
followLinks_ = new QCheckBox(tr("Follow symbolic links")); followLinks_->setChecked(o.followLinks);
retrieveOwner_ = new QCheckBox(tr("Retrieve file owner (slower)")); retrieveOwner_->setChecked(o.retrieveOwner);
accurateProgress_ = new QCheckBox(tr("Count files first for accurate progress"));
accurateProgress_->setChecked(o.accurateProgress);
useCache_ = new QCheckBox(tr("Use persistent cache for hashes and content searches"));
useCache_->setChecked(o.useCache);
maxDepth_ = new QSpinBox; maxDepth_->setRange(0, 1024); maxDepth_->setValue(o.maxDepth);
maxDepth_->setSpecialValueText(tr("Unlimited"));
maxResults_ = new QSpinBox; maxResults_->setRange(0, 100'000'000); maxResults_->setValue(o.maxResults);
maxResults_->setSpecialValueText(tr("Unlimited"));
filesForm->addRow(recursive_);
filesForm->addRow(tr("Subfolders depth:"), maxDepth_);
filesForm->addRow(findFiles_);
filesForm->addRow(findFolders_);
filesForm->addRow(followLinks_);
filesForm->addRow(retrieveOwner_);
filesForm->addRow(accurateProgress_);
filesForm->addRow(useCache_);
filesForm->addRow(tr("Stop after finding:"), maxResults_);
tabs->addTab(filesPage, tr("Files & folders"));
auto *contentPage = new QWidget;
auto *contentForm = new QFormLayout(contentPage);
contains_ = new QTextEdit(o.contains);
contains_->setMaximumHeight(110);
contentForm->addRow(tr("File contains:"), contains_);
binary_ = new QCheckBox(tr("Binary search (hex bytes, e.g. A2 C5 2F)")); binary_->setChecked(o.binary);
multiple_ = new QCheckBox(tr("Search multiple values (comma separated)")); multiple_->setChecked(o.multipleValues);
multipleMode_ = new QComboBox; multipleMode_->addItems({tr("Or"), tr("And")});
multipleMode_->setCurrentIndex(o.multipleAnd ? 1 : 0);
caseSensitive_ = new QCheckBox(tr("Case sensitive")); caseSensitive_->setChecked(o.caseSensitive);
contentForm->addRow(binary_);
contentForm->addRow(multiple_);
contentForm->addRow(tr("Multiple values match:"), multipleMode_);
contentForm->addRow(caseSensitive_);
tabs->addTab(contentPage, tr("File contents"));
auto *filterPage = new QWidget;
auto *filterForm = new QFormLayout(filterPage);
minSize_ = sizeSpin(o.minSize);
maxSize_ = sizeSpin(o.maxSize);
maxSize_->setSuffix(tr(" bytes (0 = any)"));
filterForm->addRow(tr("Minimum size:"), minSize_);
filterForm->addRow(tr("Maximum size:"), maxSize_);
timeField_ = new QComboBox;
timeField_->addItems({tr("Modified time"), tr("Accessed time"), tr("Metadata changed time")});
timeField_->setCurrentText(o.timeField);
filterForm->addRow(tr("Time field:"), timeField_);
lastMinutes_ = new QSpinBox;
lastMinutes_->setRange(0, 10'000'000);
lastMinutes_->setSuffix(tr(" minutes (0 = any)"));
lastMinutes_->setValue(o.lastMinutes);
filterForm->addRow(tr("Within last:"), lastMinutes_);
minTime_ = new QLineEdit(o.minTime ? QDateTime::fromSecsSinceEpoch(o.minTime).toString(Qt::ISODate) : QString{});
maxTime_ = new QLineEdit(o.maxTime ? QDateTime::fromSecsSinceEpoch(o.maxTime).toString(Qt::ISODate) : QString{});
minTime_->setPlaceholderText(tr("YYYY-MM-DDTHH:MM:SS"));
maxTime_->setPlaceholderText(tr("YYYY-MM-DDTHH:MM:SS"));
filterForm->addRow(tr("From time:"), minTime_);
filterForm->addRow(tr("To time:"), maxTime_);
hidden_ = attrCombo(o.hidden);
readonly_ = attrCombo(o.readonly);
executable_ = attrCombo(o.executable);
filterForm->addRow(tr("Hidden:"), hidden_);
filterForm->addRow(tr("Read-only:"), readonly_);
filterForm->addRow(tr("Executable:"), executable_);
tabs->addTab(filterPage, tr("Size, time & attributes"));
auto *buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
connect(buttons, &QDialogButtonBox::accepted, this, &OptionsDialog::accept);
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
connect(browse, &QPushButton::clicked, this, [this] {
const QStringList existingFolders = patterns(roots_->text());
const QString startFolder = existingFolders.isEmpty()
? QDir::homePath() : existingFolders.constLast();
const QString folder = QFileDialog::getExistingDirectory(
this, tr("Choose base folder"), startFolder);
if (folder.isEmpty())
return;
QStringList folders = existingFolders;
const QString cleanPath = QDir::cleanPath(folder);
if (!folders.contains(cleanPath))
folders.push_back(cleanPath);
roots_->setText(folders.join(u';'));
});
layout->addWidget(buttons);
}
SearchOptions OptionsDialog::options() const
{
SearchOptions o;
o.roots = roots_->text();
o.fileWildcards = fileMasks_->text();
o.subfolderWildcards = subfolderMasks_->text();
o.excludeFiles = excludeFiles_->text();
o.excludeFolders = excludeFolders_->text();
o.includeFolders = includeFolders_->text();
o.contains = contains_->toPlainText();
o.binary = binary_->isChecked();
o.multipleValues = multiple_->isChecked();
o.multipleAnd = multipleMode_->currentIndex() == 1;
o.caseSensitive = caseSensitive_->isChecked();
o.minSize = minSize_->value();
o.maxSize = maxSize_->value();
o.timeField = timeField_->currentText();
o.lastMinutes = lastMinutes_->value();
o.minTime = dateValue(minTime_);
o.maxTime = dateValue(maxTime_);
o.recursive = recursive_->isChecked();
o.findFiles = findFiles_->isChecked();
o.findFolders = findFolders_->isChecked();
o.followLinks = followLinks_->isChecked();
o.retrieveOwner = retrieveOwner_->isChecked();
o.accurateProgress = accurateProgress_->isChecked();
o.useCache = useCache_->isChecked();
o.maxDepth = maxDepth_->value();
o.maxResults = maxResults_->value();
o.mode = mode_->currentText();
o.duplicateNameMode = duplicateMode_->currentText();
o.duplicateNameWithoutExtension = duplicateWithoutExtension_->isChecked();
o.hidden = hidden_->currentText();
o.readonly = readonly_->currentText();
o.executable = executable_->currentText();
return o;
}
void OptionsDialog::accept()
{
if (!validateDate(minTime_, tr("From time"))
|| !validateDate(maxTime_, tr("To time")))
return;
QDialog::accept();
}
QLineEdit *OptionsDialog::addLine(QFormLayout *layout, const QString &label, const QString &value)
{
auto *edit = new QLineEdit(value);
layout->addRow(label, edit);
return edit;
}
LongLongSpinBox *OptionsDialog::sizeSpin(qint64 value)
{
auto *spin = new LongLongSpinBox;
spin->setRange(0, std::numeric_limits<qint64>::max());
spin->setSuffix(QObject::tr(" bytes"));
spin->setValue(value);
return spin;
}
qint64 OptionsDialog::dateValue(const QLineEdit *edit)
{
qint64 seconds = 0;
parseOptionalIsoDate(edit->text(), seconds);
return seconds;
}
bool OptionsDialog::validateDate(QLineEdit *edit, const QString &label)
{
qint64 seconds = 0;
if (parseOptionalIsoDate(edit->text(), seconds))
return true;
QMessageBox::warning(
this, tr("Invalid date"),
tr("%1 must be a valid ISO date and time, for example:\n2026-07-25T20:30:00")
.arg(label));
edit->setFocus();
edit->selectAll();
return false;
}
QComboBox *OptionsDialog::attrCombo(const QString &value)
{
auto *combo = new QComboBox;
combo->addItems({QObject::tr("Any"), QObject::tr("Yes"), QObject::tr("No")});
combo->setCurrentText(value);
return combo;
}
bool OptionsDialog::eventFilter(QObject *obj, QEvent *event)
{
if (obj == roots_ && event->type() == QEvent::DragEnter) {
auto *dragEnter = static_cast<QDragEnterEvent *>(event);
if (dragEnter->mimeData()->hasUrls()) {
dragEnter->acceptProposedAction();
return true;
}
return false;
}
if (obj == roots_ && event->type() == QEvent::Drop) {
auto *drop = static_cast<QDropEvent *>(event);
if (!drop->mimeData()->hasUrls())
return false;
QStringList folders = patterns(roots_->text());
for (const QUrl &url : drop->mimeData()->urls()) {
if (!url.isLocalFile())
continue;
const QString cleanPath = QDir::cleanPath(url.toLocalFile());
if (QFileInfo::exists(cleanPath) && !folders.contains(cleanPath))
folders.append(cleanPath);
}
roots_->setText(folders.join(u';'));
drop->acceptProposedAction();
return true;
}
return QDialog::eventFilter(obj, event);
}
void OptionsDialog::saveProfile()
{
bool ok = false;
const QString name = QInputDialog::getText(
this, tr("Save Profile"), tr("Profile name:"), QLineEdit::Normal, QString{}, &ok);
if (!ok || name.trimmed().isEmpty())
return;
const QString profileName = name.trimmed();
const SearchOptions opt = options();
QSettings settings;
settings.beginGroup(QStringLiteral("profiles"));
settings.beginGroup(profileName);
auto save = [&](const char *key, const QString &val) { settings.setValue(QString::fromLatin1(key), val); };
auto saveBool = [&](const char *key, bool val) { settings.setValue(QString::fromLatin1(key), val); };
auto saveInt = [&](const char *key, int val) { settings.setValue(QString::fromLatin1(key), val); };
auto saveLong = [&](const char *key, qint64 val) { settings.setValue(QString::fromLatin1(key), val); };
save("roots", opt.roots);
save("fileWildcards", opt.fileWildcards);
save("subfolderWildcards", opt.subfolderWildcards);
save("excludeFiles", opt.excludeFiles);
save("excludeFolders", opt.excludeFolders);
save("includeFolders", opt.includeFolders);
save("contains", opt.contains);
save("mode", opt.mode);
save("duplicateNameMode", opt.duplicateNameMode);
save("timeField", opt.timeField);
save("hidden", opt.hidden);
save("readonly", opt.readonly);
save("executable", opt.executable);
saveBool("binary", opt.binary);
saveBool("multipleValues", opt.multipleValues);
saveBool("multipleAnd", opt.multipleAnd);
saveBool("caseSensitive", opt.caseSensitive);
saveBool("recursive", opt.recursive);
saveBool("findFiles", opt.findFiles);
saveBool("findFolders", opt.findFolders);
saveBool("followLinks", opt.followLinks);
saveBool("retrieveOwner", opt.retrieveOwner);
saveBool("accurateProgress", opt.accurateProgress);
saveBool("useCache", opt.useCache);
saveBool("duplicateNameWithoutExtension", opt.duplicateNameWithoutExtension);
saveInt("lastMinutes", opt.lastMinutes);
saveInt("maxDepth", opt.maxDepth);
saveInt("maxResults", opt.maxResults);
saveLong("minSize", opt.minSize);
saveLong("maxSize", opt.maxSize);
saveLong("minTime", opt.minTime);
saveLong("maxTime", opt.maxTime);
settings.endGroup();
settings.endGroup();
if (profileCombo_->findText(profileName) < 0)
profileCombo_->addItem(profileName);
profileCombo_->setCurrentText(profileName);
}
void OptionsDialog::loadProfile()
{
const QString name = profileCombo_->currentText();
if (name.isEmpty() || name == tr("(default)"))
return;
QSettings settings;
settings.beginGroup(QStringLiteral("profiles"));
settings.beginGroup(name);
auto load = [&](const char *key) { return settings.value(QString::fromLatin1(key)).toString(); };
auto loadBool = [&](const char *key, bool def) { return settings.value(QString::fromLatin1(key), def).toBool(); };
auto loadInt = [&](const char *key, int def) { return settings.value(QString::fromLatin1(key), def).toInt(); };
auto loadLong = [&](const char *key, qint64 def) { return settings.value(QString::fromLatin1(key), def).toLongLong(); };
roots_->setText(load("roots"));
fileMasks_->setText(load("fileWildcards"));
subfolderMasks_->setText(load("subfolderWildcards"));
excludeFiles_->setText(load("excludeFiles"));
excludeFolders_->setText(load("excludeFolders"));
includeFolders_->setText(load("includeFolders"));
contains_->setText(load("contains"));
binary_->setChecked(loadBool("binary", false));
multiple_->setChecked(loadBool("multipleValues", false));
multipleMode_->setCurrentIndex(loadBool("multipleAnd", false) ? 1 : 0);
caseSensitive_->setChecked(loadBool("caseSensitive", false));
minSize_->setValue(loadLong("minSize", 0));
maxSize_->setValue(loadLong("maxSize", 0));
timeField_->setCurrentText(load("timeField"));
lastMinutes_->setValue(loadInt("lastMinutes", 0));
minTime_->setText(loadLong("minTime", 0)
? QDateTime::fromSecsSinceEpoch(loadLong("minTime", 0)).toString(Qt::ISODate) : QString{});
maxTime_->setText(loadLong("maxTime", 0)
? QDateTime::fromSecsSinceEpoch(loadLong("maxTime", 0)).toString(Qt::ISODate) : QString{});
recursive_->setChecked(loadBool("recursive", true));
findFiles_->setChecked(loadBool("findFiles", true));
findFolders_->setChecked(loadBool("findFolders", false));
followLinks_->setChecked(loadBool("followLinks", false));
retrieveOwner_->setChecked(loadBool("retrieveOwner", false));
accurateProgress_->setChecked(loadBool("accurateProgress", true));
useCache_->setChecked(loadBool("useCache", true));
maxDepth_->setValue(loadInt("maxDepth", 0));
maxResults_->setValue(loadInt("maxResults", 0));
mode_->setCurrentText(load("mode"));
duplicateMode_->setCurrentText(load("duplicateNameMode"));
duplicateWithoutExtension_->setChecked(loadBool("duplicateNameWithoutExtension", false));
hidden_->setCurrentText(load("hidden"));
readonly_->setCurrentText(load("readonly"));
executable_->setCurrentText(load("executable"));
settings.endGroup();
settings.endGroup();
}
+75
View File
@@ -0,0 +1,75 @@
#pragma once
#include <QCheckBox>
#include <QComboBox>
#include <QDialog>
#include <QFormLayout>
#include <QGroupBox>
#include <QLabel>
#include <QLineEdit>
#include <QPushButton>
#include <QSpinBox>
#include <QTabWidget>
#include <QTextEdit>
#include <QVBoxLayout>
#include "search_core.h"
class LongLongSpinBox;
class OptionsDialog final : public QDialog {
Q_OBJECT
public:
explicit OptionsDialog(const SearchOptions &o, QWidget *parent = nullptr);
SearchOptions options() const;
protected:
void accept() override;
private:
bool eventFilter(QObject *obj, QEvent *event) override;
static QLineEdit *addLine(QFormLayout *layout, const QString &label, const QString &value);
static LongLongSpinBox *sizeSpin(qint64 value);
static qint64 dateValue(const QLineEdit *edit);
bool validateDate(QLineEdit *edit, const QString &label);
static QComboBox *attrCombo(const QString &value);
void setupUi(const SearchOptions &o);
void saveProfile();
void loadProfile();
QComboBox *mode_{};
QComboBox *duplicateMode_{};
QCheckBox *duplicateWithoutExtension_{};
QLineEdit *roots_{};
QLineEdit *fileMasks_{};
QLineEdit *subfolderMasks_{};
QLineEdit *excludeFiles_{};
QLineEdit *excludeFolders_{};
QLineEdit *includeFolders_{};
QTextEdit *contains_{};
QCheckBox *binary_{};
QCheckBox *multiple_{};
QComboBox *multipleMode_{};
QCheckBox *caseSensitive_{};
LongLongSpinBox *minSize_{};
LongLongSpinBox *maxSize_{};
QComboBox *timeField_{};
QSpinBox *lastMinutes_{};
QLineEdit *minTime_{};
QLineEdit *maxTime_{};
QCheckBox *recursive_{};
QCheckBox *findFiles_{};
QCheckBox *findFolders_{};
QCheckBox *followLinks_{};
QCheckBox *retrieveOwner_{};
QCheckBox *accurateProgress_{};
QCheckBox *useCache_{};
QSpinBox *maxDepth_{};
QSpinBox *maxResults_{};
QComboBox *hidden_{};
QComboBox *readonly_{};
QComboBox *executable_{};
QComboBox *profileCombo_{};
};
+125
View File
@@ -0,0 +1,125 @@
#include "results_model.h"
#include <QDateTime>
#include <QFileInfo>
ResultsModel::ResultsModel(QObject *parent) : QAbstractTableModel(parent) {}
int ResultsModel::rowCount(const QModelIndex &parent) const
{
return parent.isValid() ? 0 : rows_.size();
}
int ResultsModel::columnCount(const QModelIndex &parent) const
{
return parent.isValid() ? 0 : ColumnCount;
}
QVariant ResultsModel::headerData(int section, Qt::Orientation orientation, int role) const
{
if (orientation != Qt::Horizontal)
return {};
if (role == Qt::ToolTipRole) {
if (section == DuplicateNumber)
return tr("Identifies one set of identical files");
if (section == DuplicateGroup)
return tr("Order based on the base-folder list; 1 = preferred copy to keep");
return {};
}
if (role != Qt::DisplayRole)
return {};
static const QStringList names{
tr("Filename"), tr("Folder"), tr("Size"), tr("Size On Disk"), tr("Modified Time"),
tr("Created Time"), tr("Last Accessed Time"), tr("Entry Modified Time"),
tr("Attributes"), tr("Extension"), tr("Duplicate Set"), tr("Keeper Priority"),
tr("Type"), tr("File Owner"), tr("Full Path")
};
return names.value(section);
}
QVariant ResultsModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid() || index.row() >= rows_.size())
return {};
const FileRecord &r = rows_[index.row()];
if (role == Qt::UserRole)
return r.path;
if (role == Qt::BackgroundRole && r.group && markDuplicates_) {
const int colorIndex = (r.group - 1) % 64;
return duplicateColorSet_ == 1
? QColor::fromHsv((colorIndex * 137) % 360, 38 + (colorIndex % 3) * 8, 255)
: QColor::fromHsv((colorIndex * 137) % 360, 85, 205);
}
if (role == Qt::ForegroundRole && r.group && markDuplicates_)
return duplicateColorSet_ == 1 ? QColor(Qt::black) : QColor(Qt::white);
if (role == Qt::TextAlignmentRole && index.column() == Size)
return int(Qt::AlignRight | Qt::AlignVCenter);
if (role == Qt::EditRole) {
if (index.column() == Size) return r.size;
if (index.column() == SizeOnDisk) return r.sizeOnDisk;
if (index.column() == Modified) return r.modified;
if (index.column() == Created) return r.created;
if (index.column() == Accessed) return r.accessed;
if (index.column() == Changed) return r.changed;
if (index.column() == DuplicateNumber) return r.group;
if (index.column() == DuplicateGroup) return r.duplicateCopy;
return data(index, Qt::DisplayRole);
}
if (role != Qt::DisplayRole)
return {};
const auto time = [this](qint64 value) {
QDateTime dateTime = QDateTime::fromSecsSinceEpoch(value);
if (showGmt_)
dateTime = dateTime.toUTC();
return value ? dateTime.toString(QStringLiteral("yyyy-MM-dd HH:mm:ss"))
: QString{};
};
switch (index.column()) {
case Name: return r.name;
case Folder: return r.folder;
case Size: return formattedSize(r.size);
case SizeOnDisk: return formattedSize(r.sizeOnDisk);
case Modified: return time(r.modified);
case Created: return time(r.created);
case Accessed: return time(r.accessed);
case Changed: return time(r.changed);
case Attributes: return r.attributes;
case Extension: return QFileInfo(r.name).suffix();
case DuplicateNumber: return r.group ? QVariant(r.group) : QVariant{};
case DuplicateGroup: return r.duplicateCopy ? QVariant(r.duplicateCopy) : QVariant{};
case Type: return r.type;
case Owner: return r.owner;
case FullPath: return r.path;
default: return {};
}
}
void ResultsModel::setRows(QVector<FileRecord> rows)
{
beginResetModel();
rows_ = std::move(rows);
endResetModel();
}
void ResultsModel::setDisplayOptions(int sizeUnit, bool showGmt, bool markDuplicates, int colorSet)
{
sizeUnit_ = sizeUnit;
showGmt_ = showGmt;
markDuplicates_ = markDuplicates;
duplicateColorSet_ = colorSet;
if (!rows_.isEmpty())
emit dataChanged(index(0, 0), index(rows_.size() - 1, ColumnCount - 1));
}
QString ResultsModel::formattedSize(qint64 bytes) const
{
if (sizeUnit_ == 0)
return humanSize(bytes);
static const double divisors[]{1.0, 1024.0, 1024.0 * 1024.0, 1024.0 * 1024.0 * 1024.0};
static const char *units[]{"Bytes", "KB", "MB", "GB"};
const int index = std::clamp(sizeUnit_ - 1, 0, 3);
return index == 0
? QStringLiteral("%1 Bytes").arg(bytes)
: QStringLiteral("%1 %2").arg(double(bytes) / divisors[index], 0, 'f', 2)
.arg(QString::fromLatin1(units[index]));
}
+37
View File
@@ -0,0 +1,37 @@
#pragma once
#include <QAbstractTableModel>
#include <QColor>
#include <QStringList>
#include <QVariant>
#include "search_core.h"
class ResultsModel final : public QAbstractTableModel {
Q_OBJECT
public:
enum Column { Name, Folder, Size, SizeOnDisk, Modified, Created, Accessed, Changed,
Attributes, Extension, DuplicateNumber, DuplicateGroup, Type, Owner,
FullPath, ColumnCount };
explicit ResultsModel(QObject *parent = nullptr);
int rowCount(const QModelIndex &parent = {}) const override;
int columnCount(const QModelIndex &parent = {}) const override;
QVariant headerData(int section, Qt::Orientation orientation, int role) const override;
QVariant data(const QModelIndex &index, int role) const override;
void setRows(QVector<FileRecord> rows);
const QVector<FileRecord> &rows() const { return rows_; }
void setDisplayOptions(int sizeUnit, bool showGmt, bool markDuplicates, int colorSet);
private:
QString formattedSize(qint64 bytes) const;
QVector<FileRecord> rows_;
int sizeUnit_ = 0;
bool showGmt_ = false;
bool markDuplicates_ = true;
int duplicateColorSet_ = 1;
};
+159
View File
@@ -0,0 +1,159 @@
#include "search_cache.h"
#include <QDateTime>
#include <QDataStream>
#include <QDir>
#include <QFile>
#include <QSaveFile>
#include <QStandardPaths>
#include <algorithm>
#include <utility>
SearchCache::SearchCache()
{
const QString cacheRoot = QStandardPaths::writableLocation(QStandardPaths::CacheLocation);
QDir().mkpath(cacheRoot);
path_ = QDir(cacheRoot).filePath(QStringLiteral("search-cache-v1.dat"));
load();
}
bool SearchCache::findHash(const FileRecord &record, QByteArray &digest)
{
if (!enabled_)
return false;
QMutexLocker lock(&mutex_);
auto it = entries_.find(record.path);
if (it == entries_.end() || !signatureMatches(*it, record) || it->sha256.isEmpty())
return false;
it->lastUsed = QDateTime::currentSecsSinceEpoch();
digest = it->sha256;
return true;
}
void SearchCache::storeHash(const FileRecord &record, const QByteArray &digest)
{
if (!enabled_ || digest.isEmpty())
return;
QMutexLocker lock(&mutex_);
SearchCacheEntry &entry = currentEntry(record);
entry.sha256 = digest;
}
std::optional<bool> SearchCache::findContent(const FileRecord &record, const QByteArray &queryKey)
{
if (!enabled_)
return std::nullopt;
QMutexLocker lock(&mutex_);
auto it = entries_.find(record.path);
if (it == entries_.end() || !signatureMatches(*it, record))
return std::nullopt;
const auto match = it->contentMatches.constFind(queryKey);
if (match == it->contentMatches.cend())
return std::nullopt;
it->lastUsed = QDateTime::currentSecsSinceEpoch();
return *match;
}
void SearchCache::storeContent(const FileRecord &record, const QByteArray &queryKey, bool matched)
{
if (!enabled_)
return;
QMutexLocker lock(&mutex_);
SearchCacheEntry &entry = currentEntry(record);
entry.contentMatches.insert(queryKey, matched);
}
void SearchCache::save()
{
if (!enabled_)
return;
QMutexLocker lock(&mutex_);
prune();
QSaveFile file(path_);
if (!file.open(QIODevice::WriteOnly))
return;
QDataStream stream(&file);
stream.setVersion(QDataStream::Qt_6_0);
stream << quint32(0x46534348) << quint32(1) << quint32(entries_.size());
for (auto it = entries_.cbegin(); it != entries_.cend(); ++it) {
stream << it.key() << it->size << it->modified << it->lastUsed << it->sha256;
stream << quint32(it->contentMatches.size());
for (auto match = it->contentMatches.cbegin(); match != it->contentMatches.cend(); ++match)
stream << match.key() << match.value();
}
if (stream.status() == QDataStream::Ok)
file.commit();
}
void SearchCache::clear()
{
QMutexLocker lock(&mutex_);
entries_.clear();
QFile::remove(path_);
}
bool SearchCache::signatureMatches(const SearchCacheEntry &entry, const FileRecord &record)
{
return entry.size == record.size && entry.modified == record.modified;
}
SearchCacheEntry &SearchCache::currentEntry(const FileRecord &record)
{
SearchCacheEntry &entry = entries_[record.path];
if (!signatureMatches(entry, record)) {
entry = {};
entry.size = record.size;
entry.modified = record.modified;
}
entry.lastUsed = QDateTime::currentSecsSinceEpoch();
return entry;
}
void SearchCache::load()
{
QFile file(path_);
if (!file.open(QIODevice::ReadOnly))
return;
QDataStream stream(&file);
stream.setVersion(QDataStream::Qt_6_0);
quint32 magic = 0;
quint32 version = 0;
quint32 count = 0;
stream >> magic >> version >> count;
if (magic != 0x46534348 || version != 1 || count > 200'000)
return;
for (quint32 index = 0; index < count && stream.status() == QDataStream::Ok; ++index) {
QString path;
SearchCacheEntry entry;
quint32 contentCount = 0;
stream >> path >> entry.size >> entry.modified >> entry.lastUsed >> entry.sha256;
stream >> contentCount;
if (contentCount > 256)
return;
for (quint32 contentIndex = 0; contentIndex < contentCount; ++contentIndex) {
QByteArray key;
bool matched = false;
stream >> key >> matched;
entry.contentMatches.insert(key, matched);
}
entries_.insert(path, std::move(entry));
}
if (stream.status() != QDataStream::Ok)
entries_.clear();
}
void SearchCache::prune()
{
constexpr qsizetype maxEntries = 100'000;
if (entries_.size() <= maxEntries)
return;
QVector<QPair<qint64, QString>> ages;
ages.reserve(entries_.size());
for (auto it = entries_.cbegin(); it != entries_.cend(); ++it)
ages.push_back({it->lastUsed, it.key()});
std::sort(ages.begin(), ages.end(),
[](const auto &left, const auto &right) { return left.first > right.first; });
for (qsizetype index = maxEntries; index < ages.size(); ++index)
entries_.remove(ages[index].second);
}
+42
View File
@@ -0,0 +1,42 @@
#pragma once
#include <QByteArray>
#include <QHash>
#include <QMutex>
#include <QString>
#include "search_core.h"
struct SearchCacheEntry {
qint64 size = -1;
qint64 modified = -1;
qint64 lastUsed = 0;
QByteArray sha256;
QHash<QByteArray, bool> contentMatches;
};
class SearchCache {
public:
SearchCache();
void setEnabled(bool enabled) { enabled_ = enabled; }
bool enabled() const { return enabled_; }
bool findHash(const FileRecord &record, QByteArray &digest);
void storeHash(const FileRecord &record, const QByteArray &digest);
std::optional<bool> findContent(const FileRecord &record, const QByteArray &queryKey);
void storeContent(const FileRecord &record, const QByteArray &queryKey, bool matched);
void save();
void clear();
private:
static bool signatureMatches(const SearchCacheEntry &entry, const FileRecord &record);
SearchCacheEntry &currentEntry(const FileRecord &record);
void load();
void prune();
QHash<QString, SearchCacheEntry> entries_;
QMutex mutex_;
QString path_;
bool enabled_ = true;
};
+452
View File
@@ -0,0 +1,452 @@
#include "search_engine.h"
#include <QCryptographicHash>
#include <QDataStream>
#include <QDateTime>
#include <QDir>
#include <QFileInfo>
#include <QThread>
#include <QtConcurrent>
#include <algorithm>
#include <filesystem>
#include <functional>
#include <sys/stat.h>
#include <unordered_map>
#include <utility>
#include <vector>
namespace fs = std::filesystem;
void SearchEngine::search(const SearchOptions &o)
{
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;
cache_.setEnabled(o.useCache);
cacheHits_.store(0, std::memory_order_relaxed);
cacheMisses_.store(0, std::memory_order_relaxed);
const auto started = std::chrono::steady_clock::now();
QVector<FileRecord> candidates;
const Qt::CaseSensitivity cs = o.caseSensitive ? Qt::CaseSensitive : Qt::CaseInsensitive;
const QStringList fileMasks = patterns(o.fileWildcards).isEmpty()
? QStringList{QStringLiteral("*")} : patterns(o.fileWildcards);
const QStringList subfolderMasks = patterns(o.subfolderWildcards).isEmpty()
? QStringList{QStringLiteral("*")} : patterns(o.subfolderWildcards);
const QStringList excludedFiles = exclusionPatterns(o.excludeFiles);
const QStringList excludedFolders = patterns(o.excludeFolders);
const QStringList includedFolders = patterns(o.includeFolders);
quint64 scanned = 0;
quint64 totalEntries = 0;
if (o.accurateProgress) {
emit phaseProgress(tr("Counting files"), 0, 0);
for (const QString &rootText : patterns(o.roots)) {
if (cancelled_.load(std::memory_order_relaxed))
break;
std::error_code ec;
const fs::path root = fs::path(rootText.toStdString());
fs::directory_options dirOptions = fs::directory_options::skip_permission_denied;
if (o.followLinks)
dirOptions |= fs::directory_options::follow_directory_symlink;
if (!o.recursive) {
for (fs::directory_iterator it(root, dirOptions, ec), end;
it != end && !ec && !cancelled_.load(std::memory_order_relaxed);
it.increment(ec)) {
++totalEntries;
if (totalEntries % 2048 == 0)
emit phaseProgress(tr("Counting files"), totalEntries, 0);
}
continue;
}
for (fs::recursive_directory_iterator it(root, dirOptions, ec), end;
it != end && !ec && !cancelled_.load(std::memory_order_relaxed); it.increment(ec)) {
if (it->is_directory(ec)) {
const QString name = QString::fromStdString(it->path().filename().string());
const QString fullPath = QString::fromStdString(it->path().string());
if ((!excludedFolders.isEmpty()
&& (wildcardMatch(name, excludedFolders, cs)
|| wildcardMatch(fullPath, excludedFolders, cs)))
|| (o.maxDepth > 0 && it.depth() >= o.maxDepth - 1)) {
it.disable_recursion_pending();
}
}
++totalEntries;
if (totalEntries % 2048 == 0)
emit phaseProgress(tr("Counting files"), totalEntries, 0);
}
}
emit phaseProgress(tr("Scanning files"), 0, totalEntries);
}
for (const QString &rootText : patterns(o.roots)) {
if (cancelled_.load(std::memory_order_relaxed))
break;
std::error_code ec;
fs::path root = fs::path(rootText.toStdString());
fs::directory_options dirOptions = fs::directory_options::skip_permission_denied;
if (o.followLinks)
dirOptions |= fs::directory_options::follow_directory_symlink;
auto process = [&](const fs::directory_entry &entry, const fs::path &baseRoot) {
if (cancelled_.load(std::memory_order_relaxed))
return;
const QString path = QString::fromStdString(entry.path().string());
const QString name = QString::fromStdString(entry.path().filename().string());
const bool isDir = entry.is_directory(ec);
if ((isDir && !o.findFolders) || (!isDir && !o.findFiles))
return;
const fs::path parentPath = entry.path().parent_path();
const bool isBaseFolder = parentPath == baseRoot;
const QString parentName = QString::fromStdString(parentPath.filename().string());
if (!isBaseFolder && !wildcardMatch(parentName, subfolderMasks, cs))
return;
if (!wildcardMatch(name, fileMasks, cs))
return;
if (!excludedFiles.isEmpty() && wildcardMatch(name, excludedFiles, cs))
return;
if (!includedFolders.isEmpty()) {
const QString folderPath = isDir ? path : QFileInfo(path).absolutePath();
const QString folderName = isDir ? name : QFileInfo(path).dir().dirName();
if (!wildcardMatch(folderPath, includedFolders, cs)
&& !wildcardMatch(folderName, includedFolders, cs))
return;
}
struct stat st {};
const QByteArray nativePath = QFile::encodeName(path);
if (::stat(nativePath.constData(), &st) != 0)
return;
const qint64 size = isDir ? 0 : st.st_size;
if (o.minSize > 0 && size < o.minSize)
return;
if (o.maxSize > 0 && size > o.maxSize)
return;
const qint64 selectedTime = o.timeField == QStringLiteral("Accessed time")
? st.st_atime
: o.timeField == QStringLiteral("Metadata changed time") ? st.st_ctime : st.st_mtime;
const qint64 now = QDateTime::currentSecsSinceEpoch();
if (o.lastMinutes > 0 && selectedTime < now - qint64(o.lastMinutes) * 60)
return;
if (o.minTime > 0 && selectedTime < o.minTime)
return;
if (o.maxTime > 0 && selectedTime > o.maxTime)
return;
const bool hidden = name.startsWith(u'.');
const bool readOnly = !(st.st_mode & S_IWUSR);
const bool executable = st.st_mode & S_IXUSR;
const auto attrMatches = [](const QString &wanted, bool actual) {
return wanted == QStringLiteral("Any") || (wanted == QStringLiteral("Yes")) == actual;
};
if (!attrMatches(o.hidden, hidden)
|| !attrMatches(o.readonly, readOnly)
|| !attrMatches(o.executable, executable))
return;
QString attrs;
attrs += isDir ? u'd' : u'-';
attrs += hidden ? u'h' : u'-';
attrs += readOnly ? u'r' : u'-';
attrs += executable ? u'x' : u'-';
attrs += entry.is_symlink(ec) ? u'l' : u'-';
candidates.push_back({
path, name, QFileInfo(path).absolutePath(), size,
isDir ? 0 : qint64(st.st_blocks) * 512,
st.st_mtime,
QFileInfo(path).birthTime().isValid() ? QFileInfo(path).birthTime().toSecsSinceEpoch() : 0,
st.st_atime, st.st_ctime,
isDir ? QStringLiteral("Folder") : QStringLiteral("File"),
o.retrieveOwner ? ownerName(st.st_uid) : QString{}, attrs, 0, 0
});
if (o.maxResults > 0 && o.contains.isEmpty() && candidates.size() >= o.maxResults)
cancelledByLimit_ = true;
};
if (!o.recursive) {
for (fs::directory_iterator it(root, dirOptions, ec), end;
it != end && !ec && !cancelled_.load(std::memory_order_relaxed)
&& !cancelledByLimit_;
it.increment(ec)) {
process(*it, root);
if (++scanned % 512 == 0) {
if (totalEntries)
emit phaseProgress(tr("Scanning files"), scanned, totalEntries);
else
emit progress(QString::fromStdString(it->path().string()), scanned);
}
}
continue;
}
for (fs::recursive_directory_iterator it(root, dirOptions, ec), end;
it != end && !ec && !cancelled_.load(std::memory_order_relaxed)
&& !cancelledByLimit_; it.increment(ec)) {
const QString name = QString::fromStdString(it->path().filename().string());
if (it->is_directory(ec)) {
const QString fullPath = QString::fromStdString(it->path().string());
if (!excludedFolders.isEmpty()
&& (wildcardMatch(name, excludedFolders, cs)
|| wildcardMatch(fullPath, excludedFolders, cs))) {
it.disable_recursion_pending();
}
if (o.maxDepth > 0 && it.depth() >= o.maxDepth - 1)
it.disable_recursion_pending();
}
process(*it, root);
if (++scanned % 512 == 0) {
if (totalEntries)
emit phaseProgress(tr("Scanning files"), scanned, totalEntries);
else
emit progress(QString::fromStdString(it->path().string()), scanned);
}
}
if (cancelledByLimit_)
break;
}
if (totalEntries)
emit phaseProgress(tr("Scanning files"), std::min(scanned, totalEntries), totalEntries);
if (finishIfCancelled())
return;
if (!o.contains.isEmpty() && !o.findFolders) {
emit phaseProgress(tr("Searching file contents"), 0, candidates.size());
std::atomic<qsizetype> checked = 0;
const qsizetype total = candidates.size();
QFuture<FileRecord> future = QtConcurrent::filtered(
candidates, [this, o, &checked, total](const FileRecord &r) {
if (cancelled_.load(std::memory_order_relaxed))
return false;
const bool matched = cachedFileContains(r, o);
const qsizetype done = checked.fetch_add(1, std::memory_order_relaxed) + 1;
if (done == total || done % 32 == 0)
emit phaseProgress(tr("Searching file contents"), done, total);
return matched;
});
future.waitForFinished();
if (finishIfCancelled())
return;
candidates = future.results();
if (o.maxResults > 0 && candidates.size() > o.maxResults)
candidates.resize(o.maxResults);
}
QVector<FileRecord> results;
if (o.mode == QStringLiteral("Duplicates search")
|| o.mode == QStringLiteral("Non-Duplicates search")) {
std::unordered_map<qint64, QVector<FileRecord>> bySize;
for (const auto &record : candidates)
if (record.type == QStringLiteral("File"))
bySize[record.size].push_back(record);
quint64 hashTotal = 0;
for (const auto &[size, records] : bySize) {
Q_UNUSED(size);
if (records.size() > 1)
hashTotal += records.size();
}
std::atomic<quint64> hashed = 0;
if (hashTotal)
emit phaseProgress(tr("Checking duplicates"), 0, hashTotal);
int group = 1;
QSet<QString> duplicatedPaths;
for (auto &[size, sameSize] : bySize) {
if (cancelled_.load(std::memory_order_relaxed))
break;
Q_UNUSED(size);
if (sameSize.size() < 2)
continue;
auto hashes = QtConcurrent::blockingMapped(sameSize, [this, &hashed, hashTotal](const FileRecord &r) {
auto result = std::pair<QByteArray, FileRecord>{cachedSha256(r), r};
const quint64 done = hashed.fetch_add(1, std::memory_order_relaxed) + 1;
if (done == hashTotal || done % 16 == 0)
emit phaseProgress(tr("Checking duplicates"), done, hashTotal);
return result;
});
if (cancelled_.load(std::memory_order_relaxed))
break;
QHash<QByteArray, QVector<FileRecord>> sameHash;
for (auto &[hash, record] : hashes)
if (!hash.isEmpty())
sameHash[hash].push_back(record);
for (const auto &hashCandidates : sameHash) {
if (cancelled_.load(std::memory_order_relaxed))
break;
if (hashCandidates.size() < 2)
continue;
QVector<QVector<FileRecord>> exactGroups;
for (const auto &record : hashCandidates) {
if (cancelled_.load(std::memory_order_relaxed))
break;
bool placed = false;
for (auto &exact : exactGroups) {
if (filesEqual(exact.front().path, record.path, &cancelled_)) {
exact.push_back(record);
placed = true;
break;
}
}
if (!placed)
exactGroups.push_back({record});
}
for (auto &exact : exactGroups) {
if (cancelled_.load(std::memory_order_relaxed))
break;
if (exact.size() < 2)
continue;
int duplicateCopy = 1;
for (auto &record : exact) {
record.group = group;
record.duplicateCopy = duplicateCopy++;
duplicatedPaths.insert(record.path);
if (o.mode == QStringLiteral("Duplicates search")
&& shouldShowDuplicateResult(
o.showDuplicateCopiesOnly, record.duplicateCopy)) {
results.push_back(record);
}
}
++group;
}
}
}
if (finishIfCancelled())
return;
if (o.mode == QStringLiteral("Non-Duplicates search")) {
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")) {
QHash<QString, QVector<FileRecord>> byName;
for (const auto &record : candidates) {
const QString key = o.duplicateNameWithoutExtension
? QFileInfo(record.name).completeBaseName().toCaseFolded()
: record.name.toCaseFolded();
byName[key].push_back(record);
}
int group = 1;
quint64 namesDone = 0;
emit phaseProgress(tr("Comparing duplicate names"), 0, byName.size());
for (auto sameName : byName) {
if (cancelled_.load(std::memory_order_relaxed))
break;
++namesDone;
if (namesDone % 32 == 0 || namesDone == quint64(byName.size()))
emit phaseProgress(tr("Comparing duplicate names"), namesDone, byName.size());
if (o.duplicateNameMode != QStringLiteral("All files and folders"))
sameName.erase(std::remove_if(sameName.begin(), sameName.end(),
[](const FileRecord &r) { return r.type != QStringLiteral("File"); }), sameName.end());
if (sameName.size() < 2)
continue;
if (o.duplicateNameMode.contains(QStringLiteral("identical"), Qt::CaseInsensitive)) {
const QByteArray firstHash = cachedSha256(sameName.front());
bool identical = !firstHash.isEmpty();
for (qsizetype i = 1; identical && i < sameName.size(); ++i) {
identical = cachedSha256(sameName[i]) == firstHash
&& filesEqual(
sameName.front().path, sameName[i].path, &cancelled_);
}
const bool wantsNonIdentical = o.duplicateNameMode.startsWith(QStringLiteral("Only non"));
if (identical == wantsNonIdentical)
continue;
}
int duplicateCopy = 1;
for (auto &record : sameName) {
record.group = group;
record.duplicateCopy = duplicateCopy++;
results.push_back(record);
}
++group;
}
if (finishIfCancelled())
return;
} else if (o.mode == QStringLiteral("Summary")) {
QHash<QString, QVector<FileRecord>> byFolder;
for (const auto &record : candidates) {
if (cancelled_.load(std::memory_order_relaxed))
break;
byFolder[record.folder].push_back(record);
if (o.includeSubfoldersInSummary) {
QDir parent(record.folder);
while (parent.cdUp()) {
const QString ancestor = parent.absolutePath();
bool belongsToRoot = false;
for (const QString &root : patterns(o.roots)) {
if (ancestor == QDir(root).absolutePath()) {
belongsToRoot = true;
break;
}
}
byFolder[ancestor].push_back(record);
if (belongsToRoot)
break;
}
}
}
for (auto it = byFolder.cbegin(); it != byFolder.cend(); ++it) {
if (cancelled_.load(std::memory_order_relaxed))
break;
qint64 total = 0;
qint64 totalOnDisk = 0;
qint64 newest = 0;
for (const auto &record : it.value()) {
total += record.size;
totalOnDisk += record.sizeOnDisk;
newest = std::max(newest, record.modified);
}
const QFileInfo info(it.key());
results.push_back({it.key(), info.fileName(), info.absolutePath(), total, totalOnDisk,
newest, 0, 0, 0, tr("%1 files").arg(it.value().size()),
{}, {}, 0, 0});
}
if (finishIfCancelled())
return;
} else {
results = std::move(candidates);
}
if (finishIfCancelled())
return;
const double seconds = std::chrono::duration<double>(
std::chrono::steady_clock::now() - started).count();
cache_.save();
emit finished(results, tr("%1 results, %2 entries scanned in %3 seconds; cache: %4 hits")
.arg(results.size()).arg(scanned).arg(seconds, 0, 'f', 2)
.arg(cacheHits_.load(std::memory_order_relaxed)));
}
QByteArray SearchEngine::cachedSha256(const FileRecord &record)
{
QByteArray digest;
if (cache_.findHash(record, digest)) {
cacheHits_.fetch_add(1, std::memory_order_relaxed);
return digest;
}
cacheMisses_.fetch_add(1, std::memory_order_relaxed);
digest = sha256(record.path, &cancelled_);
if (!cancelled_.load(std::memory_order_relaxed))
cache_.storeHash(record, digest);
return digest;
}
bool SearchEngine::cachedFileContains(const FileRecord &record, const SearchOptions &o)
{
QByteArray queryData;
QDataStream stream(&queryData, QIODevice::WriteOnly);
stream << o.contains << o.binary << o.multipleValues << o.multipleAnd << o.caseSensitive;
const QByteArray queryKey = QCryptographicHash::hash(queryData, QCryptographicHash::Sha256);
if (const auto cached = cache_.findContent(record, queryKey)) {
cacheHits_.fetch_add(1, std::memory_order_relaxed);
return *cached;
}
cacheMisses_.fetch_add(1, std::memory_order_relaxed);
const bool matched = fileContains(record.path, o, &cancelled_);
if (!cancelled_.load(std::memory_order_relaxed))
cache_.storeContent(record, queryKey, matched);
return matched;
}
+43
View File
@@ -0,0 +1,43 @@
#pragma once
#include <QObject>
#include <QVector>
#include <QString>
#include <atomic>
#include <chrono>
#include "search_core.h"
#include "search_cache.h"
class SearchEngine final : public QObject {
Q_OBJECT
public:
explicit SearchEngine(QObject *parent = nullptr) : QObject(parent) {}
public slots:
void stop() { cancelled_.store(true, std::memory_order_relaxed); }
void clearCache()
{
cache_.clear();
emit cacheCleared();
}
void search(const SearchOptions &o);
signals:
void progress(const QString &path, quint64 scanned);
void phaseProgress(const QString &phase, quint64 completed, quint64 total);
void cacheCleared();
void finished(const QVector<FileRecord> &results, const QString &message);
private:
QByteArray cachedSha256(const FileRecord &record);
bool cachedFileContains(const FileRecord &record, const SearchOptions &o);
SearchCache cache_;
std::atomic<quint64> cacheHits_ = 0;
std::atomic<quint64> cacheMisses_ = 0;
std::atomic_bool cancelled_ = false;
bool cancelledByLimit_ = false;
};