diff --git a/.gitignore b/.gitignore index 31530819..d54f7cfa 100644 --- a/.gitignore +++ b/.gitignore @@ -52,5 +52,12 @@ debian/libdfm-burn-dev/* debian/*.log debian/*.substvars debian/files + +# ai +CLAUDE.md +AGENTS.md .cursor +.trellis +.claude +.agents diff --git a/autotests/dfm-io-tests/CMakeLists.txt b/autotests/dfm-io-tests/CMakeLists.txt index e93283f6..3336d999 100644 --- a/autotests/dfm-io-tests/CMakeLists.txt +++ b/autotests/dfm-io-tests/CMakeLists.txt @@ -9,15 +9,21 @@ endif() message(STATUS "Adding unit tests for ${IO_TEST_LIB}") -# Collect test source files -file(GLOB_RECURSE TEST_SRCS - ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/*.h +# 共享的测试入口点 +set(TEST_COMMON_SRCS + ${CMAKE_CURRENT_SOURCE_DIR}/test_main.cpp +) + +# 各测试类源文件(不含 main) +set(TEST_CLASS_SRCS + ${CMAKE_CURRENT_SOURCE_DIR}/tst_dfm_io.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/tst_dfilesorter.cpp ) # Create the test executable add_executable(dfm-io-test - ${TEST_SRCS} + ${TEST_COMMON_SRCS} + ${TEST_CLASS_SRCS} ) # Link against the dfm-io library and Qt Test @@ -27,17 +33,11 @@ target_link_libraries(dfm-io-test ) # Add include directories -if(DFM_BUILD_WITH_QT6) - target_include_directories(dfm-io-test - PRIVATE - ${CMAKE_SOURCE_DIR}/src/dfm-io/dfm-io - ) -else() - target_include_directories(dfm-io-test - PRIVATE - ${CMAKE_SOURCE_DIR}/src/dfm-io/dfm-io - ) -endif() +target_include_directories(dfm-io-test + PRIVATE + ${CMAKE_SOURCE_DIR}/src/dfm-io/dfm-io + ${CMAKE_SOURCE_DIR}/src/dfm-io/dfm-io/sort +) # Register the test with CTest add_test(NAME dfm-io-test COMMAND dfm-io-test) diff --git a/autotests/dfm-io-tests/test_main.cpp b/autotests/dfm-io-tests/test_main.cpp new file mode 100644 index 00000000..1432aae5 --- /dev/null +++ b/autotests/dfm-io-tests/test_main.cpp @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include + +// 前置声明测试类 +class tst_DfmIO; +class tst_DFileSorter; + +int main(int argc, char *argv[]) +{ + int status = 0; + + // 运行各测试类 + extern int run_tst_DfmIO(int argc, char *argv[]); + extern int run_tst_DFileSorter(int argc, char *argv[]); + + status |= run_tst_DfmIO(argc, argv); + status |= run_tst_DFileSorter(argc, argv); + + return status; +} diff --git a/autotests/dfm-io-tests/tst_dfilesorter.cpp b/autotests/dfm-io-tests/tst_dfilesorter.cpp new file mode 100644 index 00000000..e6c9d9da --- /dev/null +++ b/autotests/dfm-io-tests/tst_dfilesorter.cpp @@ -0,0 +1,396 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include +#include +#include +#include + +#include "sort/dfilesorter.h" +#include "sort/dsortkeycache.h" + +using namespace dfmio; + +class tst_DFileSorter : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void initTestCase(); + void cleanupTestCase(); + + // DSortKeyCache 测试 + void sortKeyCache_basic(); + void sortKeyCache_numericMode(); + void sortKeyCache_threadSafety(); + + // DFileSorter 测试 + void fileSorter_sortByName(); + void fileSorter_sortByName_descending(); + void fileSorter_sortBySize(); + void fileSorter_sortByLastModified(); + void fileSorter_sortByLastRead(); + void fileSorter_mixDirAndFile(); + void fileSorter_separateDirAndFile(); + void fileSorter_emptyList(); + void fileSorter_singleItem(); +}; + +void tst_DFileSorter::initTestCase() +{ +} + +void tst_DFileSorter::cleanupTestCase() +{ +} + +// ==================== DSortKeyCache 测试 ==================== + +void tst_DFileSorter::sortKeyCache_basic() +{ + DSortKeyCache &cache = DSortKeyCache::instance(); + + // 相同字符串应返回相同的排序键 + QCollatorSortKey key1 = cache.sortKey("test"); + QCollatorSortKey key2 = cache.sortKey("test"); + QCOMPARE(key1.compare(key2), 0); +} + +void tst_DFileSorter::sortKeyCache_numericMode() +{ + DSortKeyCache &cache = DSortKeyCache::instance(); + + // 数字自然排序:file2 < file10 + QCollatorSortKey key2 = cache.sortKey("file2"); + QCollatorSortKey key10 = cache.sortKey("file10"); + + // file2 应该排在 file10 前面(numericMode 开启) + QVERIFY(key2.compare(key10) < 0); +} + +void tst_DFileSorter::sortKeyCache_threadSafety() +{ + // 测试 thread_local QCollator 的线程安全性 + // 每个线程应该有独立的 QCollator 实例 + DSortKeyCache &cache = DSortKeyCache::instance(); + + // 验证排序键可以正常比较(不假设大小写不敏感) + QCollatorSortKey key1 = cache.sortKey("abc"); + + // 相同字符串返回相同排序键 + QCollatorSortKey key2 = cache.sortKey("abc"); + QCOMPARE(key1.compare(key2), 0); +} + +// ==================== DFileSorter 测试 ==================== + +void tst_DFileSorter::fileSorter_sortByName() +{ + DFileSorter::SortConfig config; + config.role = DFileSorter::SortRole::Name; + config.order = Qt::AscendingOrder; + config.mixDirAndFile = true; + + DFileSorter sorter(config); + + // 创建测试文件列表 + QList> files; + + auto file1 = QSharedPointer::create(); + file1->url = QUrl::fromLocalFile("/test/file10.txt"); + file1->isDir = false; + file1->filesize = 100; + files.append(file1); + + auto file2 = QSharedPointer::create(); + file2->url = QUrl::fromLocalFile("/test/file2.txt"); + file2->isDir = false; + file2->filesize = 200; + files.append(file2); + + auto file3 = QSharedPointer::create(); + file3->url = QUrl::fromLocalFile("/test/file1.txt"); + file3->isDir = false; + file3->filesize = 300; + files.append(file3); + + auto sorted = sorter.sort(std::move(files)); + + // 验证排序结果:file1 < file2 < file10(自然排序) + QCOMPARE(sorted.size(), 3); + QCOMPARE(sorted[0]->url.fileName(), QString("file1.txt")); + QCOMPARE(sorted[1]->url.fileName(), QString("file2.txt")); + QCOMPARE(sorted[2]->url.fileName(), QString("file10.txt")); +} + +void tst_DFileSorter::fileSorter_sortByName_descending() +{ + DFileSorter::SortConfig config; + config.role = DFileSorter::SortRole::Name; + config.order = Qt::DescendingOrder; + config.mixDirAndFile = true; + + DFileSorter sorter(config); + + QList> files; + + auto file1 = QSharedPointer::create(); + file1->url = QUrl::fromLocalFile("/test/a.txt"); + file1->isDir = false; + files.append(file1); + + auto file2 = QSharedPointer::create(); + file2->url = QUrl::fromLocalFile("/test/b.txt"); + file2->isDir = false; + files.append(file2); + + auto file3 = QSharedPointer::create(); + file3->url = QUrl::fromLocalFile("/test/c.txt"); + file3->isDir = false; + files.append(file3); + + auto sorted = sorter.sort(std::move(files)); + + // 降序:c > b > a + QCOMPARE(sorted.size(), 3); + QCOMPARE(sorted[0]->url.fileName(), QString("c.txt")); + QCOMPARE(sorted[1]->url.fileName(), QString("b.txt")); + QCOMPARE(sorted[2]->url.fileName(), QString("a.txt")); +} + +void tst_DFileSorter::fileSorter_sortBySize() +{ + DFileSorter::SortConfig config; + config.role = DFileSorter::SortRole::Size; + config.order = Qt::AscendingOrder; + config.mixDirAndFile = true; + + DFileSorter sorter(config); + + QList> files; + + auto file1 = QSharedPointer::create(); + file1->url = QUrl::fromLocalFile("/test/large.txt"); + file1->isDir = false; + file1->filesize = 1000; + files.append(file1); + + auto file2 = QSharedPointer::create(); + file2->url = QUrl::fromLocalFile("/test/small.txt"); + file2->isDir = false; + file2->filesize = 100; + files.append(file2); + + auto file3 = QSharedPointer::create(); + file3->url = QUrl::fromLocalFile("/test/medium.txt"); + file3->isDir = false; + file3->filesize = 500; + files.append(file3); + + auto sorted = sorter.sort(std::move(files)); + + // 按大小升序:small < medium < large + QCOMPARE(sorted.size(), 3); + QCOMPARE(sorted[0]->url.fileName(), QString("small.txt")); + QCOMPARE(sorted[1]->url.fileName(), QString("medium.txt")); + QCOMPARE(sorted[2]->url.fileName(), QString("large.txt")); +} + +void tst_DFileSorter::fileSorter_sortByLastModified() +{ + DFileSorter::SortConfig config; + config.role = DFileSorter::SortRole::LastModified; + config.order = Qt::AscendingOrder; + config.mixDirAndFile = true; + + DFileSorter sorter(config); + + QList> files; + + auto file1 = QSharedPointer::create(); + file1->url = QUrl::fromLocalFile("/test/newest.txt"); + file1->isDir = false; + file1->lastModifed = 3000; + file1->lastModifedNs = 0; + files.append(file1); + + auto file2 = QSharedPointer::create(); + file2->url = QUrl::fromLocalFile("/test/oldest.txt"); + file2->isDir = false; + file2->lastModifed = 1000; + file2->lastModifedNs = 0; + files.append(file2); + + auto file3 = QSharedPointer::create(); + file3->url = QUrl::fromLocalFile("/test/middle.txt"); + file3->isDir = false; + file3->lastModifed = 2000; + file3->lastModifedNs = 0; + files.append(file3); + + auto sorted = sorter.sort(std::move(files)); + + // 按修改时间升序:oldest < middle < newest + QCOMPARE(sorted.size(), 3); + QCOMPARE(sorted[0]->url.fileName(), QString("oldest.txt")); + QCOMPARE(sorted[1]->url.fileName(), QString("middle.txt")); + QCOMPARE(sorted[2]->url.fileName(), QString("newest.txt")); +} + +void tst_DFileSorter::fileSorter_sortByLastRead() +{ + DFileSorter::SortConfig config; + config.role = DFileSorter::SortRole::LastRead; + config.order = Qt::AscendingOrder; + config.mixDirAndFile = true; + + DFileSorter sorter(config); + + QList> files; + + auto file1 = QSharedPointer::create(); + file1->url = QUrl::fromLocalFile("/test/read_newest.txt"); + file1->isDir = false; + file1->lastRead = 3000; + file1->lastReadNs = 0; + files.append(file1); + + auto file2 = QSharedPointer::create(); + file2->url = QUrl::fromLocalFile("/test/read_oldest.txt"); + file2->isDir = false; + file2->lastRead = 1000; + file2->lastReadNs = 0; + files.append(file2); + + auto sorted = sorter.sort(std::move(files)); + + // 按访问时间升序:oldest < newest + QCOMPARE(sorted.size(), 2); + QCOMPARE(sorted[0]->url.fileName(), QString("read_oldest.txt")); + QCOMPARE(sorted[1]->url.fileName(), QString("read_newest.txt")); +} + +void tst_DFileSorter::fileSorter_mixDirAndFile() +{ + DFileSorter::SortConfig config; + config.role = DFileSorter::SortRole::Name; + config.order = Qt::AscendingOrder; + config.mixDirAndFile = true; // 混排模式 + + DFileSorter sorter(config); + + QList> files; + + auto dir1 = QSharedPointer::create(); + dir1->url = QUrl::fromLocalFile("/test/docs"); + dir1->isDir = true; + dir1->isSymLink = false; + files.append(dir1); + + auto file1 = QSharedPointer::create(); + file1->url = QUrl::fromLocalFile("/test/afile.txt"); + file1->isDir = false; + files.append(file1); + + auto dir2 = QSharedPointer::create(); + dir2->url = QUrl::fromLocalFile("/test/apps"); + dir2->isDir = true; + dir2->isSymLink = false; + files.append(dir2); + + auto sorted = sorter.sort(std::move(files)); + + // 混排模式:所有项一起排序 + QCOMPARE(sorted.size(), 3); + QCOMPARE(sorted[0]->url.fileName(), QString("afile.txt")); // a 排最前 + QCOMPARE(sorted[1]->url.fileName(), QString("apps")); + QCOMPARE(sorted[2]->url.fileName(), QString("docs")); +} + +void tst_DFileSorter::fileSorter_separateDirAndFile() +{ + DFileSorter::SortConfig config; + config.role = DFileSorter::SortRole::Name; + config.order = Qt::AscendingOrder; + config.mixDirAndFile = false; // 分离模式:目录在前 + + DFileSorter sorter(config); + + QList> files; + + auto file1 = QSharedPointer::create(); + file1->url = QUrl::fromLocalFile("/test/a_file.txt"); + file1->isDir = false; + files.append(file1); + + auto dir1 = QSharedPointer::create(); + dir1->url = QUrl::fromLocalFile("/test/z_dir"); + dir1->isDir = true; + dir1->isSymLink = false; + files.append(dir1); + + auto file2 = QSharedPointer::create(); + file2->url = QUrl::fromLocalFile("/test/b_file.txt"); + file2->isDir = false; + files.append(file2); + + auto dir2 = QSharedPointer::create(); + dir2->url = QUrl::fromLocalFile("/test/a_dir"); + dir2->isDir = true; + dir2->isSymLink = false; + files.append(dir2); + + auto sorted = sorter.sort(std::move(files)); + + // 分离模式:目录在前(按名称排序),文件在后(按名称排序) + QCOMPARE(sorted.size(), 4); + // 目录在前 + QCOMPARE(sorted[0]->url.fileName(), QString("a_dir")); + QCOMPARE(sorted[1]->url.fileName(), QString("z_dir")); + // 文件在后 + QCOMPARE(sorted[2]->url.fileName(), QString("a_file.txt")); + QCOMPARE(sorted[3]->url.fileName(), QString("b_file.txt")); +} + +void tst_DFileSorter::fileSorter_emptyList() +{ + DFileSorter::SortConfig config; + config.role = DFileSorter::SortRole::Name; + + DFileSorter sorter(config); + + QList> files; + auto sorted = sorter.sort(std::move(files)); + + QVERIFY(sorted.isEmpty()); +} + +void tst_DFileSorter::fileSorter_singleItem() +{ + DFileSorter::SortConfig config; + config.role = DFileSorter::SortRole::Name; + + DFileSorter sorter(config); + + QList> files; + + auto file1 = QSharedPointer::create(); + file1->url = QUrl::fromLocalFile("/test/single.txt"); + file1->isDir = false; + files.append(file1); + + auto sorted = sorter.sort(std::move(files)); + + QCOMPARE(sorted.size(), 1); + QCOMPARE(sorted[0]->url.fileName(), QString("single.txt")); +} + +int run_tst_DFileSorter(int argc, char *argv[]) +{ + tst_DFileSorter tc; + return QTest::qExec(&tc, argc, argv); +} + +#include "tst_dfilesorter.moc" + diff --git a/autotests/dfm-io-tests/tst_dfm_io.cpp b/autotests/dfm-io-tests/tst_dfm_io.cpp index da783bca..17b33637 100644 --- a/autotests/dfm-io-tests/tst_dfm_io.cpp +++ b/autotests/dfm-io-tests/tst_dfm_io.cpp @@ -30,5 +30,10 @@ void tst_DfmIO::initialization_test() QVERIFY(true); } -QTEST_MAIN(tst_DfmIO) +int run_tst_DfmIO(int argc, char *argv[]) +{ + tst_DfmIO tc; + return QTest::qExec(&tc, argc, argv); +} + #include "tst_dfm_io.moc" diff --git a/autotests/dfm-search-tests/main.cpp b/autotests/dfm-search-tests/main.cpp index a3c00906..2728f95a 100644 --- a/autotests/dfm-search-tests/main.cpp +++ b/autotests/dfm-search-tests/main.cpp @@ -11,6 +11,7 @@ int main(int argc, char *argv[]) // Test object creation functions are defined in their respective .cpp files extern QObject *create_tst_DfmSearch(); extern QObject *create_tst_SearchUtils(); + extern QObject *create_tst_TimeRangeFilter(); // Run all test objects QObject *testObj1 = create_tst_DfmSearch(); @@ -21,5 +22,9 @@ int main(int argc, char *argv[]) result |= QTest::qExec(testObj2, argc, argv); delete testObj2; + QObject *testObj3 = create_tst_TimeRangeFilter(); + result |= QTest::qExec(testObj3, argc, argv); + delete testObj3; + return result; } diff --git a/autotests/dfm-search-tests/tst_timerangefilter.cpp b/autotests/dfm-search-tests/tst_timerangefilter.cpp new file mode 100644 index 00000000..3a61c518 --- /dev/null +++ b/autotests/dfm-search-tests/tst_timerangefilter.cpp @@ -0,0 +1,328 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later +#include +#include +#include +#include + +#include + +using namespace DFMSEARCH; + +class tst_TimeRangeFilter : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void initTestCase(); + void cleanupTestCase(); + void default_constructor_test(); + void set_time_field_test(); + void set_last_test(); + void set_today_test(); + void set_yesterday_test(); + void set_this_week_test(); + void set_last_week_test(); + void set_this_month_test(); + void set_last_month_test(); + void set_this_year_test(); + void set_last_year_test(); + void set_range_test(); + void boundary_control_test(); + void clear_test(); + void is_valid_test(); + void resolve_time_range_test(); + void fluent_interface_test(); +}; + +void tst_TimeRangeFilter::initTestCase() +{ +} + +void tst_TimeRangeFilter::cleanupTestCase() +{ +} + +void tst_TimeRangeFilter::default_constructor_test() +{ + TimeRangeFilter filter; + QVERIFY(!filter.isValid()); + QCOMPARE(filter.timeField(), TimeField::ModifyTime); + QVERIFY(filter.includeLower()); + QVERIFY(!filter.includeUpper()); +} + +void tst_TimeRangeFilter::set_time_field_test() +{ + TimeRangeFilter filter; + filter.setTimeField(TimeField::BirthTime); + QCOMPARE(filter.timeField(), TimeField::BirthTime); + + filter.setTimeField(TimeField::ModifyTime); + QCOMPARE(filter.timeField(), TimeField::ModifyTime); +} + +void tst_TimeRangeFilter::set_last_test() +{ + TimeRangeFilter filter; + filter.setLast(3, TimeUnit::Days); + QVERIFY(filter.isValid()); + + auto [start, end] = filter.resolveTimeRange(); + QVERIFY(start.isValid()); + QVERIFY(end.isValid()); + + // "Last 3 days" for setLast: rolling range from 3 days ago (00:00:00) to now + QDateTime now = QDateTime::currentDateTime(); + QDateTime expectedStart = QDateTime(now.date().addDays(-3), QTime(0, 0, 0)); + QCOMPARE(start, expectedStart); + // End should be close to now (within 2 seconds) + QVERIFY(qAbs(end.secsTo(now)) < 2); + + // Test minutes - this is precise + filter.setLast(30, TimeUnit::Minutes); + auto [start2, end2] = filter.resolveTimeRange(); + QVERIFY(start2.isValid()); + QVERIFY(end2.isValid()); + // Minutes should be precise (within 2 seconds tolerance) + QVERIFY(qAbs(start2.secsTo(now.addSecs(-30 * 60))) < 2); + + // Test hours - this is precise + filter.setLast(2, TimeUnit::Hours); + auto [start3, end3] = filter.resolveTimeRange(); + QVERIFY(start3.isValid()); + QVERIFY(end3.isValid()); + // Hours should be precise (within 2 seconds tolerance) + QVERIFY(qAbs(start3.secsTo(now.addSecs(-2 * 3600))) < 2); +} + +void tst_TimeRangeFilter::set_today_test() +{ + TimeRangeFilter filter; + filter.setToday(); + QVERIFY(filter.isValid()); + + auto [start, end] = filter.resolveTimeRange(); + QVERIFY(start.isValid()); + QVERIFY(end.isValid()); + + // Today should start at 00:00:00 + QCOMPARE(start.time(), QTime(0, 0, 0)); + QCOMPARE(start.date(), QDate::currentDate()); + + // End should be tomorrow 00:00:00 + QCOMPARE(end.date(), QDate::currentDate().addDays(1)); + QCOMPARE(end.time(), QTime(0, 0, 0)); +} + +void tst_TimeRangeFilter::set_yesterday_test() +{ + TimeRangeFilter filter; + filter.setYesterday(); + QVERIFY(filter.isValid()); + + auto [start, end] = filter.resolveTimeRange(); + QVERIFY(start.isValid()); + QVERIFY(end.isValid()); + + // Yesterday should start at 00:00:00 + QCOMPARE(start.time(), QTime(0, 0, 0)); + QCOMPARE(start.date(), QDate::currentDate().addDays(-1)); + + // End should be today 00:00:00 + QCOMPARE(end.date(), QDate::currentDate()); + QCOMPARE(end.time(), QTime(0, 0, 0)); +} + +void tst_TimeRangeFilter::set_this_week_test() +{ + TimeRangeFilter filter; + filter.setThisWeek(); + QVERIFY(filter.isValid()); + + auto [start, end] = filter.resolveTimeRange(); + QVERIFY(start.isValid()); + QVERIFY(end.isValid()); + + // Should start on Monday 00:00:00 + QCOMPARE(start.time(), QTime(0, 0, 0)); + QCOMPARE(start.date().dayOfWeek(), 1); // Monday is day 1 +} + +void tst_TimeRangeFilter::set_last_week_test() +{ + TimeRangeFilter filter; + filter.setLastWeek(); + QVERIFY(filter.isValid()); + + auto [start, end] = filter.resolveTimeRange(); + QVERIFY(start.isValid()); + QVERIFY(end.isValid()); + + // Should be a 7-day range + qint64 diff = start.daysTo(end); + QCOMPARE(diff, 7); +} + +void tst_TimeRangeFilter::set_this_month_test() +{ + TimeRangeFilter filter; + filter.setThisMonth(); + QVERIFY(filter.isValid()); + + auto [start, end] = filter.resolveTimeRange(); + QVERIFY(start.isValid()); + QVERIFY(end.isValid()); + + // Should start on the 1st + QCOMPARE(start.date().day(), 1); + QCOMPARE(start.time(), QTime(0, 0, 0)); +} + +void tst_TimeRangeFilter::set_last_month_test() +{ + TimeRangeFilter filter; + filter.setLastMonth(); + QVERIFY(filter.isValid()); + + auto [start, end] = filter.resolveTimeRange(); + QVERIFY(start.isValid()); + QVERIFY(end.isValid()); + + // Should start on the 1st of last month + QCOMPARE(start.date().day(), 1); + QCOMPARE(start.time(), QTime(0, 0, 0)); +} + +void tst_TimeRangeFilter::set_this_year_test() +{ + TimeRangeFilter filter; + filter.setThisYear(); + QVERIFY(filter.isValid()); + + auto [start, end] = filter.resolveTimeRange(); + QVERIFY(start.isValid()); + QVERIFY(end.isValid()); + + // Should start on Jan 1st + QCOMPARE(start.date().month(), 1); + QCOMPARE(start.date().day(), 1); + QCOMPARE(start.time(), QTime(0, 0, 0)); +} + +void tst_TimeRangeFilter::set_last_year_test() +{ + TimeRangeFilter filter; + filter.setLastYear(); + QVERIFY(filter.isValid()); + + auto [start, end] = filter.resolveTimeRange(); + QVERIFY(start.isValid()); + QVERIFY(end.isValid()); + + // Should be previous year + QCOMPARE(start.date().year(), QDate::currentDate().year() - 1); +} + +void tst_TimeRangeFilter::set_range_test() +{ + TimeRangeFilter filter; + QDateTime startDate(QDate(2025, 10, 1), QTime(0, 0, 0)); + QDateTime endDate(QDate(2025, 10, 31), QTime(23, 59, 59)); + + filter.setRange(startDate, endDate); + QVERIFY(filter.isValid()); + + QCOMPARE(filter.startTime(), startDate); + QCOMPARE(filter.endTime(), endDate); + + auto [resolvedStart, resolvedEnd] = filter.resolveTimeRange(); + QCOMPARE(resolvedStart, startDate); + QCOMPARE(resolvedEnd, endDate); +} + +void tst_TimeRangeFilter::boundary_control_test() +{ + TimeRangeFilter filter; + filter.setToday(); + + // Default: includeLower=true, includeUpper=false + QVERIFY(filter.includeLower()); + QVERIFY(!filter.includeUpper()); + + // Test fluent interface + filter.setIncludeLower(false).setIncludeUpper(true); + QVERIFY(!filter.includeLower()); + QVERIFY(filter.includeUpper()); +} + +void tst_TimeRangeFilter::clear_test() +{ + TimeRangeFilter filter; + filter.setToday(); + QVERIFY(filter.isValid()); + + filter.clear(); + QVERIFY(!filter.isValid()); + QCOMPARE(filter.timeField(), TimeField::ModifyTime); + QVERIFY(filter.includeLower()); + QVERIFY(!filter.includeUpper()); +} + +void tst_TimeRangeFilter::is_valid_test() +{ + TimeRangeFilter filter; + QVERIFY(!filter.isValid()); + + filter.setToday(); + QVERIFY(filter.isValid()); + + filter.clear(); + QVERIFY(!filter.isValid()); + + QDateTime start, end; + filter.setRange(start, end); + // Range with invalid datetimes is still "valid" in terms of being set + QVERIFY(filter.isValid()); +} + +void tst_TimeRangeFilter::resolve_time_range_test() +{ + // Test static method + auto [start, end] = TimeRangeFilter::resolveRelativeTimeRange(0, TimeUnit::Days); + QVERIFY(start.isValid()); + QVERIFY(end.isValid()); + QCOMPARE(start.time(), QTime(0, 0, 0)); + + // Test with different units + auto [start2, end2] = TimeRangeFilter::resolveRelativeTimeRange(30, TimeUnit::Minutes); + QVERIFY(start2.isValid()); + QVERIFY(end2.isValid()); +} + +void tst_TimeRangeFilter::fluent_interface_test() +{ + // Test method chaining + TimeRangeFilter filter; + filter.setTimeField(TimeField::BirthTime) + .setLast(7, TimeUnit::Days) + .setIncludeLower(true) + .setIncludeUpper(true); + + QCOMPARE(filter.timeField(), TimeField::BirthTime); + QVERIFY(filter.isValid()); + QVERIFY(filter.includeLower()); + QVERIFY(filter.includeUpper()); + + // Test clear returns reference + filter.clear(); + QVERIFY(!filter.isValid()); +} + +QObject *create_tst_TimeRangeFilter() +{ + return new tst_TimeRangeFilter(); +} + +#include "tst_timerangefilter.moc" diff --git a/debian/changelog b/debian/changelog index 637b5f75..3410e274 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,21 @@ +util-dfm (1.3.52) unstable; urgency=medium + + * fix: change content index directory path + * refactor: centralize Lucene field names in constants + * feat: add OCR text search support + * fix: improve path matching logic for content indexing + * feat: add birth time field name constant + * refactor: correct field names and add modify_time + * feat: implement time range filtering for search + * feat: add CLI options and output formatters for search client + * feat: enhance search result metadata and output + * refactor: optimize file search strategy logic + * chore: update search binary name and installation path + * refactor: optimize search utility path checking functions + * refactor: implement Pimpl pattern for TimeRangeFilter class + + -- Zhang Sheng Mon, 20 Apr 2026 11:23:35 +0800 + util-dfm (1.3.51) unstable; urgency=medium * chore: reorder GNUInstallDirs include in CMakeLists diff --git a/debian/control b/debian/control index 18523d38..578fd30c 100644 --- a/debian/control +++ b/debian/control @@ -117,4 +117,4 @@ Depends: ${misc:Depends}, libdfm6-search (=${binary:Version}) Description: A library about searching files dev. - A library that provides file searching interface dev. + A library that provides file searching interface dev. \ No newline at end of file diff --git a/debian/libdfm6-search.install b/debian/libdfm6-search.install index 6c79185c..9c76e128 100644 --- a/debian/libdfm6-search.install +++ b/debian/libdfm6-search.install @@ -1,2 +1,2 @@ -usr/lib/*/libdfm6-search*.so* -usr/libexec/dfm6-search-client \ No newline at end of file +usr/lib/*/libdfm6-search*.so* +usr/bin/dfm-searcher \ No newline at end of file diff --git a/include/dfm-search/dfm-search/contentsearchapi.h b/include/dfm-search/dfm-search/contentsearchapi.h index 6c2d194a..abb1414c 100644 --- a/include/dfm-search/dfm-search/contentsearchapi.h +++ b/include/dfm-search/dfm-search/contentsearchapi.h @@ -107,6 +107,9 @@ class ContentOptionsAPI * * This class extends the base SearchResult with content search specific features, * such as highlighted content preview. + * + * When detailed results are enabled, this API provides access to additional + * metadata including filename, hidden status, and time information. */ class ContentResultAPI { @@ -117,6 +120,8 @@ class ContentResultAPI */ ContentResultAPI(SearchResult &result); + // ==================== Content Attributes ==================== + /** * @brief Get the highlighted content preview * @return The highlighted content as QString @@ -129,6 +134,72 @@ class ContentResultAPI */ void setHighlightedContent(const QString &content); + // ==================== Extended Attributes ==================== + + /** + * @brief Get the file name (without path) + * @return The file name + */ + QString filename() const; + + /** + * @brief Set the file name + * @param name The file name to set + */ + void setFilename(const QString &name); + + /** + * @brief Check if the file is hidden + * @return true if the file is hidden, false otherwise + */ + bool isHidden() const; + + /** + * @brief Set whether the file is hidden + * @param hidden true if the file is hidden, false otherwise + */ + void setIsHidden(bool hidden); + + // ==================== Modification Time ==================== + + /** + * @brief Set the modification time timestamp + * @param timestamp Unix timestamp in seconds + */ + void setModifyTimestamp(qint64 timestamp); + + /** + * @brief Get the modification time timestamp + * @return Unix timestamp in seconds, 0 if not set + */ + qint64 modifyTimestamp() const; + + /** + * @brief Get the modification time as a formatted string + * @return Formatted time string (yyyy-MM-dd HH:mm:ss) + */ + QString modifyTimeString() const; + + // ==================== Birth/Creation Time ==================== + + /** + * @brief Set the birth/creation time timestamp + * @param timestamp Unix timestamp in seconds + */ + void setBirthTimestamp(qint64 timestamp); + + /** + * @brief Get the birth/creation time timestamp + * @return Unix timestamp in seconds, 0 if not set + */ + qint64 birthTimestamp() const; + + /** + * @brief Get the birth/creation time as a formatted string + * @return Formatted time string (yyyy-MM-dd HH:mm:ss) + */ + QString birthTimeString() const; + private: SearchResult &m_result; }; diff --git a/include/dfm-search/dfm-search/dsearch_global.h b/include/dfm-search/dfm-search/dsearch_global.h index add2c0c8..f18892cd 100644 --- a/include/dfm-search/dfm-search/dsearch_global.h +++ b/include/dfm-search/dfm-search/dsearch_global.h @@ -106,6 +106,30 @@ bool isContentIndexAvailable(); */ QString contentIndexDirectory(); +/** + * @brief Check if the specified path is within the OCR text index directory. + * This function verifies whether a given file path is located within the designated OCR text index directory, + * which is important for ensuring that only relevant files are included in OCR text search operations. + * @param path The file path to check. + * @return True if the path is within the OCR text index directory, false otherwise. + */ +bool isPathInOcrTextIndexDirectory(const QString &path); + +/** + * @brief Check if the OCR text index is available. + * This function checks the status of the OCR text index to determine if it is accessible and ready for search operations. + * @return True if the OCR text index is available, false otherwise. + */ +bool isOcrTextIndexAvailable(); + +/** + * @brief Get the OCR text index directory path. + * This function provides the path to the directory where the OCR text index is stored, + * which is essential for performing searches on indexed OCR-extracted text from images. + * @return The path to the OCR text index directory. + */ +QString ocrTextIndexDirectory(); + /** * @brief Check if the specified path is within the filename index directory. * This function verifies whether a given file path is located within the designated filename index directory, @@ -165,12 +189,20 @@ int fileNameIndexVersion(); */ int contentIndexVersion(); +/** + * @brief Get the version of the OCR text index from the JSON configuration file. + * This function reads the version field from the OCR text index JSON file and returns it as an integer. + * @return The version number of the OCR text index, or -1 if the version cannot be retrieved. + */ +int ocrTextIndexVersion(); + } // namespace Global // Enumeration for different types of search methods enum SearchType { FileName, // Search by file name Content, // Search by content within files + Ocr, // Search by OCR-extracted text from images Custom = 50 // User-defined search type }; Q_ENUM_NS(SearchType) @@ -193,6 +225,24 @@ enum SearchMethod { }; Q_ENUM_NS(SearchMethod) +// Enumeration for time field type +enum class TimeField { + BirthTime, // File creation time + ModifyTime // File modification time +}; +Q_ENUM_NS(TimeField) + +// Enumeration for time unit +enum class TimeUnit { + Minutes, // Minute unit + Hours, // Hour unit + Days, // Day unit + Weeks, // Week unit + Months, // Month unit + Years // Year unit +}; +Q_ENUM_NS(TimeUnit) + DFM_SEARCH_END_NS Q_DECLARE_METATYPE(DFMSEARCH::SearchType); diff --git a/include/dfm-search/dfm-search/field_names.h b/include/dfm-search/dfm-search/field_names.h new file mode 100644 index 00000000..e3d96ccc --- /dev/null +++ b/include/dfm-search/dfm-search/field_names.h @@ -0,0 +1,54 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include + +#include + +DFM_SEARCH_BEGIN_NS + +namespace LuceneFieldNames { + +// File name index field names +namespace FileName { +constexpr const wchar_t kFileType[] = L"file_type"; +constexpr const wchar_t kFileExt[] = L"file_ext"; +constexpr const wchar_t kFileName[] = L"file_name"; +constexpr const wchar_t kFileNameLower[] = L"file_name_lower"; +constexpr const wchar_t kFullPath[] = L"full_path"; +constexpr const wchar_t kIsHidden[] = L"is_hidden"; +constexpr const wchar_t kModifyTime[] = L"modify_time"; +constexpr const wchar_t kBirthTime[] = L"birth_time"; +constexpr const wchar_t kFileSizeStr[] = L"file_size_str"; +constexpr const wchar_t kPinyin[] = L"pinyin"; +constexpr const wchar_t kPinyinAcronym[] = L"pinyin_acronym"; +constexpr const wchar_t kAncestorPaths[] = L"ancestor_paths"; +} // namespace FileName + +// Content index field names +namespace Content { +constexpr const wchar_t kContents[] = L"contents"; +constexpr const wchar_t kFilename[] = L"filename"; +constexpr const wchar_t kPath[] = L"path"; +constexpr const wchar_t kIsHidden[] = L"is_hidden"; +constexpr const wchar_t kAncestorPaths[] = L"ancestor_paths"; +constexpr const wchar_t kBirthTime[] = L"birth_time"; +constexpr const wchar_t kModifyTime[] = L"modify_time"; +} // namespace Content + +// OCR text index field names +namespace OcrText { +constexpr const wchar_t kOcrContents[] = L"ocr_contents"; +constexpr const wchar_t kFilename[] = L"filename"; +constexpr const wchar_t kPath[] = L"path"; +constexpr const wchar_t kIsHidden[] = L"is_hidden"; +constexpr const wchar_t kAncestorPaths[] = L"ancestor_paths"; +constexpr const wchar_t kBirthTime[] = L"birth_time"; +constexpr const wchar_t kModifyTime[] = L"modify_time"; +} // namespace OcrText + +} // namespace LuceneFieldNames + +DFM_SEARCH_END_NS diff --git a/include/dfm-search/dfm-search/filenamesearchapi.h b/include/dfm-search/dfm-search/filenamesearchapi.h index efe48671..4af8fba7 100644 --- a/include/dfm-search/dfm-search/filenamesearchapi.h +++ b/include/dfm-search/dfm-search/filenamesearchapi.h @@ -94,6 +94,9 @@ class FileNameOptionsAPI * * This class extends the base SearchResult with file name search specific features, * such as file size, modification time, and file type information. + * + * When detailed results are enabled, this API provides access to all indexed fields + * including filename, extension, hidden status, and both modification and birth times. */ class FileNameResultAPI { @@ -104,6 +107,8 @@ class FileNameResultAPI */ explicit FileNameResultAPI(SearchResult &result); + // ==================== Basic Attributes ==================== + /** * @brief Get the file size * @return The file size as formatted string @@ -117,14 +122,16 @@ class FileNameResultAPI void setSize(const QString &size); /** - * @brief Get the file modification time + * @brief Get the file modification time (legacy, use modifyTimeString instead) * @return The modification time as formatted string + * @deprecated Use modifyTimeString() for formatted string or modifyTimestamp() for raw timestamp */ QString modifiedTime() const; /** - * @brief Set the file modification time + * @brief Set the file modification time (legacy) * @param time The modification time as formatted string + * @deprecated Use setModifyTimestamp() instead */ void setModifiedTime(const QString &time); @@ -152,6 +159,84 @@ class FileNameResultAPI */ void setFileType(const QString &type) const; + // ==================== Extended Attributes ==================== + + /** + * @brief Get the file name (without path) + * @return The file name + */ + QString filename() const; + + /** + * @brief Set the file name + * @param name The file name to set + */ + void setFilename(const QString &name); + + /** + * @brief Get the file extension + * @return The file extension (lowercase, without dot) + */ + QString fileExtension() const; + + /** + * @brief Set the file extension + * @param ext The file extension to set + */ + void setFileExtension(const QString &ext); + + /** + * @brief Check if the file is hidden + * @return true if the file is hidden, false otherwise + */ + bool isHidden() const; + + /** + * @brief Set whether the file is hidden + * @param hidden true if the file is hidden, false otherwise + */ + void setIsHidden(bool hidden); + + // ==================== Modification Time ==================== + + /** + * @brief Set the modification time timestamp + * @param timestamp Unix timestamp in seconds + */ + void setModifyTimestamp(qint64 timestamp); + + /** + * @brief Get the modification time timestamp + * @return Unix timestamp in seconds, 0 if not set + */ + qint64 modifyTimestamp() const; + + /** + * @brief Get the modification time as a formatted string + * @return Formatted time string (yyyy-MM-dd HH:mm:ss) + */ + QString modifyTimeString() const; + + // ==================== Birth/Creation Time ==================== + + /** + * @brief Set the birth/creation time timestamp + * @param timestamp Unix timestamp in seconds + */ + void setBirthTimestamp(qint64 timestamp); + + /** + * @brief Get the birth/creation time timestamp + * @return Unix timestamp in seconds, 0 if not set + */ + qint64 birthTimestamp() const; + + /** + * @brief Get the birth/creation time as a formatted string + * @return Formatted time string (yyyy-MM-dd HH:mm:ss) + */ + QString birthTimeString() const; + private: SearchResult &m_result; }; diff --git a/include/dfm-search/dfm-search/ocrtextsearchapi.h b/include/dfm-search/dfm-search/ocrtextsearchapi.h new file mode 100644 index 00000000..4e854ee2 --- /dev/null +++ b/include/dfm-search/dfm-search/ocrtextsearchapi.h @@ -0,0 +1,160 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later +#ifndef OCRTEXTSEARCHAPI_H +#define OCRTEXTSEARCHAPI_H + +#include +#include +#include + +DFM_SEARCH_BEGIN_NS + +/** + * @brief The OcrTextOptionsAPI class provides OCR text search specific options + * + * This class extends the base SearchOptions with OCR text search specific settings. + * OCR text search is a simplified version of content search without highlighting support. + */ +class OcrTextOptionsAPI +{ +public: + /** + * @brief Constructor + * @param options The SearchOptions object to operate on + */ + explicit OcrTextOptionsAPI(SearchOptions &options); + + /** + * @brief Sets whether the extended AND search behavior across 'ocr_contents' and 'filename' fields is enabled. + * @param enabled True to enable the feature, false to disable it. + * @see isFilenameOcrContentMixedAndSearchEnabled() for a detailed description of the behavior. + */ + void setFilenameOcrContentMixedAndSearchEnabled(bool enabled); + + /** + * @brief Checks if the extended AND search behavior across 'ocr_contents' and 'filename' fields is enabled. + * + * When enabled (returns true), boolean AND queries will search for terms such that: + * 1. All terms must be present, potentially distributed between the 'ocr_contents' and 'filename' fields. + * (e.g., termA in 'ocr_contents', termB in 'filename'). + * 2. A match is explicitly excluded if all search terms are found *only* within the 'filename' field. + * (e.g., termA in 'filename', termB in 'filename' -- this specific case is excluded). + * 3. Matches where all terms are in 'ocr_contents', or mixed between 'ocr_contents' and 'filename' (as in point 1), are included. + * + * If this option is disabled (returns false), or for boolean OR queries, + * the boolean search will be performed exclusively on the 'ocr_contents' field, following the original logic. + * + * @return True if the filename-OCR content mixed AND search is enabled, false otherwise. + */ + bool isFilenameOcrContentMixedAndSearchEnabled() const; + +private: + SearchOptions &m_options; +}; + +/** + * @brief The OcrTextResultAPI class provides OCR text search specific result handling + * + * This class extends the base SearchResult with OCR text search specific features. + * Note: OCR text search does not support content highlighting like content search. + * + * When detailed results are enabled, this API provides access to additional + * metadata including filename, hidden status, and time information. + */ +class OcrTextResultAPI +{ +public: + /** + * @brief Constructor + * @param result The SearchResult object to operate on + */ + OcrTextResultAPI(SearchResult &result); + + // ==================== OCR Content Attributes ==================== + + /** + * @brief Get the OCR extracted text content + * @return The OCR extracted text as QString + */ + QString ocrContent() const; + + /** + * @brief Set the OCR extracted text content + * @param content The OCR extracted text to set + */ + void setOcrContent(const QString &content); + + // ==================== Extended Attributes ==================== + + /** + * @brief Get the file name (without path) + * @return The file name + */ + QString filename() const; + + /** + * @brief Set the file name + * @param name The file name to set + */ + void setFilename(const QString &name); + + /** + * @brief Check if the file is hidden + * @return true if the file is hidden, false otherwise + */ + bool isHidden() const; + + /** + * @brief Set whether the file is hidden + * @param hidden true if the file is hidden, false otherwise + */ + void setIsHidden(bool hidden); + + // ==================== Modification Time ==================== + + /** + * @brief Set the modification time timestamp + * @param timestamp Unix timestamp in seconds + */ + void setModifyTimestamp(qint64 timestamp); + + /** + * @brief Get the modification time timestamp + * @return Unix timestamp in seconds, 0 if not set + */ + qint64 modifyTimestamp() const; + + /** + * @brief Get the modification time as a formatted string + * @return Formatted time string (yyyy-MM-dd HH:mm:ss) + */ + QString modifyTimeString() const; + + // ==================== Birth/Creation Time ==================== + + /** + * @brief Set the birth/creation time timestamp + * @param timestamp Unix timestamp in seconds + */ + void setBirthTimestamp(qint64 timestamp); + + /** + * @brief Get the birth/creation time timestamp + * @return Unix timestamp in seconds, 0 if not set + */ + qint64 birthTimestamp() const; + + /** + * @brief Get the birth/creation time as a formatted string + * @return Formatted time string (yyyy-MM-dd HH:mm:ss) + */ + QString birthTimeString() const; + +private: + SearchResult &m_result; +}; + +DFM_SEARCH_END_NS + +#endif // OCRTEXTSEARCHAPI_H diff --git a/include/dfm-search/dfm-search/searcherror.h b/include/dfm-search/dfm-search/searcherror.h index 828fef0a..4a4433d8 100644 --- a/include/dfm-search/dfm-search/searcherror.h +++ b/include/dfm-search/dfm-search/searcherror.h @@ -57,6 +57,16 @@ enum class ContentSearchErrorCode { ContentIndexException // An exception occurred with the content index }; +// Enumeration for OCR text search specific error codes +enum class OcrTextSearchErrorCode { + KeywordTooShort = 3000, // The search keyword is too short + WildcardNotSupported = 3001, // Wildcard search is not supported for OCR text search + + // Errors related to OCR text indexing + OcrTextIndexNotFound = 3200, // The OCR text index could not be found + OcrTextIndexException // An exception occurred with the OCR text index +}; + // Base class for search error categories class SearchErrorCategory : public std::error_category { @@ -82,10 +92,19 @@ class ContentSearchErrorCategory : public SearchErrorCategory std::string message(int ev) const override; // Returns a message corresponding to the content search error code }; +// Class for OCR text search error categories +class OcrTextSearchErrorCategory : public SearchErrorCategory +{ +public: + const char *name() const noexcept override { return "ocrtext_search_error"; } // Returns the name of the OCR text search error category + std::string message(int ev) const override; // Returns a message corresponding to the OCR text search error code +}; + // Functions to get singleton instances of error categories const SearchErrorCategory &search_category(); // Returns the singleton instance of the search error category const FileNameSearchErrorCategory &filename_search_category(); // Returns the singleton instance of the file name search error category const ContentSearchErrorCategory &content_search_category(); // Returns the singleton instance of the content search error category +const OcrTextSearchErrorCategory &ocrtext_search_category(); // Returns the singleton instance of the OCR text search error category // Function to create an error code from a SearchErrorCode inline std::error_code make_error_code(SearchErrorCode ec) @@ -105,6 +124,12 @@ inline std::error_code make_error_code(ContentSearchErrorCode ec) return std::error_code((int)ec, content_search_category()); } +// Function to create an error code from a OcrTextSearchErrorCode +inline std::error_code make_error_code(OcrTextSearchErrorCode ec) +{ + return std::error_code((int)ec, ocrtext_search_category()); +} + // Class to wrap search error conditions class SearchError { @@ -117,6 +142,8 @@ class SearchError : m_code(make_error_code(code)) { } // Constructor initializes with a FileNameSearchErrorCode SearchError(ContentSearchErrorCode code) : m_code(make_error_code(code)) { } // Constructor initializes with a ContentSearchErrorCode + SearchError(OcrTextSearchErrorCode code) + : m_code(make_error_code(code)) { } // Constructor initializes with a OcrTextSearchErrorCode bool isError() const { return m_code.value() != static_cast(SearchErrorCode::Success); } // Checks if there is an error const std::error_code &code() const { return m_code; } // Returns the error code diff --git a/include/dfm-search/dfm-search/searchoptions.h b/include/dfm-search/dfm-search/searchoptions.h index 017171ff..6d22b5c9 100644 --- a/include/dfm-search/dfm-search/searchoptions.h +++ b/include/dfm-search/dfm-search/searchoptions.h @@ -9,6 +9,7 @@ #include #include +#include DFM_SEARCH_BEGIN_NS @@ -218,6 +219,41 @@ class SearchOptions */ int batchTime() const; + /** + * @brief Sets the time range filter for search operations. + * + * The time range filter allows filtering search results based on file + * creation time or modification time. Both preset ranges (like "Today", + * "Last 7 days") and custom datetime ranges are supported. + * + * @param filter The TimeRangeFilter to apply + * @sa timeRangeFilter(), hasTimeRangeFilter(), clearTimeRangeFilter() + */ + void setTimeRangeFilter(const TimeRangeFilter &filter); + + /** + * @brief Returns the current time range filter. + * + * @return The current TimeRangeFilter + * @sa setTimeRangeFilter() + */ + TimeRangeFilter timeRangeFilter() const; + + /** + * @brief Checks if a time range filter is set. + * + * @return true if a valid time range filter is set, false otherwise + * @sa setTimeRangeFilter(), clearTimeRangeFilter() + */ + bool hasTimeRangeFilter() const; + + /** + * @brief Clears the time range filter. + * + * @sa setTimeRangeFilter(), hasTimeRangeFilter() + */ + void clearTimeRangeFilter(); + private: std::unique_ptr d; // PIMPL }; diff --git a/include/dfm-search/dfm-search/timerangefilter.h b/include/dfm-search/dfm-search/timerangefilter.h new file mode 100644 index 00000000..230a65c4 --- /dev/null +++ b/include/dfm-search/dfm-search/timerangefilter.h @@ -0,0 +1,241 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later +#ifndef TIMERANGEFILTER_H +#define TIMERANGEFILTER_H + +#include +#include +#include + +#include + +DFM_SEARCH_BEGIN_NS + +class TimeRangeFilterData; + +/** + * @brief The TimeRangeFilter class provides time range filtering for search operations. + * + * This class provides a fluent interface for specifying time ranges: + * + * Example usage: + * @code + * // Files modified in last 3 days + * TimeRangeFilter filter; + * filter.setTimeField(TimeField::ModifyTime).setLast(3, TimeUnit::Days); + * + * // Files created today + * TimeRangeFilter filter; + * filter.setTimeField(TimeField::BirthTime).setToday(); + * + * // Custom time range + * TimeRangeFilter filter; + * filter.setTimeField(TimeField::ModifyTime).setRange(startDate, endDate); + * @endcode + */ +class TimeRangeFilter +{ +public: + /** + * @brief Default constructor + * Creates an invalid filter (no time range set) + */ + TimeRangeFilter(); + + /** + * @brief Copy constructor + */ + TimeRangeFilter(const TimeRangeFilter &other); + + /** + * @brief Move constructor + */ + TimeRangeFilter(TimeRangeFilter &&other) noexcept; + + /** + * @brief Destructor + */ + ~TimeRangeFilter(); + + /** + * @brief Assignment operator + */ + TimeRangeFilter &operator=(const TimeRangeFilter &other); + + /** + * @brief Move assignment operator + */ + TimeRangeFilter &operator=(TimeRangeFilter &&other) noexcept; + + // ---------- Time Field ---------- + + /** + * @brief Set the time field to filter on + * @param field The time field (BirthTime or ModifyTime) + * @return Reference to this filter for method chaining + */ + TimeRangeFilter &setTimeField(TimeField field); + + /** + * @brief Get the time field being filtered on + * @return The current time field + */ + TimeField timeField() const; + + // ---------- Relative Time Range (Fluent Interface) ---------- + + /** + * @brief Set a relative time range from now + * @param value The number of time units + * @param unit The time unit (Minutes, Hours, Days, Weeks, Months, Years) + * @return Reference to this filter for method chaining + * + * Example: setLast(3, TimeUnit::Days) means files from 3 days ago to now + */ + TimeRangeFilter &setLast(int value, TimeUnit unit); + + // ---------- Fixed Presets ---------- + + /** + * @brief Set range to today (from 00:00:00 today to 00:00:00 tomorrow) + * @return Reference to this filter for method chaining + */ + TimeRangeFilter &setToday(); + + /** + * @brief Set range to yesterday + * @return Reference to this filter for method chaining + */ + TimeRangeFilter &setYesterday(); + + /** + * @brief Set range to this week (from Monday 00:00:00 to next Monday 00:00:00) + * @return Reference to this filter for method chaining + */ + TimeRangeFilter &setThisWeek(); + + /** + * @brief Set range to last week + * @return Reference to this filter for method chaining + */ + TimeRangeFilter &setLastWeek(); + + /** + * @brief Set range to this month (from 1st day 00:00:00 to 1st day of next month) + * @return Reference to this filter for method chaining + */ + TimeRangeFilter &setThisMonth(); + + /** + * @brief Set range to last month + * @return Reference to this filter for method chaining + */ + TimeRangeFilter &setLastMonth(); + + /** + * @brief Set range to this year (from Jan 1st to Jan 1st of next year) + * @return Reference to this filter for method chaining + */ + TimeRangeFilter &setThisYear(); + + /** + * @brief Set range to last year + * @return Reference to this filter for method chaining + */ + TimeRangeFilter &setLastYear(); + + // ---------- Custom Time Range ---------- + + /** + * @brief Set a custom time range + * @param start The start datetime of the range + * @param end The end datetime of the range + * @return Reference to this filter for method chaining + */ + TimeRangeFilter &setRange(const QDateTime &start, const QDateTime &end); + + /** + * @brief Get the custom start time + * @return The start datetime + */ + QDateTime startTime() const; + + /** + * @brief Get the custom end time + * @return The end datetime + */ + QDateTime endTime() const; + + // ---------- Boundary Control ---------- + + /** + * @brief Set whether the lower bound is inclusive + * @param include true to include the lower bound + * @return Reference to this filter for method chaining + */ + TimeRangeFilter &setIncludeLower(bool include); + + /** + * @brief Set whether the upper bound is inclusive + * @param include true to include the upper bound + * @return Reference to this filter for method chaining + */ + TimeRangeFilter &setIncludeUpper(bool include); + + /** + * @brief Check if lower bound is inclusive + * @return true if lower bound is inclusive + */ + bool includeLower() const; + + /** + * @brief Check if upper bound is inclusive + * @return true if upper bound is inclusive + */ + bool includeUpper() const; + + // ---------- Filter State ---------- + + /** + * @brief Clear the filter (make it invalid) + * @return Reference to this filter for method chaining + */ + TimeRangeFilter &clear(); + + /** + * @brief Check if the filter is valid + * @return true if a time range is set + */ + bool isValid() const; + + /** + * @brief Resolve the actual time range + * Calculates the actual start/end times based on current time for relative ranges. + * @return A pair of (start, end) datetimes + */ + QPair resolveTimeRange() const; + + /** + * @brief Resolve a relative time range to actual datetime range + * @param value The number of time units + * @param unit The time unit + * @return A pair of (start, end) datetimes + */ + static QPair resolveRelativeTimeRange(int value, TimeUnit unit); + +private: + /** + * @brief Resolve a fixed unit time range to actual datetime range + * @param value The number of time units (0 = this unit, 1 = last unit, etc.) + * @param unit The time unit + * @return A pair of (start, end) datetimes + */ + static QPair resolveFixedUnitTimeRange(int value, TimeUnit unit); + + std::unique_ptr d; +}; + +DFM_SEARCH_END_NS + +#endif // TIMERANGEFILTER_H diff --git a/include/dfm-search/dfm-search/timeresultapi.h b/include/dfm-search/dfm-search/timeresultapi.h new file mode 100644 index 00000000..71f51064 --- /dev/null +++ b/include/dfm-search/dfm-search/timeresultapi.h @@ -0,0 +1,91 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later +#ifndef TIMERESULTAPI_H +#define TIMERESULTAPI_H + +#include +#include + +#include + +DFM_SEARCH_BEGIN_NS + +class SearchResult; + +/** + * @brief The TimeResultAPI class provides shared time attribute access for search results + * + * This class provides a unified interface for accessing time-related attributes + * (modification time and birth/creation time) in SearchResult objects. It follows + * the DRY principle by centralizing time attribute handling across different + * search types (filename, content, OCR). + * + * Time values are stored as Unix timestamps (seconds since epoch) internally, + * and can be retrieved either as raw timestamps or as formatted time strings. + */ +class TimeResultAPI +{ +public: + /** + * @brief Constructor + * @param result The SearchResult object to operate on + */ + explicit TimeResultAPI(SearchResult &result); + + // ==================== Modification Time ==================== + + /** + * @brief Set the modification time timestamp + * @param timestamp Unix timestamp in seconds + */ + void setModifyTimestamp(qint64 timestamp); + + /** + * @brief Get the modification time timestamp + * @return Unix timestamp in seconds, 0 if not set + */ + qint64 modifyTimestamp() const; + + /** + * @brief Get the modification time as a formatted string + * @return Formatted time string (yyyy-MM-dd HH:mm:ss), empty if not set + */ + QString modifyTimeString() const; + + // ==================== Birth/Creation Time ==================== + + /** + * @brief Set the birth/creation time timestamp + * @param timestamp Unix timestamp in seconds + */ + void setBirthTimestamp(qint64 timestamp); + + /** + * @brief Get the birth/creation time timestamp + * @return Unix timestamp in seconds, 0 if not set + */ + qint64 birthTimestamp() const; + + /** + * @brief Get the birth/creation time as a formatted string + * @return Formatted time string (yyyy-MM-dd HH:mm:ss), empty if not set + */ + QString birthTimeString() const; + + // ==================== Utility Functions ==================== + + /** + * @brief Format a Unix timestamp to a human-readable string + * @param timestamp Unix timestamp in seconds + * @return Formatted time string (yyyy-MM-dd HH:mm:ss) + */ + static QString formatTimestamp(qint64 timestamp); + +private: + SearchResult &m_result; +}; + +DFM_SEARCH_END_NS + +#endif // TIMERESULTAPI_H diff --git a/src/dfm-burn/3rdparty/udfclient/config.guess b/src/dfm-burn/3rdparty/udfclient/config.guess index 56a85784..48a68460 100755 --- a/src/dfm-burn/3rdparty/udfclient/config.guess +++ b/src/dfm-burn/3rdparty/udfclient/config.guess @@ -1,10 +1,10 @@ #! /bin/sh # Attempt to guess a canonical system name. -# Copyright 1992-2022 Free Software Foundation, Inc. +# Copyright 1992-2024 Free Software Foundation, Inc. # shellcheck disable=SC2006,SC2268 # see below for rationale -timestamp='2022-01-09' +timestamp='2024-07-27' # This file is free software; you can redistribute it and/or modify it # under the terms of the GNU General Public License as published by @@ -47,7 +47,7 @@ me=`echo "$0" | sed -e 's,.*/,,'` usage="\ Usage: $0 [OPTION] -Output the configuration name of the system \`$me' is run on. +Output the configuration name of the system '$me' is run on. Options: -h, --help print this help, then exit @@ -60,13 +60,13 @@ version="\ GNU config.guess ($timestamp) Originally written by Per Bothner. -Copyright 1992-2022 Free Software Foundation, Inc. +Copyright 1992-2024 Free Software Foundation, Inc. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE." help=" -Try \`$me --help' for more information." +Try '$me --help' for more information." # Parse command line while test $# -gt 0 ; do @@ -102,8 +102,8 @@ GUESS= # temporary files to be created and, as you can see below, it is a # headache to deal with in a portable fashion. -# Historically, `CC_FOR_BUILD' used to be named `HOST_CC'. We still -# use `HOST_CC' if defined, but it is deprecated. +# Historically, 'CC_FOR_BUILD' used to be named 'HOST_CC'. We still +# use 'HOST_CC' if defined, but it is deprecated. # Portable tmp directory creation inspired by the Autoconf team. @@ -123,7 +123,7 @@ set_cc_for_build() { dummy=$tmp/dummy case ${CC_FOR_BUILD-},${HOST_CC-},${CC-} in ,,) echo "int x;" > "$dummy.c" - for driver in cc gcc c89 c99 ; do + for driver in cc gcc c17 c99 c89 ; do if ($driver -c -o "$dummy.o" "$dummy.c") >/dev/null 2>&1 ; then CC_FOR_BUILD=$driver break @@ -155,6 +155,9 @@ Linux|GNU|GNU/*) set_cc_for_build cat <<-EOF > "$dummy.c" + #if defined(__ANDROID__) + LIBC=android + #else #include #if defined(__UCLIBC__) LIBC=uclibc @@ -162,6 +165,8 @@ Linux|GNU|GNU/*) LIBC=dietlibc #elif defined(__GLIBC__) LIBC=gnu + #elif defined(__LLVM_LIBC__) + LIBC=llvm #else #include /* First heuristic to detect musl libc. */ @@ -169,6 +174,7 @@ Linux|GNU|GNU/*) LIBC=musl #endif #endif + #endif EOF cc_set_libc=`$CC_FOR_BUILD -E "$dummy.c" 2>/dev/null | grep '^LIBC' | sed 's, ,,g'` eval "$cc_set_libc" @@ -459,7 +465,7 @@ case $UNAME_MACHINE:$UNAME_SYSTEM:$UNAME_RELEASE:$UNAME_VERSION in UNAME_RELEASE=`uname -v` ;; esac - # Japanese Language versions have a version number like `4.1.3-JL'. + # Japanese Language versions have a version number like '4.1.3-JL'. SUN_REL=`echo "$UNAME_RELEASE" | sed -e 's/-/_/'` GUESS=sparc-sun-sunos$SUN_REL ;; @@ -628,7 +634,8 @@ EOF sed 's/^ //' << EOF > "$dummy.c" #include - main() + int + main () { if (!__power_pc()) exit(1); @@ -712,7 +719,8 @@ EOF #include #include - int main () + int + main () { #if defined(_SC_KERNEL_BITS) long bits = sysconf(_SC_KERNEL_BITS); @@ -904,7 +912,7 @@ EOF fi ;; *:FreeBSD:*:*) - UNAME_PROCESSOR=`/usr/bin/uname -p` + UNAME_PROCESSOR=`uname -p` case $UNAME_PROCESSOR in amd64) UNAME_PROCESSOR=x86_64 ;; @@ -966,11 +974,37 @@ EOF GNU_REL=`echo "$UNAME_RELEASE" | sed -e 's/[-(].*//'` GUESS=$UNAME_MACHINE-unknown-$GNU_SYS$GNU_REL-$LIBC ;; + x86_64:[Mm]anagarm:*:*|i?86:[Mm]anagarm:*:*) + GUESS="$UNAME_MACHINE-pc-managarm-mlibc" + ;; + *:[Mm]anagarm:*:*) + GUESS="$UNAME_MACHINE-unknown-managarm-mlibc" + ;; *:Minix:*:*) GUESS=$UNAME_MACHINE-unknown-minix ;; aarch64:Linux:*:*) - GUESS=$UNAME_MACHINE-unknown-linux-$LIBC + set_cc_for_build + CPU=$UNAME_MACHINE + LIBCABI=$LIBC + if test "$CC_FOR_BUILD" != no_compiler_found; then + ABI=64 + sed 's/^ //' << EOF > "$dummy.c" + #ifdef __ARM_EABI__ + #ifdef __ARM_PCS_VFP + ABI=eabihf + #else + ABI=eabi + #endif + #endif +EOF + cc_set_abi=`$CC_FOR_BUILD -E "$dummy.c" 2>/dev/null | grep '^ABI' | sed 's, ,,g'` + eval "$cc_set_abi" + case $ABI in + eabi | eabihf) CPU=armv8l; LIBCABI=$LIBC$ABI ;; + esac + fi + GUESS=$CPU-unknown-linux-$LIBCABI ;; aarch64_be:Linux:*:*) UNAME_MACHINE=aarch64_be @@ -1036,7 +1070,16 @@ EOF k1om:Linux:*:*) GUESS=$UNAME_MACHINE-unknown-linux-$LIBC ;; - loongarch32:Linux:*:* | loongarch64:Linux:*:* | loongarchx32:Linux:*:*) + kvx:Linux:*:*) + GUESS=$UNAME_MACHINE-unknown-linux-$LIBC + ;; + kvx:cos:*:*) + GUESS=$UNAME_MACHINE-unknown-cos + ;; + kvx:mbr:*:*) + GUESS=$UNAME_MACHINE-unknown-mbr + ;; + loongarch32:Linux:*:* | loongarch64:Linux:*:*) GUESS=$UNAME_MACHINE-unknown-linux-$LIBC ;; m32r*:Linux:*:*) @@ -1143,9 +1186,6 @@ EOF sparc:Linux:*:* | sparc64:Linux:*:*) GUESS=$UNAME_MACHINE-unknown-linux-$LIBC ;; - sw_64:Linux:*:*) - GUESS=$UNAME_MACHINE-unknown-linux-$LIBC - ;; tile*:Linux:*:*) GUESS=$UNAME_MACHINE-unknown-linux-$LIBC ;; @@ -1154,16 +1194,27 @@ EOF ;; x86_64:Linux:*:*) set_cc_for_build + CPU=$UNAME_MACHINE LIBCABI=$LIBC if test "$CC_FOR_BUILD" != no_compiler_found; then - if (echo '#ifdef __ILP32__'; echo IS_X32; echo '#endif') | \ - (CCOPTS="" $CC_FOR_BUILD -E - 2>/dev/null) | \ - grep IS_X32 >/dev/null - then - LIBCABI=${LIBC}x32 - fi + ABI=64 + sed 's/^ //' << EOF > "$dummy.c" + #ifdef __i386__ + ABI=x86 + #else + #ifdef __ILP32__ + ABI=x32 + #endif + #endif +EOF + cc_set_abi=`$CC_FOR_BUILD -E "$dummy.c" 2>/dev/null | grep '^ABI' | sed 's, ,,g'` + eval "$cc_set_abi" + case $ABI in + x86) CPU=i686 ;; + x32) LIBCABI=${LIBC}x32 ;; + esac fi - GUESS=$UNAME_MACHINE-pc-linux-$LIBCABI + GUESS=$CPU-pc-linux-$LIBCABI ;; xtensa*:Linux:*:*) GUESS=$UNAME_MACHINE-unknown-linux-$LIBC @@ -1183,7 +1234,7 @@ EOF GUESS=$UNAME_MACHINE-pc-sysv4.2uw$UNAME_VERSION ;; i*86:OS/2:*:*) - # If we were able to find `uname', then EMX Unix compatibility + # If we were able to find 'uname', then EMX Unix compatibility # is probably installed. GUESS=$UNAME_MACHINE-pc-os2-emx ;; @@ -1324,7 +1375,7 @@ EOF GUESS=ns32k-sni-sysv fi ;; - PENTIUM:*:4.0*:*) # Unisys `ClearPath HMP IX 4000' SVR4/MP effort + PENTIUM:*:4.0*:*) # Unisys 'ClearPath HMP IX 4000' SVR4/MP effort # says GUESS=i586-unisys-sysv4 ;; @@ -1370,8 +1421,11 @@ EOF BePC:Haiku:*:*) # Haiku running on Intel PC compatible. GUESS=i586-pc-haiku ;; - x86_64:Haiku:*:*) - GUESS=x86_64-unknown-haiku + ppc:Haiku:*:*) # Haiku running on Apple PowerPC + GUESS=powerpc-apple-haiku + ;; + *:Haiku:*:*) # Haiku modern gcc (not bound by BeOS compat) + GUESS=$UNAME_MACHINE-unknown-haiku ;; SX-4:SUPER-UX:*:*) GUESS=sx4-nec-superux$UNAME_RELEASE @@ -1543,6 +1597,9 @@ EOF *:Unleashed:*:*) GUESS=$UNAME_MACHINE-unknown-unleashed$UNAME_RELEASE ;; + *:Ironclad:*:*) + GUESS=$UNAME_MACHINE-unknown-ironclad + ;; esac # Do we have a guess based on uname results? @@ -1566,6 +1623,7 @@ cat > "$dummy.c" <." version="\ GNU config.sub ($timestamp) -Copyright 1992-2022 Free Software Foundation, Inc. +Copyright 1992-2024 Free Software Foundation, Inc. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE." help=" -Try \`$me --help' for more information." +Try '$me --help' for more information." # Parse command line while test $# -gt 0 ; do @@ -120,7 +120,6 @@ case $# in esac # Split fields of configuration type -# shellcheck disable=SC2162 saved_IFS=$IFS IFS="-" read field1 field2 field3 field4 <&2 + echo "Invalid configuration '$1': more than four components" >&2 exit 1 ;; *-*-*-*) @@ -142,10 +141,21 @@ case $1 in # parts maybe_os=$field2-$field3 case $maybe_os in - nto-qnx* | linux-* | uclinux-uclibc* \ - | uclinux-gnu* | kfreebsd*-gnu* | knetbsd*-gnu* | netbsd*-gnu* \ - | netbsd*-eabi* | kopensolaris*-gnu* | cloudabi*-eabi* \ - | storm-chaos* | os2-emx* | rtmk-nova*) + cloudabi*-eabi* \ + | kfreebsd*-gnu* \ + | knetbsd*-gnu* \ + | kopensolaris*-gnu* \ + | linux-* \ + | managarm-* \ + | netbsd*-eabi* \ + | netbsd*-gnu* \ + | nto-qnx* \ + | os2-emx* \ + | rtmk-nova* \ + | storm-chaos* \ + | uclinux-gnu* \ + | uclinux-uclibc* \ + | windows-* ) basic_machine=$field1 basic_os=$maybe_os ;; @@ -160,8 +170,12 @@ case $1 in esac ;; *-*) - # A lone config we happen to match not fitting any pattern case $field1-$field2 in + # Shorthands that happen to contain a single dash + convex-c[12] | convex-c3[248]) + basic_machine=$field2-convex + basic_os= + ;; decstation-3100) basic_machine=mips-dec basic_os= @@ -169,28 +183,88 @@ case $1 in *-*) # Second component is usually, but not always the OS case $field2 in - # Prevent following clause from handling this valid os + # Do not treat sunos as a manufacturer sun*os*) basic_machine=$field1 basic_os=$field2 ;; - zephyr*) - basic_machine=$field1-unknown - basic_os=$field2 - ;; # Manufacturers - dec* | mips* | sequent* | encore* | pc533* | sgi* | sony* \ - | att* | 7300* | 3300* | delta* | motorola* | sun[234]* \ - | unicom* | ibm* | next | hp | isi* | apollo | altos* \ - | convergent* | ncr* | news | 32* | 3600* | 3100* \ - | hitachi* | c[123]* | convex* | sun | crds | omron* | dg \ - | ultra | tti* | harris | dolphin | highlevel | gould \ - | cbm | ns | masscomp | apple | axis | knuth | cray \ - | microblaze* | sim | cisco \ - | oki | wec | wrs | winbond) + 3100* \ + | 32* \ + | 3300* \ + | 3600* \ + | 7300* \ + | acorn \ + | altos* \ + | apollo \ + | apple \ + | atari \ + | att* \ + | axis \ + | be \ + | bull \ + | cbm \ + | ccur \ + | cisco \ + | commodore \ + | convergent* \ + | convex* \ + | cray \ + | crds \ + | dec* \ + | delta* \ + | dg \ + | digital \ + | dolphin \ + | encore* \ + | gould \ + | harris \ + | highlevel \ + | hitachi* \ + | hp \ + | ibm* \ + | intergraph \ + | isi* \ + | knuth \ + | masscomp \ + | microblaze* \ + | mips* \ + | motorola* \ + | ncr* \ + | news \ + | next \ + | ns \ + | oki \ + | omron* \ + | pc533* \ + | rebel \ + | rom68k \ + | rombug \ + | semi \ + | sequent* \ + | siemens \ + | sgi* \ + | siemens \ + | sim \ + | sni \ + | sony* \ + | stratus \ + | sun \ + | sun[234]* \ + | tektronix \ + | tti* \ + | ultra \ + | unicom* \ + | wec \ + | winbond \ + | wrs) basic_machine=$field1-$field2 basic_os= ;; + zephyr*) + basic_machine=$field1-unknown + basic_os=$field2 + ;; *) basic_machine=$field1 basic_os=$field2 @@ -271,26 +345,6 @@ case $1 in basic_machine=arm-unknown basic_os=cegcc ;; - convex-c1) - basic_machine=c1-convex - basic_os=bsd - ;; - convex-c2) - basic_machine=c2-convex - basic_os=bsd - ;; - convex-c32) - basic_machine=c32-convex - basic_os=bsd - ;; - convex-c34) - basic_machine=c34-convex - basic_os=bsd - ;; - convex-c38) - basic_machine=c38-convex - basic_os=bsd - ;; cray) basic_machine=j90-cray basic_os=unicos @@ -713,15 +767,26 @@ case $basic_machine in vendor=dec basic_os=tops20 ;; - delta | 3300 | motorola-3300 | motorola-delta \ - | 3300-motorola | delta-motorola) + delta | 3300 | delta-motorola | 3300-motorola | motorola-delta | motorola-3300) cpu=m68k vendor=motorola ;; - dpx2*) + # This used to be dpx2*, but that gets the RS6000-based + # DPX/20 and the x86-based DPX/2-100 wrong. See + # https://oldskool.silicium.org/stations/bull_dpx20.htm + # https://www.feb-patrimoine.com/english/bull_dpx2.htm + # https://www.feb-patrimoine.com/english/unix_and_bull.htm + dpx2 | dpx2[23]00 | dpx2[23]xx) cpu=m68k vendor=bull - basic_os=sysv3 + ;; + dpx2100 | dpx21xx) + cpu=i386 + vendor=bull + ;; + dpx20) + cpu=rs6000 + vendor=bull ;; encore | umax | mmax) cpu=ns32k @@ -836,18 +901,6 @@ case $basic_machine in next | m*-next) cpu=m68k vendor=next - case $basic_os in - openstep*) - ;; - nextstep*) - ;; - ns2*) - basic_os=nextstep2 - ;; - *) - basic_os=nextstep3 - ;; - esac ;; np1) cpu=np1 @@ -936,14 +989,13 @@ case $basic_machine in ;; *-*) - # shellcheck disable=SC2162 saved_IFS=$IFS IFS="-" read cpu vendor <&2 + echo "Invalid configuration '$1': machine '$cpu-$vendor' not recognized" 1>&2 exit 1 ;; esac @@ -1307,11 +1491,12 @@ esac # Decode manufacturer-specific aliases for certain operating systems. -if test x$basic_os != x +if test x"$basic_os" != x then # First recognize some ad-hoc cases, or perhaps split kernel-os, or else just # set os. +obj= case $basic_os in gnu/linux*) kernel=linux @@ -1326,7 +1511,6 @@ case $basic_os in os=`echo "$basic_os" | sed -e 's|nto-qnx|qnx|'` ;; *-*) - # shellcheck disable=SC2162 saved_IFS=$IFS IFS="-" read kernel os <&2 + fi + ;; + *) + echo "Invalid configuration '$1': OS '$os' not recognized" 1>&2 + exit 1 + ;; +esac + +case $obj in + aout* | coff* | elf* | pe*) + ;; + '') + # empty is fine + ;; *) - echo Invalid configuration \`"$1"\': OS \`"$os"\' not recognized 1>&2 + echo "Invalid configuration '$1': Machine code format '$obj' not recognized" 1>&2 + exit 1 + ;; +esac + +# Here we handle the constraint that a (synthetic) cpu and os are +# valid only in combination with each other and nowhere else. +case $cpu-$os in + # The "javascript-unknown-ghcjs" triple is used by GHC; we + # accept it here in order to tolerate that, but reject any + # variations. + javascript-ghcjs) + ;; + javascript-* | *-ghcjs) + echo "Invalid configuration '$1': cpu '$cpu' is not valid with os '$os$obj'" 1>&2 exit 1 ;; esac # As a final step for OS-related things, validate the OS-kernel combination # (given a valid OS), if there is a kernel. -case $kernel-$os in - linux-gnu* | linux-dietlibc* | linux-android* | linux-newlib* \ - | linux-musl* | linux-relibc* | linux-uclibc* ) +case $kernel-$os-$obj in + linux-gnu*- | linux-android*- | linux-dietlibc*- | linux-llvm*- \ + | linux-mlibc*- | linux-musl*- | linux-newlib*- \ + | linux-relibc*- | linux-uclibc*- | linux-ohos*- ) + ;; + uclinux-uclibc*- | uclinux-gnu*- ) + ;; + managarm-mlibc*- | managarm-kernel*- ) ;; - uclinux-uclibc* ) + windows*-msvc*-) ;; - -dietlibc* | -newlib* | -musl* | -relibc* | -uclibc* ) + -dietlibc*- | -llvm*- | -mlibc*- | -musl*- | -newlib*- | -relibc*- \ + | -uclibc*- ) # These are just libc implementations, not actual OSes, and thus # require a kernel. - echo "Invalid configuration \`$1': libc \`$os' needs explicit kernel." 1>&2 + echo "Invalid configuration '$1': libc '$os' needs explicit kernel." 1>&2 exit 1 ;; - kfreebsd*-gnu* | kopensolaris*-gnu*) + -kernel*- ) + echo "Invalid configuration '$1': '$os' needs explicit kernel." 1>&2 + exit 1 ;; - vxworks-simlinux | vxworks-simwindows | vxworks-spe) + *-kernel*- ) + echo "Invalid configuration '$1': '$kernel' does not support '$os'." 1>&2 + exit 1 ;; - nto-qnx*) + *-msvc*- ) + echo "Invalid configuration '$1': '$os' needs 'windows'." 1>&2 + exit 1 ;; - os2-emx) + kfreebsd*-gnu*- | knetbsd*-gnu*- | netbsd*-gnu*- | kopensolaris*-gnu*-) + ;; + vxworks-simlinux- | vxworks-simwindows- | vxworks-spe-) + ;; + nto-qnx*-) ;; - *-eabi* | *-gnueabi*) + os2-emx-) ;; - -*) + rtmk-nova-) + ;; + *-eabi*- | *-gnueabi*-) + ;; + none--*) + # None (no kernel, i.e. freestanding / bare metal), + # can be paired with an machine code file format + ;; + -*-) # Blank kernel with real OS is always fine. ;; - *-*) - echo "Invalid configuration \`$1': Kernel \`$kernel' not known to work with OS \`$os'." 1>&2 + --*) + # Blank kernel and OS with real machine code file format is always fine. + ;; + *-*-*) + echo "Invalid configuration '$1': Kernel '$kernel' not known to work with OS '$os'." 1>&2 exit 1 ;; esac @@ -1810,7 +2273,7 @@ case $vendor in *-riscix*) vendor=acorn ;; - *-sunos*) + *-sunos* | *-solaris*) vendor=sun ;; *-cnk* | *-aix*) @@ -1880,7 +2343,7 @@ case $vendor in ;; esac -echo "$cpu-$vendor-${kernel:+$kernel-}$os" +echo "$cpu-$vendor${kernel:+-$kernel}${os:+-$os}${obj:+-$obj}" exit # Local variables: diff --git a/src/dfm-io/dfm-io/denumerator.cpp b/src/dfm-io/dfm-io/denumerator.cpp index 6c129566..bc14e046 100644 --- a/src/dfm-io/dfm-io/denumerator.cpp +++ b/src/dfm-io/dfm-io/denumerator.cpp @@ -5,6 +5,7 @@ #include "private/denumerator_p.h" #include "utils/dlocalhelper.h" +#include "sort/dfilesorter.h" #include #include @@ -803,14 +804,29 @@ QList> DEnumerator::fileInfoList() QList> DEnumerator::sortFileInfoList() { - if (!d->fts) - d->openDirByfts(); + // 使用 FTS 遍历但不预排序(传 nullptr 作为比较函数) + if (!d->fts) { + char *paths[2] = { nullptr, nullptr }; + paths[0] = d->filePath(d->uri); + if (!paths[0]) { + qWarning() << "Failed to get file path for uri:" << d->uri; + return {}; + } + d->fts = fts_open(paths, FTS_COMFOLLOW, nullptr); + free(paths[0]); + + if (!d->fts) { + qWarning() << "fts_open open error : " << QString::fromLocal8Bit(strerror(errno)); + d->error.setCode(DFMIOErrorCode::DFM_IO_ERROR_FTS_OPEN); + return {}; + } + } if (!d->fts) return {}; - QList> listFile; - QList> listDir; + // 收集所有文件信息 + QList> allFiles; QSet hideList; QUrl urlHidden = d->buildUrl(d->uri, ".hidden"); hideList = DLocalHelper::hideListFromUrl(urlHidden); @@ -819,7 +835,8 @@ QList> DEnumerator::sortFileInfoList() qWarning() << "Failed to get file path for uri:" << d->uri; return {}; } - while (1) { + + while (true) { FTSENT *ent = fts_read(d->fts); if (ent == nullptr) { @@ -834,20 +851,30 @@ QList> DEnumerator::sortFileInfoList() if (strcmp(ent->fts_path, dirPath) == 0 || flag == FTS_DP) continue; - d->insertSortFileInfoList(listFile, listDir, ent, d->fts, hideList); + auto sortInfo = DLocalHelper::createSortFileInfo(ent, hideList); + // 跳过子目录遍历 + if (sortInfo->isDir && !sortInfo->isSymLink) { + fts_set(d->fts, ent, FTS_SKIP); + } + allFiles.append(sortInfo); } fts_close(d->fts); d->fts = nullptr; - - // Clean up allocated memory free(dirPath); - if (d->isMixDirAndFile) - return listFile; - - listDir.append(listFile); - return listDir; + // 使用 DFileSorter 进行排序 + DFileSorter::SortConfig config; + // 映射排序角色:kSortRoleCompareFileName(1) -> Name(0), 以此类推 + // kSortRoleCompareDefault(0) 默认按名称排序 + config.role = (d->sortRoleFlag == SortRoleCompareFlag::kSortRoleCompareDefault) + ? DFileSorter::SortRole::Name + : static_cast(static_cast(d->sortRoleFlag) - 1); + config.order = d->sortOrder; + config.mixDirAndFile = d->isMixDirAndFile; + + DFileSorter sorter(config); + return sorter.sort(std::move(allFiles)); } DFMIOError DEnumerator::lastError() const diff --git a/src/dfm-io/dfm-io/sort/dfilesorter.cpp b/src/dfm-io/dfm-io/sort/dfilesorter.cpp new file mode 100644 index 00000000..49f511b3 --- /dev/null +++ b/src/dfm-io/dfm-io/sort/dfilesorter.cpp @@ -0,0 +1,153 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "dfilesorter.h" +#include "dsortkeycache.h" + +#include + +USING_IO_NAMESPACE + +DFileSorter::DFileSorter(const SortConfig &config) + : m_config(config) +{ +} + +QList> DFileSorter::sort( + QList> &&files) +{ + if (files.isEmpty()) { + return files; + } + + QList> result; + + if (m_config.mixDirAndFile) { + // 混排模式:所有文件一起排序 + std::stable_sort(files.begin(), files.end(), getCompareFunc()); + applySortOrder(files); + result = std::move(files); + } else { + // 分离模式:目录在前,文件在后,分别排序 + QList> dirs; + QList> regularFiles; + + separateDirAndFile(std::move(files), dirs, regularFiles); + + std::stable_sort(dirs.begin(), dirs.end(), getCompareFunc()); + applySortOrder(dirs); + + std::stable_sort(regularFiles.begin(), regularFiles.end(), getCompareFunc()); + applySortOrder(regularFiles); + + // 目录排在前面 + result = std::move(dirs); + result.append(std::move(regularFiles)); + } + + return result; +} + +QList> DFileSorter::sort( + const QList> &files) +{ + // 拷贝后排序 + QList> copy = files; + return sort(std::move(copy)); +} + +DFileSorter::CompareFunc DFileSorter::getCompareFunc() const +{ + switch (m_config.role) { + case SortRole::Name: + return [this](const auto &a, const auto &b) { return compareByName(a, b); }; + case SortRole::Size: + return [this](const auto &a, const auto &b) { return compareBySize(a, b); }; + case SortRole::LastModified: + return [this](const auto &a, const auto &b) { return compareByLastModified(a, b); }; + case SortRole::LastRead: + return [this](const auto &a, const auto &b) { return compareByLastRead(a, b); }; + default: + return [this](const auto &a, const auto &b) { return compareByName(a, b); }; + } +} + +bool DFileSorter::compareByName( + const QSharedPointer &a, + const QSharedPointer &b) const +{ + // 使用预缓存的排序键进行比较 + QString nameA = a->url.fileName(); + QString nameB = b->url.fileName(); + + QCollatorSortKey keyA = DSortKeyCache::instance().sortKey(nameA); + QCollatorSortKey keyB = DSortKeyCache::instance().sortKey(nameB); + + return keyA < keyB; +} + +bool DFileSorter::compareBySize( + const QSharedPointer &a, + const QSharedPointer &b) const +{ + // 先按大小比较,大小相同时按名称排序 + if (a->filesize != b->filesize) { + return a->filesize < b->filesize; + } + return compareByName(a, b); +} + +bool DFileSorter::compareByLastModified( + const QSharedPointer &a, + const QSharedPointer &b) const +{ + // 先比较秒,再比较纳秒 + if (a->lastModifed != b->lastModifed) { + return a->lastModifed < b->lastModifed; + } + if (a->lastModifedNs != b->lastModifedNs) { + return a->lastModifedNs < b->lastModifedNs; + } + // 时间相同,按名称排序 + return compareByName(a, b); +} + +bool DFileSorter::compareByLastRead( + const QSharedPointer &a, + const QSharedPointer &b) const +{ + // 先比较秒,再比较纳秒 + if (a->lastRead != b->lastRead) { + return a->lastRead < b->lastRead; + } + if (a->lastReadNs != b->lastReadNs) { + return a->lastReadNs < b->lastReadNs; + } + // 时间相同,按名称排序 + return compareByName(a, b); +} + +void DFileSorter::separateDirAndFile( + QList> &&files, + QList> &dirs, + QList> ®ularFiles) const +{ + dirs.reserve(files.size()); + regularFiles.reserve(files.size()); + + for (auto &file : files) { + if (file->isDir && !file->isSymLink) { + dirs.append(std::move(file)); + } else { + regularFiles.append(std::move(file)); + } + } +} + +void DFileSorter::applySortOrder(QList> &list) const +{ + if (m_config.order == Qt::DescendingOrder) { + std::reverse(list.begin(), list.end()); + } +} diff --git a/src/dfm-io/dfm-io/sort/dfilesorter.h b/src/dfm-io/dfm-io/sort/dfilesorter.h new file mode 100644 index 00000000..09067af9 --- /dev/null +++ b/src/dfm-io/dfm-io/sort/dfilesorter.h @@ -0,0 +1,139 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef DFILESORTER_H +#define DFILESORTER_H + +#include +#include + +#include +#include + +#include + +BEGIN_IO_NAMESPACE + +/** + * @brief 文件排序器 + * + * 提供高性能的文件列表排序功能,支持多种排序角色和排序顺序。 + * 使用 QCollator::sortKey() 进行预缓存排序键,优化性能。 + * + * 用法示例: + * @code + * DFileSorter::SortConfig config; + * config.role = DFileSorter::SortRole::Name; + * config.order = Qt::AscendingOrder; + * config.mixDirAndFile = false; + * + * DFileSorter sorter(config); + * auto sortedList = sorter.sort(std::move(fileList)); + * @endcode + */ +class DFileSorter +{ +public: + /** + * @brief 排序角色 + */ + enum class SortRole : uint8_t { + Name = 0, // 按文件名排序 + Size = 1, // 按文件大小排序 + LastModified = 2, // 按最后修改时间排序 + LastRead = 3 // 按最后访问时间排序 + }; + + /** + * @brief 排序配置 + */ + struct SortConfig { + SortRole role { SortRole::Name }; // 排序角色 + Qt::SortOrder order { Qt::AscendingOrder }; // 排序顺序 + bool mixDirAndFile { false }; // 是否混排目录和文件 + }; + + /** + * @brief 构造函数 + * + * @param config 排序配置 + */ + explicit DFileSorter(const SortConfig &config); + + /** + * @brief 对文件列表进行排序 + * + * @param files 文件列表(使用移动语义避免拷贝) + * @return 排序后的文件列表 + */ + QList> sort( + QList> &&files); + + /** + * @brief 对文件列表进行排序(拷贝版本) + * + * @param files 文件列表 + * @return 排序后的文件列表 + */ + QList> sort( + const QList> &files); + +private: + using CompareFunc = std::function &, + const QSharedPointer &)>; + + /** + * @brief 获取排序比较函数 + */ + CompareFunc getCompareFunc() const; + + /** + * @brief 按名称比较 + */ + bool compareByName( + const QSharedPointer &a, + const QSharedPointer &b) const; + + /** + * @brief 按大小比较 + */ + bool compareBySize( + const QSharedPointer &a, + const QSharedPointer &b) const; + + /** + * @brief 按最后修改时间比较 + */ + bool compareByLastModified( + const QSharedPointer &a, + const QSharedPointer &b) const; + + /** + * @brief 按最后访问时间比较 + */ + bool compareByLastRead( + const QSharedPointer &a, + const QSharedPointer &b) const; + + /** + * @brief 分离目录和文件 + */ + void separateDirAndFile( + QList> &&files, + QList> &dirs, + QList> ®ularFiles) const; + + /** + * @brief 应用排序顺序 + */ + void applySortOrder(QList> &list) const; + +private: + SortConfig m_config; +}; + +END_IO_NAMESPACE + +#endif // DFILESORTER_H diff --git a/src/dfm-io/dfm-io/sort/dsortkeycache.cpp b/src/dfm-io/dfm-io/sort/dsortkeycache.cpp new file mode 100644 index 00000000..003ee411 --- /dev/null +++ b/src/dfm-io/dfm-io/sort/dsortkeycache.cpp @@ -0,0 +1,60 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "dsortkeycache.h" + +USING_IO_NAMESPACE + +DSortKeyCache &DSortKeyCache::instance() +{ + static DSortKeyCache instance; + return instance; +} + +QCollatorSortKey DSortKeyCache::sortKey(const QString &text) +{ + // 先尝试读锁查找缓存 + { + QReadLocker locker(&m_lock); + auto it = m_cache.find(text); + if (it != m_cache.end()) { + return *it; + } + } + + // 缓存未命中,需要创建 + QWriteLocker locker(&m_lock); + + // 双重检查,防止其他线程已经创建 + auto it = m_cache.find(text); + if (it != m_cache.end()) { + return *it; + } + + QCollatorSortKey key = collator().sortKey(text); + m_cache.insert(text, key); + return key; +} + +void DSortKeyCache::clear() +{ + QWriteLocker locker(&m_lock); + m_cache.clear(); +} + +QCollator &DSortKeyCache::collator() +{ + // 线程局部存储,每个线程有自己的 QCollator 实例 + // 避免多线程竞争问题 + thread_local static QCollator s_collator = []() { + QCollator c; + c.setNumericMode(true); // 支持数字自然排序,如 "file2" < "file10" + return c; + }(); + return s_collator; +} + +DSortKeyCache::DSortKeyCache() +{ +} diff --git a/src/dfm-io/dfm-io/sort/dsortkeycache.h b/src/dfm-io/dfm-io/sort/dsortkeycache.h new file mode 100644 index 00000000..48923278 --- /dev/null +++ b/src/dfm-io/dfm-io/sort/dsortkeycache.h @@ -0,0 +1,75 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef DSORTKEYCACHE_H +#define DSORTKEYCACHE_H + +#include + +#include +#include +#include +#include +#include + +BEGIN_IO_NAMESPACE + +/** + * @brief 排序键缓存管理器 + * + * 单例模式,提供线程安全的 QCollatorSortKey 缓存。 + * 使用 QCollator 配置 numericMode 进行国际化自然排序。 + * + * 性能优化: + * - 同一字符串只生成一次排序键 + * - 线程安全的 QCollator 实例(thread_local) + * - 读写锁保护缓存访问 + */ +class DSortKeyCache +{ +public: + /** + * @brief 获取单例实例 + */ + static DSortKeyCache &instance(); + + /** + * @brief 获取或创建字符串的排序键 + * + * 如果缓存中存在则直接返回,否则创建新排序键并缓存。 + * + * @param text 需要排序的文本 + * @return QCollatorSortKey 排序键 + */ + QCollatorSortKey sortKey(const QString &text); + + /** + * @brief 清理缓存 + * + * 在语言环境变化时调用。 + */ + void clear(); + + /** + * @brief 获取配置好的 QCollator 实例 + * + * 线程安全,每个线程有自己的实例。 + * + * @return QCollator& QCollator 引用 + */ + static QCollator &collator(); + +private: + DSortKeyCache(); + ~DSortKeyCache() = default; + DSortKeyCache(const DSortKeyCache &) = delete; + DSortKeyCache &operator=(const DSortKeyCache &) = delete; + + QHash m_cache; + QReadWriteLock m_lock; +}; + +END_IO_NAMESPACE + +#endif // DSORTKEYCACHE_H diff --git a/src/dfm-search/dfm-search-client/CMakeLists.txt b/src/dfm-search/dfm-search-client/CMakeLists.txt index 4e880b03..dcba205b 100644 --- a/src/dfm-search/dfm-search-client/CMakeLists.txt +++ b/src/dfm-search/dfm-search-client/CMakeLists.txt @@ -1,9 +1,18 @@ cmake_minimum_required(VERSION 3.10) -project(dfm${DFM_VERSION_MAJOR}-search-client) +project(dfm-searcher) set(SRCS main.cpp + cli_options.cpp + cli_options.h + time_parser.cpp + time_parser.h + output/output_formatter.h + output/text_output.cpp + output/text_output.h + output/json_output.cpp + output/json_output.h ) find_package(Qt${QT_VERSION_MAJOR}Core REQUIRED) @@ -21,4 +30,4 @@ target_link_libraries( ) -install(TARGETS ${PROJECT_NAME} DESTINATION ${CMAKE_INSTALL_LIBEXECDIR}) +install(TARGETS ${PROJECT_NAME} DESTINATION ${CMAKE_INSTALL_BINDIR}) diff --git a/src/dfm-search/dfm-search-client/cli_options.cpp b/src/dfm-search/dfm-search-client/cli_options.cpp new file mode 100644 index 00000000..0327cb3c --- /dev/null +++ b/src/dfm-search/dfm-search-client/cli_options.cpp @@ -0,0 +1,306 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "cli_options.h" +#include "time_parser.h" + +#include +#include +#include + +using namespace dfmsearch; +using namespace std; + +CliOptions::CliOptions() + : m_typeOption(QStringList() << "type", + "Search type (filename, content or ocr)", "type", "filename"), + m_methodOption(QStringList() << "method", "Search method (indexed or realtime)", "method", "indexed"), + m_queryOption(QStringList() << "query", "Query type (simple, boolean or wildcard)", "query", "simple"), + m_caseSensitiveOption(QStringList() << "case-sensitive", "Enable case sensitivity"), + m_includeHiddenOption(QStringList() << "include-hidden", "Include hidden files"), + m_pinyinOption(QStringList() << "pinyin", "Enable pinyin search (for filename search)"), + m_pinyinAcronymOption(QStringList() << "pinyin-acronym", "Enable pinyin acronym search (for filename search)"), + m_fileTypesOption(QStringList() << "file-types", "Filter by file types, comma separated", "types"), + m_fileExtensionsOption(QStringList() << "file-extensions", "Filter by file extensions, comma separated", "extensions"), + m_maxResultsOption(QStringList() << "max-results", "Maximum number of results", "number", "100"), + m_maxPreviewOption(QStringList() << "max-preview", "Max content preview length", "length", "200"), + m_wildcardOption(QStringList() << "wildcard", "Enable wildcard search with * and ? patterns"), + m_jsonOption(QStringList() << "json" + << "j", + "Output results in JSON format"), + m_verboseOption(QStringList() << "verbose" + << "v", + "Enable verbose output with detailed result information"), + m_timeFieldOption(QStringList() << "time-field", "Time field to filter (birth or modify)", "field", "modify"), + m_timeLastOption(QStringList() << "time-last", "Rolling time window (e.g., 3d, 2h, 30m)", "duration"), + m_timeTodayOption(QStringList() << "time-today", "Filter files from today"), + m_timeYesterdayOption(QStringList() << "time-yesterday", "Filter files from yesterday"), + m_timeThisWeekOption(QStringList() << "time-this-week", "Filter files from this week"), + m_timeLastWeekOption(QStringList() << "time-last-week", "Filter files from last week"), + m_timeThisMonthOption(QStringList() << "time-this-month", "Filter files from this month"), + m_timeLastMonthOption(QStringList() << "time-last-month", "Filter files from last month"), + m_timeThisYearOption(QStringList() << "time-this-year", "Filter files from this year"), + m_timeLastYearOption(QStringList() << "time-last-year", "Filter files from last year"), + m_timeRangeOption(QStringList() << "time-range", "Custom time range (start,end)", "range") +{ + setupOptions(); +} + +void CliOptions::setupOptions() +{ + m_parser.setApplicationDescription("DFM Search Client"); + m_parser.addHelpOption(); + + // 基本选项 + m_parser.addOption(m_typeOption); + m_parser.addOption(m_methodOption); + m_parser.addOption(m_queryOption); + m_parser.addOption(m_caseSensitiveOption); + m_parser.addOption(m_includeHiddenOption); + m_parser.addOption(m_pinyinOption); + m_parser.addOption(m_pinyinAcronymOption); + m_parser.addOption(m_fileTypesOption); + m_parser.addOption(m_fileExtensionsOption); + m_parser.addOption(m_maxResultsOption); + m_parser.addOption(m_maxPreviewOption); + m_parser.addOption(m_wildcardOption); + m_parser.addOption(m_jsonOption); + m_parser.addOption(m_verboseOption); + + // 时间范围过滤选项 + m_parser.addOption(m_timeFieldOption); + m_parser.addOption(m_timeLastOption); + m_parser.addOption(m_timeTodayOption); + m_parser.addOption(m_timeYesterdayOption); + m_parser.addOption(m_timeThisWeekOption); + m_parser.addOption(m_timeLastWeekOption); + m_parser.addOption(m_timeThisMonthOption); + m_parser.addOption(m_timeLastMonthOption); + m_parser.addOption(m_timeThisYearOption); + m_parser.addOption(m_timeLastYearOption); + m_parser.addOption(m_timeRangeOption); + + // 位置参数 + m_parser.addPositionalArgument("keyword", "Search keyword"); + m_parser.addPositionalArgument("search_path", "Path to search in"); +} + +void CliOptions::printHelp() const +{ + std::cout << "Usage: dfm-searcher [options] " << std::endl; + std::cout << std::endl; + std::cout << "Search Types:" << std::endl; + std::cout << " --type= Search type (default: filename)" << std::endl; + std::cout << " filename: Search by file name" << std::endl; + std::cout << " content: Search by file content" << std::endl; + std::cout << " ocr: Search by OCR text in images" << std::endl; + std::cout << std::endl; + std::cout << "Search Options:" << std::endl; + std::cout << " --method= Search method (default: indexed)" << std::endl; + std::cout << " --query= Query type (default: simple)" << std::endl; + std::cout << " --wildcard Enable wildcard search with * and ? patterns" << std::endl; + std::cout << " --case-sensitive Enable case sensitivity" << std::endl; + std::cout << " --include-hidden Include hidden files" << std::endl; + std::cout << " --pinyin Enable pinyin search (for filename search)" << std::endl; + std::cout << " --pinyin-acronym Enable pinyin acronym search (for filename search)" << std::endl; + std::cout << " --file-types= Filter by file types, comma separated" << std::endl; + std::cout << " --file-extensions= Filter by file extensions, comma separated" << std::endl; + std::cout << " --max-results= Maximum number of results" << std::endl; + std::cout << " --max-preview= Max content preview length (for content search)" << std::endl; + std::cout << std::endl; + std::cout << "Time Range Filter Options:" << std::endl; + std::cout << " --time-field= Time field to filter (birth=creation, modify=modification)" << std::endl; + std::cout << " --time-last= Rolling time window: N units ago to now" << std::endl; + std::cout << " Units: m=minutes, h=hours, d=days, w=weeks, M=months, y=years" << std::endl; + std::cout << " Examples: --time-last=3d (last 3 days), --time-last=2h (last 2 hours)" << std::endl; + std::cout << " --time-today Filter files from today" << std::endl; + std::cout << " --time-yesterday Filter files from yesterday" << std::endl; + std::cout << " --time-this-week Filter files from this week" << std::endl; + std::cout << " --time-last-week Filter files from last week" << std::endl; + std::cout << " --time-this-month Filter files from this month" << std::endl; + std::cout << " --time-last-month Filter files from last month" << std::endl; + std::cout << " --time-this-year Filter files from this year" << std::endl; + std::cout << " --time-last-year Filter files from last year" << std::endl; + std::cout << " --time-range=, Custom time range (format: YYYY-MM-DD or \"YYYY-MM-DD HH:MM\")" << std::endl; + std::cout << " Example: --time-range=\"2025-01-01,2025-12-31\"" << std::endl; + std::cout << std::endl; + std::cout << "Output Options:" << std::endl; + std::cout << " --json, -j Output results in JSON format" << std::endl; + std::cout << " --verbose, -v Enable verbose output with detailed result information" << std::endl; + std::cout << " --help Display this help" << std::endl; + std::cout << std::endl; + std::cout << "Examples:" << std::endl; + std::cout << " # Basic filename search" << std::endl; + std::cout << " dfm-searcher \"document\" /home/user" << std::endl; + std::cout << std::endl; + std::cout << " # Content search" << std::endl; + std::cout << " dfm-searcher --type=content \"hello world\" /home/user/Documents" << std::endl; + std::cout << std::endl; + std::cout << " # OCR search in images" << std::endl; + std::cout << " dfm-searcher --type=ocr \"screenshot\" /home/user/Pictures" << std::endl; + std::cout << std::endl; + std::cout << " # Realtime search with time filter" << std::endl; + std::cout << " dfm-searcher --method=realtime --time-last=7d \"report\" /home/user" << std::endl; +} + +bool CliOptions::parse(QCoreApplication &app, SearchCliConfig &config) +{ + m_parser.process(app); + + QStringList positionalArgs = m_parser.positionalArguments(); + if (positionalArgs.size() < 2) { + printHelp(); + return false; + } + + config.keyword = positionalArgs.at(0); + config.searchPath = positionalArgs.at(1); + + // 验证搜索路径 + QFileInfo pathInfo(config.searchPath); + if (!pathInfo.exists() || !pathInfo.isDir()) { + std::cerr << "Error: Search path does not exist or is not a directory" << std::endl; + return false; + } + + // 解析搜索类型 + QString typeStr = m_parser.value(m_typeOption); + if (typeStr == "content") { + config.searchType = SearchType::Content; + } else if (typeStr == "ocr") { + config.searchType = SearchType::Ocr; + } else if (typeStr != "filename") { + std::cerr << "Error: Invalid search type. Use 'filename', 'content', or 'ocr'" << std::endl; + return false; + } + + // 解析搜索方法 + QString methodStr = m_parser.value(m_methodOption); + if (methodStr == "realtime") { + config.searchMethod = SearchMethod::Realtime; + } else if (methodStr != "indexed") { + std::cerr << "Error: Invalid search method. Use 'indexed' or 'realtime'" << std::endl; + return false; + } + + // 解析查询类型 + QString queryStr = m_parser.value(m_queryOption); + if (queryStr == "boolean") { + config.queryType = SearchQuery::Type::Boolean; + } else if (queryStr == "wildcard" || m_parser.isSet(m_wildcardOption)) { + config.queryType = SearchQuery::Type::Wildcard; + } else if (queryStr != "simple") { + std::cerr << "Error: Invalid query type. Use 'simple', 'boolean', or 'wildcard'" << std::endl; + return false; + } + + // 解析开关选项 + config.caseSensitive = m_parser.isSet(m_caseSensitiveOption); + config.includeHidden = m_parser.isSet(m_includeHiddenOption); + config.pinyinEnabled = m_parser.isSet(m_pinyinOption); + config.pinyinAcronymEnabled = m_parser.isSet(m_pinyinAcronymOption); + config.jsonOutput = m_parser.isSet(m_jsonOption); + config.verbose = m_parser.isSet(m_verboseOption); + + // 解析过滤选项 + if (m_parser.isSet(m_fileTypesOption)) { +#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) + config.fileTypes = m_parser.value(m_fileTypesOption).split(',', Qt::SkipEmptyParts); +#else + config.fileTypes = m_parser.value(m_fileTypesOption).split(',', QString::SkipEmptyParts); +#endif + } + + if (m_parser.isSet(m_fileExtensionsOption)) { +#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) + config.fileExtensions = m_parser.value(m_fileExtensionsOption).split(',', Qt::SkipEmptyParts); +#else + config.fileExtensions = m_parser.value(m_fileExtensionsOption).split(',', QString::SkipEmptyParts); +#endif + } + + // 解析数值选项 + if (m_parser.isSet(m_maxResultsOption)) { + bool ok; + int maxResults = m_parser.value(m_maxResultsOption).toInt(&ok); + if (ok && maxResults > 0) { + config.maxResults = maxResults; + } + } + + if (m_parser.isSet(m_maxPreviewOption)) { + bool ok; + int previewLength = m_parser.value(m_maxPreviewOption).toInt(&ok); + if (ok && previewLength > 0) { + config.maxPreviewLength = previewLength; + } + } + + // 解析时间范围选项 + return parseTimeOptions(config); +} + +bool CliOptions::parseTimeOptions(SearchCliConfig &config) +{ + // 设置时间字段 + QString timeFieldStr = m_parser.value(m_timeFieldOption); + if (timeFieldStr == "birth") { + config.timeFilter.setTimeField(DFMSEARCH::TimeField::BirthTime); + } else if (timeFieldStr == "modify") { + config.timeFilter.setTimeField(DFMSEARCH::TimeField::ModifyTime); + } else { + std::cerr << "Error: Invalid time field. Use 'birth' or 'modify'" << std::endl; + return false; + } + + // 处理时间范围选项(只能激活一个) + if (m_parser.isSet(m_timeLastOption)) { + QString lastArg = m_parser.value(m_timeLastOption); + int value; + DFMSEARCH::TimeUnit unit; + if (TimeParser::parseTimeLast(lastArg, value, unit)) { + config.timeFilter.setLast(value, unit); + config.hasTimeFilter = true; + } else { + std::cerr << "Error: Invalid --time-last format. Use format like '3d', '2h', '30m'" << std::endl; + return false; + } + } else if (m_parser.isSet(m_timeTodayOption)) { + config.timeFilter.setToday(); + config.hasTimeFilter = true; + } else if (m_parser.isSet(m_timeYesterdayOption)) { + config.timeFilter.setYesterday(); + config.hasTimeFilter = true; + } else if (m_parser.isSet(m_timeThisWeekOption)) { + config.timeFilter.setThisWeek(); + config.hasTimeFilter = true; + } else if (m_parser.isSet(m_timeLastWeekOption)) { + config.timeFilter.setLastWeek(); + config.hasTimeFilter = true; + } else if (m_parser.isSet(m_timeThisMonthOption)) { + config.timeFilter.setThisMonth(); + config.hasTimeFilter = true; + } else if (m_parser.isSet(m_timeLastMonthOption)) { + config.timeFilter.setLastMonth(); + config.hasTimeFilter = true; + } else if (m_parser.isSet(m_timeThisYearOption)) { + config.timeFilter.setThisYear(); + config.hasTimeFilter = true; + } else if (m_parser.isSet(m_timeLastYearOption)) { + config.timeFilter.setLastYear(); + config.hasTimeFilter = true; + } else if (m_parser.isSet(m_timeRangeOption)) { + QString rangeArg = m_parser.value(m_timeRangeOption); + QDateTime start, end; + if (TimeParser::parseTimeRange(rangeArg, start, end)) { + config.timeFilter.setRange(start, end); + config.hasTimeFilter = true; + } else { + std::cerr << "Error: Invalid --time-range format. Use format 'YYYY-MM-DD,YYYY-MM-DD' or 'YYYY-MM-DD HH:MM,YYYY-MM-DD HH:MM'" << std::endl; + return false; + } + } + + return true; +} diff --git a/src/dfm-search/dfm-search-client/cli_options.h b/src/dfm-search/dfm-search-client/cli_options.h new file mode 100644 index 00000000..f93a6817 --- /dev/null +++ b/src/dfm-search/dfm-search-client/cli_options.h @@ -0,0 +1,116 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef CLI_OPTIONS_H +#define CLI_OPTIONS_H + +#include +#include +#include + +#include +#include +#include + +namespace dfmsearch { + +/** + * @brief 命令行搜索参数配置 + * + * 封装所有命令行解析结果,遵循单一职责原则 + */ +struct SearchCliConfig +{ + // 基本参数 + QString keyword; + QString searchPath; + SearchType searchType = SearchType::FileName; + SearchMethod searchMethod = SearchMethod::Indexed; + SearchQuery::Type queryType = SearchQuery::Type::Simple; + + // 开关选项 + bool caseSensitive = false; + bool includeHidden = false; + bool pinyinEnabled = false; + bool pinyinAcronymEnabled = false; + bool wildcardEnabled = false; + bool jsonOutput = false; + bool verbose = false; // 详细输出模式 + + // 过滤选项 + QStringList fileTypes; + QStringList fileExtensions; + int maxResults = 100; + int maxPreviewLength = 200; + + // 时间范围过滤 + bool hasTimeFilter = false; + DFMSEARCH::TimeRangeFilter timeFilter; +}; + +/** + * @brief 命令行选项管理器 + * + * 负责定义和解析命令行参数,遵循单一职责原则 + */ +class CliOptions +{ +public: + CliOptions(); + ~CliOptions() = default; + + /** + * @brief 解析命令行参数 + * @param app QCoreApplication实例 + * @param config 输出的配置结构 + * @return 解析成功返回true,失败返回false + */ + bool parse(QCoreApplication &app, SearchCliConfig &config); + + /** + * @brief 打印帮助信息 + */ + void printHelp() const; + +private: + void setupOptions(); + bool validateConfig(SearchCliConfig &config); + bool parseTimeOptions(SearchCliConfig &config); + +private: + QCommandLineParser m_parser; + + // 基本选项 + QCommandLineOption m_typeOption; + QCommandLineOption m_methodOption; + QCommandLineOption m_queryOption; + QCommandLineOption m_caseSensitiveOption; + QCommandLineOption m_includeHiddenOption; + QCommandLineOption m_pinyinOption; + QCommandLineOption m_pinyinAcronymOption; + QCommandLineOption m_fileTypesOption; + QCommandLineOption m_fileExtensionsOption; + QCommandLineOption m_maxResultsOption; + QCommandLineOption m_maxPreviewOption; + QCommandLineOption m_wildcardOption; + QCommandLineOption m_jsonOption; + QCommandLineOption m_verboseOption; + + // 时间范围过滤选项 + QCommandLineOption m_timeFieldOption; + QCommandLineOption m_timeLastOption; + QCommandLineOption m_timeTodayOption; + QCommandLineOption m_timeYesterdayOption; + QCommandLineOption m_timeThisWeekOption; + QCommandLineOption m_timeLastWeekOption; + QCommandLineOption m_timeThisMonthOption; + QCommandLineOption m_timeLastMonthOption; + QCommandLineOption m_timeThisYearOption; + QCommandLineOption m_timeLastYearOption; + QCommandLineOption m_timeRangeOption; +}; + +} // namespace dfmsearch + +#endif // CLI_OPTIONS_H diff --git a/src/dfm-search/dfm-search-client/main.cpp b/src/dfm-search/dfm-search-client/main.cpp index ff3104ff..11241f88 100644 --- a/src/dfm-search/dfm-search-client/main.cpp +++ b/src/dfm-search/dfm-search-client/main.cpp @@ -3,562 +3,192 @@ // SPDX-License-Identifier: GPL-3.0-or-later #include -#include -#include #include -#include -#include -#include -#include -#include -#include -#include -#include -#include #include #include #include #include #include #include -#include "../dfm-search-lib/utils/filenameblacklistmatcher.h" +#include -#include +#include "cli_options.h" +#include "output/text_output.h" +#include "output/json_output.h" using namespace dfmsearch; -using namespace std; -// # 基本文件名搜索 -// dfm6-search-client "document" /home/user - -// # 内容搜索 -// dfm6-search-client --type=content "hello world" /home/user/Documents - -// # 使用realtime搜索 -// dfm6-search-client --method=realtime "report" /home/user - -// # 区分大小写的搜索 -// dfm6-search-client --case-sensitive "README" /home/user - -// # 文件类型过滤 -// dfm6-search-client --file-types=doc,pic "" /home/user - -// # 文件后缀过滤 -// dfm6-search-client --file-extensions=txt,pdf "" /home/user - -// # 文件类型和后缀组合过滤 -// dfm6-search-client --file-types=doc --file-extensions=docx,odt "report" /home/user - -// # 布尔查询 -// dfm6-search-client --query=boolean "meeting,notes,2023" /home/user/Documents - -// # Combined -// dfm6-search-client --file-types="dir,doc" --query=boolean "dde,file" / -// dfm6-search-client --pinyin --query=boolean "wendang,xinjian" / -// dfm6-search-client --pinyin --file-types="doc,pic" --query=boolean "wen,dang" / -// dfm6-search-client --pinyin-acronym "nh" /home/user # 搜索"你好"的拼音首字母 -// dfm6-search-client --pinyin-acronym --query=boolean "wd,xj" / # 搜索"文档,新建"的拼音首字母 -// dfm6-search-client --pinyin --pinyin-acronym "wendang" / # 智能模式:有效拼音用拼音搜索 -// dfm6-search-client --pinyin --pinyin-acronym "wd" / # 智能模式:有效首字母用首字母搜索 -// dfm6-search-client --pinyin --pinyin-acronym "nh123" / # 智能模式:首字母+数字用首字母搜索 -// dfm6-search-client --pinyin --pinyin-acronym "abc@#" / # 智能模式:无效输入fallback到普通搜索 -// dfm6-search-client --file-extensions="txt,pdf" --query=boolean "report,data" / - -void printUsage() -{ - std::cout << "Usage: dfm6-search-client [options] " << std::endl; - std::cout << "Options:" << std::endl; - std::cout << " --type= Search type (default: filename)" << std::endl; - std::cout << " --method= Search method (default: indexed)" << std::endl; - std::cout << " --query= Query type (default: simple)" << std::endl; - std::cout << " --wildcard Enable wildcard search with * and ? patterns" << std::endl; - std::cout << " --case-sensitive Enable case sensitivity" << std::endl; - std::cout << " --include-hidden Include hidden files" << std::endl; - std::cout << " --pinyin Enable pinyin search (for filename search)" << std::endl; - std::cout << " --pinyin-acronym Enable pinyin acronym search (for filename search)" << std::endl; - std::cout << " --file-types= Filter by file types, comma separated" << std::endl; - std::cout << " --file-extensions= Filter by file extensions, comma separated" << std::endl; - std::cout << " --max-results= Maximum number of results" << std::endl; - std::cout << " --max-preview= Max content preview length (for content search)" << std::endl; - std::cout << " --json, -j Output results in JSON format" << std::endl; - std::cout << " --help Display this help" << std::endl; -} - -void printSearchResult(const SearchResult &result, SearchType searchType) +/** + * @brief 配置搜索引擎选项 + */ +static void configureSearchOptions(SearchOptions &options, const SearchCliConfig &config) { - std::cout << "Found: " << result.path().toStdString() << std::endl; + options.setSearchMethod(config.searchMethod); + options.setCaseSensitive(config.caseSensitive); + options.setIncludeHidden(config.includeHidden); + options.setSearchPath(config.searchPath); + options.setMaxResults(config.maxResults); + options.setDetailedResultsEnabled(config.verbose); // 使用 verbose 选项控制详细输出 + + if (config.searchMethod == SearchMethod::Realtime) { + options.setResultFoundEnabled(true); + } - if (searchType == SearchType::FileName) { - FileNameResultAPI resultAPI(const_cast(result)); + // 配置类型特定选项 + if (config.searchType == SearchType::FileName) { + FileNameOptionsAPI fileNameOptions(options); + fileNameOptions.setPinyinEnabled(config.pinyinEnabled); + fileNameOptions.setPinyinAcronymEnabled(config.pinyinAcronymEnabled); - if (resultAPI.isDirectory()) { - std::cout << " Type: Directory" << std::endl; - } else { - std::cout << " Type: " << resultAPI.fileType().toStdString() << std::endl; - std::cout << " Size: " << resultAPI.size().toStdString() << " bytes" << std::endl; + if (!config.fileTypes.isEmpty()) { + fileNameOptions.setFileTypes(config.fileTypes); } - - std::cout << " Modified: " << resultAPI.modifiedTime().toStdString() << std::endl; - } else if (searchType == SearchType::Content) { - ContentResultAPI contentResult(const_cast(result)); - std::cout << " Content match: " << contentResult.highlightedContent().toStdString() << std::endl; + if (!config.fileExtensions.isEmpty()) { + fileNameOptions.setFileExtensions(config.fileExtensions); + } + } else if (config.searchType == SearchType::Content) { + ContentOptionsAPI contentOptions(options); + contentOptions.setMaxPreviewLength(config.maxPreviewLength); + contentOptions.setFullTextRetrievalEnabled(true); + contentOptions.setSearchResultHighlightEnabled(true); + contentOptions.setFilenameContentMixedAndSearchEnabled(true); + } else if (config.searchType == SearchType::Ocr) { + OcrTextOptionsAPI ocrTextOptions(options); + ocrTextOptions.setFilenameOcrContentMixedAndSearchEnabled(true); } - std::cout << std::endl; -} - -//-------------------------------------------------------------------- -// JSON Output Helpers -//-------------------------------------------------------------------- - -QJsonValue resultToJson(const SearchResult &result, SearchType searchType) -{ - if (searchType == SearchType::FileName) { - // 文件名搜索:直接返回路径字符串 - return result.path(); - } else if (searchType == SearchType::Content) { - // 内容搜索:返回包含路径和内容匹配的对象 - QJsonObject obj; - obj["path"] = result.path(); - ContentResultAPI contentResult(const_cast(result)); - obj["contentMatch"] = contentResult.highlightedContent(); - return obj; + // 应用时间范围过滤 + if (config.hasTimeFilter) { + options.setTimeRangeFilter(config.timeFilter); } - return result.path(); -} - -void printJsonLine(const QJsonObject &obj) -{ - QJsonDocument doc(obj); - std::cout << doc.toJson(QJsonDocument::Compact).constData() << std::endl; } -// 流式模式:输出搜索开始 -void printJsonSearchStart(const QString &keyword, const QString &searchPath, - SearchType searchType, SearchMethod searchMethod, - const SearchOptions &options) +/** + * @brief 创建输出格式化器 + */ +static OutputFormatter *createOutputFormatter(const SearchCliConfig &config, QObject *parent) { - QJsonObject startObj; - startObj["type"] = "search_started"; - - QJsonObject searchInfo; - searchInfo["keyword"] = keyword; - searchInfo["searchPath"] = searchPath; - searchInfo["searchType"] = (searchType == SearchType::FileName ? "filename" : "content"); - searchInfo["searchMethod"] = (searchMethod == SearchMethod::Indexed ? "indexed" : "realtime"); - searchInfo["caseSensitive"] = options.caseSensitive(); - searchInfo["includeHidden"] = options.includeHidden(); - - startObj["search"] = searchInfo; - startObj["timestamp"] = QDateTime::currentDateTime().toString(Qt::ISODate); - - printJsonLine(startObj); -} - -// 流式模式:输出单个结果 -void printJsonResult(const SearchResult &result, SearchType searchType) -{ - QJsonObject resultObj; - resultObj["type"] = "result"; - resultObj["data"] = resultToJson(result, searchType); - printJsonLine(resultObj); + if (config.jsonOutput) { + // JSON 输出:实时搜索使用流式,索引搜索使用完整输出 + bool streaming = (config.searchMethod == SearchMethod::Realtime); + return new JsonOutput(streaming, parent); + } + return new TextOutput(parent); } -// 流式模式:输出搜索结束 -void printJsonSearchEnd(int totalResults, qint64 elapsedMs) +/** + * @brief 连接搜索引擎信号到输出格式化器 + */ +static void connectSignals(SearchEngine *engine, OutputFormatter *formatter, + const SearchCliConfig &config, QCoreApplication &app) { - QJsonObject endObj; - endObj["type"] = "search_finished"; - - QJsonObject status; - status["state"] = "success"; - status["totalResults"] = totalResults; - - endObj["status"] = status; - - QJsonObject timestamps; - timestamps["finished"] = QDateTime::currentDateTime().toString(Qt::ISODate); - timestamps["duration"] = elapsedMs; - - endObj["timestamps"] = timestamps; - printJsonLine(endObj); + // 连接 finished 信号以退出应用 + QObject::connect(formatter, &OutputFormatter::finished, &app, &QCoreApplication::quit); + + // 搜索开始 + QObject::connect(engine, &SearchEngine::searchStarted, [formatter]() { + formatter->outputSearchStarted(); + }); + + // 结果到达 + QObject::connect(engine, &SearchEngine::resultsFound, [formatter](const SearchResultList &results) { + for (const auto &result : results) { + formatter->outputResult(result); + } + }); + + // 搜索完成 + QObject::connect(engine, &SearchEngine::searchFinished, [formatter](const QList &results) { + formatter->outputSearchFinished(results); + }); + + // 搜索取消 + QObject::connect(engine, &SearchEngine::searchCancelled, [formatter]() { + formatter->outputSearchCancelled(); + }); + + // 错误处理 + QObject::connect(engine, &SearchEngine::errorOccurred, [formatter](const DFMSEARCH::SearchError &error) { + formatter->outputError(error); + }); } -// 完整 JSON 输出(非流式) -struct JsonOutputContext +/** + * @brief 创建搜索查询 + */ +static SearchQuery createSearchQuery(const SearchCliConfig &config) { - QString keyword; - QString searchPath; - SearchType searchType; - SearchMethod searchMethod; - SearchOptions options; - QDateTime startTime; - QJsonArray results; -}; - -void printJsonComplete(const JsonOutputContext &ctx) -{ - QJsonObject root; - - // 搜索信息 - QJsonObject searchInfo; - searchInfo["keyword"] = ctx.keyword; - searchInfo["searchPath"] = ctx.searchPath; - searchInfo["searchType"] = (ctx.searchType == SearchType::FileName ? "filename" : "content"); - searchInfo["searchMethod"] = (ctx.searchMethod == SearchMethod::Indexed ? "indexed" : "realtime"); - searchInfo["caseSensitive"] = ctx.options.caseSensitive(); - searchInfo["includeHidden"] = ctx.options.includeHidden(); - root["search"] = searchInfo; - - // 时间戳 - QDateTime endTime = QDateTime::currentDateTime(); - qint64 duration = ctx.startTime.msecsTo(endTime); - - QJsonObject timestamps; - timestamps["started"] = ctx.startTime.toString(Qt::ISODate); - timestamps["finished"] = endTime.toString(Qt::ISODate); - timestamps["duration"] = duration; - root["timestamps"] = timestamps; - - // 状态 - QJsonObject status; - status["state"] = "success"; - status["totalResults"] = ctx.results.size(); - root["status"] = status; - - // 结果数组 - root["results"] = ctx.results; - - // 输出完整 JSON - QJsonDocument doc(root); - std::cout << doc.toJson(QJsonDocument::Indented).constData() << std::endl; + if (config.queryType == SearchQuery::Type::Simple) { + return SearchFactory::createQuery(config.keyword, SearchQuery::Type::Simple); + } else if (config.queryType == SearchQuery::Type::Wildcard) { + return SearchFactory::createQuery(config.keyword, SearchQuery::Type::Wildcard); + } else { + // Boolean 查询:按逗号分割关键字 +#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) + QStringList keywords = config.keyword.split(',', Qt::SkipEmptyParts); +#else + QStringList keywords = config.keyword.split(',', QString::SkipEmptyParts); +#endif + SearchQuery query = SearchFactory::createQuery(keywords, SearchQuery::Type::Boolean); + query.setBooleanOperator(SearchQuery::BooleanOperator::AND); + return query; + } } int main(int argc, char *argv[]) { - QCoreApplication a(argc, argv); - - QCommandLineParser parser; - parser.setApplicationDescription("DFM Search Client"); - parser.addHelpOption(); - - // Add command line options - QCommandLineOption typeOption(QStringList() << "type", "Search type (filename or content)", "type", "filename"); - QCommandLineOption methodOption(QStringList() << "method", "Search method (indexed or realtime)", "method", "indexed"); - QCommandLineOption queryOption(QStringList() << "query", "Query type (simple or boolean)", "query", "simple"); - QCommandLineOption caseSensitiveOption(QStringList() << "case-sensitive", "Enable case sensitivity"); - QCommandLineOption includeHiddenOption(QStringList() << "include-hidden", "Include hidden files"); - QCommandLineOption pinyinOption(QStringList() << "pinyin", "Enable pinyin search (for filename search)"); - QCommandLineOption pinyinAcronymOption(QStringList() << "pinyin-acronym", "Enable pinyin acronym search (for filename search)"); - QCommandLineOption fileTypesOption(QStringList() << "file-types", "Filter by file types, comma separated", "types"); - QCommandLineOption fileExtensionsOption(QStringList() << "file-extensions", "Filter by file extensions, comma separated", "extensions"); - QCommandLineOption maxResultsOption(QStringList() << "max-results", "Maximum number of results", "number", "100"); - QCommandLineOption maxPreviewOption(QStringList() << "max-preview", "Max content preview length", "length", "200"); - QCommandLineOption wildcardOption(QStringList() << "wildcard", "Enable wildcard search with * and ? patterns"); - QCommandLineOption jsonOption(QStringList() << "json" - << "j", - "Output results in JSON format"); - - parser.addOption(typeOption); - parser.addOption(methodOption); - parser.addOption(queryOption); - parser.addOption(caseSensitiveOption); - parser.addOption(includeHiddenOption); - parser.addOption(pinyinOption); - parser.addOption(pinyinAcronymOption); - parser.addOption(fileTypesOption); - parser.addOption(fileExtensionsOption); - parser.addOption(maxResultsOption); - parser.addOption(maxPreviewOption); - parser.addOption(wildcardOption); - parser.addOption(jsonOption); - - // Setup positional arguments - parser.addPositionalArgument("keyword", "Search keyword"); - parser.addPositionalArgument("search_path", "Path to search in"); - - // Process arguments - parser.process(a); - - QStringList positionalArgs = parser.positionalArguments(); - if (positionalArgs.size() < 2) { - printUsage(); - return 1; - } + QCoreApplication app(argc, argv); - QString keyword = positionalArgs.at(0); - QString searchPath = positionalArgs.at(1); - - // Validate search path - QFileInfo pathInfo(searchPath); - if (!pathInfo.exists() || !pathInfo.isDir()) { - std::cerr << "Error: Search path does not exist or is not a directory" << std::endl; - return 1; - } - - // Get search type - SearchType searchType = SearchType::FileName; - QString typeStr = parser.value(typeOption); - if (typeStr == "content") { - searchType = SearchType::Content; - } else if (typeStr != "filename") { - std::cerr << "Error: Invalid search type. Use 'filename' or 'content'" << std::endl; - return 1; - } - - // Get search method - SearchMethod searchMethod = SearchMethod::Indexed; - QString methodStr = parser.value(methodOption); - if (methodStr == "realtime") { - searchMethod = SearchMethod::Realtime; - } else if (methodStr != "indexed") { - std::cerr << "Error: Invalid search method. Use 'indexed' or 'realtime'" << std::endl; - return 1; - } - - // Get query type - SearchQuery::Type queryType = SearchQuery::Type::Simple; - QString queryStr = parser.value(queryOption); - if (queryStr == "boolean") { - queryType = SearchQuery::Type::Boolean; - } else if (queryStr == "wildcard" || parser.isSet(wildcardOption)) { - queryType = SearchQuery::Type::Wildcard; - } else if (queryStr != "simple") { - std::cerr << "Error: Invalid query type. Use 'simple', 'boolean', or 'wildcard'" << std::endl; + // 解析命令行参数 + CliOptions cliOptions; + SearchCliConfig config; + if (!cliOptions.parse(app, config)) { return 1; } - // Create search engine - SearchEngine *engine = SearchFactory::createEngine(searchType, &a); + // 创建搜索引擎 + SearchEngine *engine = SearchFactory::createEngine(config.searchType, &app); if (!engine) { - std::cerr << "Error: Failed to create search engine" << std::endl; + qCritical() << "Error: Failed to create search engine"; return 1; } - // Setup search options + // 配置搜索选项 SearchOptions options; - options.setSearchMethod(searchMethod); - options.setCaseSensitive(parser.isSet(caseSensitiveOption)); - options.setIncludeHidden(parser.isSet(includeHiddenOption)); - options.setSearchPath(searchPath); - if (searchMethod == SearchMethod::Realtime) { - options.setResultFoundEnabled(true); - options.setDetailedResultsEnabled(true); - } - - // Set max results if specified - if (parser.isSet(maxResultsOption)) { - bool ok; - int maxResults = parser.value(maxResultsOption).toInt(&ok); - if (ok && maxResults > 0) { - options.setMaxResults(maxResults); - } - } - - // Set type-specific options - if (searchType == SearchType::FileName) { - FileNameOptionsAPI fileNameOptions(options); - fileNameOptions.setPinyinEnabled(parser.isSet(pinyinOption)); - fileNameOptions.setPinyinAcronymEnabled(parser.isSet(pinyinAcronymOption)); - - if (parser.isSet(fileTypesOption)) { -#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) - QStringList types = parser.value(fileTypesOption).split(',', Qt::SkipEmptyParts); -#else - QStringList types = parser.value(fileTypesOption).split(',', QString::SkipEmptyParts); -#endif - fileNameOptions.setFileTypes(types); - } - - if (parser.isSet(fileExtensionsOption)) { -#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) - QStringList extensions = parser.value(fileExtensionsOption).split(',', Qt::SkipEmptyParts); -#else - QStringList extensions = parser.value(fileExtensionsOption).split(',', QString::SkipEmptyParts); -#endif - fileNameOptions.setFileExtensions(extensions); - } - } else if (searchType == SearchType::Content) { - ContentOptionsAPI contentOptions(options); - - // if (parser.isSet(fileTypesOption)) { - // QStringList extensions = parser.value(fileTypesOption).split(',', Qt::SkipEmptyParts); - // contentOptions.setFileTypeFilters(extensions); - // } - contentOptions.setMaxPreviewLength(200); - contentOptions.setFullTextRetrievalEnabled(true); - contentOptions.setSearchResultHighlightEnabled(true); - contentOptions.setFilenameContentMixedAndSearchEnabled(true); - if (parser.isSet(maxPreviewOption)) { - bool ok; - int previewLength = parser.value(maxPreviewOption).toInt(&ok); - if (ok && previewLength > 0) { - contentOptions.setMaxPreviewLength(previewLength); - } - } - } - + configureSearchOptions(options, config); engine->setSearchOptions(options); - // Create and configure search query - SearchQuery query; - if (queryType == SearchQuery::Type::Simple) { - query = SearchFactory::createQuery(keyword, SearchQuery::Type::Simple); - } else if (queryType == SearchQuery::Type::Wildcard) { - query = SearchFactory::createQuery(keyword, SearchQuery::Type::Wildcard); - } else { - // For boolean query, split keywords by comma and create a boolean query -#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) - QStringList keywords = keyword.split(',', Qt::SkipEmptyParts); -#else - QStringList keywords = keyword.split(',', QString::SkipEmptyParts); -#endif - - query = SearchFactory::createQuery(keywords, SearchQuery::Type::Boolean); - query.setBooleanOperator(SearchQuery::BooleanOperator::AND); + // 创建输出格式化器 + OutputFormatter *formatter = createOutputFormatter(config, &app); + + // 设置输出格式化器上下文 + formatter->setSearchContext(config.keyword, config.searchPath, + config.searchType, config.searchMethod); + + // 为文本输出设置额外信息 + TextOutput *textOutput = qobject_cast(formatter); + if (textOutput) { + textOutput->setSearchOptions(options); + textOutput->setTimeFilterInfo(config.hasTimeFilter, config.timeFilter); + textOutput->setVerbose(config.verbose); + if (!config.fileExtensions.isEmpty()) { + textOutput->setFileExtensionsFilter(config.fileExtensions.join(',')); + } } - // 检测是否使用 JSON 输出模式 - bool useJsonOutput = parser.isSet(jsonOption); - - // JSON 模式上下文(用于非流式模式) - JsonOutputContext jsonContext; - if (useJsonOutput && searchMethod == SearchMethod::Indexed) { - // 非流式模式:收集所有结果后统一输出 - jsonContext.keyword = keyword; - jsonContext.searchPath = searchPath; - jsonContext.searchType = searchType; - jsonContext.searchMethod = searchMethod; - jsonContext.options = options; - jsonContext.startTime = QDateTime::currentDateTime(); + // 为JSON输出设置额外信息 + JsonOutput *jsonOutput = qobject_cast(formatter); + if (jsonOutput) { + jsonOutput->setSearchOptions(options); } - // Connect signals - if (useJsonOutput) { - // JSON 输出模式 - if (searchMethod == SearchMethod::Realtime) { - // 流式 JSON 输出(实时搜索) - QObject::connect(engine, &SearchEngine::searchStarted, [keyword, searchPath, searchType, searchMethod, &options]() { - printJsonSearchStart(keyword, searchPath, searchType, searchMethod, options); - }); - - QObject::connect(engine, &SearchEngine::resultsFound, [searchType](const SearchResultList &results) { - for (const auto &result : results) - printJsonResult(result, searchType); - }); - - // 记录搜索开始时间 - QDateTime searchStartTime = QDateTime::currentDateTime(); - - QObject::connect(engine, &SearchEngine::searchFinished, [searchStartTime](const QList &results) { - qint64 elapsedMs = searchStartTime.msecsTo(QDateTime::currentDateTime()); - printJsonSearchEnd(results.size(), elapsedMs); - QCoreApplication::quit(); - }); - - QObject::connect(engine, &SearchEngine::searchCancelled, [] { - QJsonObject cancelObj; - cancelObj["type"] = "search_cancelled"; - cancelObj["timestamp"] = QDateTime::currentDateTime().toString(Qt::ISODate); - printJsonLine(cancelObj); - QCoreApplication::quit(); - }); - - QObject::connect(engine, &SearchEngine::errorOccurred, [](const DFMSEARCH::SearchError &error) { - QJsonObject errorObj; - errorObj["type"] = "error"; - errorObj["name"] = error.name(); - errorObj["message"] = error.message(); - errorObj["timestamp"] = QDateTime::currentDateTime().toString(Qt::ISODate); - printJsonLine(errorObj); - }); - - } else { - // 完整 JSON 输出(索引搜索) - QObject::connect(engine, &SearchEngine::searchStarted, []() { - // 非流式模式:开始时不输出 - }); - - QObject::connect(engine, &SearchEngine::resultsFound, [&jsonContext, searchType](const SearchResultList &results) { - for (const auto &result : results) { - jsonContext.results.append(resultToJson(result, searchType)); - } - }); - - QObject::connect(engine, &SearchEngine::searchFinished, [&jsonContext, searchType](const QList &results) { - // 如果结果没有被 resultsFound 收集(禁用了 resultFoundEnabled),则在这里转换 - if (jsonContext.results.isEmpty() && !results.isEmpty()) { - for (const auto &result : results) { - jsonContext.results.append(resultToJson(result, searchType)); - } - } - printJsonComplete(jsonContext); - QCoreApplication::quit(); - }); - - QObject::connect(engine, &SearchEngine::searchCancelled, [] { - QJsonObject cancelObj; - cancelObj["type"] = "search_cancelled"; - cancelObj["timestamp"] = QDateTime::currentDateTime().toString(Qt::ISODate); - printJsonLine(cancelObj); - QCoreApplication::quit(); - }); - - QObject::connect(engine, &SearchEngine::errorOccurred, [](const DFMSEARCH::SearchError &error) { - QJsonObject errorObj; - errorObj["type"] = "error"; - errorObj["name"] = error.name(); - errorObj["message"] = error.message(); - errorObj["timestamp"] = QDateTime::currentDateTime().toString(Qt::ISODate); - printJsonLine(errorObj); - }); - } - } else { - // 文本输出模式(原有逻辑) - QObject::connect(engine, &SearchEngine::searchStarted, [] { - std::cout << "Search started..." << std::endl; - }); - - QObject::connect(engine, &SearchEngine::resultsFound, [searchType](const SearchResultList &results) { - for (const auto &result : results) - printSearchResult(result, searchType); - }); - - QObject::connect(engine, &SearchEngine::searchFinished, [options, searchType](const QList &results) { - std::cout << "Search finished. Total results: " << results.size() << std::endl; - if (!options.resultFoundEnabled()) { - std::for_each(results.begin(), results.end(), [searchType](const SearchResult &result) { - printSearchResult(result, searchType); - }); - } - QCoreApplication::quit(); - }); - - QObject::connect(engine, &SearchEngine::searchCancelled, [] { - std::cout << "Search cancelled" << std::endl; - QCoreApplication::quit(); - }); - - QObject::connect(engine, &SearchEngine::errorOccurred, [](const DFMSEARCH::SearchError &error) { - std::cerr << "[Error]: " << error.code() - << "[Name]: " << error.name().toStdString() - << "[Message]:" << error.message().toStdString() << std::endl; - }); - - // Start search - std::cout << "Searching for: " << keyword.toStdString() << std::endl; - std::cout << "In path: " << searchPath.toStdString() << std::endl; - std::cout << "Search type: " << (searchType == SearchType::FileName ? "Filename" : "Content") << std::endl; - std::cout << "Search method: " << (searchMethod == SearchMethod::Indexed ? "Indexed" : "Realtime") << std::endl; - - // Print file extensions if set - if (searchType == SearchType::FileName && parser.isSet(fileExtensionsOption)) { - std::cout << "File extensions filter: " << parser.value(fileExtensionsOption).toStdString() << std::endl; - } - } + // 连接信号 + connectSignals(engine, formatter, config, app); + // 创建并执行搜索查询 + SearchQuery query = createSearchQuery(config); engine->search(query); - return a.exec(); + return app.exec(); } diff --git a/src/dfm-search/dfm-search-client/output/json_output.cpp b/src/dfm-search/dfm-search-client/output/json_output.cpp new file mode 100644 index 00000000..f1811862 --- /dev/null +++ b/src/dfm-search/dfm-search-client/output/json_output.cpp @@ -0,0 +1,350 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "json_output.h" +#include +#include +#include + +#include +#include + +using namespace dfmsearch; +using namespace std; + +void JsonOutput::setSearchContext(const QString &keyword, const QString &searchPath, + SearchType searchType, SearchMethod searchMethod) +{ + m_keyword = keyword; + m_searchPath = searchPath; + m_searchType = searchType; + m_searchMethod = searchMethod; +} + +QJsonValue JsonOutput::resultToJson(const SearchResult &result) +{ + // 如果不启用详细结果,只返回路径 + if (!m_options.detailedResultsEnabled()) { + return result.path(); + } + + if (m_searchType == SearchType::FileName) { + // 文件名搜索:返回详细对象 + QJsonObject obj; + obj["path"] = result.path(); + + FileNameResultAPI resultAPI(const_cast(result)); + obj["isDirectory"] = resultAPI.isDirectory(); + + if (!resultAPI.isDirectory()) { + obj["fileType"] = resultAPI.fileType(); + obj["size"] = resultAPI.size(); + } + + QString filename = resultAPI.filename(); + if (!filename.isEmpty()) { + obj["filename"] = filename; + } + + QString ext = resultAPI.fileExtension(); + if (!ext.isEmpty()) { + obj["extension"] = ext; + } + + obj["isHidden"] = resultAPI.isHidden(); + + // 修改时间(包含时间戳和时间字符串) + qint64 modifyTs = resultAPI.modifyTimestamp(); + if (modifyTs > 0) { + QJsonObject modifyTimeObj; + modifyTimeObj["timestamp"] = modifyTs; + modifyTimeObj["formatted"] = resultAPI.modifyTimeString(); + obj["modifyTime"] = modifyTimeObj; + } + + // 创建时间(包含时间戳和时间字符串) + qint64 birthTs = resultAPI.birthTimestamp(); + if (birthTs > 0) { + QJsonObject birthTimeObj; + birthTimeObj["timestamp"] = birthTs; + birthTimeObj["formatted"] = resultAPI.birthTimeString(); + obj["birthTime"] = birthTimeObj; + } + + return obj; + } else if (m_searchType == SearchType::Content) { + // 内容搜索:返回包含路径和内容匹配的对象 + QJsonObject obj; + obj["path"] = result.path(); + + ContentResultAPI resultAPI(const_cast(result)); + obj["contentMatch"] = resultAPI.highlightedContent(); + + QString filename = resultAPI.filename(); + if (!filename.isEmpty()) { + obj["filename"] = filename; + } + + obj["isHidden"] = resultAPI.isHidden(); + + // 修改时间 + qint64 modifyTs = resultAPI.modifyTimestamp(); + if (modifyTs > 0) { + QJsonObject modifyTimeObj; + modifyTimeObj["timestamp"] = modifyTs; + modifyTimeObj["formatted"] = resultAPI.modifyTimeString(); + obj["modifyTime"] = modifyTimeObj; + } + + // 创建时间 + qint64 birthTs = resultAPI.birthTimestamp(); + if (birthTs > 0) { + QJsonObject birthTimeObj; + birthTimeObj["timestamp"] = birthTs; + birthTimeObj["formatted"] = resultAPI.birthTimeString(); + obj["birthTime"] = birthTimeObj; + } + + return obj; + } else if (m_searchType == SearchType::Ocr) { + // OCR 搜索:返回详细对象 + QJsonObject obj; + obj["path"] = result.path(); + + OcrTextResultAPI resultAPI(const_cast(result)); + + QString ocrContent = resultAPI.ocrContent(); + if (!ocrContent.isEmpty()) { + obj["ocrContent"] = ocrContent; + } + + QString filename = resultAPI.filename(); + if (!filename.isEmpty()) { + obj["filename"] = filename; + } + + obj["isHidden"] = resultAPI.isHidden(); + + // 修改时间 + qint64 modifyTs = resultAPI.modifyTimestamp(); + if (modifyTs > 0) { + QJsonObject modifyTimeObj; + modifyTimeObj["timestamp"] = modifyTs; + modifyTimeObj["formatted"] = resultAPI.modifyTimeString(); + obj["modifyTime"] = modifyTimeObj; + } + + // 创建时间 + qint64 birthTs = resultAPI.birthTimestamp(); + if (birthTs > 0) { + QJsonObject birthTimeObj; + birthTimeObj["timestamp"] = birthTs; + birthTimeObj["formatted"] = resultAPI.birthTimeString(); + obj["birthTime"] = birthTimeObj; + } + + return obj; + } + return result.path(); +} + +void JsonOutput::printJsonLine(const QJsonObject &obj) +{ + QJsonDocument doc(obj); + std::cout << doc.toJson(QJsonDocument::Compact).constData() << std::endl; +} + +void JsonOutput::outputSearchStarted() +{ + m_startTime = QDateTime::currentDateTime(); + m_collectedResults = QJsonArray(); // 清空收集的结果 + + if (m_streaming) { + outputStreamingStart(); + } + // 非流式模式:开始时不输出 +} + +void JsonOutput::outputStreamingStart() +{ + QJsonObject startObj; + startObj["type"] = "search_started"; + + QString searchTypeStr; + switch (m_searchType) { + case SearchType::FileName: + searchTypeStr = "filename"; + break; + case SearchType::Content: + searchTypeStr = "content"; + break; + case SearchType::Ocr: + searchTypeStr = "ocr"; + break; + default: + searchTypeStr = "unknown"; + } + + QJsonObject searchInfo; + searchInfo["keyword"] = m_keyword; + searchInfo["searchPath"] = m_searchPath; + searchInfo["searchType"] = searchTypeStr; + searchInfo["searchMethod"] = (m_searchMethod == SearchMethod::Indexed ? "indexed" : "realtime"); + searchInfo["caseSensitive"] = m_options.caseSensitive(); + searchInfo["includeHidden"] = m_options.includeHidden(); + + // 添加时间范围过滤信息 + if (m_options.hasTimeRangeFilter()) { + DFMSEARCH::TimeRangeFilter timeFilter = m_options.timeRangeFilter(); + QJsonObject timeFilterInfo; + timeFilterInfo["field"] = (timeFilter.timeField() == DFMSEARCH::TimeField::BirthTime) ? "birth" : "modify"; + auto [start, end] = timeFilter.resolveTimeRange(); + timeFilterInfo["startTime"] = start.toString(Qt::ISODate); + timeFilterInfo["endTime"] = end.toString(Qt::ISODate); + searchInfo["timeRangeFilter"] = timeFilterInfo; + } + + startObj["search"] = searchInfo; + startObj["timestamp"] = QDateTime::currentDateTime().toString(Qt::ISODate); + + printJsonLine(startObj); +} + +void JsonOutput::outputResult(const SearchResult &result) +{ + if (m_streaming) { + outputStreamingResult(result); + } else { + // 非流式模式:收集结果 + m_collectedResults.append(resultToJson(result)); + } +} + +void JsonOutput::outputStreamingResult(const SearchResult &result) +{ + QJsonObject resultObj; + resultObj["type"] = "result"; + resultObj["data"] = resultToJson(result); + printJsonLine(resultObj); +} + +void JsonOutput::outputSearchFinished(const QList &results) +{ + if (m_streaming) { + outputStreamingFinish(results); + } else { + outputCompleteResult(results); + } + + emit finished(); +} + +void JsonOutput::outputStreamingFinish(const QList &results) +{ + QJsonObject endObj; + endObj["type"] = "search_finished"; + + QJsonObject status; + status["state"] = "success"; + status["totalResults"] = results.size(); + + endObj["status"] = status; + + QJsonObject timestamps; + timestamps["finished"] = QDateTime::currentDateTime().toString(Qt::ISODate); + timestamps["duration"] = m_startTime.msecsTo(QDateTime::currentDateTime()); + + endObj["timestamps"] = timestamps; + printJsonLine(endObj); +} + +void JsonOutput::outputCompleteResult(const QList &results) +{ + QJsonObject root; + + // 搜索信息 + QJsonObject searchInfo; + searchInfo["keyword"] = m_keyword; + searchInfo["searchPath"] = m_searchPath; + + QString searchTypeStr; + switch (m_searchType) { + case SearchType::FileName: + searchTypeStr = "filename"; + break; + case SearchType::Content: + searchTypeStr = "content"; + break; + case SearchType::Ocr: + searchTypeStr = "ocr"; + break; + default: + searchTypeStr = "unknown"; + } + searchInfo["searchType"] = searchTypeStr; + searchInfo["searchMethod"] = (m_searchMethod == SearchMethod::Indexed ? "indexed" : "realtime"); + searchInfo["caseSensitive"] = m_options.caseSensitive(); + searchInfo["includeHidden"] = m_options.includeHidden(); + + // 添加时间范围过滤信息 + if (m_options.hasTimeRangeFilter()) { + DFMSEARCH::TimeRangeFilter timeFilter = m_options.timeRangeFilter(); + QJsonObject timeFilterInfo; + timeFilterInfo["field"] = (timeFilter.timeField() == DFMSEARCH::TimeField::BirthTime) ? "birth" : "modify"; + auto [start, end] = timeFilter.resolveTimeRange(); + timeFilterInfo["startTime"] = start.toString(Qt::ISODate); + timeFilterInfo["endTime"] = end.toString(Qt::ISODate); + searchInfo["timeRangeFilter"] = timeFilterInfo; + } + + root["search"] = searchInfo; + + // 时间戳 + QDateTime endTime = QDateTime::currentDateTime(); + qint64 duration = m_startTime.msecsTo(endTime); + + QJsonObject timestamps; + timestamps["started"] = m_startTime.toString(Qt::ISODate); + timestamps["finished"] = endTime.toString(Qt::ISODate); + timestamps["duration"] = duration; + root["timestamps"] = timestamps; + + // 状态 + QJsonObject status; + status["state"] = "success"; + status["totalResults"] = m_collectedResults.isEmpty() ? results.size() : m_collectedResults.size(); + root["status"] = status; + + // 结果数组 + if (m_collectedResults.isEmpty() && !results.isEmpty()) { + // 如果结果没有被 resultsFound 收集,则在这里转换 + for (const auto &result : results) { + m_collectedResults.append(resultToJson(result)); + } + } + root["results"] = m_collectedResults; + + // 输出完整 JSON + QJsonDocument doc(root); + std::cout << doc.toJson(QJsonDocument::Indented).constData() << std::endl; +} + +void JsonOutput::outputSearchCancelled() +{ + QJsonObject cancelObj; + cancelObj["type"] = "search_cancelled"; + cancelObj["timestamp"] = QDateTime::currentDateTime().toString(Qt::ISODate); + printJsonLine(cancelObj); + emit finished(); +} + +void JsonOutput::outputError(const DFMSEARCH::SearchError &error) +{ + QJsonObject errorObj; + errorObj["type"] = "error"; + errorObj["name"] = error.name(); + errorObj["message"] = error.message(); + errorObj["timestamp"] = QDateTime::currentDateTime().toString(Qt::ISODate); + printJsonLine(errorObj); +} diff --git a/src/dfm-search/dfm-search-client/output/json_output.h b/src/dfm-search/dfm-search-client/output/json_output.h new file mode 100644 index 00000000..69d4db2f --- /dev/null +++ b/src/dfm-search/dfm-search-client/output/json_output.h @@ -0,0 +1,76 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef JSON_OUTPUT_H +#define JSON_OUTPUT_H + +#include "output_formatter.h" +#include + +#include +#include + +namespace dfmsearch { + +/** + * @brief JSON格式输出器 + * + * 支持流式和非流式两种JSON输出模式 + */ +class JsonOutput : public OutputFormatter +{ + Q_OBJECT + +public: + explicit JsonOutput(bool streaming = false, QObject *parent = nullptr) + : OutputFormatter(parent), m_streaming(streaming) { } + + void setSearchContext(const QString &keyword, const QString &searchPath, + SearchType searchType, SearchMethod searchMethod) override; + + void outputSearchStarted() override; + void outputResult(const SearchResult &result) override; + void outputSearchFinished(const QList &results) override; + void outputSearchCancelled() override; + void outputError(const DFMSEARCH::SearchError &error) override; + + /** + * @brief 设置搜索选项 + */ + void setSearchOptions(const SearchOptions &options) { m_options = options; } + + /** + * @brief 设置是否启用详细输出模式 + * @param verbose true 启用详细输出 + */ + void setVerbose(bool verbose) { m_verbose = verbose; } + +private: + QJsonValue resultToJson(const SearchResult &result); + void printJsonLine(const QJsonObject &obj); + + // 流式输出方法 + void outputStreamingStart(); + void outputStreamingResult(const SearchResult &result); + void outputStreamingFinish(const QList &results); + + // 非流式输出方法 + void outputCompleteResult(const QList &results); + +private: + QString m_keyword; + QString m_searchPath; + SearchType m_searchType = SearchType::FileName; + SearchMethod m_searchMethod = SearchMethod::Indexed; + SearchOptions m_options; + QDateTime m_startTime; + + bool m_streaming; + bool m_verbose = false; + QJsonArray m_collectedResults; +}; + +} // namespace dfmsearch + +#endif // JSON_OUTPUT_H diff --git a/src/dfm-search/dfm-search-client/output/output_formatter.h b/src/dfm-search/dfm-search-client/output/output_formatter.h new file mode 100644 index 00000000..9312e35e --- /dev/null +++ b/src/dfm-search/dfm-search-client/output/output_formatter.h @@ -0,0 +1,77 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef OUTPUT_FORMATTER_H +#define OUTPUT_FORMATTER_H + +#include +#include +#include + +#include +#include + +namespace dfmsearch { + +/** + * @brief 搜索结果输出格式化器基类 + * + * 定义输出格式化的抽象接口,遵循依赖倒置原则 (DIP) + */ +class OutputFormatter : public QObject +{ + Q_OBJECT + +public: + explicit OutputFormatter(QObject *parent = nullptr) + : QObject(parent) { } + virtual ~OutputFormatter() = default; + + /** + * @brief 设置搜索上下文信息 + */ + virtual void setSearchContext(const QString &keyword, const QString &searchPath, + SearchType searchType, SearchMethod searchMethod) = 0; + + /** + * @brief 输出搜索开始 + */ + virtual void outputSearchStarted() = 0; + + /** + * @brief 输出单个搜索结果 + */ + virtual void outputResult(const SearchResult &result) = 0; + + /** + * @brief 输出搜索结束 + */ + virtual void outputSearchFinished(const QList &results) = 0; + + /** + * @brief 输出搜索取消 + */ + virtual void outputSearchCancelled() = 0; + + /** + * @brief 设置是否启用详细输出模式 + * @param verbose true 启用详细输出, false 只禁用简易输出 + */ + virtual void setVerbose(bool verbose) = 0; + + /** + * @brief 输出错误 + */ + virtual void outputError(const DFMSEARCH::SearchError &error) = 0; + +Q_SIGNALS: + /** + * @brief 输出完成信号(用于退出应用) + */ + void finished(); +}; + +} // namespace dfmsearch + +#endif // OUTPUT_FORMATTER_H diff --git a/src/dfm-search/dfm-search-client/output/text_output.cpp b/src/dfm-search/dfm-search-client/output/text_output.cpp new file mode 100644 index 00000000..15b0bd87 --- /dev/null +++ b/src/dfm-search/dfm-search-client/output/text_output.cpp @@ -0,0 +1,199 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "text_output.h" +#include +#include +#include + +#include + +using namespace dfmsearch; +using namespace std; + +void TextOutput::setSearchContext(const QString &keyword, const QString &searchPath, + SearchType searchType, SearchMethod searchMethod) +{ + m_keyword = keyword; + m_searchPath = searchPath; + m_searchType = searchType; + m_searchMethod = searchMethod; +} + +void TextOutput::setTimeFilterInfo(bool hasFilter, const DFMSEARCH::TimeRangeFilter &filter) +{ + m_hasTimeFilter = hasFilter; + m_timeFilter = filter; +} + +void TextOutput::outputSearchStarted() +{ + std::cout << "Search started..." << std::endl; + std::cout << "Searching for: " << m_keyword.toStdString() << std::endl; + std::cout << "In path: " << m_searchPath.toStdString() << std::endl; + + QString typeStr = "Filename"; + if (m_searchType == SearchType::Content) + typeStr = "Content"; + else if (m_searchType == SearchType::Ocr) + typeStr = "Ocr"; + std::cout << "Search type: " << typeStr.toStdString() << std::endl; + std::cout << "Search method: " << (m_searchMethod == SearchMethod::Indexed ? "Indexed" : "Realtime") << std::endl; + + // 打印文件扩展名过滤 + if (!m_fileExtensionsFilter.isEmpty()) { + std::cout << "File extensions filter: " << m_fileExtensionsFilter.toStdString() << std::endl; + } + + // 打印时间范围过滤 + if (m_hasTimeFilter) { + std::cout << "Time range filter: "; + std::cout << (m_timeFilter.timeField() == DFMSEARCH::TimeField::BirthTime ? "birth_time" : "modify_time"); + auto [start, end] = m_timeFilter.resolveTimeRange(); + std::cout << " from " << start.toString("yyyy-MM-dd HH:mm:ss").toStdString() + << " to " << end.toString("yyyy-MM-dd HH:mm:ss").toStdString(); + std::cout << std::endl; + } +} + +void TextOutput::printSearchResult(const SearchResult &result) +{ + // 非详细模式下只输出路径 + if (!m_verbose) { + std::cout << result.path().toStdString() << std::endl; + return; + } + + // 详细模式下输出所有信息 + std::cout << "Found: " << result.path().toStdString() << std::endl; + + if (m_searchType == SearchType::FileName) { + FileNameResultAPI resultAPI(const_cast(result)); + + if (resultAPI.isDirectory()) { + std::cout << " Type: Directory" << std::endl; + } else { + std::cout << " Type: " << resultAPI.fileType().toStdString() << std::endl; + std::cout << " Size: " << resultAPI.size().toStdString() << " bytes" << std::endl; + } + + // 文件名和扩展名 + QString filename = resultAPI.filename(); + if (!filename.isEmpty()) { + std::cout << " Filename: " << filename.toStdString() << std::endl; + } + QString ext = resultAPI.fileExtension(); + if (!ext.isEmpty()) { + std::cout << " Extension: " << ext.toStdString() << std::endl; + } + + // 隐藏状态 + std::cout << " Hidden: " << (resultAPI.isHidden() ? "Yes" : "No") << std::endl; + + // 修改时间(同时输出时间戳和时间字符串) + qint64 modifyTs = resultAPI.modifyTimestamp(); + if (modifyTs > 0) { + std::cout << " Modified: " << resultAPI.modifyTimeString().toStdString() + << " (timestamp: " << modifyTs << ")" << std::endl; + } + + // 创建时间(同时输出时间戳和时间字符串) + qint64 birthTs = resultAPI.birthTimestamp(); + if (birthTs > 0) { + std::cout << " Created: " << resultAPI.birthTimeString().toStdString() + << " (timestamp: " << birthTs << ")" << std::endl; + } + } else if (m_searchType == SearchType::Content) { + ContentResultAPI resultAPI(const_cast(result)); + + std::cout << " Content match: " << resultAPI.highlightedContent().toStdString() << std::endl; + + // 文件名 + QString filename = resultAPI.filename(); + if (!filename.isEmpty()) { + std::cout << " Filename: " << filename.toStdString() << std::endl; + } + + // 隐藏状态 + std::cout << " Hidden: " << (resultAPI.isHidden() ? "Yes" : "No") << std::endl; + + // 修改时间 + qint64 modifyTs = resultAPI.modifyTimestamp(); + if (modifyTs > 0) { + std::cout << " Modified: " << resultAPI.modifyTimeString().toStdString() + << " (timestamp: " << modifyTs << ")" << std::endl; + } + + // 创建时间 + qint64 birthTs = resultAPI.birthTimestamp(); + if (birthTs > 0) { + std::cout << " Created: " << resultAPI.birthTimeString().toStdString() + << " (timestamp: " << birthTs << ")" << std::endl; + } + } else if (m_searchType == SearchType::Ocr) { + OcrTextResultAPI resultAPI(const_cast(result)); + + QString ocrContent = resultAPI.ocrContent(); + if (!ocrContent.isEmpty()) { + std::cout << " OCR content: " << ocrContent.toStdString() << std::endl; + } + + // 文件名 + QString filename = resultAPI.filename(); + if (!filename.isEmpty()) { + std::cout << " Filename: " << filename.toStdString() << std::endl; + } + + // 隐藏状态 + std::cout << " Hidden: " << (resultAPI.isHidden() ? "Yes" : "No") << std::endl; + + // 修改时间 + qint64 modifyTs = resultAPI.modifyTimestamp(); + if (modifyTs > 0) { + std::cout << " Modified: " << resultAPI.modifyTimeString().toStdString() + << " (timestamp: " << modifyTs << ")" << std::endl; + } + + // 创建时间 + qint64 birthTs = resultAPI.birthTimestamp(); + if (birthTs > 0) { + std::cout << " Created: " << resultAPI.birthTimeString().toStdString() + << " (timestamp: " << birthTs << ")" << std::endl; + } + } + + std::cout << std::endl; +} + +void TextOutput::outputResult(const SearchResult &result) +{ + printSearchResult(result); +} + +void TextOutput::outputSearchFinished(const QList &results) +{ + std::cout << "Search finished. Total results: " << results.size() << std::endl; + + // 如果禁用了 resultFoundEnabled,则在这里输出结果 + if (!m_options.resultFoundEnabled()) { + for (const auto &result : results) { + printSearchResult(result); + } + } + + emit finished(); +} + +void TextOutput::outputSearchCancelled() +{ + std::cout << "Search cancelled" << std::endl; + emit finished(); +} + +void TextOutput::outputError(const DFMSEARCH::SearchError &error) +{ + std::cerr << "[Error]: " << error.code() + << "[Name]: " << error.name().toStdString() + << "[Message]:" << error.message().toStdString() << std::endl; +} diff --git a/src/dfm-search/dfm-search-client/output/text_output.h b/src/dfm-search/dfm-search-client/output/text_output.h new file mode 100644 index 00000000..4168fa28 --- /dev/null +++ b/src/dfm-search/dfm-search-client/output/text_output.h @@ -0,0 +1,73 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef TEXT_OUTPUT_H +#define TEXT_OUTPUT_H + +#include "output_formatter.h" +#include + +namespace dfmsearch { + +/** + * @brief 文本格式输出器 + * + * 以人类可读的文本格式输出搜索结果 + */ +class TextOutput : public OutputFormatter +{ + Q_OBJECT + +public: + explicit TextOutput(QObject *parent = nullptr) + : OutputFormatter(parent) { } + + void setSearchContext(const QString &keyword, const QString &searchPath, + SearchType searchType, SearchMethod searchMethod) override; + + void outputSearchStarted() override; + void outputResult(const SearchResult &result) override; + void outputSearchFinished(const QList &results) override; + void outputSearchCancelled() override; + void outputError(const DFMSEARCH::SearchError &error) override; + + /** + * @brief 设置文件扩展名过滤(用于输出提示) + */ + void setFileExtensionsFilter(const QString &extensions) { m_fileExtensionsFilter = extensions; } + + /** + * @brief 设置时间过滤信息 + */ + void setTimeFilterInfo(bool hasFilter, const DFMSEARCH::TimeRangeFilter &filter); + + /** + * @brief 设置搜索选项 + */ + void setSearchOptions(const SearchOptions &options) { m_options = options; } + + /** + * @brief 设置是否启用详细输出模式 + * @param verbose true 启用详细输出, false 只输出路径 + */ + void setVerbose(bool verbose) { m_verbose = verbose; } + +private: + void printSearchResult(const SearchResult &result); + +private: + QString m_keyword; + QString m_searchPath; + SearchType m_searchType = SearchType::FileName; + SearchMethod m_searchMethod = SearchMethod::Indexed; + SearchOptions m_options; + QString m_fileExtensionsFilter; + bool m_hasTimeFilter = false; + DFMSEARCH::TimeRangeFilter m_timeFilter; + bool m_verbose = false; +}; + +} // namespace dfmsearch + +#endif // TEXT_OUTPUT_H diff --git a/src/dfm-search/dfm-search-client/time_parser.cpp b/src/dfm-search/dfm-search-client/time_parser.cpp new file mode 100644 index 00000000..c5485784 --- /dev/null +++ b/src/dfm-search/dfm-search-client/time_parser.cpp @@ -0,0 +1,79 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "time_parser.h" + +#include +#include + +using namespace dfmsearch; + +bool TimeParser::parseTimeLast(const QString &arg, int &value, DFMSEARCH::TimeUnit &unit) +{ + if (arg.isEmpty()) + return false; + + // 提取数字和单位 + QRegularExpression re("^(\\d+)([mhdwMy])$"); + QRegularExpressionMatch match = re.match(arg); + + if (!match.hasMatch()) + return false; + + bool ok; + value = match.captured(1).toInt(&ok); + if (!ok || value < 0) + return false; + + QString unitChar = match.captured(2); + if (unitChar == "m") { + unit = DFMSEARCH::TimeUnit::Minutes; + } else if (unitChar == "h") { + unit = DFMSEARCH::TimeUnit::Hours; + } else if (unitChar == "d") { + unit = DFMSEARCH::TimeUnit::Days; + } else if (unitChar == "w") { + unit = DFMSEARCH::TimeUnit::Weeks; + } else if (unitChar == "M") { + unit = DFMSEARCH::TimeUnit::Months; + } else if (unitChar == "y") { + unit = DFMSEARCH::TimeUnit::Years; + } else { + return false; + } + + return true; +} + +bool TimeParser::parseTimeRange(const QString &arg, QDateTime &start, QDateTime &end) +{ + if (arg.isEmpty()) + return false; + + QStringList parts = arg.split(','); + if (parts.size() != 2) + return false; + + QString startStr = parts[0].trimmed(); + QString endStr = parts[1].trimmed(); + + // 优先尝试带时间的格式,再尝试不带时间的 + start = QDateTime::fromString(startStr, "yyyy-MM-dd HH:mm"); + if (!start.isValid()) { + start = QDateTime::fromString(startStr, "yyyy-MM-dd"); + if (start.isValid()) { + start.setTime(QTime(0, 0, 0)); + } + } + + end = QDateTime::fromString(endStr, "yyyy-MM-dd HH:mm"); + if (!end.isValid()) { + end = QDateTime::fromString(endStr, "yyyy-MM-dd"); + if (end.isValid()) { + end.setTime(QTime(23, 59, 59)); + } + } + + return start.isValid() && end.isValid(); +} diff --git a/src/dfm-search/dfm-search-client/time_parser.h b/src/dfm-search/dfm-search-client/time_parser.h new file mode 100644 index 00000000..eacd9406 --- /dev/null +++ b/src/dfm-search/dfm-search-client/time_parser.h @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef TIME_PARSER_H +#define TIME_PARSER_H + +#include +#include +#include + +namespace dfmsearch { + +/** + * @brief 时间范围解析工具类 + * + * 提供时间参数解析功能,遵循单一职责原则 + */ +class TimeParser +{ +public: + /** + * @brief 解析 --time-last 参数(如 "3d", "2h", "30m") + * @param arg 输入字符串 + * @param value 输出数值 + * @param unit 输出时间单位 + * @return 解析成功返回true + */ + static bool parseTimeLast(const QString &arg, int &value, DFMSEARCH::TimeUnit &unit); + + /** + * @brief 解析 --time-range 参数(如 "2025-01-01,2025-12-31") + * @param arg 输入字符串 + * @param start 输出开始时间 + * @param end 输出结束时间 + * @return 解析成功返回true + */ + static bool parseTimeRange(const QString &arg, QDateTime &start, QDateTime &end); +}; + +} // namespace dfmsearch + +#endif // TIME_PARSER_H diff --git a/src/dfm-search/dfm-search-lib/contentsearch/contentsearchapi.cpp b/src/dfm-search/dfm-search-lib/contentsearch/contentsearchapi.cpp index e4edce5c..913df0ca 100644 --- a/src/dfm-search/dfm-search-lib/contentsearch/contentsearchapi.cpp +++ b/src/dfm-search/dfm-search-lib/contentsearch/contentsearchapi.cpp @@ -2,6 +2,9 @@ // // SPDX-License-Identifier: GPL-3.0-or-later #include +#include + +#include DFM_SEARCH_BEGIN_NS @@ -72,4 +75,62 @@ void ContentResultAPI::setHighlightedContent(const QString &content) m_result.setCustomAttribute("highlightedContent", content); } +// ==================== Extended Attributes ==================== + +QString ContentResultAPI::filename() const +{ + return m_result.customAttribute("filename").toString(); +} + +void ContentResultAPI::setFilename(const QString &name) +{ + m_result.setCustomAttribute("filename", name); +} + +bool ContentResultAPI::isHidden() const +{ + return m_result.customAttribute("isHidden").toBool(); +} + +void ContentResultAPI::setIsHidden(bool hidden) +{ + m_result.setCustomAttribute("isHidden", hidden); +} + +// ==================== Modification Time ==================== + +void ContentResultAPI::setModifyTimestamp(qint64 timestamp) +{ + m_result.setCustomAttribute("modifyTimestamp", timestamp); +} + +qint64 ContentResultAPI::modifyTimestamp() const +{ + return m_result.customAttribute("modifyTimestamp").toLongLong(); +} + +QString ContentResultAPI::modifyTimeString() const +{ + qint64 ts = modifyTimestamp(); + return ts > 0 ? TimeResultAPI::formatTimestamp(ts) : QString(); +} + +// ==================== Birth/Creation Time ==================== + +void ContentResultAPI::setBirthTimestamp(qint64 timestamp) +{ + m_result.setCustomAttribute("birthTimestamp", timestamp); +} + +qint64 ContentResultAPI::birthTimestamp() const +{ + return m_result.customAttribute("birthTimestamp").toLongLong(); +} + +QString ContentResultAPI::birthTimeString() const +{ + qint64 ts = birthTimestamp(); + return ts > 0 ? TimeResultAPI::formatTimestamp(ts) : QString(); +} + DFM_SEARCH_END_NS diff --git a/src/dfm-search/dfm-search-lib/contentsearch/contentstrategies/indexedstrategy.cpp b/src/dfm-search/dfm-search-lib/contentsearch/contentstrategies/indexedstrategy.cpp index db59130b..bc498aa6 100644 --- a/src/dfm-search/dfm-search-lib/contentsearch/contentstrategies/indexedstrategy.cpp +++ b/src/dfm-search/dfm-search-lib/contentsearch/contentstrategies/indexedstrategy.cpp @@ -16,12 +16,16 @@ #include #include +#include +#include + #include "3rdparty/fulltext/chineseanalyzer.h" #include "utils/cancellablecollector.h" #include "utils/contenthighlighter.h" #include "utils/lucenequeryutils.h" #include "utils/searchutility.h" #include "utils/lucene_cancellation_compat.h" +#include "utils/timerangeutils.h" using namespace Lucene; @@ -69,7 +73,7 @@ Lucene::QueryPtr ContentIndexedStrategy::buildLuceneQuery(const SearchQuery &que Lucene::QueryParserPtr contentsParser = newLucene( Lucene::LuceneVersion::LUCENE_CURRENT, - L"contents", + LuceneFieldNames::Content::kContents, analyzer); Lucene::QueryPtr mainQuery; @@ -99,13 +103,43 @@ Lucene::QueryPtr ContentIndexedStrategy::buildLuceneQuery(const SearchQuery &que // Add path prefix query optimization if (mainQuery && SearchUtility::isContentIndexAncestorPathsSupported() && SearchUtility::shouldUsePathPrefixQuery(searchPath)) { - QueryPtr pathPrefixQuery = LuceneQueryUtils::buildPathPrefixQuery(searchPath, "ancestor_paths"); + QueryPtr pathPrefixQuery = LuceneQueryUtils::buildPathPrefixQuery(searchPath, + QString::fromWCharArray(LuceneFieldNames::Content::kAncestorPaths)); if (pathPrefixQuery) { BooleanQueryPtr finalQuery = newLucene(); finalQuery->add(mainQuery, BooleanClause::MUST); finalQuery->add(pathPrefixQuery, BooleanClause::MUST); qInfo() << "Using path prefix query for content search optimization:" << searchPath; - return finalQuery; + mainQuery = finalQuery; + } + } + + // Add time range filter query + if (m_options.hasTimeRangeFilter()) { + TimeRangeFilter filter = m_options.timeRangeFilter(); + auto [start, end] = filter.resolveTimeRange(); + + qint64 startEpoch = TimeRangeUtils::toEpochSecs(start); + qint64 endEpoch = TimeRangeUtils::toEpochSecs(end); + + const wchar_t *fieldName = (filter.timeField() == TimeField::BirthTime) + ? LuceneFieldNames::Content::kBirthTime + : LuceneFieldNames::Content::kModifyTime; + + QueryPtr timeQuery = TimeRangeUtils::buildNumericRangeQuery( + fieldName, startEpoch, endEpoch, + filter.includeLower(), filter.includeUpper()); + + if (timeQuery) { + if (mainQuery) { + BooleanQueryPtr finalQuery = newLucene(); + finalQuery->add(mainQuery, BooleanClause::MUST); + finalQuery->add(timeQuery, BooleanClause::MUST); + mainQuery = finalQuery; + } else { + // Time filter alone is a valid query + mainQuery = timeQuery; + } } } @@ -126,7 +160,7 @@ QueryPtr ContentIndexedStrategy::buildAdvancedAndQuery(const SearchQuery &query, // It requires its own filenameParser. Lucene::QueryParserPtr filenameParser = newLucene( Lucene::LuceneVersion::LUCENE_CURRENT, - L"filename", + LuceneFieldNames::Content::kFilename, analyzer); Lucene::BooleanQueryPtr overallQuery = newLucene(); @@ -259,7 +293,7 @@ void ContentIndexedStrategy::processSearchResults(const Lucene::IndexSearcherPtr // Safely get path Lucene::String pathField; try { - pathField = doc->get(L"path"); + pathField = doc->get(LuceneFieldNames::Content::kPath); if (pathField.empty()) { qWarning() << "Document missing path field at index:" << scoreDoc->doc; continue; @@ -283,7 +317,7 @@ void ContentIndexedStrategy::processSearchResults(const Lucene::IndexSearcherPtr // Safely check hidden status if (Q_LIKELY(!m_options.includeHidden())) { try { - Lucene::String hiddenField = doc->get(L"is_hidden"); + Lucene::String hiddenField = doc->get(LuceneFieldNames::Content::kIsHidden); if (!hiddenField.empty() && QString::fromStdWString(hiddenField).toLower() == "y") { continue; } @@ -303,7 +337,7 @@ void ContentIndexedStrategy::processSearchResults(const Lucene::IndexSearcherPtr if (enableRetrieval) { try { // Safely get contents with null check - Lucene::String contentField = doc->get(L"contents"); + Lucene::String contentField = doc->get(LuceneFieldNames::Content::kContents); if (!contentField.empty()) { const QString content = QString::fromStdWString(contentField); const QString highlightedContent = ContentHighlighter::customHighlight(m_keywords, content, previewLen, enableHTML); @@ -318,6 +352,41 @@ void ContentIndexedStrategy::processSearchResults(const Lucene::IndexSearcherPtr } } + // 设置详细结果(如果启用) + if (Q_UNLIKELY(m_options.detailedResultsEnabled())) { + // 文件名 + Lucene::String filenameField = doc->get(LuceneFieldNames::Content::kFilename); + if (!filenameField.empty()) { + resultApi.setFilename(QString::fromStdWString(filenameField)); + } + + // 隐藏状态 + Lucene::String hiddenField = doc->get(LuceneFieldNames::Content::kIsHidden); + if (!hiddenField.empty()) { + resultApi.setIsHidden(QString::fromStdWString(hiddenField).toLower() == "y"); + } + + // 修改时间 + Lucene::String modifyTimeField = doc->get(LuceneFieldNames::Content::kModifyTime); + if (!modifyTimeField.empty()) { + bool ok = false; + qint64 timestamp = QString::fromStdWString(modifyTimeField).toLongLong(&ok); + if (ok && timestamp > 0) { + resultApi.setModifyTimestamp(timestamp); + } + } + + // 创建时间 + Lucene::String birthTimeField = doc->get(LuceneFieldNames::Content::kBirthTime); + if (!birthTimeField.empty()) { + bool ok = false; + qint64 timestamp = QString::fromStdWString(birthTimeField).toLongLong(&ok); + if (ok && timestamp > 0) { + resultApi.setBirthTimestamp(timestamp); + } + } + } + // 添加到结果集合 m_results.append(result); diff --git a/src/dfm-search/dfm-search-lib/core/searchengine.cpp b/src/dfm-search/dfm-search-lib/core/searchengine.cpp index e5ca9a78..cb2e01a7 100644 --- a/src/dfm-search/dfm-search-lib/core/searchengine.cpp +++ b/src/dfm-search/dfm-search-lib/core/searchengine.cpp @@ -9,6 +9,7 @@ #include "contentsearch/contentsearchengine.h" #include "filenamesearch/filenamesearchengine.h" +#include "ocrtextsearch/ocrtextsearchengine.h" DFM_SEARCH_BEGIN_NS @@ -57,6 +58,9 @@ void SearchEngine::setSearchType(SearchType type) case SearchType::Content: d_ptr = std::make_unique(); break; + case SearchType::Ocr: + d_ptr = std::make_unique(); + break; default: qWarning("Unsupported search type: %d", static_cast(type)); return; diff --git a/src/dfm-search/dfm-search-lib/core/searcherror.cpp b/src/dfm-search/dfm-search-lib/core/searcherror.cpp index 61423909..aa2780a7 100644 --- a/src/dfm-search/dfm-search-lib/core/searcherror.cpp +++ b/src/dfm-search/dfm-search-lib/core/searcherror.cpp @@ -75,6 +75,22 @@ std::string ContentSearchErrorCategory::message(int ev) const } } +std::string OcrTextSearchErrorCategory::message(int ev) const +{ + switch (static_cast(ev)) { + case OcrTextSearchErrorCode::KeywordTooShort: + return "Keyword too short: The search keyword is too short to perform an OCR text search. Please provide a longer keyword."; + case OcrTextSearchErrorCode::WildcardNotSupported: + return "Wildcard not supported: Wildcard search is not supported for OCR text search. Please use simple or boolean query instead."; + case OcrTextSearchErrorCode::OcrTextIndexNotFound: + return "OCR text index not found: The OCR text index could not be found. Please ensure the index is created."; + case OcrTextSearchErrorCode::OcrTextIndexException: + return "OCR text index exception: An error occurred while accessing the OCR text index. Please check the index integrity."; + default: + return "Unknown OCR text search error: An unknown error occurred related to OCR text search. Please contact support."; + } +} + // ... 实现其他错误分类的消息方法 ... // 获取错误分类单例 @@ -96,6 +112,12 @@ const ContentSearchErrorCategory &content_search_category() return c; } +const OcrTextSearchErrorCategory &ocrtext_search_category() +{ + static OcrTextSearchErrorCategory c; + return c; +} + // ... 实现其他错误分类单例 ... QString SearchError::message() const diff --git a/src/dfm-search/dfm-search-lib/core/searchfactory.cpp b/src/dfm-search/dfm-search-lib/core/searchfactory.cpp index f73c9b3e..4d092ccf 100644 --- a/src/dfm-search/dfm-search-lib/core/searchfactory.cpp +++ b/src/dfm-search/dfm-search-lib/core/searchfactory.cpp @@ -17,8 +17,11 @@ SearchEngine *SearchFactory::createEngine(SearchType type, QObject *parent) case SearchType::Content: engine = new SearchEngine(type, parent); break; + case SearchType::Ocr: + engine = new SearchEngine(type, parent); + break; case SearchType::Custom: - // TODO: 由应用程序基于provider自行创建 + // TODO: Created by application based on provider break; } diff --git a/src/dfm-search/dfm-search-lib/core/searchoptions.cpp b/src/dfm-search/dfm-search-lib/core/searchoptions.cpp index 12bd9c36..8620b773 100644 --- a/src/dfm-search/dfm-search-lib/core/searchoptions.cpp +++ b/src/dfm-search/dfm-search-lib/core/searchoptions.cpp @@ -177,4 +177,24 @@ int SearchOptions::batchTime() const return d->batchTimeMs; } +void SearchOptions::setTimeRangeFilter(const TimeRangeFilter &filter) +{ + d->timeRangeFilter = filter; +} + +TimeRangeFilter SearchOptions::timeRangeFilter() const +{ + return d->timeRangeFilter; +} + +bool SearchOptions::hasTimeRangeFilter() const +{ + return d->timeRangeFilter.isValid(); +} + +void SearchOptions::clearTimeRangeFilter() +{ + d->timeRangeFilter.clear(); +} + DFM_SEARCH_END_NS diff --git a/src/dfm-search/dfm-search-lib/core/searchoptionsdata.h b/src/dfm-search/dfm-search-lib/core/searchoptionsdata.h index ddf28f97..b5813887 100644 --- a/src/dfm-search/dfm-search-lib/core/searchoptionsdata.h +++ b/src/dfm-search/dfm-search-lib/core/searchoptionsdata.h @@ -8,6 +8,7 @@ #include #include +#include DFM_SEARCH_BEGIN_NS @@ -37,6 +38,7 @@ class SearchOptionsData bool detailedResultsEnabled; ///< Whether to include detailed information in search results int syncSearchTimeoutSecs { 60 }; int batchTimeMs { 1000 }; ///< Batch processing time interval in milliseconds + TimeRangeFilter timeRangeFilter; ///< Time range filter for search }; DFM_SEARCH_END_NS diff --git a/src/dfm-search/dfm-search-lib/core/timerangefilter.cpp b/src/dfm-search/dfm-search-lib/core/timerangefilter.cpp new file mode 100644 index 00000000..aaede170 --- /dev/null +++ b/src/dfm-search/dfm-search-lib/core/timerangefilter.cpp @@ -0,0 +1,371 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later +#include + +#include + +DFM_SEARCH_BEGIN_NS + +/** + * @brief Internal enum for range mode + */ +enum class RangeMode { + Invalid, // No range set + Relative, // Relative time (setLast) - rolling range from N units ago to now + FixedUnit, // Fixed unit range (yesterday, last week, etc.) - complete unit + Custom // Custom start/end +}; + +class TimeRangeFilterData +{ +public: + TimeRangeFilterData() + : field(TimeField::ModifyTime), mode(RangeMode::Invalid), relativeValue(0), relativeUnit(TimeUnit::Days), includeLower(true), includeUpper(false) + { + } + + TimeRangeFilterData(const TimeRangeFilterData &other) + : field(other.field), mode(other.mode), relativeValue(other.relativeValue), relativeUnit(other.relativeUnit), startTime(other.startTime), endTime(other.endTime), includeLower(other.includeLower), includeUpper(other.includeUpper) + { + } + + TimeField field; + RangeMode mode; + + // For relative/fixed mode + int relativeValue; + TimeUnit relativeUnit; + + // For custom mode + QDateTime startTime; + QDateTime endTime; + + bool includeLower; + bool includeUpper; +}; + +TimeRangeFilter::TimeRangeFilter() + : d(std::make_unique()) +{ +} + +TimeRangeFilter::TimeRangeFilter(const TimeRangeFilter &other) + : d(std::make_unique(*other.d)) +{ +} + +TimeRangeFilter::TimeRangeFilter(TimeRangeFilter &&other) noexcept + : d(std::move(other.d)) +{ +} + +TimeRangeFilter::~TimeRangeFilter() = default; + +TimeRangeFilter &TimeRangeFilter::operator=(const TimeRangeFilter &other) +{ + if (this != &other) { + d = std::make_unique(*other.d); + } + return *this; +} + +TimeRangeFilter &TimeRangeFilter::operator=(TimeRangeFilter &&other) noexcept +{ + if (this != &other) { + d = std::move(other.d); + } + return *this; +} + +TimeRangeFilter &TimeRangeFilter::setTimeField(TimeField field) +{ + d->field = field; + return *this; +} + +TimeField TimeRangeFilter::timeField() const +{ + return d->field; +} + +TimeRangeFilter &TimeRangeFilter::setLast(int value, TimeUnit unit) +{ + d->mode = RangeMode::Relative; // Rolling range from N units ago to now + d->relativeValue = value; + d->relativeUnit = unit; + d->startTime = QDateTime(); + d->endTime = QDateTime(); + return *this; +} + +TimeRangeFilter &TimeRangeFilter::setToday() +{ + d->mode = RangeMode::FixedUnit; // Complete unit (today) + d->relativeValue = 0; + d->relativeUnit = TimeUnit::Days; + return *this; +} + +TimeRangeFilter &TimeRangeFilter::setYesterday() +{ + d->mode = RangeMode::FixedUnit; // Complete unit (yesterday) + d->relativeValue = 1; + d->relativeUnit = TimeUnit::Days; + return *this; +} + +TimeRangeFilter &TimeRangeFilter::setThisWeek() +{ + d->mode = RangeMode::FixedUnit; + d->relativeValue = 0; + d->relativeUnit = TimeUnit::Weeks; + return *this; +} + +TimeRangeFilter &TimeRangeFilter::setLastWeek() +{ + d->mode = RangeMode::FixedUnit; + d->relativeValue = 1; + d->relativeUnit = TimeUnit::Weeks; + return *this; +} + +TimeRangeFilter &TimeRangeFilter::setThisMonth() +{ + d->mode = RangeMode::FixedUnit; + d->relativeValue = 0; + d->relativeUnit = TimeUnit::Months; + return *this; +} + +TimeRangeFilter &TimeRangeFilter::setLastMonth() +{ + d->mode = RangeMode::FixedUnit; + d->relativeValue = 1; + d->relativeUnit = TimeUnit::Months; + return *this; +} + +TimeRangeFilter &TimeRangeFilter::setThisYear() +{ + d->mode = RangeMode::FixedUnit; + d->relativeValue = 0; + d->relativeUnit = TimeUnit::Years; + return *this; +} + +TimeRangeFilter &TimeRangeFilter::setLastYear() +{ + d->mode = RangeMode::FixedUnit; + d->relativeValue = 1; + d->relativeUnit = TimeUnit::Years; + return *this; +} + +TimeRangeFilter &TimeRangeFilter::setRange(const QDateTime &start, const QDateTime &end) +{ + d->mode = RangeMode::Custom; + d->startTime = start; + d->endTime = end; + d->relativeValue = 0; + d->relativeUnit = TimeUnit::Days; + return *this; +} + +QDateTime TimeRangeFilter::startTime() const +{ + return d->startTime; +} + +QDateTime TimeRangeFilter::endTime() const +{ + return d->endTime; +} + +TimeRangeFilter &TimeRangeFilter::setIncludeLower(bool include) +{ + d->includeLower = include; + return *this; +} + +TimeRangeFilter &TimeRangeFilter::setIncludeUpper(bool include) +{ + d->includeUpper = include; + return *this; +} + +bool TimeRangeFilter::includeLower() const +{ + return d->includeLower; +} + +bool TimeRangeFilter::includeUpper() const +{ + return d->includeUpper; +} + +TimeRangeFilter &TimeRangeFilter::clear() +{ + d->mode = RangeMode::Invalid; + d->startTime = QDateTime(); + d->endTime = QDateTime(); + d->relativeValue = 0; + d->relativeUnit = TimeUnit::Days; + d->includeLower = true; + d->includeUpper = false; + return *this; +} + +bool TimeRangeFilter::isValid() const +{ + return d->mode != RangeMode::Invalid; +} + +QPair TimeRangeFilter::resolveTimeRange() const +{ + if (d->mode == RangeMode::Custom) { + return qMakePair(d->startTime, d->endTime); + } + + if (d->mode == RangeMode::Relative) { + return resolveRelativeTimeRange(d->relativeValue, d->relativeUnit); + } + + if (d->mode == RangeMode::FixedUnit) { + return resolveFixedUnitTimeRange(d->relativeValue, d->relativeUnit); + } + + return qMakePair(QDateTime(), QDateTime()); +} + +QPair TimeRangeFilter::resolveRelativeTimeRange(int value, TimeUnit unit) +{ + // Relative mode: from N units ago to now (rolling window) + QDateTime now = QDateTime::currentDateTime(); + QDateTime start; + QDateTime end = now; + + switch (unit) { + case TimeUnit::Minutes: + // "Last N minutes": from N minutes ago to now + start = now.addSecs(-value * 60); + break; + + case TimeUnit::Hours: + // "Last N hours": from N hours ago to now + start = now.addSecs(-value * 3600); + break; + + case TimeUnit::Days: + // "Last N days": from N days ago (00:00:00) to now + start = QDateTime(now.date().addDays(-value), QTime(0, 0, 0)); + break; + + case TimeUnit::Weeks: + // "Last N weeks": from N weeks ago (Monday 00:00:00) to now + { + int daysToMonday = now.date().dayOfWeek() - 1; + QDate thisMonday = now.date().addDays(-daysToMonday); + QDate startMonday = thisMonday.addDays(-value * 7); + start = QDateTime(startMonday, QTime(0, 0, 0)); + } + break; + + case TimeUnit::Months: + // "Last N months": from N months ago (1st day 00:00:00) to now + { + QDate firstOfThisMonth(now.date().year(), now.date().month(), 1); + QDate startMonth = firstOfThisMonth.addMonths(-value); + start = QDateTime(startMonth, QTime(0, 0, 0)); + } + break; + + case TimeUnit::Years: + // "Last N years": from N years ago (Jan 1st 00:00:00) to now + { + QDate startYear(now.date().year() - value, 1, 1); + start = QDateTime(startYear, QTime(0, 0, 0)); + } + break; + } + + return qMakePair(start, end); +} + +QPair TimeRangeFilter::resolveFixedUnitTimeRange(int value, TimeUnit unit) +{ + // Fixed unit mode: complete unit range (today, yesterday, this week, etc.) + QDateTime now = QDateTime::currentDateTime(); + QDateTime start; + QDateTime end; + + switch (unit) { + case TimeUnit::Minutes: + case TimeUnit::Hours: + // For minutes/hours, fixed unit doesn't make much sense, treat as relative + return resolveRelativeTimeRange(value, unit); + + case TimeUnit::Days: + if (value == 0) { + // Today: from 00:00:00 today to 00:00:00 tomorrow + start = QDateTime(now.date(), QTime(0, 0, 0)); + end = QDateTime(now.date().addDays(1), QTime(0, 0, 0)); + } else { + // Yesterday/N days ago: complete day from 00:00:00 to 00:00:00 next day + start = QDateTime(now.date().addDays(-value), QTime(0, 0, 0)); + end = QDateTime(now.date().addDays(-value + 1), QTime(0, 0, 0)); + } + break; + + case TimeUnit::Weeks: + if (value == 0) { + // This week: from Monday 00:00:00 to next Monday 00:00:00 + int daysToMonday = now.date().dayOfWeek() - 1; + QDate monday = now.date().addDays(-daysToMonday); + start = QDateTime(monday, QTime(0, 0, 0)); + end = QDateTime(monday.addDays(7), QTime(0, 0, 0)); + } else { + // Last week/N weeks ago: complete week + int daysToMonday = now.date().dayOfWeek() - 1; + QDate thisMonday = now.date().addDays(-daysToMonday); + QDate startMonday = thisMonday.addDays(-value * 7); + start = QDateTime(startMonday, QTime(0, 0, 0)); + end = QDateTime(startMonday.addDays(7), QTime(0, 0, 0)); + } + break; + + case TimeUnit::Months: + if (value == 0) { + // This month: from 1st day 00:00:00 to 1st day of next month + QDate firstOfMonth(now.date().year(), now.date().month(), 1); + start = QDateTime(firstOfMonth, QTime(0, 0, 0)); + end = QDateTime(firstOfMonth.addMonths(1), QTime(0, 0, 0)); + } else { + // Last month/N months ago: complete month + QDate firstOfThisMonth(now.date().year(), now.date().month(), 1); + QDate startMonth = firstOfThisMonth.addMonths(-value); + start = QDateTime(startMonth, QTime(0, 0, 0)); + end = QDateTime(startMonth.addMonths(1), QTime(0, 0, 0)); + } + break; + + case TimeUnit::Years: + if (value == 0) { + // This year: from Jan 1st 00:00:00 to Jan 1st of next year + QDate firstOfYear(now.date().year(), 1, 1); + start = QDateTime(firstOfYear, QTime(0, 0, 0)); + end = QDateTime(QDate(now.date().year() + 1, 1, 1), QTime(0, 0, 0)); + } else { + // Last year/N years ago: complete year + QDate startYear(now.date().year() - value, 1, 1); + start = QDateTime(startYear, QTime(0, 0, 0)); + end = QDateTime(QDate(now.date().year() - value + 1, 1, 1), QTime(0, 0, 0)); + } + break; + } + + return qMakePair(start, end); +} + +DFM_SEARCH_END_NS diff --git a/src/dfm-search/dfm-search-lib/core/timeresultapi.cpp b/src/dfm-search/dfm-search-lib/core/timeresultapi.cpp new file mode 100644 index 00000000..7f7320c8 --- /dev/null +++ b/src/dfm-search/dfm-search-lib/core/timeresultapi.cpp @@ -0,0 +1,54 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later +#include +#include + +DFM_SEARCH_BEGIN_NS + +TimeResultAPI::TimeResultAPI(SearchResult &result) + : m_result(result) +{ +} + +void TimeResultAPI::setModifyTimestamp(qint64 timestamp) +{ + m_result.setCustomAttribute("modifyTimestamp", timestamp); +} + +qint64 TimeResultAPI::modifyTimestamp() const +{ + return m_result.customAttribute("modifyTimestamp").toLongLong(); +} + +QString TimeResultAPI::modifyTimeString() const +{ + qint64 ts = modifyTimestamp(); + return ts > 0 ? formatTimestamp(ts) : QString(); +} + +void TimeResultAPI::setBirthTimestamp(qint64 timestamp) +{ + m_result.setCustomAttribute("birthTimestamp", timestamp); +} + +qint64 TimeResultAPI::birthTimestamp() const +{ + return m_result.customAttribute("birthTimestamp").toLongLong(); +} + +QString TimeResultAPI::birthTimeString() const +{ + qint64 ts = birthTimestamp(); + return ts > 0 ? formatTimestamp(ts) : QString(); +} + +QString TimeResultAPI::formatTimestamp(qint64 timestamp) +{ + if (timestamp <= 0) { + return QString(); + } + return QDateTime::fromSecsSinceEpoch(timestamp).toString("yyyy-MM-dd HH:mm:ss"); +} + +DFM_SEARCH_END_NS diff --git a/src/dfm-search/dfm-search-lib/filenamesearch/filenamesearchapi.cpp b/src/dfm-search/dfm-search-lib/filenamesearch/filenamesearchapi.cpp index 283e70ea..8770125a 100644 --- a/src/dfm-search/dfm-search-lib/filenamesearch/filenamesearchapi.cpp +++ b/src/dfm-search/dfm-search-lib/filenamesearch/filenamesearchapi.cpp @@ -2,6 +2,9 @@ // // SPDX-License-Identifier: GPL-3.0-or-later #include +#include + +#include DFM_SEARCH_BEGIN_NS @@ -97,4 +100,72 @@ void FileNameResultAPI::setFileType(const QString &type) const m_result.setCustomAttribute("fileType", type); } +// ==================== Extended Attributes ==================== + +QString FileNameResultAPI::filename() const +{ + return m_result.customAttribute("filename").toString(); +} + +void FileNameResultAPI::setFilename(const QString &name) +{ + m_result.setCustomAttribute("filename", name); +} + +QString FileNameResultAPI::fileExtension() const +{ + return m_result.customAttribute("fileExtension").toString(); +} + +void FileNameResultAPI::setFileExtension(const QString &ext) +{ + m_result.setCustomAttribute("fileExtension", ext); +} + +bool FileNameResultAPI::isHidden() const +{ + return m_result.customAttribute("isHidden").toBool(); +} + +void FileNameResultAPI::setIsHidden(bool hidden) +{ + m_result.setCustomAttribute("isHidden", hidden); +} + +// ==================== Modification Time ==================== + +void FileNameResultAPI::setModifyTimestamp(qint64 timestamp) +{ + m_result.setCustomAttribute("modifyTimestamp", timestamp); +} + +qint64 FileNameResultAPI::modifyTimestamp() const +{ + return m_result.customAttribute("modifyTimestamp").toLongLong(); +} + +QString FileNameResultAPI::modifyTimeString() const +{ + qint64 ts = modifyTimestamp(); + return ts > 0 ? TimeResultAPI::formatTimestamp(ts) : QString(); +} + +// ==================== Birth/Creation Time ==================== + +void FileNameResultAPI::setBirthTimestamp(qint64 timestamp) +{ + m_result.setCustomAttribute("birthTimestamp", timestamp); +} + +qint64 FileNameResultAPI::birthTimestamp() const +{ + return m_result.customAttribute("birthTimestamp").toLongLong(); +} + +QString FileNameResultAPI::birthTimeString() const +{ + qint64 ts = birthTimestamp(); + return ts > 0 ? TimeResultAPI::formatTimestamp(ts) : QString(); +} + DFM_SEARCH_END_NS diff --git a/src/dfm-search/dfm-search-lib/filenamesearch/filenamestrategies/indexedstrategy.cpp b/src/dfm-search/dfm-search-lib/filenamesearch/filenamestrategies/indexedstrategy.cpp index f77441f5..6746fa2f 100644 --- a/src/dfm-search/dfm-search-lib/filenamesearch/filenamestrategies/indexedstrategy.cpp +++ b/src/dfm-search/dfm-search-lib/filenamesearch/filenamestrategies/indexedstrategy.cpp @@ -2,9 +2,6 @@ // // SPDX-License-Identifier: GPL-3.0-or-later #include "indexedstrategy.h" -#include "utils/cancellablecollector.h" -#include "utils/searchutility.h" -#include "utils/lucenequeryutils.h" #include #include @@ -15,7 +12,14 @@ #include #include +#include +#include + #include "3rdparty/fulltext/chineseanalyzer.h" +#include "utils/cancellablecollector.h" +#include "utils/searchutility.h" +#include "utils/lucenequeryutils.h" +#include "utils/timerangeutils.h" DFM_SEARCH_BEGIN_NS @@ -39,7 +43,7 @@ Lucene::QueryPtr QueryBuilder::buildTypeQuery(const QStringList &types) const QString cleanType = type.trimmed().toLower(); if (!cleanType.isEmpty()) { QueryPtr termQuery = newLucene( - newLucene(L"file_type", + newLucene(LuceneFieldNames::FileName::kFileType, StringUtils::toUnicode(cleanType.toStdString()))); typeQuery->add(termQuery, BooleanClause::SHOULD); } @@ -60,7 +64,7 @@ Lucene::QueryPtr QueryBuilder::buildExtQuery(const QStringList &extensions) cons QString cleanExt = ext.trimmed().toLower(); if (!cleanExt.isEmpty()) { QueryPtr termQuery = newLucene( - newLucene(L"file_ext", + newLucene(LuceneFieldNames::FileName::kFileExt, StringUtils::toUnicode(cleanExt.toStdString()))); extQuery->add(termQuery, BooleanClause::SHOULD); } @@ -81,7 +85,8 @@ Lucene::QueryPtr QueryBuilder::buildPinyinQuery(const QStringList &pinyins, Sear QString cleanPinyin = pinyin.trimmed(); if (!cleanPinyin.isEmpty() && Global::isPinyinSequence(cleanPinyin)) { // 复用buildCommonQuery,指定pinyin字段,让分析器自动处理匹配 - QueryPtr termQuery = buildCommonQuery(cleanPinyin, false, newLucene(), "pinyin", false); + QueryPtr termQuery = buildCommonQuery(cleanPinyin, false, newLucene(), + QString::fromWCharArray(LuceneFieldNames::FileName::kPinyin), false); if (termQuery) { pinyinQuery->add(termQuery, op == SearchQuery::BooleanOperator::AND ? BooleanClause::MUST : BooleanClause::SHOULD); } @@ -103,7 +108,9 @@ Lucene::QueryPtr QueryBuilder::buildPinyinAcronymQuery(const QStringList &acrony QString cleanAcronym = acronym.trimmed(); if (!cleanAcronym.isEmpty()) { // 复用buildCommonQuery,指定pinyin_acronym字段,让分析器自动处理匹配 - QueryPtr termQuery = buildCommonQuery(cleanAcronym, false, newLucene(), "pinyin_acronym", false); + QueryPtr termQuery = buildCommonQuery(cleanAcronym, false, + newLucene(), + QString::fromWCharArray(LuceneFieldNames::FileName::kPinyinAcronym), false); if (termQuery) { acronymQuery->add(termQuery, op == SearchQuery::BooleanOperator::AND ? BooleanClause::MUST : BooleanClause::SHOULD); } @@ -121,7 +128,7 @@ Lucene::QueryPtr QueryBuilder::buildCommonQuery(const QString &keyword, bool cas Lucene::QueryParserPtr parser = newLucene( Lucene::LuceneVersion::LUCENE_CURRENT, - L"file_name", + LuceneFieldNames::FileName::kFileName, analyzer); if (allowWildcard) { @@ -160,12 +167,12 @@ Lucene::QueryPtr QueryBuilder::buildWildcardQuery(const QString &keyword, bool c return nullptr; } - // 对于通配符查询,使用file_name_lower字段(非分词)而非file_name(分词) + // 对于通配符查询,使用 file_name_lower 字段(非分词)而非 file_name(分词) QString processedKeyword = caseSensitive ? keyword : keyword.toLower(); // 直接构建WildcardQuery,不使用QueryParser避免分词干扰 return newLucene( - newLucene(L"file_name_lower", + newLucene(LuceneFieldNames::FileName::kFileNameLower, StringUtils::toUnicode(processedKeyword.toStdString()))); } @@ -339,20 +346,15 @@ FileNameIndexedStrategy::SearchType FileNameIndexedStrategy::determineSearchType bool hasFileExts = !fileExtensions.isEmpty(); bool isBoolean = (query.type() == SearchQuery::Type::Boolean); - // 检查是否需要组合搜索 + // 检查是否需要组合搜索(关键词 + 文件类型/后缀) bool combinedWithTypes = (hasKeyword || isBoolean) && (hasFileTypes || hasFileExts); if (combinedWithTypes) { return SearchType::Combined; } - // 空关键词但有文件类型,使用文件类型搜索 - if (!hasKeyword && hasFileTypes) { - return SearchType::FileType; - } - - // 空关键词但有文件后缀,使用文件后缀搜索 - if (!hasKeyword && hasFileExts) { - return SearchType::FileExt; + // 无关键词时,文件类型和后缀组合搜索 + if (!hasKeyword && (hasFileTypes || hasFileExts)) { + return SearchType::Combined; } // 通配符查询类型(显式指定) @@ -427,15 +429,14 @@ FileNameIndexedStrategy::IndexQuery FileNameIndexedStrategy::buildIndexQuery( case SearchType::PinyinAcronym: result.terms.append(query.keyword()); break; - case SearchType::FileType: - result.fileTypes = fileTypes; - break; - case SearchType::FileExt: - result.fileExtensions = fileExtensions; - break; case SearchType::Combined: - result.terms = query.type() == SearchQuery::Type::Boolean ? SearchUtility::extractBooleanKeywords(query) : QStringList { query.keyword() }; - result.booleanOp = query.type() == SearchQuery::Type::Boolean ? query.booleanOperator() : SearchQuery::BooleanOperator::AND; + // 只有当有关键词时才设置 terms + if (query.type() == SearchQuery::Type::Boolean) { + result.terms = SearchUtility::extractBooleanKeywords(query); + result.booleanOp = query.booleanOperator(); + } else if (!query.keyword().isEmpty()) { + result.terms = QStringList { query.keyword() }; + } result.combineWithFileType = !fileTypes.isEmpty(); result.combineWithFileExt = !fileExtensions.isEmpty(); break; @@ -530,7 +531,7 @@ void FileNameIndexedStrategy::executeIndexQuery(const IndexQuery &query, const Q try { ScoreDocPtr scoreDoc = scoreDocs[i]; DocumentPtr doc = searcher->doc(scoreDoc->doc); - QString path = QString::fromStdWString(doc->get(L"full_path")); + QString path = QString::fromStdWString(doc->get(LuceneFieldNames::FileName::kFullPath)); if (!path.startsWith(searchPath)) { continue; @@ -542,16 +543,13 @@ void FileNameIndexedStrategy::executeIndexQuery(const IndexQuery &query, const Q } if (Q_LIKELY(!m_options.includeHidden())) { - if (QString::fromStdWString(doc->get(L"is_hidden")).toLower() == "y") + if (QString::fromStdWString(doc->get(LuceneFieldNames::FileName::kIsHidden)).toLower() == "y") continue; } // 处理搜索结果 if (Q_UNLIKELY(m_options.detailedResultsEnabled())) { - QString type = QString::fromStdWString(doc->get(L"file_type")); - QString time = QString::fromStdWString(doc->get(L"modify_time_str")); - QString size = QString::fromStdWString(doc->get(L"file_size_str")); - m_results.append(processSearchResult(path, type, time, size)); + m_results.append(processDetailedSearchResult(path, doc)); } else { // perf: quickly SearchResult result(path); @@ -571,16 +569,54 @@ void FileNameIndexedStrategy::executeIndexQuery(const IndexQuery &query, const Q qInfo() << "Filename result processing time:" << resultTimer.elapsed() << "ms"; } -SearchResult FileNameIndexedStrategy::processSearchResult(const QString &path, const QString &type, const QString &time, const QString &size) +SearchResult FileNameIndexedStrategy::processDetailedSearchResult( + const QString &path, + const Lucene::DocumentPtr &doc) { // 创建搜索结果 SearchResult result(path); FileNameResultAPI api(result); - api.setSize(size); - api.setModifiedTime(time); - api.setIsDirectory(type == "dir"); + + // 文件类型 + QString type = QString::fromStdWString(doc->get(LuceneFieldNames::FileName::kFileType)); api.setFileType(type); + api.setIsDirectory(type == "dir"); + + // 文件名和扩展名 + QString fileName = QString::fromStdWString(doc->get(LuceneFieldNames::FileName::kFileName)); + api.setFilename(fileName); + + QString fileExt = QString::fromStdWString(doc->get(LuceneFieldNames::FileName::kFileExt)); + api.setFileExtension(fileExt); + + // 文件大小 + QString size = QString::fromStdWString(doc->get(LuceneFieldNames::FileName::kFileSizeStr)); + api.setSize(size); + + // 隐藏状态 + QString isHiddenStr = QString::fromStdWString(doc->get(LuceneFieldNames::FileName::kIsHidden)).toLower(); + api.setIsHidden(isHiddenStr == "y"); + + // 修改时间 + QString modifyTimeStr = QString::fromStdWString(doc->get(LuceneFieldNames::FileName::kModifyTime)); + if (!modifyTimeStr.isEmpty()) { + bool ok = false; + qint64 modifyTimestamp = modifyTimeStr.toLongLong(&ok); + if (ok && modifyTimestamp > 0) { + api.setModifyTimestamp(modifyTimestamp); + } + } + + // 创建时间 + QString birthTimeStr = QString::fromStdWString(doc->get(LuceneFieldNames::FileName::kBirthTime)); + if (!birthTimeStr.isEmpty()) { + bool ok = false; + qint64 birthTimestamp = birthTimeStr.toLongLong(&ok); + if (ok && birthTimestamp > 0) { + api.setBirthTimestamp(birthTimestamp); + } + } return result; } @@ -669,24 +705,6 @@ Lucene::QueryPtr FileNameIndexedStrategy::buildLuceneQuery(const IndexQuery &que } } break; - case SearchType::FileType: - if (!query.fileTypes.isEmpty()) { - QueryPtr typeQuery = m_queryBuilder->buildTypeQuery(query.fileTypes); - if (typeQuery) { - finalQuery->add(typeQuery, BooleanClause::MUST); - hasValidQuery = true; - } - } - break; - case SearchType::FileExt: - if (!query.fileExtensions.isEmpty()) { - QueryPtr extQuery = m_queryBuilder->buildExtQuery(query.fileExtensions); - if (extQuery) { - finalQuery->add(extQuery, BooleanClause::MUST); - hasValidQuery = true; - } - } - break; case SearchType::Combined: if (!query.terms.isEmpty()) { BooleanQueryPtr combinedQuery = buildBooleanTermsQuery(query, analyzer); @@ -716,10 +734,33 @@ Lucene::QueryPtr FileNameIndexedStrategy::buildLuceneQuery(const IndexQuery &que break; } + // Add time range filter query + if (m_options.hasTimeRangeFilter()) { + TimeRangeFilter filter = m_options.timeRangeFilter(); + auto [start, end] = filter.resolveTimeRange(); + + qint64 startEpoch = TimeRangeUtils::toEpochSecs(start); + qint64 endEpoch = TimeRangeUtils::toEpochSecs(end); + + const wchar_t *fieldName = (filter.timeField() == TimeField::BirthTime) + ? LuceneFieldNames::FileName::kBirthTime + : LuceneFieldNames::FileName::kModifyTime; + + QueryPtr timeQuery = TimeRangeUtils::buildNumericRangeQuery( + fieldName, startEpoch, endEpoch, + filter.includeLower(), filter.includeUpper()); + + if (timeQuery) { + finalQuery->add(timeQuery, BooleanClause::MUST); + hasValidQuery = true; + } + } + // Add path prefix query optimization if (hasValidQuery && SearchUtility::isFilenameIndexAncestorPathsSupported() && SearchUtility::shouldUsePathPrefixQuery(searchPath)) { - QueryPtr pathPrefixQuery = LuceneQueryUtils::buildPathPrefixQuery(searchPath, "ancestor_paths"); + QueryPtr pathPrefixQuery = LuceneQueryUtils::buildPathPrefixQuery(searchPath, + QString::fromWCharArray(LuceneFieldNames::FileName::kAncestorPaths)); if (pathPrefixQuery) { finalQuery->add(pathPrefixQuery, BooleanClause::MUST); qInfo() << "Using path prefix query for optimization:" << searchPath; @@ -730,7 +771,7 @@ Lucene::QueryPtr FileNameIndexedStrategy::buildLuceneQuery(const IndexQuery &que if (hasValidQuery && Q_LIKELY(!m_options.includeHidden())) { QueryPtr hiddenQuery = Lucene::newLucene( Lucene::newLucene( - Lucene::StringUtils::toUnicode("is_hidden"), + LuceneFieldNames::FileName::kIsHidden, Lucene::StringUtils::toUnicode("Y"))); finalQuery->add(hiddenQuery, Lucene::BooleanClause::MUST_NOT); } diff --git a/src/dfm-search/dfm-search-lib/filenamesearch/filenamestrategies/indexedstrategy.h b/src/dfm-search/dfm-search-lib/filenamesearch/filenamestrategies/indexedstrategy.h index 974f062f..2d4f143f 100644 --- a/src/dfm-search/dfm-search-lib/filenamesearch/filenamestrategies/indexedstrategy.h +++ b/src/dfm-search/dfm-search-lib/filenamesearch/filenamestrategies/indexedstrategy.h @@ -35,9 +35,7 @@ class FileNameIndexedStrategy : public FileNameBaseStrategy Boolean, // 布尔多关键词搜索 Pinyin, // 拼音搜索 PinyinAcronym, // 拼音首字母搜索 - FileType, // 文件类型搜索 - FileExt, // 文件后缀搜索 - Combined // 组合搜索(关键词+文件类型/拼音/文件后缀) + Combined // 组合搜索(关键词+文件类型/拼音/文件后缀/文件类型和后缀) }; // 索引查询结构 @@ -93,8 +91,8 @@ class FileNameIndexedStrategy : public FileNameBaseStrategy // 构建布尔查询的辅助方法 BooleanQueryPtr buildBooleanTermsQuery(const IndexQuery &query, const AnalyzerPtr &analyzer) const; - // 处理搜索结果 - SearchResult processSearchResult(const QString &path, const QString &type, const QString &time, const QString &size); + // 处理详细搜索结果(读取所有索引字段) + SearchResult processDetailedSearchResult(const QString &path, const Lucene::DocumentPtr &doc); // 成员变量 QString m_indexDir; // 索引目录路径 diff --git a/src/dfm-search/dfm-search-lib/filenamesearch/filenamestrategies/realtimestrategy.cpp b/src/dfm-search/dfm-search-lib/filenamesearch/filenamestrategies/realtimestrategy.cpp index 13631aab..dd0ff80f 100644 --- a/src/dfm-search/dfm-search-lib/filenamesearch/filenamestrategies/realtimestrategy.cpp +++ b/src/dfm-search/dfm-search-lib/filenamesearch/filenamestrategies/realtimestrategy.cpp @@ -11,6 +11,8 @@ #include #include +#include + DFM_SEARCH_BEGIN_NS FileNameRealTimeStrategy::FileNameRealTimeStrategy(const SearchOptions &options, QObject *parent) @@ -117,8 +119,13 @@ void FileNameRealTimeStrategy::search(const SearchQuery &query) QString fileName = info.fileName(); bool matches = false; + // 如果只有时间过滤没有关键词,直接匹配 + bool hasKeyword = !query.keyword().isEmpty() || query.type() == SearchQuery::Type::Boolean; + if (!hasKeyword && m_options.hasTimeRangeFilter()) { + matches = true; + } // 简单查询模式 - if (query.type() == SearchQuery::Type::Simple) { + else if (query.type() == SearchQuery::Type::Simple) { matches = fileName.contains(query.keyword(), caseSensitive ? Qt::CaseSensitive : Qt::CaseInsensitive); } @@ -139,15 +146,61 @@ void FileNameRealTimeStrategy::search(const SearchQuery &query) } } + // 时间范围过滤 + if (matches && m_options.hasTimeRangeFilter()) { + TimeRangeFilter filter = m_options.timeRangeFilter(); + auto [start, end] = filter.resolveTimeRange(); + + QDateTime fileTime = (filter.timeField() == TimeField::BirthTime) + ? info.birthTime() + : info.lastModified(); + + // 时间范围检查 + bool timeMatch = true; + if (start.isValid()) { + if (filter.includeLower()) { + timeMatch = timeMatch && (fileTime >= start); + } else { + timeMatch = timeMatch && (fileTime > start); + } + } + if (end.isValid()) { + if (filter.includeUpper()) { + timeMatch = timeMatch && (fileTime <= end); + } else { + timeMatch = timeMatch && (fileTime < end); + } + } + matches = timeMatch; + } + if (matches) { // 创建搜索结果 SearchResult result(info.filePath()); if (detailedResults) { FileNameResultAPI api(result); - api.setModifiedTime(info.lastModified().toString()); api.setIsDirectory(info.isDir()); - api.setFileType(info.suffix().isEmpty() ? (info.isDir() ? "directory" : "unknown") : info.suffix()); + + if (!info.isDir()) { + api.setFileType(info.suffix().isEmpty() ? "unknown" : info.suffix().toLower()); + api.setFileExtension(info.suffix().toLower()); + api.setSize(QString::number(info.size())); + } else { + api.setFileType("dir"); + } + + api.setFilename(info.fileName()); + api.setIsHidden(info.isHidden()); + + // 设置修改时间戳 + api.setModifyTimestamp(info.lastModified().toSecsSinceEpoch()); + + // 设置创建时间戳 + QDateTime birthTime = info.birthTime(); + if (birthTime.isValid()) { + api.setBirthTimestamp(birthTime.toSecsSinceEpoch()); + } } // 实时发送结果 diff --git a/src/dfm-search/dfm-search-lib/ocrtextsearch/ocrtextsearchapi.cpp b/src/dfm-search/dfm-search-lib/ocrtextsearch/ocrtextsearchapi.cpp new file mode 100644 index 00000000..d7d1b06c --- /dev/null +++ b/src/dfm-search/dfm-search-lib/ocrtextsearch/ocrtextsearchapi.cpp @@ -0,0 +1,102 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later +#include +#include + +#include + +DFM_SEARCH_BEGIN_NS + +OcrTextOptionsAPI::OcrTextOptionsAPI(SearchOptions &options) + : m_options(options) +{ + // init default + if (!m_options.hasCustomOption("filenameOcrContentMixedAndSearchEnabled")) + setFilenameOcrContentMixedAndSearchEnabled(false); +} + +void OcrTextOptionsAPI::setFilenameOcrContentMixedAndSearchEnabled(bool enabled) +{ + m_options.setCustomOption("filenameOcrContentMixedAndSearchEnabled", enabled); +} + +bool OcrTextOptionsAPI::isFilenameOcrContentMixedAndSearchEnabled() const +{ + return m_options.customOption("filenameOcrContentMixedAndSearchEnabled").toBool(); +} + +OcrTextResultAPI::OcrTextResultAPI(SearchResult &result) + : m_result(result) +{ +} + +QString OcrTextResultAPI::ocrContent() const +{ + return m_result.customAttribute("ocrContent").toString(); +} + +void OcrTextResultAPI::setOcrContent(const QString &content) +{ + m_result.setCustomAttribute("ocrContent", content); +} + +// ==================== Extended Attributes ==================== + +QString OcrTextResultAPI::filename() const +{ + return m_result.customAttribute("filename").toString(); +} + +void OcrTextResultAPI::setFilename(const QString &name) +{ + m_result.setCustomAttribute("filename", name); +} + +bool OcrTextResultAPI::isHidden() const +{ + return m_result.customAttribute("isHidden").toBool(); +} + +void OcrTextResultAPI::setIsHidden(bool hidden) +{ + m_result.setCustomAttribute("isHidden", hidden); +} + +// ==================== Modification Time ==================== + +void OcrTextResultAPI::setModifyTimestamp(qint64 timestamp) +{ + m_result.setCustomAttribute("modifyTimestamp", timestamp); +} + +qint64 OcrTextResultAPI::modifyTimestamp() const +{ + return m_result.customAttribute("modifyTimestamp").toLongLong(); +} + +QString OcrTextResultAPI::modifyTimeString() const +{ + qint64 ts = modifyTimestamp(); + return ts > 0 ? TimeResultAPI::formatTimestamp(ts) : QString(); +} + +// ==================== Birth/Creation Time ==================== + +void OcrTextResultAPI::setBirthTimestamp(qint64 timestamp) +{ + m_result.setCustomAttribute("birthTimestamp", timestamp); +} + +qint64 OcrTextResultAPI::birthTimestamp() const +{ + return m_result.customAttribute("birthTimestamp").toLongLong(); +} + +QString OcrTextResultAPI::birthTimeString() const +{ + qint64 ts = birthTimestamp(); + return ts > 0 ? TimeResultAPI::formatTimestamp(ts) : QString(); +} + +DFM_SEARCH_END_NS diff --git a/src/dfm-search/dfm-search-lib/ocrtextsearch/ocrtextsearchengine.cpp b/src/dfm-search/dfm-search-lib/ocrtextsearch/ocrtextsearchengine.cpp new file mode 100644 index 00000000..ed9172bb --- /dev/null +++ b/src/dfm-search/dfm-search-lib/ocrtextsearch/ocrtextsearchengine.cpp @@ -0,0 +1,67 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later +#include "ocrtextsearchengine.h" + +#include "ocrtextstrategies/indexedstrategy.h" + +DFM_SEARCH_BEGIN_NS +DCORE_USE_NAMESPACE + +OcrTextSearchEngine::OcrTextSearchEngine(QObject *parent) + : GenericSearchEngine(parent) +{ +} + +OcrTextSearchEngine::~OcrTextSearchEngine() = default; + +void OcrTextSearchEngine::setupStrategyFactory() +{ + // Set up OCR text search strategy factory + auto factory = std::make_unique(); + m_worker->setStrategyFactory(std::move(factory)); +} + +SearchError OcrTextSearchEngine::validateSearchConditions() +{ + // First execute base class validation + auto result = GenericSearchEngine::validateSearchConditions(); + if (result.isError()) { + return result; + } + + // OCR text search specific validation + if (m_options.method() != SearchMethod::Indexed) { + return SearchError(SearchErrorCode::InvalidSerchMethod); + } + + // Check for unsupported Wildcard query type + if (m_currentQuery.type() == SearchQuery::Type::Wildcard) { + return SearchError(OcrTextSearchErrorCode::WildcardNotSupported); + } + + if (m_currentQuery.type() == SearchQuery::Type::Simple + && m_currentQuery.keyword().toUtf8().size() < Global::kMinContentSearchKeywordLength) { + return SearchError(OcrTextSearchErrorCode::KeywordTooShort); + } + + return result; +} + +std::unique_ptr OcrTextSearchStrategyFactory::createStrategy( + SearchType searchType, const SearchOptions &options) +{ + // Ensure correct search type + if (searchType != SearchType::Ocr) { + return nullptr; + } + + // Create corresponding strategy based on search method + if (options.method() == SearchMethod::Indexed) { + return std::make_unique(options); + } + + return nullptr; +} + +DFM_SEARCH_END_NS diff --git a/src/dfm-search/dfm-search-lib/ocrtextsearch/ocrtextsearchengine.h b/src/dfm-search/dfm-search-lib/ocrtextsearch/ocrtextsearchengine.h new file mode 100644 index 00000000..4d6d6f59 --- /dev/null +++ b/src/dfm-search/dfm-search-lib/ocrtextsearch/ocrtextsearchengine.h @@ -0,0 +1,49 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later +#ifndef OCRTEXT_SEARCH_ENGINE_H +#define OCRTEXT_SEARCH_ENGINE_H + +#include "core/genericsearchengine.h" + +DFM_SEARCH_BEGIN_NS + +/** + * @brief OCR text search engine + * + * Implements search functionality for text extracted from images using OCR. + * This engine uses Lucene indexes similar to content search, but with + * simplified features (no highlighting support). + */ +class OcrTextSearchEngine : public GenericSearchEngine +{ + Q_OBJECT + +public: + explicit OcrTextSearchEngine(QObject *parent = nullptr); + ~OcrTextSearchEngine() override; + + // Implement search type + SearchType searchType() const override { return SearchType::Ocr; } + +protected: + // Setup strategy factory + void setupStrategyFactory() override; + + // Override validation to add specific checks + SearchError validateSearchConditions() override; +}; + +/** + * @brief OCR text search strategy factory + */ +class OcrTextSearchStrategyFactory : public SearchStrategyFactory +{ +public: + std::unique_ptr createStrategy( + SearchType searchType, const SearchOptions &options) override; +}; + +DFM_SEARCH_END_NS + +#endif // OCRTEXT_SEARCH_ENGINE_H diff --git a/src/dfm-search/dfm-search-lib/ocrtextsearch/ocrtextstrategies/basestrategy.h b/src/dfm-search/dfm-search-lib/ocrtextsearch/ocrtextstrategies/basestrategy.h new file mode 100644 index 00000000..0774e059 --- /dev/null +++ b/src/dfm-search/dfm-search-lib/ocrtextsearch/ocrtextstrategies/basestrategy.h @@ -0,0 +1,28 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later +#ifndef OCRTEXT_BASE_STRATEGY_H +#define OCRTEXT_BASE_STRATEGY_H + +#include "core/searchstrategy/basesearchstrategy.h" +#include + +DFM_SEARCH_BEGIN_NS + +/** + * @brief OCR text search strategy base class + */ +class OcrTextBaseStrategy : public BaseSearchStrategy +{ + Q_OBJECT + +public: + explicit OcrTextBaseStrategy(const SearchOptions &options, QObject *parent = nullptr) + : BaseSearchStrategy(options, parent) { } + + SearchType searchType() const override { return SearchType::Ocr; } +}; + +DFM_SEARCH_END_NS + +#endif // OCRTEXT_BASE_STRATEGY_H diff --git a/src/dfm-search/dfm-search-lib/ocrtextsearch/ocrtextstrategies/indexedstrategy.cpp b/src/dfm-search/dfm-search-lib/ocrtextsearch/ocrtextstrategies/indexedstrategy.cpp new file mode 100644 index 00000000..0a496d77 --- /dev/null +++ b/src/dfm-search/dfm-search-lib/ocrtextsearch/ocrtextstrategies/indexedstrategy.cpp @@ -0,0 +1,482 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later +#include "indexedstrategy.h" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "3rdparty/fulltext/chineseanalyzer.h" +#include "utils/cancellablecollector.h" +#include "utils/lucenequeryutils.h" +#include "utils/searchutility.h" +#include "utils/lucene_cancellation_compat.h" +#include "utils/timerangeutils.h" + +using namespace Lucene; + +DFM_SEARCH_BEGIN_NS + +OcrTextIndexedStrategy::OcrTextIndexedStrategy(const SearchOptions &options, QObject *parent) + : OcrTextBaseStrategy(options, parent) +{ + initializeIndexing(); +} + +OcrTextIndexedStrategy::~OcrTextIndexedStrategy() = default; + +void OcrTextIndexedStrategy::initializeIndexing() +{ + // Get OCR text index directory + m_indexDir = Global::ocrTextIndexDirectory(); + + // Check if index directory exists + if (!QDir(m_indexDir).exists()) { + qWarning() << "OCR text index directory does not exist:" << m_indexDir; + } +} + +void OcrTextIndexedStrategy::search(const SearchQuery &query) +{ + m_cancelled.store(false); + m_results.clear(); + + try { + // Perform OCR text index search + performOcrTextSearch(query); + } catch (const std::exception &e) { + qWarning() << "OCR Text Index Search Exception:" << e.what(); + emit errorOccurred(SearchError(OcrTextSearchErrorCode::OcrTextIndexException)); + } +} + +Lucene::QueryPtr OcrTextIndexedStrategy::buildLuceneQuery(const SearchQuery &query, const Lucene::AnalyzerPtr &analyzer, const QString &searchPath) +{ + try { + m_keywords.clear(); + OcrTextOptionsAPI optAPI(m_options); + bool mixedAndEnabled = optAPI.isFilenameOcrContentMixedAndSearchEnabled(); + + Lucene::QueryParserPtr ocrContentsParser = newLucene( + Lucene::LuceneVersion::LUCENE_CURRENT, + LuceneFieldNames::OcrText::kOcrContents, + analyzer); + + Lucene::QueryPtr mainQuery; + if (query.type() == SearchQuery::Type::Simple) { + mainQuery = buildSimpleOcrContentsQuery(query, ocrContentsParser); + } else if (query.type() == SearchQuery::Type::Boolean) { + if (query.subQueries().isEmpty()) { + // For an empty boolean query, match nothing. + mainQuery = newLucene(); + } else { + // Determine which logic path to take for boolean queries + if (mixedAndEnabled && query.booleanOperator() == SearchQuery::BooleanOperator::AND) { + // New "advanced" AND logic for ocr_contents/filename + mainQuery = buildAdvancedAndQuery(query, ocrContentsParser, analyzer); + } else { + // "Standard" ocr_contents-only logic + mainQuery = buildStandardBooleanOcrContentsQuery(query, ocrContentsParser); + } + } + } else { + qWarning() << "Unknown SearchQuery type encountered."; + mainQuery = newLucene(); // Should not happen + } + + // Add path prefix query optimization + if (mainQuery && SearchUtility::isOcrTextIndexAncestorPathsSupported() + && SearchUtility::shouldUsePathPrefixQuery(searchPath)) { + QueryPtr pathPrefixQuery = LuceneQueryUtils::buildPathPrefixQuery(searchPath, + QString::fromWCharArray(LuceneFieldNames::OcrText::kAncestorPaths)); + if (pathPrefixQuery) { + BooleanQueryPtr finalQuery = newLucene(); + finalQuery->add(mainQuery, BooleanClause::MUST); + finalQuery->add(pathPrefixQuery, BooleanClause::MUST); + qInfo() << "Using path prefix query for OCR text search optimization:" << searchPath; + mainQuery = finalQuery; + } + } + + // Add time range filter query + if (m_options.hasTimeRangeFilter()) { + TimeRangeFilter filter = m_options.timeRangeFilter(); + auto [start, end] = filter.resolveTimeRange(); + + qint64 startEpoch = TimeRangeUtils::toEpochSecs(start); + qint64 endEpoch = TimeRangeUtils::toEpochSecs(end); + + const wchar_t *fieldName = (filter.timeField() == TimeField::BirthTime) + ? LuceneFieldNames::OcrText::kBirthTime + : LuceneFieldNames::OcrText::kModifyTime; + + QueryPtr timeQuery = TimeRangeUtils::buildNumericRangeQuery( + fieldName, startEpoch, endEpoch, + filter.includeLower(), filter.includeUpper()); + + if (timeQuery) { + if (mainQuery) { + BooleanQueryPtr finalQuery = newLucene(); + finalQuery->add(mainQuery, BooleanClause::MUST); + finalQuery->add(timeQuery, BooleanClause::MUST); + mainQuery = finalQuery; + } else { + // Time filter alone is a valid query + mainQuery = timeQuery; + } + } + } + + return mainQuery; + + } catch (const Lucene::LuceneException &e) { + qWarning() << "Error building Lucene query:" << QString::fromStdWString(e.getError()); + return nullptr; + } catch (const std::exception &e) { + qWarning() << "Standard exception building Lucene query:" << e.what(); + return nullptr; + } +} + +QueryPtr OcrTextIndexedStrategy::buildAdvancedAndQuery(const SearchQuery &query, const Lucene::QueryParserPtr &ocrContentsParser, const Lucene::AnalyzerPtr &analyzer) +{ + // This method implements the "mixed" AND logic similar to content search. + // It requires its own filenameParser. + Lucene::QueryParserPtr filenameParser = newLucene( + Lucene::LuceneVersion::LUCENE_CURRENT, + LuceneFieldNames::OcrText::kFilename, + analyzer); + + Lucene::BooleanQueryPtr overallQuery = newLucene(); + Lucene::BooleanQueryPtr mainAndClausesQuery = newLucene(); + Lucene::BooleanQueryPtr allOcrContentsQuery = newLucene(); + Lucene::BooleanQueryPtr allFilenamesQuery = newLucene(); + bool hasValidKeywords = false; + + for (const auto &subQuery : query.subQueries()) { + m_keywords.append(subQuery.keyword()); + if (subQuery.keyword().isEmpty()) { + continue; // Skip empty keywords + } + hasValidKeywords = true; + + // Use LuceneQueryUtils to process special characters + Lucene::String processedKeyword = LuceneQueryUtils::processQueryString(subQuery.keyword(), false); + Lucene::QueryPtr ocrContentsTermQuery = ocrContentsParser->parse(processedKeyword); + Lucene::QueryPtr filenameTermQuery = filenameParser->parse(processedKeyword); + + // Build (ocr_contents:keyword OR filename:keyword) + Lucene::BooleanQueryPtr combinedTermQuery = newLucene(); + combinedTermQuery->add(ocrContentsTermQuery, Lucene::BooleanClause::SHOULD); + combinedTermQuery->add(filenameTermQuery, Lucene::BooleanClause::SHOULD); + + mainAndClausesQuery->add(combinedTermQuery, Lucene::BooleanClause::MUST); + allOcrContentsQuery->add(ocrContentsTermQuery, Lucene::BooleanClause::MUST); + allFilenamesQuery->add(filenameTermQuery, Lucene::BooleanClause::MUST); + } + + if (!hasValidKeywords) { // All subQuery keywords were empty + qWarning() << "No valid keywords found in advanced AND query"; + return newLucene(); // Matches nothing + } + + // Exclude pure filename-only matches + // Final query: ( (ocr:k1 OR f:k1) AND ... ) AND NOT (f:k1 AND f:k2 ... AND NOT (ocr:k1 AND ocr:k2 ...)) + Lucene::BooleanQueryPtr pureFilenameQuery = newLucene(); + pureFilenameQuery->add(allFilenamesQuery, Lucene::BooleanClause::MUST); + pureFilenameQuery->add(allOcrContentsQuery, Lucene::BooleanClause::MUST_NOT); + + overallQuery->add(mainAndClausesQuery, Lucene::BooleanClause::MUST); + overallQuery->add(pureFilenameQuery, Lucene::BooleanClause::MUST_NOT); + + return overallQuery; +} + +QueryPtr OcrTextIndexedStrategy::buildStandardBooleanOcrContentsQuery(const SearchQuery &query, const Lucene::QueryParserPtr &ocrContentsParser) +{ + // This method implements the "original" boolean logic, searching only "ocr_contents". + Lucene::BooleanQueryPtr booleanQuery = newLucene(); + + for (const auto &subQuery : query.subQueries()) { + m_keywords.append(subQuery.keyword()); + if (subQuery.keyword().isEmpty()) { + continue; // Skip empty keywords + } + + // Use LuceneQueryUtils to process special characters + Lucene::QueryPtr termQuery = ocrContentsParser->parse(LuceneQueryUtils::processQueryString(subQuery.keyword(), false)); + booleanQuery->add(termQuery, + query.booleanOperator() == SearchQuery::BooleanOperator::AND ? Lucene::BooleanClause::MUST : Lucene::BooleanClause::SHOULD); + } + + return booleanQuery; +} + +QueryPtr OcrTextIndexedStrategy::buildSimpleOcrContentsQuery(const SearchQuery &query, const Lucene::QueryParserPtr &ocrContentsParser) +{ + m_keywords.append(query.keyword()); + if (query.keyword().isEmpty()) { + return newLucene(); // Match nothing for empty keyword + } + // Use LuceneQueryUtils to process special characters + return ocrContentsParser->parse(LuceneQueryUtils::processQueryString(query.keyword(), false)); +} + +void OcrTextIndexedStrategy::processSearchResults(const Lucene::IndexSearcherPtr &searcher, + const Lucene::Collection &scoreDocs) +{ + // Measure the time taken to process search results + QElapsedTimer resultTimer; + resultTimer.start(); + + QString searchPath = m_options.searchPath(); + const QStringList &searchExcludedPaths = m_options.searchExcludedPaths(); + auto docsSize = scoreDocs.size(); + + for (int32_t i = 0; i < docsSize; ++i) { + if (m_cancelled.load()) { + qInfo() << "OCR text search cancelled"; + break; + } + + try { + Lucene::ScoreDocPtr scoreDoc = scoreDocs[i]; + if (!scoreDoc) { + qWarning() << "Null ScoreDoc encountered at index" << i; + continue; + } + + // Defensive check: verify document ID is valid + if (scoreDoc->doc < 0) { + qWarning() << "Invalid document ID:" << scoreDoc->doc; + continue; + } + + // Safely retrieve document (could throw if index is corrupted) + Lucene::DocumentPtr doc; + try { + doc = searcher->doc(scoreDoc->doc); + if (!doc) { + qWarning() << "Failed to retrieve document at index:" << scoreDoc->doc; + continue; + } + } catch (const Lucene::LuceneException &e) { + qWarning() << "Exception while retrieving document:" << QString::fromStdWString(e.getError()); + continue; + } catch (const std::exception &e) { + qWarning() << "Standard exception while retrieving document:" << e.what(); + continue; + } + + // Safely get path + Lucene::String pathField; + try { + pathField = doc->get(LuceneFieldNames::OcrText::kPath); + if (pathField.empty()) { + qWarning() << "Document missing path field at index:" << scoreDoc->doc; + continue; + } + } catch (const std::exception &e) { + qWarning() << "Exception retrieving path field:" << e.what(); + continue; + } + + QString path = QString::fromStdWString(pathField); + + if (!path.startsWith(searchPath)) { + continue; + } + + if (std::any_of(searchExcludedPaths.cbegin(), searchExcludedPaths.cend(), + [&path](const auto &excluded) { return path.startsWith(excluded); })) { + continue; + } + + // Safely check hidden status + if (Q_LIKELY(!m_options.includeHidden())) { + try { + Lucene::String hiddenField = doc->get(LuceneFieldNames::OcrText::kIsHidden); + if (!hiddenField.empty() && QString::fromStdWString(hiddenField).toLower() == "y") { + continue; + } + } catch (const std::exception &e) { + qWarning() << "Exception retrieving is_hidden field:" << e.what(); + // Default to visible if field can't be read + } + } + + // Create search result + SearchResult result(path); + + // 设置详细结果(如果启用) + if (Q_UNLIKELY(m_options.detailedResultsEnabled())) { + OcrTextResultAPI resultApi(result); + + // OCR 内容 + Lucene::String ocrContentField = doc->get(LuceneFieldNames::OcrText::kOcrContents); + if (!ocrContentField.empty()) { + resultApi.setOcrContent(QString::fromStdWString(ocrContentField)); + } + + // 文件名 + Lucene::String filenameField = doc->get(LuceneFieldNames::OcrText::kFilename); + if (!filenameField.empty()) { + resultApi.setFilename(QString::fromStdWString(filenameField)); + } + + // 隐藏状态 + Lucene::String hiddenField = doc->get(LuceneFieldNames::OcrText::kIsHidden); + if (!hiddenField.empty()) { + resultApi.setIsHidden(QString::fromStdWString(hiddenField).toLower() == "y"); + } + + // 修改时间 + Lucene::String modifyTimeField = doc->get(LuceneFieldNames::OcrText::kModifyTime); + if (!modifyTimeField.empty()) { + bool ok = false; + qint64 timestamp = QString::fromStdWString(modifyTimeField).toLongLong(&ok); + if (ok && timestamp > 0) { + resultApi.setModifyTimestamp(timestamp); + } + } + + // 创建时间 + Lucene::String birthTimeField = doc->get(LuceneFieldNames::OcrText::kBirthTime); + if (!birthTimeField.empty()) { + bool ok = false; + qint64 timestamp = QString::fromStdWString(birthTimeField).toLongLong(&ok); + if (ok && timestamp > 0) { + resultApi.setBirthTimestamp(timestamp); + } + } + } + + // Add to result collection + m_results.append(result); + + // Real-time result emission + if (Q_UNLIKELY(m_options.resultFoundEnabled())) + emit resultFound(result); + + } catch (const Lucene::LuceneException &e) { + qWarning() << "Error processing result:" << QString::fromStdWString(e.getError()); + continue; + } catch (const std::exception &e) { + qWarning() << "Standard exception:" << e.what(); + continue; + } catch (...) { + qWarning() << "Unknown exception during result processing"; + continue; + } + } + + qInfo() << "OCR text result processing time:" << resultTimer.elapsed() << "ms"; + emit searchFinished(m_results); +} + +void OcrTextIndexedStrategy::performOcrTextSearch(const SearchQuery &query) +{ + // RAII guard: automatically manage cancellation flag lifecycle + SearchCancellationGuard guard(&m_cancelled); + + try { + // Get index directory + FSDirectoryPtr directory = FSDirectory::open(m_indexDir.toStdWString()); + if (!directory) { + qWarning() << "Failed to open OCR text index directory:" << m_indexDir; + emit errorOccurred(SearchError(OcrTextSearchErrorCode::OcrTextIndexNotFound)); + return; + } + + // Get index reader + IndexReaderPtr reader = IndexReader::open(directory, true); + if (!reader || reader->numDocs() == 0) { + qWarning() << "OCR text index is empty or cannot be opened"; + emit errorOccurred(SearchError(OcrTextSearchErrorCode::OcrTextIndexNotFound)); + return; + } + + // Create searcher + IndexSearcherPtr searcher = newLucene(reader); + + // Create analyzer (reuse ChineseAnalyzer for OCR text) + AnalyzerPtr analyzer = newLucene(); + + // Build query + m_currentQuery = buildLuceneQuery(query, analyzer, m_options.searchPath()); + if (!m_currentQuery) { + qWarning() << "Failed to build Lucene query for OCR text search"; + emit errorOccurred(SearchError(OcrTextSearchErrorCode::OcrTextIndexException)); + return; + } + + // Execute search + QElapsedTimer searchTimer; + searchTimer.start(); + + int32_t maxResults = m_options.maxResults() > 0 ? m_options.maxResults() : reader->numDocs(); + + // Use custom CancellableCollector for interruptible search + Collection scoreDocs; + try { + // Create cancellable collector + boost::shared_ptr collector = newLucene(&m_cancelled, maxResults); + + // Execute search with custom collector + qInfo() << "OCR text search execution start:" << query.keyword(); + searcher->search(m_currentQuery, collector); + // Get collected documents + scoreDocs = collector->getScoreDocs(); + + qInfo() << "OCR text search execution time:" << searchTimer.elapsed() << "ms" + << "Total hits:" << collector->getTotalHits() + << "Collected:" << scoreDocs.size() + << "Keyword:" << query.keyword() + << "Cancelled" << m_cancelled.load(); + } catch (const SearchCancelledException &e) { + qInfo() << "OCR text search cancelled during execution"; + emit searchFinished(m_results); + return; + } catch (const RuntimeException &e) { +#if LUCENE_HAS_SEARCH_CANCELLATION + // Check if this is a cancellation exception thrown in ExactPhraseScorer::phraseFreq() + QString errorMsg = QString::fromStdWString(e.getError()); + if (errorMsg.contains("cancelled", Qt::CaseInsensitive)) { + qInfo() << "OCR text search cancelled in phraseFreq():" << errorMsg; + emit searchFinished(m_results); + return; + } +#endif + // Other runtime exceptions, rethrow + throw; + } + + // Process search results + processSearchResults(searcher, scoreDocs); + } catch (const LuceneException &e) { + qWarning() << "Lucene search exception:" << QString::fromStdWString(e.getError()); + emit errorOccurred(SearchError(OcrTextSearchErrorCode::OcrTextIndexException)); + } catch (const std::exception &e) { + qWarning() << "Standard exception:" << e.what(); + emit errorOccurred(SearchError(OcrTextSearchErrorCode::OcrTextIndexException)); + } +} + +void OcrTextIndexedStrategy::cancel() +{ + m_cancelled.store(true); +} + +DFM_SEARCH_END_NS diff --git a/src/dfm-search/dfm-search-lib/ocrtextsearch/ocrtextstrategies/indexedstrategy.h b/src/dfm-search/dfm-search-lib/ocrtextsearch/ocrtextstrategies/indexedstrategy.h new file mode 100644 index 00000000..b09aead1 --- /dev/null +++ b/src/dfm-search/dfm-search-lib/ocrtextsearch/ocrtextstrategies/indexedstrategy.h @@ -0,0 +1,72 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later +#ifndef OCRTEXT_INDEXED_STRATEGY_H +#define OCRTEXT_INDEXED_STRATEGY_H + +#include "basestrategy.h" + +#include +#include +#include +#include +#include + +DFM_SEARCH_BEGIN_NS + +/** + * @brief OCR text index search strategy + * + * This strategy searches for text extracted from images using OCR technology. + * It uses a Lucene index similar to content search, but with simplified logic + * (no highlighting support). + */ +class OcrTextIndexedStrategy : public OcrTextBaseStrategy +{ + Q_OBJECT + +public: + explicit OcrTextIndexedStrategy(const SearchOptions &options, QObject *parent = nullptr); + ~OcrTextIndexedStrategy() override; + + void search(const SearchQuery &query) override; + void cancel() override; + +private: + // Initialize index directory + void initializeIndexing(); + + // Perform OCR text search + void performOcrTextSearch(const SearchQuery &query); + + // Build Lucene query + Lucene::QueryPtr buildLuceneQuery(const SearchQuery &query, const Lucene::AnalyzerPtr &analyzer, const QString &searchPath); + + // Helper for simple queries + Lucene::QueryPtr buildSimpleOcrContentsQuery( + const SearchQuery &query, + const Lucene::QueryParserPtr &ocrContentsParser); + + // Helper for "standard" boolean logic + Lucene::QueryPtr buildStandardBooleanOcrContentsQuery( + const SearchQuery &query, + const Lucene::QueryParserPtr &ocrContentsParser); + + // Helper for "advanced" mixed AND logic (searches "ocr_contents" and "filename") + Lucene::QueryPtr buildAdvancedAndQuery( + const SearchQuery &query, + const Lucene::QueryParserPtr &ocrContentsParser, + const Lucene::AnalyzerPtr &analyzer); + + // Process search results + void processSearchResults(const Lucene::IndexSearcherPtr &searcher, + const Lucene::Collection &scoreDocs); + + QString m_indexDir; + Lucene::QueryPtr m_currentQuery; + QStringList m_keywords; +}; + +DFM_SEARCH_END_NS + +#endif // OCRTEXT_INDEXED_STRATEGY_H diff --git a/src/dfm-search/dfm-search-lib/utils/searchutility.cpp b/src/dfm-search/dfm-search-lib/utils/searchutility.cpp index 2c0dbb17..c960bd58 100644 --- a/src/dfm-search/dfm-search-lib/utils/searchutility.cpp +++ b/src/dfm-search/dfm-search-lib/utils/searchutility.cpp @@ -28,6 +28,7 @@ namespace Global { namespace IndexVersionThresholds { constexpr int FILENAME_ANCESTOR_PATHS = 3; constexpr int CONTENT_ANCESTOR_PATHS = 1; +constexpr int OCRTEXT_ANCESTOR_PATHS = 1; } /** @@ -577,14 +578,45 @@ QStringList defaultBlacklistPaths() return pathsFromDConfig; } +/** + * @brief Check if a path is within any of the specified directories. + * @param path The path to check + * @param dirs The directories to check against + * @return true if the path is within any of the directories, false otherwise + */ +static bool isPathInAnyDirectory(const QString &path, const QStringList &dirs) +{ + // Normalize the path once (it doesn't change during iteration) + QString normalizedPath = path; + if (normalizedPath.endsWith('/') && normalizedPath.length() > 1) { + normalizedPath.chop(1); + } + + return std::any_of(dirs.cbegin(), dirs.cend(), [&normalizedPath](const QString &dir) { + QString normalizedDir = dir; + + if (normalizedDir.endsWith('/') && normalizedDir.length() > 1) { + normalizedDir.chop(1); + } + + // Exact match - the path is the indexed directory itself + if (normalizedPath == normalizedDir) { + return true; + } + + // Check if path is within the directory by ensuring proper path separation + const QString dirWithSeparator = normalizedDir + '/'; + return normalizedPath.startsWith(dirWithSeparator); + }); +} + bool isPathInContentIndexDirectory(const QString &path) { if (!isContentIndexAvailable()) return false; - const QStringList &dirs = defaultIndexedDirectory(); - return std::any_of(dirs.cbegin(), dirs.cend(), - [&path](const QString &dir) { return path.startsWith(dir); }); + static const QStringList &kDirs = DFMSEARCH::Global::defaultIndexedDirectory(); + return isPathInAnyDirectory(path, kDirs); } bool isContentIndexAvailable() @@ -621,23 +653,73 @@ bool isContentIndexAvailable() QString contentIndexDirectory() { - QString configDir = QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation); - QDir deepinDir(configDir); - QString indexPath = deepinDir.filePath("deepin/dde-file-manager/index"); + QString dataDir = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation); + QDir deepinDir(dataDir); + QString indexPath = deepinDir.filePath("deepin/dde-file-manager/fulltext-index"); return indexPath; } +QString ocrTextIndexDirectory() +{ + QString dataDir = + QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation); + QDir deepinDir(dataDir); + QString indexPath = deepinDir.filePath("deepin/dde-file-manager/ocrtext-index"); + return indexPath; +} + +bool isPathInOcrTextIndexDirectory(const QString &path) +{ + if (!isOcrTextIndexAvailable()) + return false; + + static const QStringList &kDirs = DFMSEARCH::Global::defaultIndexedDirectory(); + return isPathInAnyDirectory(path, kDirs); +} + +bool isOcrTextIndexAvailable() +{ + const QString &dir = ocrTextIndexDirectory(); + if (!IndexReader::indexExists(FSDirectory::open(dir.toStdWString()))) + return false; + + const QString &statusFile = dir + "/index_status.json"; + + // 1. 尝试打开文件 + QFile file(statusFile); + if (!file.open(QIODevice::ReadOnly)) { + return false; // 文件无法打开 + } + + // 2. 读取并解析 JSON + QJsonDocument doc = QJsonDocument::fromJson(file.readAll()); + file.close(); + + if (doc.isNull() || !doc.isObject()) { + return false; // JSON 格式无效 + } + + // 3. 检查 lastUpdateTime 字段 + QJsonObject obj = doc.object(); + if (!obj.contains("lastUpdateTime")) { + return false; // 字段不存在 + } + + const QString lastUpdateTime = obj["lastUpdateTime"].toString(); + return !lastUpdateTime.isEmpty(); // 字段值非空则为有效 +} + bool isPathInFileNameIndexDirectory(const QString &path) { if (!isFileNameIndexDirectoryAvailable()) return false; - if (BlacklistMatcher::isPathBlacklisted(path, defaultBlacklistPaths())) + static const QStringList &kBlackPaths = defaultBlacklistPaths(); + if (BlacklistMatcher::isPathBlacklisted(path, kBlackPaths)) return false; - const QStringList &dirs = defaultIndexedDirectory(); - return std::any_of(dirs.cbegin(), dirs.cend(), - [&path](const QString &dir) { return path.startsWith(dir); }); + static const QStringList &kIndexedDirs = DFMSEARCH::Global::defaultIndexedDirectory(); + return isPathInAnyDirectory(path, kIndexedDirs); } bool isFileNameIndexDirectoryAvailable() @@ -740,6 +822,11 @@ int contentIndexVersion() return readIndexVersion(contentIndexDirectory(), "index_status.json"); } +int ocrTextIndexVersion() +{ + return readIndexVersion(ocrTextIndexDirectory(), "index_status.json"); +} + } // namespace Global namespace SearchUtility { @@ -754,6 +841,11 @@ bool isContentIndexAncestorPathsSupported() return Global::contentIndexVersion() > Global::IndexVersionThresholds::CONTENT_ANCESTOR_PATHS; } +bool isOcrTextIndexAncestorPathsSupported() +{ + return Global::ocrTextIndexVersion() > Global::IndexVersionThresholds::OCRTEXT_ANCESTOR_PATHS; +} + QStringList extractBooleanKeywords(const SearchQuery &query) { QStringList keywords; diff --git a/src/dfm-search/dfm-search-lib/utils/searchutility.h b/src/dfm-search/dfm-search-lib/utils/searchutility.h index 2c0407c0..1fa74fd8 100644 --- a/src/dfm-search/dfm-search-lib/utils/searchutility.h +++ b/src/dfm-search/dfm-search-lib/utils/searchutility.h @@ -54,6 +54,13 @@ bool isFilenameIndexAncestorPathsSupported(); */ bool isContentIndexAncestorPathsSupported(); +/** + * @brief Check if the OCR text index supports the ancestor_paths field. + * This function checks the OCR text index version and returns true if the version supports ancestor_paths. + * @return true if the OCR text index supports ancestor_paths, false otherwise. + */ +bool isOcrTextIndexAncestorPathsSupported(); + } // namespace SearchUtility DFM_SEARCH_END_NS diff --git a/src/dfm-search/dfm-search-lib/utils/timerangeutils.cpp b/src/dfm-search/dfm-search-lib/utils/timerangeutils.cpp new file mode 100644 index 00000000..3fddbf35 --- /dev/null +++ b/src/dfm-search/dfm-search-lib/utils/timerangeutils.cpp @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later +#include "timerangeutils.h" + +#include +#include + +DFM_SEARCH_BEGIN_NS + +namespace TimeRangeUtils { + +qint64 toEpochSecs(const QDateTime &dt) +{ + if (!dt.isValid()) { + return 0; + } + return dt.toSecsSinceEpoch(); +} + +Lucene::QueryPtr buildNumericRangeQuery( + const wchar_t *fieldName, + qint64 startEpoch, + qint64 endEpoch, + bool includeLower, + bool includeUpper) +{ + // Use INT64_MIN and INT64_MAX for unbounded ranges + int64_t minVal = (startEpoch == 0) ? INT64_MIN : startEpoch; + int64_t maxVal = (endEpoch == 0) ? INT64_MAX : endEpoch; + + // Use the default precisionStep (4) + return Lucene::NumericRangeQuery::newLongRange( + Lucene::String(fieldName), + minVal, + maxVal, + includeLower, + includeUpper); +} + +} // namespace TimeRangeUtils + +DFM_SEARCH_END_NS diff --git a/src/dfm-search/dfm-search-lib/utils/timerangeutils.h b/src/dfm-search/dfm-search-lib/utils/timerangeutils.h new file mode 100644 index 00000000..72e9196c --- /dev/null +++ b/src/dfm-search/dfm-search-lib/utils/timerangeutils.h @@ -0,0 +1,47 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later +#ifndef TIMERANGEUTILS_H +#define TIMERANGEUTILS_H + +#include + +#include + +#include + +DFM_SEARCH_BEGIN_NS + +/** + * @brief TimeRangeUtils provides utility functions for time range operations + */ +namespace TimeRangeUtils { + +/** + * @brief Convert QDateTime to Unix timestamp in seconds + * @param dt The datetime to convert + * @return Unix timestamp in seconds (0 if invalid) + */ +qint64 toEpochSecs(const QDateTime &dt); + +/** + * @brief Build a Lucene NumericRangeQuery for time range filtering + * @param fieldName The field name to query (e.g., L"birth_time", L"modify_time") + * @param startEpoch The start timestamp (in seconds), use 0 for no lower bound + * @param endEpoch The end timestamp (in seconds), use INT64_MAX for no upper bound + * @param includeLower Whether to include the lower bound + * @param includeUpper Whether to include the upper bound + * @return A NumericRangeQuery pointer, or nullptr if range is invalid + */ +Lucene::QueryPtr buildNumericRangeQuery( + const wchar_t *fieldName, + qint64 startEpoch, + qint64 endEpoch, + bool includeLower, + bool includeUpper); + +} // namespace TimeRangeUtils + +DFM_SEARCH_END_NS + +#endif // TIMERANGEUTILS_H