23 次程式碼提交

作者 SHA1 備註 提交日期
2b1dc72410 Release v0.9, Update docs 2023-05-07 20:18:55 +02:00
22fee1d064 shared: TokenType: FILTER_TAG_ASSIGNED is not a content search token 2023-05-07 17:11:31 +02:00
50a5c399c4 gui: PreviewGeneratorPlainText: Don't add header if we have nothing to show 2023-05-07 17:11:31 +02:00
4b3ebb08c2 cli: commandadd: Improve help message 2023-05-07 17:11:31 +02:00
4c5643e342 cli,shared: Add remove, show and list for tags 2023-05-07 17:11:31 +02:00
e8d217e191 cli: CommandAdd: Add verbose (-v) 2023-05-07 17:11:31 +02:00
4604970f9d shared: Indexer: Add setProgressReportThreshold() 2023-05-01 23:52:45 +02:00
6cd7a92576 shared: Indexer: Fix case where errors did not increase counter 2023-05-01 23:51:00 +02:00
9540f27c95 gui: Indexing: Disable 'Add' and '...' button when indexing 2023-04-10 19:55:24 +02:00
244e6aa95e gui: Add tag checkboxes and inputdialog for search context menu 2023-04-10 19:55:24 +02:00
3e3a4d0cd4 cli: CommandTag: Use TagManager 2023-04-10 19:55:24 +02:00
94fbdb5a92 shared: sqlitesearch: Allow filtering by tags 2023-04-10 19:55:24 +02:00
abd1b94235 shared: LooqsQuery: Add tag:(),t:() filters to filter by tags 2023-04-10 19:55:24 +02:00
d2dcc2f95b shared: sqlitedbservice: Add setTags(),getTagsForPath(),getTags() 2023-04-10 19:55:24 +02:00
f324da0369 shared: Begin TagManager 2023-04-10 18:31:22 +02:00
a3cfb7ade1 shared: FileScanWorker: Catch LooqsGeneralException for better error msgs 2023-04-02 21:41:35 +02:00
44b9986166 cli: Begin 'tag' command 2023-04-02 21:41:35 +02:00
4fe745e858 shared: sqlitedbservice: Begin addTag() 2023-04-02 21:41:35 +02:00
a0b95479e2 migrations: Add 5.sql: Tag support 2023-04-02 21:41:35 +02:00
07630c3b36 gui: main: Fix exiting after failed migration 2023-04-02 21:41:35 +02:00
a7c4ad5e7c gui: Only enable 'Previews' tab if previews can be generated 2023-04-02 21:41:35 +02:00
32c2653b0f shared: FileSaver: addFile(): Consider fillExistingContentless 2023-04-02 21:41:35 +02:00
a869d677a3 shared: sqlitedbservice: Introduce exec(),execBool(). Refactor 2023-04-02 21:41:35 +02:00
共有 29 個文件被更改,包括 737 次插入66 次删除

查看文件

@ -1,5 +1,24 @@
# looqs: Release notes
## 2023-05-07 - v0.9
Highlights: Tag support. Also begin new index mode to only index metadata (currently only path + file size, more to come).
Note: Upgrading can take some time as new column indexes will be added
CHANGES:
- gui: Improve font rendering in previews
- gui: Allow indexing only metadata
- gui: Allow adding content for files which only had metadata indexed before
- gui: Allow assigning tags by right clicking on paths
- cli: "add" command: Implement --verbose (-v)
- cli: "add" command: Implement --no-content and --fill-content
- cli: Add "tag" command which allows managing tags for paths.
- search: Add "tag:()", "t:()" filters
- Minor improvements and refactorings under the hood
- Add packages: Ubuntu 23.04.
## 2022-11-19 - v0.8.1
CHANGES:

查看文件

@ -28,7 +28,7 @@ There is no need to write the long form of filters. There are also booleans avai
The screenshots in this section may occasionally be slightly outdated, but they are usually recent enough to get an overall impression of the current state of the GUI.
## Current status
Latest version: 2022-11-19, v0.8.1
Latest version: 2023-05-07, v0.9
Please keep in mind: looqs is still at an early stage and may exhibit some weirdness and contain bugs.
@ -76,7 +76,7 @@ To build on Ubuntu and Debian, clone the repo and then run:
```
git submodule init
git submodule update
sudo apt install build-essential qtbase5-dev libpoppler-qt5-dev libuchardet-dev libquazip5-dev
sudo apt install build-essential qtbase5-dev libqt5sql5-sqlite libpoppler-qt5-dev libuchardet-dev libquazip5-dev
qmake
make
```
@ -97,7 +97,9 @@ The GUI is located in `gui/looqs-gui`, the binary for the CLI is in `cli/looqs`
## Packages
At this point, looqs is not in any official distro package repo, but I maintain some packages.
### Ubuntu 22.04, 22.10
### Ubuntu 23.04, 22.10, 22.04
Latest release can be installed using apt from the repo.
```
# First, obtain key, assume it's trusted.
@ -108,6 +110,8 @@ echo "deb [arch=amd64 signed-by=/usr/share/keyrings/repo.quitesimple.org.gpg] ht
sudo apt-get update
sudo apt-get install looqs
```
### Gentoo (EXPERIMENTAL)
Available in this overlay: https://github.com/quitesimpleorg/quitesimple-overlay
### Prebuilt tarball (distro-agnostic) (EXPERIMENTAL)
looqs is also distributed as a tarball containing prebuilt binaries and its library dependencies. The tarball is
@ -134,7 +138,7 @@ An AppImage may accompany the tarball in the future.
### Other distros
I'll probably add a package for voidlinux at some point and maybe will provide a Gentoo ebuild. However, I would appreciate help for others distros. If you create a package, let me know!
I appreciate help for others distros. If you create a package, let me know!
### Signature verification

查看文件

@ -165,6 +165,8 @@ A number of search filters are available.
| path.begins:(term) | pb:(term) | Filters path beginning with the specified term |
| contains:(terms) | c:(terms) | Full-text search, also understands quotes |
| limit:(integer) | - | Limits the number of results. The default is 1000. Say "limit:0" to see all results |
| tag:(tagname) | t:(tagname) | Filter for files that have been tagged with the corresponding tag |
Filters can be combined. The booleans AND and OR are supported. Negations can be applied too, except for c:(). Negations are specified with "!".
The AND boolean is implicit and thus entering it strictly optional.
@ -177,11 +179,5 @@ Examples:
|p:(notes) (pe:(odt) OR pe:(docx))          |Finds files such as notes.docx, notes.odt but also any .docs and .odt when the path contains the string 'notes'|
|memcpy !(pe:(.c) OR pe:(.cpp))| Performs a FTS search for 'memcpy' but excludes .cpp and .c files.|
|c:("I think, therefore")|Performs a FTS search for the phrase "I think, therefore".|
|c:("invoice") Downloads|This query is equivalent to c:("invoice") p:("Downloads")|
|c:("invoice") Downloads|Equivalent to c:("invoice") p:("Downloads")|
|p:(Downloads) invoice|Equivalent to c:("invoice") p:("Downloads")|

查看文件

@ -15,6 +15,7 @@ DEFINES += QT_DEPRECATED_WARNINGS
# You can also select to disable deprecated APIs only up to a certain version of Qt.
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0
SOURCES += \
commandtag.cpp \
main.cpp \
commandadd.cpp \
commanddelete.cpp \
@ -27,6 +28,7 @@ HEADERS += \
command.h \
commandadd.h \
commanddelete.h \
commandtag.h \
commandupdate.h \
commandsearch.h \
commandlist.h

查看文件

@ -2,7 +2,6 @@
#include <QThread>
#include <QDebug>
#include "command.h"
#include "looqsgeneralexception.h"
void Command::execute()
{

查看文件

@ -41,12 +41,13 @@ int CommandAdd::handle(QStringList arguments)
{
QCommandLineParser parser;
parser.addOptions({{{"c", "continue"},
"Continue adding files, don't exit on first error. If this option is not given, looqs will "
"exit asap, but it's possible that a few files will still be processed. "
"Continue adding files, don't exit on first error. Exit code will be 0. If this option is not "
"given, looqs will "
"exit asap, but it's possible that a few files will still be processed."
"Set -t 1 to avoid this behavior, but processing will be slower. "},
{{"n", "no-content"}, "Only add paths to database. Do not index content"},
{{"v", "verbose"}, "Print paths of files being processed"},
{{"f", "fill-content"}, "Index content for files previously indexed with -n"},
{"tags", "Comma-separated list of tags to assign"},
{{"t", "threads"}, "Number of threads to use.", "threads"}});
parser.addHelpOption();
parser.addPositionalArgument("add", "Add paths to the index",
@ -56,6 +57,8 @@ int CommandAdd::handle(QStringList arguments)
this->keepGoing = parser.isSet("continue");
bool pathsOnly = parser.isSet("no-content");
bool fillContent = parser.isSet("fill-content");
bool verbose = parser.isSet("verbose");
if(parser.isSet("threads"))
{
QString threadsCount = parser.value("threads");
@ -85,18 +88,43 @@ int CommandAdd::handle(QStringList arguments)
fileSaverOptions.keepGoing = keepGoing;
fileSaverOptions.fillExistingContentless = fillContent;
fileSaverOptions.metadataOnly = pathsOnly;
fileSaverOptions.verbose = false;
fileSaverOptions.verbose = verbose;
indexer = new Indexer(*this->dbService);
indexer->setFileSaverOptions(fileSaverOptions);
indexer->setTargetPaths(files.toVector());
if(verbose)
{
indexer->setProgressReportThreshold(1);
}
connect(indexer, &Indexer::pathsCountChanged, this,
[](int pathsCount) { Logger::info() << "Found paths: " << pathsCount << Qt::endl; });
connect(indexer, &Indexer::indexProgress, this,
[](int pathsCount, unsigned int /*added*/, unsigned int /*skipped*/, unsigned int /*failed*/,
unsigned int /*totalCount*/) { Logger::info() << "Processed files: " << pathsCount << Qt::endl; });
[verbose, this](int pathsCount, unsigned int /*added*/, unsigned int /*skipped*/, unsigned int /*failed*/,
unsigned int /*totalCount*/)
{
Logger::info() << "Processed files: " << pathsCount << Qt::endl;
if(verbose)
{
IndexResult indexResult = indexer->getResult();
int newlyAdded = indexResult.results.count() - currentResult.results.count();
if(newlyAdded > 0)
{
int newOffset = indexResult.results.count() - newlyAdded;
for(int i = newOffset; i < indexResult.results.count(); i++)
{
auto result = indexResult.results.at(i);
Logger::info() << SaveFileResultToString(result.second) << result.first << Qt::endl;
}
}
this->currentResult = indexResult;
}
}
);
connect(indexer, &Indexer::finished, this, &CommandAdd::indexerFinished);
this->autoFinish = false;

查看文件

@ -13,6 +13,8 @@ class CommandAdd : public Command
bool keepGoing = true;
protected:
IndexResult currentResult;
public:
using Command::Command;

查看文件

@ -1,6 +1,5 @@
#include <QCommandLineParser>
#include "commandsearch.h"
#include "databasefactory.h"
#include "logger.h"
int CommandSearch::handle(QStringList arguments)

153
cli/commandtag.cpp Normal file
查看文件

@ -0,0 +1,153 @@
#include <QCommandLineParser>
#include "commandtag.h"
#include "logger.h"
#include "tagmanager.h"
bool CommandTag::ensureAbsolutePaths(const QVector<QString> &paths, QVector<QString> &absolutePaths)
{
for(const QString &path : paths)
{
QFileInfo info{path};
if(!info.exists())
{
Logger::error() << "Can't add tag for file " + info.absoluteFilePath() + " because it does not exist"
<< Qt::endl;
return false;
}
QString absolutePath = info.absoluteFilePath();
if(!this->dbService->fileExistsInDatabase(absolutePath))
{
Logger::error() << "Only files that have been indexed can be tagged. File not in index: " + absolutePath
<< Qt::endl;
return false;
}
absolutePaths.append(absolutePath);
}
return true;
}
int CommandTag::handle(QStringList arguments)
{
QCommandLineParser parser;
parser.addPositionalArgument("add", "Adds a tag to a file",
"add [tag] [paths...]. Adds the tag to the specified paths");
parser.addPositionalArgument("remove", "Removes a path associated to a tag", "remove [tag] [path]");
parser.addPositionalArgument("delete", "Deletes a tag", "delete [tag]");
parser.addPositionalArgument("list", "Lists paths associated with a tag, or all tags", "list [tag]");
parser.addPositionalArgument("show", "Lists tags associated with a path", "show [path]");
parser.addHelpOption();
parser.parse(arguments);
QStringList args = parser.positionalArguments();
if(args.length() == 0)
{
parser.showHelp(EXIT_FAILURE);
return EXIT_FAILURE;
}
TagManager tagManager{*this->dbService};
QString cmd = args[0];
if(cmd == "add")
{
if(args.length() < 3)
{
Logger::error() << "Not enough arguments provided. 'add' requires a tag followed by at least one path"
<< Qt::endl;
return EXIT_FAILURE;
}
QString tag = args[1];
QVector<QString> paths = args.mid(2).toVector();
QVector<QString> absolutePaths;
if(!ensureAbsolutePaths(paths, absolutePaths))
{
return EXIT_FAILURE;
}
bool result = tagManager.addPathsToTag(tag, absolutePaths);
if(!result)
{
Logger::error() << "Failed to assign tags" << Qt::endl;
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
if(cmd == "list")
{
QString tag;
if(args.length() >= 2)
{
tag = args[1];
}
QVector<QString> entries;
if(tag.isEmpty())
{
entries = tagManager.getTags();
}
else
{
entries = tagManager.getPaths(tag);
}
for(const QString &entry : entries)
{
Logger::info() << entry << Qt::endl;
}
}
if(cmd == "remove")
{
if(args.length() < 3)
{
Logger::error() << "Not enough arguments provided. 'remove' requires a tag followed by at least one path"
<< Qt::endl;
return EXIT_FAILURE;
}
QString tag = args[1];
QVector<QString> paths = args.mid(2).toVector();
QVector<QString> absolutePaths;
if(!ensureAbsolutePaths(paths, absolutePaths))
{
return EXIT_FAILURE;
}
if(!tagManager.removePathsForTag(tag, absolutePaths))
{
Logger::error() << "Failed to remove path assignments" << Qt::endl;
return EXIT_FAILURE;
}
}
if(cmd == "delete")
{
if(args.length() != 2)
{
Logger::error() << "The 'delete' command requires the tag to delete" << Qt::endl;
return EXIT_FAILURE;
}
if(!tagManager.deleteTag(args[1]))
{
Logger::error() << "Failed to delete tag" << Qt::endl;
return EXIT_FAILURE;
}
}
if(cmd == "show")
{
if(args.length() != 2)
{
Logger::error() << "The 'show' command requires a path to show the assigned tags" << Qt::endl;
return EXIT_FAILURE;
}
QString path = args[1];
QVector<QString> absolutePaths;
if(!ensureAbsolutePaths({path}, absolutePaths))
{
return EXIT_FAILURE;
}
QVector<QString> tags = tagManager.getTags(absolutePaths.at(0));
for(const QString &entry : tags)
{
Logger::info() << entry << Qt::endl;
}
}
return EXIT_SUCCESS;
}

16
cli/commandtag.h Normal file
查看文件

@ -0,0 +1,16 @@
#ifndef COMMANDTAG_H
#define COMMANDTAG_H
#include "command.h"
class CommandTag : public Command
{
protected:
bool ensureAbsolutePaths(const QVector<QString> &paths, QVector<QString> &absolutePaths);
public:
using Command::Command;
int handle(QStringList arguments) override;
};
#endif // COMMANDTAG_H

查看文件

@ -21,6 +21,7 @@
#include "commandupdate.h"
#include "commandsearch.h"
#include "commandlist.h"
#include "commandtag.h"
#include "databasefactory.h"
#include "logger.h"
#include "sandboxedprocessor.h"
@ -31,7 +32,7 @@
void printUsage(QString argv0)
{
qInfo() << "Usage:" << argv0 << "command";
qInfo() << "Valid commands: add, update, delete, search, list. Each command has a --help option.";
qInfo() << "Valid commands: add, update, search, delete, tag, list. Each command has a --help option.";
}
Command *commandFromName(QString name, SqliteDbService &dbService)
@ -56,6 +57,10 @@ Command *commandFromName(QString name, SqliteDbService &dbService)
{
return new CommandList(dbService);
}
if(name == "tag")
{
return new CommandTag(dbService);
}
return nullptr;
}

查看文件

@ -193,7 +193,7 @@ int main(int argc, char *argv[])
Logger::error() << error << Qt::endl;
QMessageBox::critical(nullptr, "Error during upgrade",
error);
qApp->quit();
exit(EXIT_FAILURE);
}
);

查看文件

@ -16,6 +16,9 @@
#include <QScreen>
#include <QProgressDialog>
#include <QDesktopWidget>
#include <QWidgetAction>
#include <QInputDialog>
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include "clicklabel.h"
@ -42,6 +45,9 @@ MainWindow::MainWindow(QWidget *parent, QString socketPath)
indexer = new Indexer(*(this->dbService));
indexer->setParent(this);
tagManager = new TagManager(*(this->dbService));
connectSignals();
ui->treeResultsList->setContextMenuPolicy(Qt::ContextMenuPolicy::CustomContextMenu);
ui->tabWidget->setCurrentIndex(0);
@ -287,6 +293,7 @@ void MainWindow::startIndexing()
if(this->indexer->isRunning())
{
ui->btnStartIndexing->setEnabled(false);
ui->btnStartIndexing->setText("Start indexing");
this->indexer->requestCancellation();
return;
@ -296,6 +303,8 @@ void MainWindow::startIndexing()
ui->resultsTab->setEnabled(false);
ui->settingsTab->setEnabled(false);
ui->txtPathScanAdd->setEnabled(false);
ui->btnAddPath->setEnabled(false);
ui->btnChoosePath->setEnabled(false);
ui->txtSearch->setEnabled(false);
ui->previewProcessBar->setValue(0);
ui->previewProcessBar->setVisible(true);
@ -343,6 +352,8 @@ void MainWindow::finishIndexing()
ui->resultsTab->setEnabled(true);
ui->settingsTab->setEnabled(true);
ui->txtPathScanAdd->setEnabled(true);
ui->btnAddPath->setEnabled(true);
ui->btnChoosePath->setEnabled(true);
ui->txtSearch->setEnabled(true);
if(result.erroredPaths > 0)
{
@ -877,6 +888,8 @@ void MainWindow::handleSearchResults(const QVector<SearchResult> &results)
makePreviews(1);
}
ui->tabWidget->setTabEnabled(1, previewDirty);
QString statusText = "Results: " + QString::number(results.size()) + " files";
statusText += ", previewable: " + QString::number(this->previewCoordinator.previewableCount());
if(hasDeleted)
@ -1006,6 +1019,65 @@ void MainWindow::createSearchResultMenu(QMenu &menu, const QFileInfo &fileInfo)
this->ui->comboPreviewFiles->setCurrentText(fileInfo.absoluteFilePath());
});
}
QMenu *tagMenu = menu.addMenu("Tag file with: ");
QVector<QString> allTags = this->dbService->getTags();
QHash<QString, bool> fileTags;
QString path = fileInfo.absoluteFilePath();
for(const QString &fileTag : this->dbService->getTagsForPath(path))
{
fileTags[fileTag] = true;
}
for(const QString &tag : allTags)
{
QCheckBox *checkBox = new QCheckBox(tagMenu);
QWidgetAction *checkableAction = new QWidgetAction(tagMenu);
checkableAction->setDefaultWidget(checkBox);
checkBox->setText(tag);
checkBox->setChecked(fileTags.contains(tag));
tagMenu->addAction(checkableAction);
connect(checkBox, &QCheckBox::stateChanged, this,
[this, checkBox, path]
{
QVector<QString> currentTags = this->dbService->getTagsForPath(path);
QString checkBoxText = checkBox->text();
if(checkBox->isChecked())
{
if(!this->tagManager->addTagsToPath(path, {checkBoxText}))
{
QMessageBox::critical(this, "Error while adding tag",
"An error occured while trying to add the tag");
}
}
else
{
if(!this->tagManager->removeTagsForPath(path, {checkBoxText}))
{
QMessageBox::critical(this, "Error while removing tag",
"An error occured while trying to remove the tag");
}
}
});
}
tagMenu->addAction("Add new tags", this,
[this, path]
{
bool ok;
QString text =
QInputDialog::getText(this, tr("Enter new tags"), tr("New tags (comma separated):"),
QLineEdit::Normal, "", &ok);
if(ok && !this->tagManager->addTagsToPath(path, text, ','))
{
QMessageBox::critical(this, "Error while trying to add tags",
"An error occured while trying to add tags");
}
});
}
void MainWindow::openDocument(QString path, int num)
@ -1060,6 +1132,7 @@ MainWindow::~MainWindow()
delete this->dbService;
delete this->dbFactory;
delete this->indexer;
delete this->tagManager;
delete ui;
}

查看文件

@ -14,6 +14,7 @@
#include "../shared/indexsyncer.h"
#include "previewcoordinator.h"
#include "indexer.h"
#include "tagmanager.h"
namespace Ui
{
class MainWindow;
@ -39,6 +40,9 @@ class MainWindow : public QMainWindow
QFutureWatcher<QVector<SearchResult>> searchWatcher;
LooqsQuery contentSearchQuery;
QVector<QString> searchHistory;
TagManager *tagManager;
int currentSearchHistoryIndex = 0;
QString currentSavedSearchText;
bool previewDirty = false;

查看文件

@ -246,17 +246,21 @@ QString PreviewGeneratorPlainText::generateLineBasedPreviewText(QTextStream &in,
totalWordCountMap[it->first] = totalWordCountMap.value(it->first, 0) + it->second;
}
}
if(isTruncated)
if(!resultText.isEmpty())
{
header += "(truncated) ";
}
for(QString &word : config.wordsToHighlight)
{
header += word + ": " + QString::number(totalWordCountMap[word]) + " ";
}
header += "<hr>";
if(isTruncated)
{
header += "(truncated) ";
}
for(QString &word : config.wordsToHighlight)
{
header += word + ": " + QString::number(totalWordCountMap[word]) + " ";
}
header += "<hr>";
return header + resultText;
resultText = header + resultText;
}
return resultText;
}
QSharedPointer<PreviewResult> PreviewGeneratorPlainText::generate(RenderConfig config, QString documentPath,

查看文件

@ -146,10 +146,7 @@ SaveFileResult FileSaver::saveFile(const QFileInfo &fileInfo)
if(mustFillContent)
{
auto filetype = this->dbService->queryFileType(fileInfo.absolutePath());
if(filetype)
{
mustFillContent = filetype.value() == 'c';
}
mustFillContent = !filetype.has_value() || filetype.value() == 'c';
}
}

查看文件

@ -21,11 +21,18 @@ void FileScanWorker::run()
{
sfr = saver.addFile(path);
}
catch(LooqsGeneralException &e)
{
Logger::error() << e.message << Qt::endl;
sfr = PROCESSFAIL;
}
catch(std::exception &e)
{
Logger::error() << e.what();
Logger::error() << e.what() << Qt::endl;
sfr = PROCESSFAIL; // well...
}
emit result({path, sfr});
if(stopToken->load(std::memory_order_relaxed)) // TODO: relaxed should suffice here, but recheck
{

查看文件

@ -111,6 +111,20 @@ void Indexer::dirScanProgress(int current, int total)
void Indexer::processFileScanResult(FileScanResult result)
{
/* TODO: OK_WASEMPTY might need a special list */
if(result.second == OK || result.second == OK_WASEMPTY)
{
++this->currentIndexResult.addedPaths;
}
else if(result.second == SKIPPED)
{
++this->currentIndexResult.skippedPaths;
}
else
{
++this->currentIndexResult.erroredPaths;
}
if(isErrorSaveFileResult(result.second))
{
this->currentIndexResult.results.append(result);
@ -129,20 +143,6 @@ void Indexer::processFileScanResult(FileScanResult result)
}
}
/* TODO: OK_WASEMPTY might need a special list */
if(result.second == OK || result.second == OK_WASEMPTY)
{
++this->currentIndexResult.addedPaths;
}
else if(result.second == SKIPPED)
{
++this->currentIndexResult.skippedPaths;
}
else
{
++this->currentIndexResult.erroredPaths;
}
QTime currentTime = QTime::currentTime();
if(currentScanProcessedCount++ == progressReportThreshold || this->lastProgressReportTime.secsTo(currentTime) >= 10)
{
@ -171,3 +171,8 @@ void Indexer::setFileSaverOptions(FileSaverOptions options)
{
this->fileSaverOptions = options;
}
void Indexer::setProgressReportThreshold(int threshold)
{
this->progressReportThreshold = threshold;
}

查看文件

@ -82,6 +82,8 @@ class Indexer : public QObject
void setFileSaverOptions(FileSaverOptions options);
void setProgressReportThreshold(int threshold);
void requestCancellation();
Indexer(SqliteDbService &db);

查看文件

@ -285,6 +285,10 @@ LooqsQuery LooqsQuery::build(QString expression, TokenType loneWordsTokenType, b
{
tokenType = FILTER_CONTENT_PAGE;
}
else if(filtername == "t" || filtername == "tag")
{
tokenType = FILTER_TAG_ASSIGNED;
}
// TODO: given this is not really a "filter", this feels slightly misplaced here
else if(filtername == "sort")
{

6
shared/migrations/5.sql Normal file
查看文件

@ -0,0 +1,6 @@
CREATE TABLE tag(id integer PRIMARY KEY, name varchar(128) UNIQUE);
CREATE TABLE filetag(fileid integer, tagid integer);
CREATE INDEX filetag_fileid ON filetag(fileid);
CREATE INDEX tag_id ON tag(id);
CREATE INDEX file_path ON file ( path );
UPDATE file SET filetype='c' WHERE filetype='f';

查看文件

@ -4,5 +4,6 @@
<file>2.sql</file>
<file>3.sql</file>
<file>4.sql</file>
<file>5.sql</file>
</qresource>
</RCC>

查看文件

@ -60,6 +60,7 @@ SOURCES += sqlitesearch.cpp \
processor.cpp \
sandboxedprocessor.cpp \
sqlitedbservice.cpp \
tagmanager.cpp \
tagstripperprocessor.cpp \
utils.cpp \
../submodules/exile.h/exile.c \
@ -93,6 +94,7 @@ HEADERS += sqlitesearch.h \
savefileresult.h \
searchresult.h \
sqlitedbservice.h \
tagmanager.h \
tagstripperprocessor.h \
token.h \
common.h \

查看文件

@ -2,6 +2,7 @@
#include <QFileInfo>
#include <QDateTime>
#include <QSqlError>
#include "looqsgeneralexception.h"
#include "sqlitedbservice.h"
#include "filedata.h"
#include "logger.h"
@ -15,7 +16,7 @@ QVector<SearchResult> SqliteDbService::search(const LooqsQuery &query)
std::optional<QChar> SqliteDbService::queryFileType(QString absPath)
{
auto query = exec("SELCET filetype FROM file WHERE path = ?", {absPath});
auto query = exec("SELECT filetype FROM file WHERE path = ?", {absPath});
if(!query.next())
{
return {};
@ -103,6 +104,117 @@ unsigned int SqliteDbService::getFiles(QVector<FileData> &results, QString wildC
return processedRows;
}
QVector<QString> SqliteDbService::getTags()
{
QVector<QString> result;
auto query = QSqlQuery(dbFactory->forCurrentThread());
query.prepare("SELECT name FROM tag ORDER by name ASC");
query.setForwardOnly(true);
if(!query.exec())
{
throw LooqsGeneralException("Error while trying to retrieve tags from database: " + query.lastError().text());
}
while(query.next())
{
QString tagname = query.value(0).toString();
result.append(tagname);
}
return result;
}
QVector<QString> SqliteDbService::getTagsForPath(QString path)
{
QVector<QString> result;
auto query = QSqlQuery(dbFactory->forCurrentThread());
query.prepare("SELECT name FROM tag INNER JOIN filetag ON tag.id = filetag.tagid INNER JOIN file ON filetag.fileid "
"= file.id WHERE file.path = ? ORDER BY name ASC");
query.addBindValue(path);
query.setForwardOnly(true);
if(!query.exec())
{
throw LooqsGeneralException("Error while trying to retrieve tags from database: " + query.lastError().text());
}
while(query.next())
{
QString tagname = query.value(0).toString();
result.append(tagname);
}
return result;
}
QVector<QString> SqliteDbService::getPathsForTag(QString tag)
{
QVector<QString> result;
auto query = QSqlQuery(dbFactory->forCurrentThread());
query.prepare(
"SELECT file.path FROM tag INNER JOIN filetag ON tag.id = filetag.tagid INNER JOIN file ON filetag.fileid "
"= file.id WHERE tag.name = ?");
query.addBindValue(tag.toLower());
query.setForwardOnly(true);
if(!query.exec())
{
throw LooqsGeneralException("Error while trying to retrieve paths from database: " + query.lastError().text());
}
while(query.next())
{
QString path = query.value(0).toString();
result.append(path);
}
return result;
}
bool SqliteDbService::setTags(QString path, const QSet<QString> &tags)
{
QSqlDatabase db = dbFactory->forCurrentThread();
if(!db.transaction())
{
Logger::error() << "Failed to open transaction for " << path << " : " << db.lastError() << Qt::endl;
return false;
}
QSqlQuery deletionQuery = QSqlQuery(db);
deletionQuery.prepare("DELETE FROM filetag WHERE fileid = (SELECT id FROM file WHERE path = ?)");
deletionQuery.addBindValue(path);
if(!deletionQuery.exec())
{
db.rollback();
Logger::error() << "Failed to delete existing tags " << deletionQuery.lastError() << Qt::endl;
return false;
}
for(const QString &tag : tags)
{
QSqlQuery tagQuery = QSqlQuery(db);
tagQuery.prepare("INSERT OR IGNORE INTO tag (name) VALUES(?)");
tagQuery.addBindValue(tag.toLower());
if(!tagQuery.exec())
{
db.rollback();
Logger::error() << "Failed to insert tag " << tagQuery.lastError() << Qt::endl;
return false;
}
QSqlQuery fileTagQuery(db);
fileTagQuery.prepare(
"INSERT INTO filetag(fileid, tagid) VALUES((SELECT id FROM file WHERE path = ?), (SELECT id "
"FROM tag WHERE name = ?))");
fileTagQuery.bindValue(0, path);
fileTagQuery.bindValue(1, tag);
if(!fileTagQuery.exec())
{
db.rollback();
Logger::error() << "Failed to assign tag to file" << Qt::endl;
return false;
}
}
if(!db.commit())
{
db.rollback();
Logger::error() << "Failed to commit transaction when saving tags" << Qt::endl;
return false;
}
return true;
}
bool SqliteDbService::insertToFTS(bool useTrigrams, QSqlDatabase &db, int fileid, QVector<PageData> &pageData)
{
QString ftsInsertStatement;
@ -151,7 +263,7 @@ QSqlQuery SqliteDbService::exec(QString querystr, std::initializer_list<QVariant
}
if(!query.exec())
{
throw LooqsGeneralException("Error while exec(): " + query.lastError().text());
throw LooqsGeneralException("Error while exec(): " + query.lastError().text() + " for query: " + querystr);
}
return query;
}
@ -233,3 +345,123 @@ SaveFileResult SqliteDbService::saveFile(QFileInfo fileInfo, QVector<PageData> &
}
return OK;
}
bool SqliteDbService::addTag(QString tag, QString path)
{
QVector<QString> paths;
paths.append(path);
return addTag(tag, paths);
}
bool SqliteDbService::addTag(QString tag, const QVector<QString> &paths)
{
QSqlDatabase db = dbFactory->forCurrentThread();
QSqlQuery tagQuery(db);
QSqlQuery fileTagQuery(db);
tag = tag.toLower();
tagQuery.prepare("INSERT OR IGNORE INTO tag (name) VALUES(?)");
tagQuery.addBindValue(tag);
fileTagQuery.prepare("INSERT INTO filetag(fileid, tagid) VALUES((SELECT id FROM file WHERE path = ?), (SELECT id "
"FROM tag WHERE name = ?))");
fileTagQuery.bindValue(1, tag);
if(!db.transaction())
{
Logger::error() << "Failed to open transaction to add paths for tag " << tag << " : " << db.lastError()
<< Qt::endl;
return false;
}
if(!tagQuery.exec())
{
db.rollback();
Logger::error() << "Failed INSERT query" << tagQuery.lastError() << Qt::endl;
return false;
}
for(const QString &path : paths)
{
fileTagQuery.bindValue(0, path);
if(!fileTagQuery.exec())
{
db.rollback();
Logger::error() << "Failed to add paths to tag" << Qt::endl;
return false;
}
}
if(!db.commit())
{
db.rollback();
Logger::error() << "Failed to commit tag insertion transaction" << db.lastError() << Qt::endl;
return false;
}
return true;
}
bool SqliteDbService::removePathsForTag(QString tag, const QVector<QString> &paths)
{
QSqlDatabase db = dbFactory->forCurrentThread();
QSqlQuery tagQuery(db);
QSqlQuery fileTagQuery(db);
tag = tag.toLower();
fileTagQuery.prepare(
"DELETE FROM filetag WHERE fileid = (SELECT id FROM file WHERE path = ?) AND tagid = (SELECT id "
"FROM tag WHERE name = ?)");
fileTagQuery.bindValue(1, tag);
for(const QString &path : paths)
{
fileTagQuery.bindValue(0, path);
if(!fileTagQuery.exec())
{
Logger::error() << "An error occured while trying to remove paths from tag assignment" << Qt::endl;
return false;
}
}
return true;
}
bool SqliteDbService::deleteTag(QString tag)
{
QSqlDatabase db = dbFactory->forCurrentThread();
if(!db.transaction())
{
Logger::error() << "Failed to open transaction while trying to delete tag " << tag << " : " << db.lastError()
<< Qt::endl;
return false;
}
tag = tag.toLower();
QSqlQuery assignmentDeleteQuery(db);
assignmentDeleteQuery.prepare("DELETE FROM filetag WHERE tagid = (SELECT id FROM tag WHERE name = ?)");
assignmentDeleteQuery.addBindValue(tag);
if(!assignmentDeleteQuery.exec())
{
db.rollback();
Logger::error() << "Error while trying to delete tag: " << db.lastError() << Qt::endl;
return false;
}
QSqlQuery deleteTagQuery(db);
deleteTagQuery.prepare("DELETE FROM tag WHERE name = ?");
deleteTagQuery.addBindValue(tag);
if(!deleteTagQuery.exec())
{
db.rollback();
Logger::error() << "Error while trying to delete tag: " << db.lastError() << Qt::endl;
return false;
}
if(!db.commit())
{
db.rollback();
Logger::error() << "Error while trying to delete tag: " << db.lastError() << Qt::endl;
return false;
}
return true;
}

查看文件

@ -23,11 +23,22 @@ class SqliteDbService
public:
SqliteDbService(DatabaseFactory &dbFactory);
SaveFileResult saveFile(QFileInfo fileInfo, QVector<PageData> &pageData, bool pathsOnly);
unsigned int getFiles(QVector<FileData> &results, QString wildCardPattern, int offset, int limit);
bool deleteFile(QString path);
bool fileExistsInDatabase(QString path);
bool fileExistsInDatabase(QString path, qint64 mtime);
bool fileExistsInDatabase(QString path, qint64 mtime, QChar filetype);
unsigned int getFiles(QVector<FileData> &results, QString wildCardPattern, int offset, int limit);
bool addTag(QString tag, QString path);
bool addTag(QString tag, const QVector<QString> &paths);
QVector<QString> getTags();
QVector<QString> getTagsForPath(QString path);
QVector<QString> getPathsForTag(QString path);
bool setTags(QString path, const QSet<QString> &tags);
bool removePathsForTag(QString tag, const QVector<QString> &paths);
bool deleteTag(QString tag);
QVector<SearchResult> search(const LooqsQuery &query);
std::optional<QChar> queryFileType(QString absPath);

查看文件

@ -143,6 +143,11 @@ QPair<QString, QVector<QString>> SqliteSearch::createSql(const Token &token)
{
return {" fts MATCH ? ", {escapeFtsArgument(value)}};
}
if(token.type == FILTER_TAG_ASSIGNED)
{
return {" file.id IN (SELECT fileid FROM filetag WHERE tagid = (SELECT id FROM tag WHERE name = ?)) ",
{value.toLower()}};
}
throw LooqsGeneralException("Unknown token passed (should not happen)");
}
@ -179,18 +184,18 @@ QSqlQuery SqliteSearch::makeSqlQuery(const LooqsQuery &query)
}
QString whereSqlTrigram = whereSql;
whereSqlTrigram.replace("fts MATCH", "fts_trigram MATCH"); // A bit dirty...
prepSql =
"SELECT DISTINCT path, page, mtime, size, filetype FROM ("
"SELECT file.path AS path, content.page AS page, file.mtime AS mtime, file.size AS size, "
"file.filetype AS filetype, 0 AS prio, fts.rank AS rank FROM file INNER JOIN content ON file.id = "
"content.fileid "
"INNER JOIN fts ON content.ftsid = fts.ROWID WHERE 1=1 AND " +
whereSql +
"UNION ALL SELECT file.path AS path, content.page AS page, file.mtime AS mtime, file.size AS size, "
"file.filetype AS filetype, 1 as prio, fts_trigram.rank AS rank FROM file INNER JOIN content ON file.id = "
"content.fileid " +
"INNER JOIN fts_trigram ON content.fts_trigramid = fts_trigram.ROWID WHERE 1=1 AND " + whereSqlTrigram +
" ) " + sortSql;
prepSql = "SELECT DISTINCT path, page, mtime, size, filetype FROM ("
"SELECT file.path AS path, content.page AS page, file.mtime AS mtime, file.size AS size, "
"file.filetype AS filetype, 0 AS prio, fts.rank AS rank FROM file INNER JOIN content ON file.id = "
"content.fileid "
"INNER JOIN fts ON content.ftsid = fts.ROWID WHERE 1=1 AND " +
whereSql +
"UNION ALL SELECT file.path AS path, content.page AS page, file.mtime AS mtime, file.size AS size, "
"file.filetype AS filetype, 1 as prio, fts_trigram.rank AS rank FROM file INNER JOIN content ON "
"file.id = "
"content.fileid " +
"INNER JOIN fts_trigram ON content.fts_trigramid = fts_trigram.ROWID WHERE 1=1 AND " +
whereSqlTrigram + " ) " + sortSql;
++bindIterations;
}
else

66
shared/tagmanager.cpp Normal file
查看文件

@ -0,0 +1,66 @@
#include "tagmanager.h"
TagManager::TagManager(SqliteDbService &dbService)
{
this->dbService = &dbService;
}
bool TagManager::addTagsToPath(QString path, const QSet<QString> &tags)
{
QVector<QString> currentTags = this->dbService->getTagsForPath(path);
for(const QString &tag : tags)
{
currentTags.append(tag.toLower());
}
QSet<QString> newTags{currentTags.begin(), currentTags.end()};
return this->dbService->setTags(path, newTags);
}
bool TagManager::removeTagsForPath(QString path, const QSet<QString> &tags)
{
QVector<QString> currentTags = this->dbService->getTagsForPath(path);
for(const QString &tag : tags)
{
currentTags.removeAll(tag);
}
QSet<QString> newTags{currentTags.begin(), currentTags.end()};
return this->dbService->setTags(path, newTags);
}
bool TagManager::removePathsForTag(QString tag, const QVector<QString> &paths)
{
return this->dbService->removePathsForTag(tag, paths);
}
bool TagManager::deleteTag(QString tag)
{
return this->dbService->deleteTag(tag);
}
QVector<QString> TagManager::getTags(QString path)
{
return this->dbService->getTagsForPath(path);
}
QVector<QString> TagManager::getTags()
{
return this->dbService->getTags();
}
QVector<QString> TagManager::getPaths(QString tag)
{
return this->dbService->getPathsForTag(tag);
}
bool TagManager::addTagsToPath(QString path, QString tagstring, QChar delim)
{
auto splitted = tagstring.split(delim);
return addTagsToPath(path, QSet<QString>{splitted.begin(), splitted.end()});
}
bool TagManager::addPathsToTag(QString tag, const QVector<QString> &paths)
{
return this->dbService->addTag(tag, paths);
}

28
shared/tagmanager.h Normal file
查看文件

@ -0,0 +1,28 @@
#ifndef TAGMANAGER_H
#define TAGMANAGER_H
#include "sqlitedbservice.h"
class TagManager
{
private:
SqliteDbService *dbService = nullptr;
bool ensurePathOkay(QString inpath);
public:
TagManager(SqliteDbService &dbService);
bool addTagsToPath(QString path, const QSet<QString> &tags);
bool addTagsToPath(QString path, QString tagstring, QChar delim);
bool addPathsToTag(QString tag, const QVector<QString> &paths);
bool removeTagsForPath(QString path, const QSet<QString> &tags);
bool removePathsForTag(QString tag, const QVector<QString> &paths);
bool deleteTag(QString tag);
QVector<QString> getTags(QString path);
QVector<QString> getTags();
QVector<QString> getPaths(QString tag);
};
#endif // TAGMANAGER_H

查看文件

@ -19,7 +19,8 @@ enum TokenType
FILTER_PATH_SIZE,
FILTER_PATH_ENDS,
FILTER_PATH_STARTS,
FILTER_CONTENT = 512,
FILTER_TAG_ASSIGNED,
FILTER_CONTENT = 512, /* Everything below here is content search (except LIMIT) */
FILTER_CONTENT_CONTAINS,
FILTER_CONTENT_PAGE,
LIMIT = 1024