Show relevant ignore pattern for items in file browser

This commit is contained in:
Martchus 2024-05-23 00:54:09 +02:00
parent fc47cea9e5
commit 3ffe62b289
9 changed files with 667 additions and 2 deletions

View File

@ -19,6 +19,7 @@ set(HEADER_FILES
syncthingconnectionsettings.h
syncthingnotifier.h
syncthingconfig.h
syncthingignorepattern.h
syncthingprocess.h
syncthingservice.h
qstringhash.h
@ -31,6 +32,7 @@ set(SRC_FILES
syncthingconnectionsettings.cpp
syncthingnotifier.cpp
syncthingconfig.cpp
syncthingignorepattern.cpp
syncthingprocess.cpp
syncthingservice.cpp
utils.cpp)

View File

@ -59,6 +59,11 @@ enum class SyncthingItemType {
};
struct LIB_SYNCTHING_CONNECTOR_EXPORT SyncthingItem {
/// \brief The matching ignore pattern was not initialized for this item.
static constexpr auto ignorePatternNotInitialized = std::numeric_limits<std::size_t>::max();
/// \brief The item did not match any of the current ignore patterns.
static constexpr auto ignorePatternNoMatch = ignorePatternNotInitialized - 1;
/// \brief The name of the filesystem item or error/loading message in case of those item types.
QString name;
/// \brief The modification time. Only populated with a meaningful value for files and directories.
@ -75,6 +80,9 @@ struct LIB_SYNCTHING_CONNECTOR_EXPORT SyncthingItem {
QString path;
/// \brief The index of the item within its parent.
std::size_t index = std::size_t();
/// \brief The index of the ignore pattern (in the current list of ignore patterns) this item matches.
/// \remarks Not populated by default.
std::size_t ignorePattern = ignorePatternNotInitialized;
/// \brief The level of nesting, does *not* include levels of the prefix.
int level = 0;
/// \brief Whether children are populated (depends on the requested level).

View File

@ -0,0 +1,408 @@
#include "./syncthingignorepattern.h"
namespace Data {
/// \cond
namespace SyncthingIgnorePatternState {
enum State {
Escaping, // passed the esacping character "\"
AppendRangeLower, // passed a "[" marking the start of a character range
AppendRangeUpper, // passed a "-" within a range marking the start of an upper-bound range character
AppendAlternative, // passed a "{" marking the start of an alternative set
MatchVerbatimly, // initial/default state
MatchRange, // passed a "]" marking the end of a character range
MatchAlternatives, // passed a "}" marking the end of an alternative set
MatchAny, // passed the "?" character that allows matching any character but the path separator
MatchManyAny, // passed the "*" character that allows matching many arbitrary characters except the path separator
MatchManyAnyIncludingDirSep, // passed a sequence of two "*" characters allowing to match also the path separator
};
struct Asterisk {
QString::const_iterator pos;
SyncthingIgnorePatternState::State state;
bool visited = false;
};
struct CharacterRange {
QChar lowerBound, upperBound;
};
struct AlternativeRange {
explicit AlternativeRange(QString::const_iterator beg)
: beg(beg)
, end(beg)
{
}
QString::const_iterator beg, end;
};
} // namespace SyncthingIgnorePatternState
/// \endcond
/*!
* \brief Parses the specified \a pattern populating the struct's fields.
*/
SyncthingIgnorePattern::SyncthingIgnorePattern(QString &&pattern)
: pattern(std::move(pattern))
{
if (glob.startsWith(QLatin1String("//"))) {
comment = true;
ignore = false;
return;
}
glob = this->pattern;
for (;;) {
if (glob.startsWith(QLatin1String("!"))) {
ignore = !ignore;
glob.remove(0, 1);
} else if (glob.startsWith(QLatin1String("(?i)"))) {
caseInsensitive = true;
glob.remove(0, 4);
} else if (glob.startsWith(QLatin1String("(?d)"))) {
allowRemovalOnParentDirRemoval = true;
glob.remove(0, 4);
} else {
break;
}
}
}
/*!
* \brief Moves the ignore pattern.
*/
SyncthingIgnorePattern::SyncthingIgnorePattern(SyncthingIgnorePattern &&) = default;
/*!
* \brief Destroys the ignore pattern.
*/
SyncthingIgnorePattern::~SyncthingIgnorePattern()
{
}
/*!
* \brief Matches the assigned glob against the specified \a path.
* \remarks
* - Returns always false if the pattern is flagged as comment or the glob is empty.
* - This function tries to follow rules outlined on https://docs.syncthing.net/users/ignoring.html.
* - The specified \a path is *not* supposed to start with a "/". It must always be a path relative to the root of the
* Syncthing folder it is contained by. A pattern that is only supposed to match from the root of the Syncthing folder
* is supposed to start with a "/", though.
* - This function probably doesn't work if the pattern or \a path contain a surrogate pair.
*/
bool SyncthingIgnorePattern::matches(const QString &path) const
{
if (comment || glob.isEmpty()) {
return false;
}
// get iterators
auto globIter = glob.begin(), globEnd = glob.end();
auto pathIter = path.begin(), pathEnd = path.end();
// handle pattners starting with "/" indicating the pattern must match from the root (see last remark in docstring)
static constexpr auto pathSep = QChar('/');
const auto matchFromRoot = *globIter == pathSep;
if (matchFromRoot) {
++globIter;
}
// define variables to track the state when processing the glob pattern
using namespace SyncthingIgnorePatternState;
auto state = MatchVerbatimly;
auto escapedState = MatchVerbatimly;
auto inAsterisk = false;
asterisks.clear();
// define behavior to handle the current character in the glob pattern not matching the current pattern in the path
const auto handleMismatch = [&, this] {
// fail the match immediately if the glob pattern started with a "/" indicating it is supposed to match only from the root
if (matchFromRoot) {
return false;
}
// deal with the mismatch by trying to match previous asterisks more greedily
while (!asterisks.empty() && asterisks.back().visited) {
// do not consider asterisks we have already visited, though (as it would lead to an endless loop)
asterisks.pop_back();
}
if (!asterisks.empty()) {
// rewind back to when we have passed the last non-visited asterisk
auto &asterisk = asterisks.back();
globIter = asterisk.pos;
inAsterisk = asterisk.visited = true;
state = asterisk.state;
return true;
}
// deal with the mismatch by checking the path as of the next path element
for (; pathIter != pathEnd; ++pathIter) {
// forward to the next path separator
if (*pathIter != pathSep) {
continue;
}
// skip the path separator itself and give up when the end of the path is reached
if (++pathIter == pathEnd) {
return false;
}
break;
}
// give up when the end of the path is reached
if (pathIter == pathEnd) {
return false;
}
// start matching the glob pattern from the beginning
globIter = glob.begin();
asterisks.clear();
inAsterisk = false;
return true;
};
// define function to handle single match
const auto handleSingleMatch = [&, this] {
// proceed with the next characters on a match
++globIter;
inAsterisk = false;
if (!asterisks.empty()) {
asterisks.back().visited = false;
}
};
// define function to match single character against the current character in the path
const auto matchSingleChar
= [&, this](QChar singleChar) { return caseInsensitive ? pathIter->toCaseFolded() == singleChar.toCaseFolded() : *pathIter == singleChar; };
// try to match each character of the glob against a character in the path
match:
while (globIter != globEnd) {
// decide what to do next depending on the current glob pattern character and state transitioning the state accordingly
switch (state) {
case Escaping:
// treat every character as-is in "escaping" state
state = escapedState;
break;
default:
// transition state according to special meaning of the current glob pattern character
switch (globIter->unicode()) {
case '\\':
// transition into "escaping" state
escapedState = state;
state = Escaping;
break;
case '[':
state = AppendRangeLower;
characterRange.clear();
++globIter;
continue;
case ']':
switch (state) {
case AppendRangeLower:
case AppendRangeUpper:
state = MatchRange;
break;
default:
state = MatchVerbatimly;
}
break;
case '-':
switch (state) {
case AppendRangeLower:
state = AppendRangeUpper;
++globIter;
continue;
default:
state = MatchVerbatimly;
}
break;
case '{':
switch (state) {
case AppendAlternative:
continue;
default:
state = AppendAlternative;
alternatives.clear();
alternatives.emplace_back(++globIter);
}
continue;
case '}':
switch (state) {
case AppendAlternative:
alternatives.back().end = globIter;
state = MatchAlternatives;
break;
default:
state = MatchVerbatimly;
}
break;
case ',':
switch (state) {
case AppendAlternative:
alternatives.back().end = globIter;
alternatives.emplace_back(++globIter);
continue;
default:
state = MatchVerbatimly;
}
break;
case '?':
// transition into "match any" state
state = MatchAny;
break;
case '*':
// transition into one of the "match many any" state (depending on current state)
switch (state) {
case MatchManyAny:
state = MatchManyAnyIncludingDirSep;
break;
default:
state = MatchManyAny;
}
break;
default:
// try to match/append all other non-special characters as-is
switch (state) {
case AppendRangeLower:
case AppendRangeUpper:
case AppendAlternative:
break;
default:
state = MatchVerbatimly;
}
}
}
// proceed according to state
switch (state) {
case Escaping:
// proceed with the next character in the glob pattern which will be matched as-is (even if it is special)
[[fallthrough]];
case AppendAlternative:
// just move on to the next character (alternatives are populated completely in the previous switch-case)
++globIter;
break;
case AppendRangeLower:
// add the current character in the glob pattern as start of a new range
characterRange.emplace_back().lowerBound = *globIter++;
break;
case AppendRangeUpper:
// add the current character in the glob pattern as end of a new or the current range
(characterRange.empty() ? characterRange.emplace_back() : characterRange.back()).upperBound = *globIter++;
state = AppendRangeLower;
break;
case MatchVerbatimly:
// match the current character in the glob pattern verbatimly against the current character in the path
if (pathIter != pathEnd && matchSingleChar(*globIter)) {
++pathIter;
handleSingleMatch();
} else if (inAsterisk && (asterisks.back().state == MatchManyAnyIncludingDirSep || (pathIter == pathEnd || *pathIter != pathSep))) {
// consider the path character dealt with despite no match if we have just passed an asterisk in the glob pattern
if (pathIter != pathEnd) {
++pathIter;
} else {
inAsterisk = false;
}
} else if (!handleMismatch()) {
return false;
}
break;
case MatchRange:
// match the concluded character range in the glob pattern against the current character in the path
if (pathIter != pathEnd) {
auto inRange = false;
for (const auto &bounds : characterRange) {
if ((!bounds.upperBound.isNull() && *pathIter >= bounds.lowerBound && *pathIter <= bounds.upperBound)
|| (bounds.upperBound.isNull() && matchSingleChar(bounds.lowerBound))) {
inRange = true;
break;
}
}
if (inRange) {
characterRange.clear();
state = MatchVerbatimly;
++pathIter;
handleSingleMatch();
break;
}
}
if (!handleMismatch()) {
return false;
}
break;
case MatchAlternatives:
// match the current alternatives as of the current character in the path
if (pathIter != pathEnd) {
const auto pathStart = pathIter;
for (auto &alternative : alternatives) {
// match characters in the alternative against the path
// note: Special characters like "*" are matched verbatimly. Is that the correct behavior?
pathIter = pathStart;
for (; alternative.beg != alternative.end && pathIter != pathEnd; ++alternative.beg) {
if (*alternative.beg == QChar('\\')) {
continue;
}
if (!matchSingleChar(*alternative.beg)) {
break;
}
++pathIter;
}
// go with the first alternative that fully matched
// note: What is the correct behavior? Should this be most/least greedy (matching the longest/shortest possible alternative) instead?
if (alternative.beg == alternative.end) {
alternatives.clear();
break;
}
}
if (alternatives.empty()) {
state = MatchVerbatimly;
handleSingleMatch();
break;
}
}
if (!handleMismatch()) {
return false;
}
break;
case MatchAny:
// allow the current character in the path to be anything but a path separator; otherwise consider it as mismatch as in the case for an exact match
if (pathIter == pathEnd || *pathIter != pathSep) {
++globIter, ++pathIter;
} else if (!handleMismatch()) {
return false;
}
break;
case MatchManyAny: {
// take record of the asterisks
auto &glob = asterisks.emplace_back();
glob.pos = ++globIter;
glob.state = MatchManyAny;
inAsterisk = true;
break;
}
case MatchManyAnyIncludingDirSep: {
// take record of the second asterisks
auto &glob = asterisks.back();
glob.pos = ++globIter;
glob.state = MatchManyAnyIncludingDirSep;
break;
}
}
}
// check whether all characters of the glob have been matched against all characters of the path
if (globIter == globEnd) {
if (pathIter == pathEnd) {
return true;
}
// try again as of the next path segment if the glob fully matched but there are still characters in the path to be matched
// note: This allows "foo" to match against "foo/foo" even tough the glob characters have already consumed after matching the first path segment.
if (!matchFromRoot && *pathIter == pathSep) {
state = MatchVerbatimly;
++pathIter;
globIter = glob.begin();
asterisks.clear();
inAsterisk = false;
goto match;
}
}
// consider the match a success if there are still characters in the path to be matched but the glob ended with a "*"
return state == MatchManyAny || state == MatchManyAnyIncludingDirSep;
}
} // namespace Data

View File

@ -0,0 +1,50 @@
#ifndef DATA_SYNCTHINGIGNOREPATTERN_H
#define DATA_SYNCTHINGIGNOREPATTERN_H
#include "./global.h"
#include <QString>
namespace Data {
namespace SyncthingIgnorePatternState {
struct Asterisk;
struct CharacterRange;
struct AlternativeRange;
} // namespace SyncthingIgnorePatternState
/*!
* \brief The SyncthingIgnorePattern struct allows matching a Syncthing ignore pattern against a path.
* \sa
* - https://docs.syncthing.net/users/ignoring.html
* - https://docs.syncthing.net/rest/db-ignores-get.html
*/
struct LIB_SYNCTHING_CONNECTOR_EXPORT SyncthingIgnorePattern {
explicit SyncthingIgnorePattern(QString &&pattern);
SyncthingIgnorePattern(const SyncthingIgnorePattern &) = delete;
SyncthingIgnorePattern(SyncthingIgnorePattern &&);
~SyncthingIgnorePattern();
bool matches(const QString &path) const;
/// \brief The full ignore pattern as passed to the c'tor (unless modified).
QString pattern;
/// \brief The part of the pattern that will actually be used for globbing by the matches() function.
QString glob;
/// \brief Whether the pattern is a comment.
bool comment = false;
/// \brief Whether the pattern will lead to ignoring matching items (the default).
bool ignore = true;
/// \brief Whether the pattern will match case-insensetively.
bool caseInsensitive = false;
/// \brief Whether matching items may be removed when the otherwise empty parent directory would be removed.
bool allowRemovalOnParentDirRemoval = false;
private:
mutable std::vector<SyncthingIgnorePatternState::Asterisk> asterisks;
mutable std::vector<SyncthingIgnorePatternState::CharacterRange> characterRange;
mutable std::vector<SyncthingIgnorePatternState::AlternativeRange> alternatives;
};
} // namespace Data
#endif // DATA_SYNCTHINGIGNOREPATTERN_H

View File

@ -1,6 +1,7 @@
#include "../syncthingconfig.h"
#include "../syncthingconnection.h"
#include "../syncthingconnectionsettings.h"
#include "../syncthingignorepattern.h"
#include "../syncthingprocess.h"
#include "../syncthingservice.h"
#include "../utils.h"
@ -39,6 +40,7 @@ class MiscTests : public TestFixture {
#endif
CPPUNIT_TEST(testConnectionSettingsAndLoadingSelfSignedCert);
CPPUNIT_TEST(testSyncthingDir);
CPPUNIT_TEST(testIgnorePatternMatching);
CPPUNIT_TEST_SUITE_END();
public:
@ -52,6 +54,7 @@ public:
#endif
void testConnectionSettingsAndLoadingSelfSignedCert();
void testSyncthingDir();
void testIgnorePatternMatching();
void setUp() override;
void tearDown() override;
@ -279,3 +282,122 @@ void MiscTests::testSyncthingDir()
CPPUNIT_ASSERT_MESSAGE("same status again not considered an update",
!dir.assignStatus(QStringLiteral("idle"), updateEvent += 1, updateTime += TimeSpan::fromMinutes(1.5)));
}
void MiscTests::testIgnorePatternMatching()
{
auto p1 = SyncthingIgnorePattern(QStringLiteral("foo"));
CPPUNIT_ASSERT(!p1.comment);
CPPUNIT_ASSERT(!p1.caseInsensitive);
CPPUNIT_ASSERT(p1.ignore);
CPPUNIT_ASSERT(!p1.allowRemovalOnParentDirRemoval);
CPPUNIT_ASSERT_EQUAL(QStringLiteral("foo"), p1.glob);
CPPUNIT_ASSERT(p1.matches(QStringLiteral("foo")));
CPPUNIT_ASSERT(!p1.matches(QStringLiteral("foofoo")));
CPPUNIT_ASSERT(p1.matches(QStringLiteral("foo/foo")));
auto p2 = SyncthingIgnorePattern(QStringLiteral("foo*"));
CPPUNIT_ASSERT(p2.matches(QStringLiteral("foo")));
CPPUNIT_ASSERT(p2.matches(QStringLiteral("foobar")));
CPPUNIT_ASSERT(!p2.matches(QStringLiteral("barfoo")));
CPPUNIT_ASSERT(p2.matches(QStringLiteral("bar/foo")));
auto p3 = SyncthingIgnorePattern(QStringLiteral("fo*ar"));
CPPUNIT_ASSERT(!p3.matches(QStringLiteral("foo")));
CPPUNIT_ASSERT(p3.matches(QStringLiteral("foar")));
CPPUNIT_ASSERT(p3.matches(QStringLiteral("foobar")));
CPPUNIT_ASSERT(!p3.matches(QStringLiteral("foobaR")));
CPPUNIT_ASSERT(p3.matches(QStringLiteral("foobaRar")));
CPPUNIT_ASSERT(p3.matches(QStringLiteral("foo*bar")));
auto p4 = SyncthingIgnorePattern(QStringLiteral("fo\\*ar"));
CPPUNIT_ASSERT(!p4.matches(QStringLiteral("foar")));
CPPUNIT_ASSERT(!p4.matches(QStringLiteral("foobar")));
CPPUNIT_ASSERT(!p4.matches(QStringLiteral("foo*bar")));
CPPUNIT_ASSERT(p4.matches(QStringLiteral("fo*ar")));
auto p5 = SyncthingIgnorePattern(QStringLiteral("te*ne"));
CPPUNIT_ASSERT(p5.matches(QStringLiteral("telephone")));
CPPUNIT_ASSERT(p5.matches(QStringLiteral("subdir/telephone")));
CPPUNIT_ASSERT(!p5.matches(QStringLiteral("tele/phone")));
auto p6 = SyncthingIgnorePattern(QStringLiteral("te**ne"));
CPPUNIT_ASSERT(p6.matches(QStringLiteral("telephone")));
CPPUNIT_ASSERT(p6.matches(QStringLiteral("subdir/telephone")));
CPPUNIT_ASSERT(p6.matches(QStringLiteral("tele/phone")));
auto p7 = SyncthingIgnorePattern(QStringLiteral("te??st"));
CPPUNIT_ASSERT(p7.matches(QStringLiteral("tebest")));
CPPUNIT_ASSERT(!p7.matches(QStringLiteral("teb/st")));
CPPUNIT_ASSERT(!p7.matches(QStringLiteral("test")));
auto p8 = SyncthingIgnorePattern(QStringLiteral("fo*ar*y"));
CPPUNIT_ASSERT(!p8.matches(QStringLiteral("fooy")));
CPPUNIT_ASSERT(p8.matches(QStringLiteral("foary")));
CPPUNIT_ASSERT(p8.matches(QStringLiteral("foobary")));
CPPUNIT_ASSERT(!p8.matches(QStringLiteral("foobaRy")));
CPPUNIT_ASSERT(p8.matches(QStringLiteral("foobaRary")));
CPPUNIT_ASSERT(!p8.matches(QStringLiteral("foobaRaRy")));
CPPUNIT_ASSERT(p8.matches(QStringLiteral("foobaRaRarxy")));
CPPUNIT_ASSERT(p8.matches(QStringLiteral("bar/foobaRaRarxy")));
auto p9 = SyncthingIgnorePattern(QStringLiteral("/foo*"));
CPPUNIT_ASSERT(p9.matches(QStringLiteral("foo")));
CPPUNIT_ASSERT(p9.matches(QStringLiteral("foobar")));
CPPUNIT_ASSERT(!p9.matches(QStringLiteral("barfoo")));
CPPUNIT_ASSERT(!p9.matches(QStringLiteral("bar/foo")));
auto p10 = SyncthingIgnorePattern(QStringLiteral("/fo?"));
CPPUNIT_ASSERT(p10.matches(QStringLiteral("foO")));
CPPUNIT_ASSERT(!p10.matches(QStringLiteral("foO/")));
CPPUNIT_ASSERT(!p10.matches(QStringLiteral("fo/")));
CPPUNIT_ASSERT(!p10.matches(QStringLiteral("bar/foO")));
auto p11 = SyncthingIgnorePattern(QStringLiteral("/fo[o0.]/bar"));
CPPUNIT_ASSERT(p11.matches(QStringLiteral("foo/bar")));
CPPUNIT_ASSERT(p11.matches(QStringLiteral("fo0/bar")));
CPPUNIT_ASSERT(p11.matches(QStringLiteral("fo./bar")));
CPPUNIT_ASSERT(!p11.matches(QStringLiteral("foO/bar")));
CPPUNIT_ASSERT(!p11.matches(QStringLiteral("fo?/bar")));
p11.caseInsensitive = true;
CPPUNIT_ASSERT(p11.matches(QStringLiteral("foO/bar")));
auto p12 = SyncthingIgnorePattern(QStringLiteral("/f[oA-C0-3.]o/bar"));
CPPUNIT_ASSERT(p12.matches(QStringLiteral("f0o/bar")));
CPPUNIT_ASSERT(p12.matches(QStringLiteral("f1o/bar")));
CPPUNIT_ASSERT(p12.matches(QStringLiteral("f2o/bar")));
CPPUNIT_ASSERT(p12.matches(QStringLiteral("f3o/bar")));
CPPUNIT_ASSERT(!p12.matches(QStringLiteral("f4o/bar")));
CPPUNIT_ASSERT(p12.matches(QStringLiteral("foo/bar")));
CPPUNIT_ASSERT(!p12.matches(QStringLiteral("foO/bar")));
CPPUNIT_ASSERT(p12.matches(QStringLiteral("fAo/bar")));
CPPUNIT_ASSERT(p12.matches(QStringLiteral("fBo/bar")));
CPPUNIT_ASSERT(p12.matches(QStringLiteral("fCo/bar")));
CPPUNIT_ASSERT(!p12.matches(QStringLiteral("fDo/bar")));
auto p13 = SyncthingIgnorePattern(QStringLiteral("/f{o,0o0,l}o/{bar,biz}"));
CPPUNIT_ASSERT(p13.matches(QStringLiteral("foo/bar")));
CPPUNIT_ASSERT(p13.matches(QStringLiteral("foo/biz")));
CPPUNIT_ASSERT(!p13.matches(QStringLiteral("f/biz")));
CPPUNIT_ASSERT(!p13.matches(QStringLiteral("f0o0/bar")));
CPPUNIT_ASSERT(p13.matches(QStringLiteral("f0o0o/bar")));
CPPUNIT_ASSERT(!p13.matches(QStringLiteral("f{o,0o0,}o/{bar,biz}")));
auto p14 = SyncthingIgnorePattern(QStringLiteral("/rust/**/target"));
CPPUNIT_ASSERT(p14.matches(QStringLiteral("rust/formatter/target")));
CPPUNIT_ASSERT(!p14.matches(QStringLiteral("rust/formatter/target/CACHEDIR.TAG")));
auto p14a = SyncthingIgnorePattern(QStringLiteral("/rust/**/target/"));
CPPUNIT_ASSERT(!p14a.matches(QStringLiteral("rust/formatter/target")));
CPPUNIT_ASSERT(!p14a.matches(QStringLiteral("rust/formatter/target/CACHEDIR.TAG")));
auto p15 = SyncthingIgnorePattern(QStringLiteral("/fo[\\]o]/bar"));
CPPUNIT_ASSERT(p15.matches(QStringLiteral("foo/bar")));
CPPUNIT_ASSERT(p15.matches(QStringLiteral("fo]/bar")));
CPPUNIT_ASSERT(!p15.matches(QStringLiteral("fo\\/bar")));
auto p16 = SyncthingIgnorePattern(QStringLiteral("/fo{o\\},o}/bar"));
CPPUNIT_ASSERT(p16.matches(QStringLiteral("foo/bar")));
CPPUNIT_ASSERT(p16.matches(QStringLiteral("foo}/bar")));
CPPUNIT_ASSERT(!p16.matches(QStringLiteral("/foo\\,o}/bar")));
}

View File

@ -74,13 +74,16 @@ SyncthingFileModel::SyncthingFileModel(SyncthingConnection &connection, const Sy
m_root->type = SyncthingItemType::Directory;
m_root->path = QStringLiteral(""); // assign an empty QString that is not null
m_fetchQueue.append(m_root->path);
queryIgnores();
processFetchQueue();
}
SyncthingFileModel::~SyncthingFileModel()
{
QObject::disconnect(m_pendingRequest.connection);
QObject::disconnect(m_ignorePatternsRequest.connection);
delete m_pendingRequest.reply;
delete m_ignorePatternsRequest.reply;
}
QHash<int, QByteArray> SyncthingFileModel::roleNames() const
@ -99,7 +102,7 @@ QHash<int, QByteArray> SyncthingFileModel::roleNames() const
QModelIndex SyncthingFileModel::index(int row, int column, const QModelIndex &parent) const
{
if (row < 0 || column < 0 || column > 2 || parent.column() > 0) {
if (row < 0 || column < 0 || column > 3 || parent.column() > 0) {
return QModelIndex();
}
if (!parent.isValid()) {
@ -177,6 +180,8 @@ QVariant SyncthingFileModel::headerData(int section, Qt::Orientation orientation
return tr("Size");
case 2:
return tr("Last modified");
case 3:
return tr("Ignore pattern");
}
break;
default:;
@ -236,6 +241,13 @@ QVariant SyncthingFileModel::data(const QModelIndex &index, int role) const
default:
return QString();
}
case 3:
if (item->ignorePattern == SyncthingItem::ignorePatternNotInitialized) {
matchItemAgainstIgnorePatterns(*item);
}
if (item->ignorePattern < m_presentIgnorePatterns.size()) {
return m_presentIgnorePatterns[item->ignorePattern].pattern;
}
}
break;
case Qt::CheckStateRole:
@ -411,7 +423,7 @@ int SyncthingFileModel::rowCount(const QModelIndex &parent) const
int SyncthingFileModel::columnCount(const QModelIndex &parent) const
{
Q_UNUSED(parent)
return 3;
return 4;
}
bool SyncthingFileModel::canFetchMore(const QModelIndex &parent) const
@ -606,6 +618,44 @@ void SyncthingFileModel::processFetchQueue(const QString &lastItemPath)
}
}
void SyncthingFileModel::queryIgnores()
{
m_connection.ignores(m_dirId, [this](SyncthingIgnores &&ignores, QString &&errorMessage) {
m_ignorePatternsRequest.reply = nullptr;
m_hasIgnorePatterns = errorMessage.isEmpty();
m_presentIgnorePatterns.clear();
if (!m_hasIgnorePatterns) {
return;
}
m_presentIgnorePatterns.reserve(static_cast<std::size_t>(ignores.ignore.size()));
for (auto &ignorePattern : ignores.ignore) {
m_presentIgnorePatterns.emplace_back(std::move(ignorePattern));
}
invalidateAllIndicies(QVector<int>{ Qt::DisplayRole }, 3, QModelIndex());
});
}
void SyncthingFileModel::matchItemAgainstIgnorePatterns(SyncthingItem &item) const
{
if (!m_hasIgnorePatterns) {
item.ignorePattern = SyncthingItem::ignorePatternNotInitialized;
return;
}
item.ignorePattern = SyncthingItem::ignorePatternNoMatch;
if (!item.isFilesystemItem()) {
return;
}
auto index = std::size_t();
for (const auto &ignorePattern : m_presentIgnorePatterns) {
if (ignorePattern.matches(item.path)) {
item.ignorePattern = index;
break;
} else {
++index;
}
}
}
void SyncthingFileModel::handleLocalLookupFinished()
{
// get refreshed index/item

View File

@ -4,12 +4,14 @@
#include "./syncthingmodel.h"
#include <syncthingconnector/syncthingconnection.h>
#include <syncthingconnector/syncthingignorepattern.h>
#include <QFuture>
#include <QFutureWatcher>
#include <map>
#include <memory>
#include <vector>
QT_FORWARD_DECLARE_CLASS(QAction)
@ -67,6 +69,8 @@ private Q_SLOTS:
private:
void setCheckState(const QModelIndex &index, Qt::CheckState checkState);
void processFetchQueue(const QString &lastItemPath = QString());
void queryIgnores();
void matchItemAgainstIgnorePatterns(SyncthingItem &item) const;
private:
using SyncthingItems = std::vector<std::unique_ptr<SyncthingItem>>;
@ -81,11 +85,14 @@ private:
SyncthingConnection &m_connection;
QString m_dirId;
QString m_localPath;
std::vector<SyncthingIgnorePattern> m_presentIgnorePatterns;
QStringList m_fetchQueue;
SyncthingConnection::QueryResult m_ignorePatternsRequest;
QueryResult m_pendingRequest;
QFutureWatcher<LocalLookupRes> m_localItemLookup;
std::unique_ptr<SyncthingItem> m_root;
bool m_selectionMode;
bool m_hasIgnorePatterns;
};
inline bool SyncthingFileModel::isSelectionModeEnabled() const

View File

@ -57,6 +57,23 @@ void SyncthingModel::invalidateAllIndicies(const QVector<int> &affectedRoles, co
}
}
void SyncthingModel::invalidateAllIndicies(const QVector<int> &affectedRoles, int column, const QModelIndex &parentIndex)
{
const auto rows = rowCount(parentIndex);
const auto columns = columnCount(parentIndex);
if (rows <= 0 || column >= columns) {
return;
}
const auto topLeftIndex = index(0, column, parentIndex);
const auto bottomRightIndex = index(rows - 1, column, parentIndex);
emit dataChanged(topLeftIndex, bottomRightIndex, affectedRoles);
for (auto row = 0; row != rows; ++row) {
if (const auto idx = index(row, 0, parentIndex); idx.isValid()) {
invalidateAllIndicies(affectedRoles, column, idx);
}
}
}
void SyncthingModel::setBrightColors(bool brightColors)
{
if (m_brightColors == brightColors) {

View File

@ -35,6 +35,7 @@ protected:
void invalidateTopLevelIndicies(const QVector<int> &affectedRoles);
void invalidateNestedIndicies(const QVector<int> &affectedRoles);
void invalidateAllIndicies(const QVector<int> &affectedRoles, const QModelIndex &parentIndex = QModelIndex());
void invalidateAllIndicies(const QVector<int> &affectedRoles, int column, const QModelIndex &parentIndex = QModelIndex());
private Q_SLOTS:
virtual void handleConfigInvalidated();