530 qCWarning(m_loggingCat ? *m_loggingCat : detail::lcMetadataPackScan())
531 <<
"MetadataPackScanStrategy: no parser configured, skipping scan";
540 struct EntryFingerprint
542 qint64 metadataSize = 0;
543 qint64 metadataMtimeMs = 0;
557 std::vector<Entry> entries;
561 QSet<QString> seenIds;
563 QStringList desiredWatches;
565 const QLoggingCategory& log = m_loggingCat ? *m_loggingCat : detail::lcMetadataPackScan();
573 const QString canonicalUserPath = m_userPath.isEmpty() ? QString() : QFileInfo(m_userPath).canonicalFilePath();
575 bool capTripped =
false;
584 int subdirsConsidered = 0;
591 for (
auto dirIt = directoriesInScanOrder.crbegin(); dirIt != directoriesInScanOrder.crend() && !capTripped;
593 const QString& searchPath = *dirIt;
594 QDir dirObj(searchPath);
595 if (!dirObj.exists()) {
596 qCDebug(log) <<
"MetadataPackScanStrategy: search path does not exist:" << searchPath;
600 const bool isUserDir =
601 !canonicalUserPath.isEmpty() && QFileInfo(searchPath).canonicalFilePath() == canonicalUserPath;
612 desiredWatches.append(m_perDirWatch(searchPath));
628 const QStringList subdirs = dirObj.entryList(QDir::Dirs | QDir::NoDotAndDotDot, QDir::Name);
629 for (
const QString& subdir : subdirs) {
646 if (subdirsConsidered >= m_maxEntries) {
651 if (m_subdirSkip && m_subdirSkip(subdir)) {
655 const QString subdirPath = dirObj.filePath(subdir);
656 const QString metadataPath = subdirPath + QStringLiteral(
"/metadata.json");
662 desiredWatches.append(metadataPath);
664 const QFileInfo metadataInfo(metadataPath);
672 if (!metadataInfo.isFile()) {
683 desiredWatches.append(subdirPath);
684 qCDebug(log) <<
"MetadataPackScanStrategy: skipping subdir, no metadata.json:" << subdirPath;
694 qCWarning(log) <<
"MetadataPackScanStrategy: skipping oversized metadata.json:" << metadataPath <<
"("
699 QFile file(metadataPath);
700 if (!file.open(QIODevice::ReadOnly)) {
701 qCWarning(log) <<
"MetadataPackScanStrategy: failed to open metadata.json:" << metadataPath;
716 qCWarning(log) <<
"MetadataPackScanStrategy: skipping oversized metadata.json:" << metadataPath <<
"("
721 QJsonParseError parseError{};
722 const QJsonDocument doc = QJsonDocument::fromJson(file.readAll(), &parseError);
723 if (parseError.error != QJsonParseError::NoError) {
724 qCWarning(log) <<
"MetadataPackScanStrategy: parse error in" << metadataPath <<
":"
725 << parseError.errorString();
728 if (!doc.isObject()) {
729 qCWarning(log) <<
"MetadataPackScanStrategy: non-object root in" << metadataPath;
737 std::optional<Payload> parsed = m_parser(subdirPath, doc.object(), isUserDir);
738 if (!parsed.has_value()) {
739 qCDebug(log) <<
"MetadataPackScanStrategy: parser declined" << metadataPath;
742 if (parsed->id.isEmpty()) {
743 qCWarning(log) <<
"MetadataPackScanStrategy: skipping" << metadataPath <<
": empty 'id' field";
750 if (seenIds.contains(parsed->id)) {
751 qCDebug(log) <<
"MetadataPackScanStrategy: id" << parsed->id
752 <<
"already registered from a higher-priority dir; shadowed at:" << subdirPath;
757 if (m_perEntryWatch) {
758 desiredWatches.append(m_perEntryWatch(*parsed));
773 QString
id = parsed->id;
777 EntryFingerprint{metadataInfo.size(), metadataInfo.lastModified().toMSecsSinceEpoch(), isUserDir},
778 std::move(*parsed)});
783 qCWarning(log).nospace() <<
"MetadataPackScanStrategy: reached entry cap (" << m_maxEntries
784 <<
") — later entries skipped to protect the GUI thread. Prune the watched search "
785 "paths or raise the cap.";
800 std::sort(entries.begin(), entries.end(), [](
const Entry& a,
const Entry& b) {
825 QCryptographicHash hasher(QCryptographicHash::Sha1);
826 for (
const Entry& e : entries) {
827 hasher.addData(e.id.toUtf8());
828 hasher.addData(QByteArrayView(
"|"));
829 hasher.addData(e.fp.isUser ? QByteArrayView(
"u") : QByteArrayView(
"s"));
830 hasher.addData(QByteArrayView(
"|"));
831 hasher.addData(QByteArray::number(e.fp.metadataSize));
832 hasher.addData(QByteArrayView(
"|"));
833 hasher.addData(QByteArray::number(e.fp.metadataMtimeMs));
834 hasher.addData(QByteArrayView(
"|"));
836 m_sigContrib(hasher, e.payload);
838 hasher.addData(QByteArrayView(
"\n"));
849 QStringList sortedWatches = desiredWatches;
850 sortedWatches.removeDuplicates();
851 std::sort(sortedWatches.begin(), sortedWatches.end());
852 for (
const QString& path : sortedWatches) {
853 hasher.addData(path.toUtf8());
854 hasher.addData(QByteArrayView(
"|"));
855 const QFileInfo fi(path);
857 hasher.addData(QByteArray::number(fi.size()));
858 hasher.addData(QByteArrayView(
"|"));
859 hasher.addData(QByteArray::number(fi.lastModified().toMSecsSinceEpoch()));
865 hasher.addData(QByteArrayView(
"missing"));
867 hasher.addData(QByteArrayView(
"\n"));
869 const QByteArray signature = hasher.result();
871 QHash<QString, Payload> fresh;
872 fresh.reserve(
static_cast<int>(entries.size()));
873 for (Entry& e : entries) {
874 fresh.insert(e.id, std::move(e.payload));
877 const bool isFirstScan = !m_signatureSeeded;
878 const bool changed = isFirstScan ? !fresh.isEmpty() : signature != m_lastSignature;
880 m_packs = std::move(fresh);
881 m_lastSignature = signature;
882 m_signatureSeeded =
true;
884 if (changed && m_onCommit) {
898 return sortedWatches;