24 Commits

Author SHA1 Message Date
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
2550af307f gui: mainwindow: Default to index everything 2023-03-26 15:36:23 +02:00
30 changed files with 796 additions and 101 deletions

View File

@ -1,5 +1,24 @@
# looqs: Release notes # 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 ## 2022-11-19 - v0.8.1
CHANGES: CHANGES:

View File

@ -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. 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 ## 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. 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 init
git submodule update 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 qmake
make make
``` ```
@ -97,7 +97,9 @@ The GUI is located in `gui/looqs-gui`, the binary for the CLI is in `cli/looqs`
## Packages ## Packages
At this point, looqs is not in any official distro package repo, but I maintain some 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. Latest release can be installed using apt from the repo.
``` ```
# First, obtain key, assume it's trusted. # 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 update
sudo apt-get install looqs sudo apt-get install looqs
``` ```
### Gentoo (EXPERIMENTAL)
Available in this overlay: https://github.com/quitesimpleorg/quitesimple-overlay
### Prebuilt tarball (distro-agnostic) (EXPERIMENTAL) ### Prebuilt tarball (distro-agnostic) (EXPERIMENTAL)
looqs is also distributed as a tarball containing prebuilt binaries and its library dependencies. The tarball is 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 ### 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 ### Signature verification

View File

@ -165,6 +165,8 @@ A number of search filters are available.
| path.begins:(term) | pb:(term) | Filters path beginning with the specified term | | path.begins:(term) | pb:(term) | Filters path beginning with the specified term |
| contains:(terms) | c:(terms) | Full-text search, also understands quotes | | 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 | | 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 "!". 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. 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'| |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.| |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:("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")|

View File

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

View File

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

View File

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

View File

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

View File

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

153
cli/commandtag.cpp Normal file
View 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
View 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

View File

@ -21,6 +21,7 @@
#include "commandupdate.h" #include "commandupdate.h"
#include "commandsearch.h" #include "commandsearch.h"
#include "commandlist.h" #include "commandlist.h"
#include "commandtag.h"
#include "databasefactory.h" #include "databasefactory.h"
#include "logger.h" #include "logger.h"
#include "sandboxedprocessor.h" #include "sandboxedprocessor.h"
@ -31,7 +32,7 @@
void printUsage(QString argv0) void printUsage(QString argv0)
{ {
qInfo() << "Usage:" << argv0 << "command"; 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) Command *commandFromName(QString name, SqliteDbService &dbService)
@ -56,6 +57,10 @@ Command *commandFromName(QString name, SqliteDbService &dbService)
{ {
return new CommandList(dbService); return new CommandList(dbService);
} }
if(name == "tag")
{
return new CommandTag(dbService);
}
return nullptr; return nullptr;
} }

View File

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

View File

@ -16,6 +16,9 @@
#include <QScreen> #include <QScreen>
#include <QProgressDialog> #include <QProgressDialog>
#include <QDesktopWidget> #include <QDesktopWidget>
#include <QWidgetAction>
#include <QInputDialog>
#include "mainwindow.h" #include "mainwindow.h"
#include "ui_mainwindow.h" #include "ui_mainwindow.h"
#include "clicklabel.h" #include "clicklabel.h"
@ -42,6 +45,9 @@ MainWindow::MainWindow(QWidget *parent, QString socketPath)
indexer = new Indexer(*(this->dbService)); indexer = new Indexer(*(this->dbService));
indexer->setParent(this); indexer->setParent(this);
tagManager = new TagManager(*(this->dbService));
connectSignals(); connectSignals();
ui->treeResultsList->setContextMenuPolicy(Qt::ContextMenuPolicy::CustomContextMenu); ui->treeResultsList->setContextMenuPolicy(Qt::ContextMenuPolicy::CustomContextMenu);
ui->tabWidget->setCurrentIndex(0); ui->tabWidget->setCurrentIndex(0);
@ -287,6 +293,7 @@ void MainWindow::startIndexing()
if(this->indexer->isRunning()) if(this->indexer->isRunning())
{ {
ui->btnStartIndexing->setEnabled(false); ui->btnStartIndexing->setEnabled(false);
ui->btnStartIndexing->setText("Start indexing"); ui->btnStartIndexing->setText("Start indexing");
this->indexer->requestCancellation(); this->indexer->requestCancellation();
return; return;
@ -296,6 +303,8 @@ void MainWindow::startIndexing()
ui->resultsTab->setEnabled(false); ui->resultsTab->setEnabled(false);
ui->settingsTab->setEnabled(false); ui->settingsTab->setEnabled(false);
ui->txtPathScanAdd->setEnabled(false); ui->txtPathScanAdd->setEnabled(false);
ui->btnAddPath->setEnabled(false);
ui->btnChoosePath->setEnabled(false);
ui->txtSearch->setEnabled(false); ui->txtSearch->setEnabled(false);
ui->previewProcessBar->setValue(0); ui->previewProcessBar->setValue(0);
ui->previewProcessBar->setVisible(true); ui->previewProcessBar->setVisible(true);
@ -343,6 +352,8 @@ void MainWindow::finishIndexing()
ui->resultsTab->setEnabled(true); ui->resultsTab->setEnabled(true);
ui->settingsTab->setEnabled(true); ui->settingsTab->setEnabled(true);
ui->txtPathScanAdd->setEnabled(true); ui->txtPathScanAdd->setEnabled(true);
ui->btnAddPath->setEnabled(true);
ui->btnChoosePath->setEnabled(true);
ui->txtSearch->setEnabled(true); ui->txtSearch->setEnabled(true);
if(result.erroredPaths > 0) if(result.erroredPaths > 0)
{ {
@ -877,6 +888,8 @@ void MainWindow::handleSearchResults(const QVector<SearchResult> &results)
makePreviews(1); makePreviews(1);
} }
ui->tabWidget->setTabEnabled(1, previewDirty);
QString statusText = "Results: " + QString::number(results.size()) + " files"; QString statusText = "Results: " + QString::number(results.size()) + " files";
statusText += ", previewable: " + QString::number(this->previewCoordinator.previewableCount()); statusText += ", previewable: " + QString::number(this->previewCoordinator.previewableCount());
if(hasDeleted) if(hasDeleted)
@ -1006,6 +1019,65 @@ void MainWindow::createSearchResultMenu(QMenu &menu, const QFileInfo &fileInfo)
this->ui->comboPreviewFiles->setCurrentText(fileInfo.absoluteFilePath()); 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) void MainWindow::openDocument(QString path, int num)
@ -1060,6 +1132,7 @@ MainWindow::~MainWindow()
delete this->dbService; delete this->dbService;
delete this->dbFactory; delete this->dbFactory;
delete this->indexer; delete this->indexer;
delete this->tagManager;
delete ui; delete ui;
} }

View File

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

View File

@ -421,16 +421,22 @@
<property name="text"> <property name="text">
<string>Index everything (metadata + file content)</string> <string>Index everything (metadata + file content)</string>
</property> </property>
<property name="checked">
<bool>true</bool>
</property>
</widget> </widget>
</item> </item>
<item> <item>
<widget class="QCheckBox" name="chkFillContentForContentless"> <widget class="QCheckBox" name="chkFillContentForContentless">
<property name="enabled"> <property name="enabled">
<bool>false</bool> <bool>true</bool>
</property> </property>
<property name="text"> <property name="text">
<string>Index content for files previously indexed without content</string> <string>Index content for files previously indexed without content</string>
</property> </property>
<property name="checked">
<bool>false</bool>
</property>
</widget> </widget>
</item> </item>
<item> <item>

View File

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

View File

@ -25,10 +25,22 @@ SaveFileResult FileSaver::addFile(QString path)
QString absPath = info.absoluteFilePath(); QString absPath = info.absoluteFilePath();
auto mtime = info.lastModified().toSecsSinceEpoch(); auto mtime = info.lastModified().toSecsSinceEpoch();
if(this->dbService->fileExistsInDatabase(absPath, mtime))
bool exists = false;
if(this->fileSaverOptions.fillExistingContentless)
{
exists = this->dbService->fileExistsInDatabase(absPath, mtime, 'c');
}
else
{
exists = this->dbService->fileExistsInDatabase(absPath, mtime);
}
if(exists)
{ {
return SKIPPED; return SKIPPED;
} }
return saveFile(info); return saveFile(info);
} }
@ -134,10 +146,7 @@ SaveFileResult FileSaver::saveFile(const QFileInfo &fileInfo)
if(mustFillContent) if(mustFillContent)
{ {
auto filetype = this->dbService->queryFileType(fileInfo.absolutePath()); auto filetype = this->dbService->queryFileType(fileInfo.absolutePath());
if(filetype) mustFillContent = !filetype.has_value() || filetype.value() == 'c';
{
mustFillContent = filetype.value() == 'c';
}
} }
} }

View File

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

View File

@ -111,6 +111,20 @@ void Indexer::dirScanProgress(int current, int total)
void Indexer::processFileScanResult(FileScanResult result) 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)) if(isErrorSaveFileResult(result.second))
{ {
this->currentIndexResult.results.append(result); 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(); QTime currentTime = QTime::currentTime();
if(currentScanProcessedCount++ == progressReportThreshold || this->lastProgressReportTime.secsTo(currentTime) >= 10) if(currentScanProcessedCount++ == progressReportThreshold || this->lastProgressReportTime.secsTo(currentTime) >= 10)
{ {
@ -171,3 +171,8 @@ void Indexer::setFileSaverOptions(FileSaverOptions options)
{ {
this->fileSaverOptions = options; this->fileSaverOptions = options;
} }
void Indexer::setProgressReportThreshold(int threshold)
{
this->progressReportThreshold = threshold;
}

View File

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

View File

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

6
shared/migrations/5.sql Normal file
View 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';

View File

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

View File

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

View File

@ -2,25 +2,10 @@
#include <QFileInfo> #include <QFileInfo>
#include <QDateTime> #include <QDateTime>
#include <QSqlError> #include <QSqlError>
#include "looqsgeneralexception.h"
#include "sqlitedbservice.h" #include "sqlitedbservice.h"
#include "filedata.h" #include "filedata.h"
#include "logger.h" #include "logger.h"
bool SqliteDbService::fileExistsInDatabase(QString path, qint64 mtime)
{
auto query = QSqlQuery(dbFactory->forCurrentThread());
query.prepare("SELECT 1 FROM file WHERE path = ? and mtime = ?");
query.addBindValue(path);
query.addBindValue(mtime);
if(!query.exec())
{
throw LooqsGeneralException("Error while trying to query for file existance: " + query.lastError().text());
}
if(!query.next())
{
return false;
}
return query.value(0).toBool();
}
QVector<SearchResult> SqliteDbService::search(const LooqsQuery &query) QVector<SearchResult> SqliteDbService::search(const LooqsQuery &query)
{ {
@ -31,13 +16,7 @@ QVector<SearchResult> SqliteDbService::search(const LooqsQuery &query)
std::optional<QChar> SqliteDbService::queryFileType(QString absPath) std::optional<QChar> SqliteDbService::queryFileType(QString absPath)
{ {
auto query = QSqlQuery(dbFactory->forCurrentThread()); auto query = exec("SELECT filetype FROM file WHERE path = ?", {absPath});
query.prepare("SELECT filetype FROM file WHERE path = ?");
query.addBindValue(absPath);
if(!query.exec())
{
throw LooqsGeneralException("Error while trying to query for file type: " + query.lastError().text());
}
if(!query.next()) if(!query.next())
{ {
return {}; return {};
@ -47,18 +26,17 @@ std::optional<QChar> SqliteDbService::queryFileType(QString absPath)
bool SqliteDbService::fileExistsInDatabase(QString path) bool SqliteDbService::fileExistsInDatabase(QString path)
{ {
auto query = QSqlQuery(dbFactory->forCurrentThread()); return execBool("SELECT 1 FROM file WHERE path = ?", {path});
query.prepare("SELECT 1 FROM file WHERE path = ?"); }
query.addBindValue(path);
if(!query.exec()) bool SqliteDbService::fileExistsInDatabase(QString path, qint64 mtime)
{ {
throw LooqsGeneralException("Error while trying to query for file existance: " + query.lastError().text()); return execBool("SELECT 1 FROM file WHERE path = ? AND mtime = ?", {path, mtime});
} }
if(!query.next())
{ bool SqliteDbService::fileExistsInDatabase(QString path, qint64 mtime, QChar fileType)
return false; {
} return execBool("SELECT 1 FROM file WHERE path = ? AND mtime = ? AND filetype = ?", {path, mtime, fileType});
return query.value(0).toBool();
} }
SqliteDbService::SqliteDbService(DatabaseFactory &dbFactory) SqliteDbService::SqliteDbService(DatabaseFactory &dbFactory)
@ -126,6 +104,117 @@ unsigned int SqliteDbService::getFiles(QVector<FileData> &results, QString wildC
return processedRows; 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) bool SqliteDbService::insertToFTS(bool useTrigrams, QSqlDatabase &db, int fileid, QVector<PageData> &pageData)
{ {
QString ftsInsertStatement; QString ftsInsertStatement;
@ -164,6 +253,31 @@ bool SqliteDbService::insertToFTS(bool useTrigrams, QSqlDatabase &db, int fileid
return true; return true;
} }
QSqlQuery SqliteDbService::exec(QString querystr, std::initializer_list<QVariant> args)
{
auto query = QSqlQuery(dbFactory->forCurrentThread());
query.prepare(querystr);
for(const QVariant &v : args)
{
query.addBindValue(v);
}
if(!query.exec())
{
throw LooqsGeneralException("Error while exec(): " + query.lastError().text() + " for query: " + querystr);
}
return query;
}
bool SqliteDbService::execBool(QString querystr, std::initializer_list<QVariant> args)
{
auto query = exec(querystr, args);
if(!query.next())
{
return false;
}
return query.value(0).toBool();
}
SaveFileResult SqliteDbService::saveFile(QFileInfo fileInfo, QVector<PageData> &pageData, bool pathsOnly) SaveFileResult SqliteDbService::saveFile(QFileInfo fileInfo, QVector<PageData> &pageData, bool pathsOnly)
{ {
QString absPath = fileInfo.absoluteFilePath(); QString absPath = fileInfo.absoluteFilePath();
@ -231,3 +345,123 @@ SaveFileResult SqliteDbService::saveFile(QFileInfo fileInfo, QVector<PageData> &
} }
return OK; 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;
}

View File

@ -17,13 +17,28 @@ class SqliteDbService
DatabaseFactory *dbFactory = nullptr; DatabaseFactory *dbFactory = nullptr;
bool insertToFTS(bool useTrigrams, QSqlDatabase &db, int fileid, QVector<PageData> &pageData); bool insertToFTS(bool useTrigrams, QSqlDatabase &db, int fileid, QVector<PageData> &pageData);
QSqlQuery exec(QString query, std::initializer_list<QVariant> args);
bool execBool(QString querystr, std::initializer_list<QVariant> args);
public: public:
SqliteDbService(DatabaseFactory &dbFactory); SqliteDbService(DatabaseFactory &dbFactory);
SaveFileResult saveFile(QFileInfo fileInfo, QVector<PageData> &pageData, bool pathsOnly); 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 deleteFile(QString path);
bool fileExistsInDatabase(QString path); bool fileExistsInDatabase(QString path);
bool fileExistsInDatabase(QString path, qint64 mtime); 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); QVector<SearchResult> search(const LooqsQuery &query);
std::optional<QChar> queryFileType(QString absPath); std::optional<QChar> queryFileType(QString absPath);

View File

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

66
shared/tagmanager.cpp Normal file
View 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
View 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

View File

@ -19,7 +19,8 @@ enum TokenType
FILTER_PATH_SIZE, FILTER_PATH_SIZE,
FILTER_PATH_ENDS, FILTER_PATH_ENDS,
FILTER_PATH_STARTS, 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_CONTAINS,
FILTER_CONTENT_PAGE, FILTER_CONTENT_PAGE,
LIMIT = 1024 LIMIT = 1024