diff --git a/autotests/kdirlistertest.cpp b/autotests/kdirlistertest.cpp index 3c55eeda..3f159bd0 100644 --- a/autotests/kdirlistertest.cpp +++ b/autotests/kdirlistertest.cpp @@ -1,1355 +1,1367 @@ /* This file is part of the KDE project Copyright (C) 2007 David Faure This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kdirlistertest.h" #include #include #include QTEST_MAIN(KDirListerTest) #include #include "kiotesthelper.h" #include #include #include #include #include #define WORKAROUND_BROKEN_INOTIFY 0 void MyDirLister::handleError(KIO::Job *job) { // Currently we don't expect any errors. qCritical() << "KDirLister called handleError!" << job << job->error() << job->errorString(); qFatal("aborting"); } void KDirListerTest::initTestCase() { // To avoid a runtime dependency on klauncher qputenv("KDE_FORK_SLAVES", "yes"); // To avoid failing on broken locally defined mime types QStandardPaths::setTestModeEnabled(true); KIO::setDefaultJobUiDelegateExtension(nullptr); // no "skip" dialogs m_exitCount = 1; s_referenceTimeStamp = QDateTime::currentDateTime().addSecs(-120); // 2 minutes ago // Create test data: /* * PATH/toplevelfile_1 * PATH/toplevelfile_2 * PATH/toplevelfile_3 * PATH/subdir * PATH/subdir/testfile * PATH/subdir/subsubdir * PATH/subdir/subsubdir/testfile */ const QString path = m_tempDir.path() + '/'; createTestFile(path + "toplevelfile_1"); createTestFile(path + "toplevelfile_2"); createTestFile(path + "toplevelfile_3"); createTestDirectory(path + "subdir"); createTestDirectory(path + "subdir/subsubdir"); qRegisterMetaType > >(); } void KDirListerTest::cleanup() { m_dirLister.clearSpies(); disconnect(&m_dirLister, nullptr, this, nullptr); } void KDirListerTest::testOpenUrl() { m_items.clear(); const QString path = m_tempDir.path() + '/'; connect(&m_dirLister, &KCoreDirLister::newItems, this, &KDirListerTest::slotNewItems); // The call to openUrl itself, emits started m_dirLister.openUrl(QUrl::fromLocalFile(path), KDirLister::NoFlags); QCOMPARE(m_dirLister.spyStarted.count(), 1); QCOMPARE(m_dirLister.spyCompleted.count(), 0); QCOMPARE(m_dirLister.spyCompletedQUrl.count(), 0); QCOMPARE(m_dirLister.spyCanceled.count(), 0); QCOMPARE(m_dirLister.spyCanceledQUrl.count(), 0); QCOMPARE(m_dirLister.spyClear.count(), 1); QCOMPARE(m_dirLister.spyClearQUrl.count(), 0); QCOMPARE(m_dirLister.spyRedirection.count(), 0); QCOMPARE(m_items.count(), 0); QVERIFY(!m_dirLister.isFinished()); // then wait for completed qDebug("waiting for completed"); QTRY_COMPARE(m_dirLister.spyStarted.count(), 1); QTRY_COMPARE(m_dirLister.spyCompleted.count(), 1); QCOMPARE(m_dirLister.spyCompletedQUrl.count(), 1); QCOMPARE(m_dirLister.spyCanceled.count(), 0); QCOMPARE(m_dirLister.spyCanceledQUrl.count(), 0); QCOMPARE(m_dirLister.spyClear.count(), 1); QCOMPARE(m_dirLister.spyClearQUrl.count(), 0); QCOMPARE(m_dirLister.spyRedirection.count(), 0); //qDebug() << m_items; //qDebug() << "In dir" << QDir(path).entryList( QDir::AllEntries | QDir::NoDotAndDotDot); QCOMPARE(m_items.count(), fileCount()); QVERIFY(m_dirLister.isFinished()); disconnect(&m_dirLister, nullptr, this, nullptr); const QString fileName = QStringLiteral("toplevelfile_3"); const QUrl itemUrl = QUrl::fromLocalFile(path + fileName); KFileItem byName = m_dirLister.findByName(fileName); QVERIFY(!byName.isNull()); QCOMPARE(byName.url().toString(), itemUrl.toString()); QCOMPARE(byName.entry().stringValue(KIO::UDSEntry::UDS_NAME), fileName); KFileItem byUrl = m_dirLister.findByUrl(itemUrl); QVERIFY(!byUrl.isNull()); QCOMPARE(byUrl.url().toString(), itemUrl.toString()); QCOMPARE(byUrl.entry().stringValue(KIO::UDSEntry::UDS_NAME), fileName); KFileItem itemForUrl = KDirLister::cachedItemForUrl(itemUrl); QVERIFY(!itemForUrl.isNull()); QCOMPARE(itemForUrl.url().toString(), itemUrl.toString()); QCOMPARE(itemForUrl.entry().stringValue(KIO::UDSEntry::UDS_NAME), fileName); KFileItem rootByUrl = m_dirLister.findByUrl(QUrl::fromLocalFile(path)); QVERIFY(!rootByUrl.isNull()); QCOMPARE(QString(rootByUrl.url().toLocalFile() + '/'), path); m_dirLister.clearSpies(); // for the tests that call testOpenUrl for setup } // This test assumes testOpenUrl was run before. So m_dirLister is holding the items already. void KDirListerTest::testOpenUrlFromCache() { // Do the same again, it should behave the same, even with the items in the cache testOpenUrl(); // Get into the case where another dirlister is holding the items { m_items.clear(); const QString path = m_tempDir.path() + '/'; MyDirLister secondDirLister; connect(&secondDirLister, &KCoreDirLister::newItems, this, &KDirListerTest::slotNewItems); secondDirLister.openUrl(QUrl::fromLocalFile(path), KDirLister::NoFlags); QCOMPARE(secondDirLister.spyStarted.count(), 1); QCOMPARE(secondDirLister.spyCompleted.count(), 0); QCOMPARE(secondDirLister.spyCompletedQUrl.count(), 0); QCOMPARE(secondDirLister.spyCanceled.count(), 0); QCOMPARE(secondDirLister.spyCanceledQUrl.count(), 0); QCOMPARE(secondDirLister.spyClear.count(), 1); QCOMPARE(secondDirLister.spyClearQUrl.count(), 0); QCOMPARE(m_items.count(), 0); QVERIFY(!secondDirLister.isFinished()); // then wait for completed qDebug("waiting for completed"); QTRY_COMPARE(secondDirLister.spyStarted.count(), 1); QTRY_COMPARE(secondDirLister.spyCompleted.count(), 1); QCOMPARE(secondDirLister.spyCompletedQUrl.count(), 1); QCOMPARE(secondDirLister.spyCanceled.count(), 0); QCOMPARE(secondDirLister.spyCanceledQUrl.count(), 0); QCOMPARE(secondDirLister.spyClear.count(), 1); QCOMPARE(secondDirLister.spyClearQUrl.count(), 0); QCOMPARE(m_items.count(), 4); QVERIFY(secondDirLister.isFinished()); } disconnect(&m_dirLister, nullptr, this, nullptr); } // This test assumes testOpenUrl was run before. So m_dirLister is holding the items already. // This test creates 1 file in the temporary directory void KDirListerTest::testNewItem() { QCOMPARE(m_items.count(), 4); const QString path = m_tempDir.path() + '/'; connect(&m_dirLister, &KCoreDirLister::newItems, this, &KDirListerTest::slotNewItems); qDebug() << "Creating a new file"; const QString fileName = QStringLiteral("toplevelfile_new"); createSimpleFile(path + fileName); QTRY_COMPARE(m_items.count(), 5); QCOMPARE(m_dirLister.spyStarted.count(), 1); // Updates call started QCOMPARE(m_dirLister.spyCompleted.count(), 1); // and completed QCOMPARE(m_dirLister.spyCompletedQUrl.count(), 1); QCOMPARE(m_dirLister.spyCanceled.count(), 0); QCOMPARE(m_dirLister.spyCanceledQUrl.count(), 0); QCOMPARE(m_dirLister.spyClear.count(), 0); QCOMPARE(m_dirLister.spyClearQUrl.count(), 0); const QUrl itemUrl = QUrl::fromLocalFile(path + fileName); KFileItem itemForUrl = KDirLister::cachedItemForUrl(itemUrl); QVERIFY(!itemForUrl.isNull()); QCOMPARE(itemForUrl.url().toString(), itemUrl.toString()); QCOMPARE(itemForUrl.entry().stringValue(KIO::UDSEntry::UDS_NAME), fileName); disconnect(&m_dirLister, nullptr, this, nullptr); } // This test assumes testNewItem was run before. So m_dirLister is holding the items already. // This test creates 100 more files in the temporary directory in reverse order void KDirListerTest::testNewItems() { QCOMPARE(m_items.count(), 5); connect(&m_dirLister, &KCoreDirLister::newItems, this, &KDirListerTest::slotNewItems); const QString path = m_tempDir.path() + '/'; qDebug() << "Creating 100 new files"; for (int i = 50; i > 0; i--) { createSimpleFile(path + QString("toplevelfile_new_%1").arg(i)); } QTest::qWait(1000); // Create them with 1s difference for (int i = 100; i > 50; i--) { createSimpleFile(path + QString("toplevelfile_new_%1").arg(i)); } // choose one of the new created files const QString fileName = QStringLiteral("toplevelfile_new_50"); QTRY_COMPARE(m_items.count(), 105); QVERIFY(m_dirLister.spyStarted.count() > 0 && m_dirLister.spyStarted.count() < 3); // Updates call started, probably twice QVERIFY(m_dirLister.spyCompleted.count() > 0 && m_dirLister.spyCompleted.count() < 3); // and completed, probably twice QVERIFY(m_dirLister.spyCompletedQUrl.count() < 3); QCOMPARE(m_dirLister.spyCanceled.count(), 0); QCOMPARE(m_dirLister.spyCanceledQUrl.count(), 0); QCOMPARE(m_dirLister.spyClear.count(), 0); QCOMPARE(m_dirLister.spyClearQUrl.count(), 0); const QUrl itemUrl = QUrl::fromLocalFile(path + fileName); KFileItem itemForUrl = KDirLister::cachedItemForUrl(itemUrl); QVERIFY(!itemForUrl.isNull()); QCOMPARE(itemForUrl.url().toString(), itemUrl.toString()); QCOMPARE(itemForUrl.entry().stringValue(KIO::UDSEntry::UDS_NAME), fileName); } +void KDirListerTest::benchFindByUrl() +{ + // The time used should be in the order of O(100*log2(100)) + const QString path = m_tempDir.path() + '/'; + QBENCHMARK { + for (int i = 100; i > 0; i--) { + KFileItem cachedItem = m_dirLister.findByUrl(QUrl::fromLocalFile(path + QString("toplevelfile_new_%1").arg(i))); + QVERIFY(!cachedItem.isNull()); + } + } +} + void KDirListerTest::testNewItemByCopy() { // This test creates a file using KIO::copyAs, like knewmenu.cpp does. // Useful for testing #192185, i.e. whether we catch the kdirwatch event and avoid // a KFileItem::refresh(). const int origItemCount = m_items.count(); const QString path = m_tempDir.path() + '/'; connect(&m_dirLister, &KCoreDirLister::newItems, this, &KDirListerTest::slotNewItems); QTest::qWait(1000); // We need a 1s timestamp difference on the dir, otherwise FAM won't notice anything. const QString fileName = QStringLiteral("toplevelfile_copy"); const QUrl itemUrl = QUrl::fromLocalFile(path + fileName); KIO::CopyJob *job = KIO::copyAs(QUrl::fromLocalFile(path + "toplevelfile_3"), itemUrl, KIO::HideProgressInfo); job->exec(); // Give time for KDirWatch/KDirNotify to notify us QTRY_COMPARE(m_items.count(), origItemCount + 1); QCOMPARE(m_dirLister.spyStarted.count(), 1); // Updates call started QCOMPARE(m_dirLister.spyCompleted.count(), 1); // and completed QCOMPARE(m_dirLister.spyCompletedQUrl.count(), 1); QCOMPARE(m_dirLister.spyCanceled.count(), 0); QCOMPARE(m_dirLister.spyCanceledQUrl.count(), 0); QCOMPARE(m_dirLister.spyClear.count(), 0); QCOMPARE(m_dirLister.spyClearQUrl.count(), 0); // Give some time to KDirWatch QTest::qWait(1000); KFileItem itemForUrl = KDirLister::cachedItemForUrl(itemUrl); QVERIFY(!itemForUrl.isNull()); QCOMPARE(itemForUrl.url().toString(), itemUrl.toString()); QCOMPARE(itemForUrl.entry().stringValue(KIO::UDSEntry::UDS_NAME), fileName); } void KDirListerTest::testNewItemsInSymlink() // #213799 { const int origItemCount = m_items.count(); QCOMPARE(fileCount(), origItemCount); const QString path = m_tempDir.path() + '/'; QTemporaryFile tempFile; QVERIFY(tempFile.open()); const QString symPath = tempFile.fileName() + "_link"; tempFile.close(); const bool symlinkOk = KIOPrivate::createSymlink(path, symPath); if (!symlinkOk) { const QString error = QString::fromLatin1("Failed to create symlink '%1' pointing to '%2': %3") .arg(symPath, path, QString::fromLocal8Bit(strerror(errno))); QVERIFY2(symlinkOk, qPrintable(error)); } MyDirLister dirLister2; m_items2.clear(); connect(&dirLister2, &KCoreDirLister::newItems, this, &KDirListerTest::slotNewItems2); connect(&m_dirLister, &KCoreDirLister::newItems, this, &KDirListerTest::slotNewItems); // The initial listing dirLister2.openUrl(QUrl::fromLocalFile(symPath), KDirLister::NoFlags); QTRY_COMPARE(m_items.count(), origItemCount); QTRY_VERIFY(dirLister2.isFinished()); QTest::qWait(1000); // We need a 1s timestamp difference on the dir, otherwise FAM won't notice anything. qDebug() << "Creating new file"; const QString fileName = QStringLiteral("toplevelfile_newinlink"); createSimpleFile(path + fileName); #if WORKAROUND_BROKEN_INOTIFY org::kde::KDirNotify::emitFilesAdded(path); #endif // Give time for KDirWatch to notify us QTRY_COMPARE(m_items2.count(), origItemCount + 1); QTRY_COMPARE(m_items.count(), origItemCount + 1); // Now create an item using the symlink-path const QString fileName2 = QStringLiteral("toplevelfile_newinlink2"); { createSimpleFile(path + fileName2); // Give time for KDirWatch to notify us QTRY_COMPARE(m_items2.count(), origItemCount + 2); QTRY_COMPARE(m_items.count(), origItemCount + 2); } QCOMPARE(fileCount(), m_items.count()); // Test file deletion { qDebug() << "Deleting" << (path + fileName); QTest::qWait(1000); // for timestamp difference QFile::remove(path + fileName); QTRY_COMPARE(dirLister2.spyItemsDeleted.count(), 1); const KFileItem item = dirLister2.spyItemsDeleted[0][0].value().at(0); QCOMPARE(item.url().toLocalFile(), QString(symPath + '/' + fileName)); } // TODO: test file update. disconnect(&m_dirLister, nullptr, this, nullptr); QFile::remove(symPath); } // This test assumes testOpenUrl was run before. So m_dirLister is holding the items already. // Modifies one of the files to have html content void KDirListerTest::testRefreshItems() { m_refreshedItems.clear(); const QString path = m_tempDir.path() + '/'; const QString fileName = path + "toplevelfile_1"; KFileItem cachedItem = m_dirLister.findByUrl(QUrl::fromLocalFile(fileName)); QVERIFY(!cachedItem.isNull()); QCOMPARE(cachedItem.mimetype(), QString("application/octet-stream")); connect(&m_dirLister, &KCoreDirLister::refreshItems, this, &KDirListerTest::slotRefreshItems); QFile file(fileName); QVERIFY(file.open(QIODevice::Append)); file.write(QByteArray("")); file.close(); QCOMPARE(QFileInfo(fileName).size(), 11LL /*Hello world*/ + 6 /**/); QTRY_VERIFY(!m_refreshedItems.isEmpty()); QCOMPARE(m_dirLister.spyStarted.count(), 0); // fast path: no directory listing needed QVERIFY(m_dirLister.spyCompleted.count() < 2); QVERIFY(m_dirLister.spyCompletedQUrl.count() < 2); QCOMPARE(m_dirLister.spyCanceled.count(), 0); QCOMPARE(m_dirLister.spyCanceledQUrl.count(), 0); QCOMPARE(m_dirLister.spyClear.count(), 0); QCOMPARE(m_dirLister.spyClearQUrl.count(), 0); QCOMPARE(m_refreshedItems.count(), 1); QPair entry = m_refreshedItems.first(); QCOMPARE(entry.first.url().toLocalFile(), fileName); QCOMPARE(entry.first.size(), KIO::filesize_t(11)); QCOMPARE(entry.first.mimetype(), QString("application/octet-stream")); QCOMPARE(entry.second.url().toLocalFile(), fileName); QCOMPARE(entry.second.size(), KIO::filesize_t(11 /*Hello world*/ + 6 /**/)); QCOMPARE(entry.second.mimetype(), QString("text/html")); // Let's see what KDirLister has in cache now cachedItem = m_dirLister.findByUrl(QUrl::fromLocalFile(fileName)); QCOMPARE(cachedItem.size(), KIO::filesize_t(11 /*Hello world*/ + 6 /**/)); m_refreshedItems.clear(); } // Refresh the root item, plus a hidden file, e.g. changing its icon. #190535 void KDirListerTest::testRefreshRootItem() { // This test assumes testOpenUrl was run before. So m_dirLister is holding the items already. m_refreshedItems.clear(); m_refreshedItems2.clear(); // The item will be the root item of dirLister2, but also a child item // of m_dirLister. // In #190535 it would show "." instead of the subdir name, after a refresh... const QString path = m_tempDir.path() + '/' + "subdir"; MyDirLister dirLister2; fillDirLister2(dirLister2, path); // Change the subdir by creating a file in it waitUntilMTimeChange(path); const QString foobar = path + "/.foobar"; createSimpleFile(foobar); connect(&m_dirLister, &KCoreDirLister::refreshItems, this, &KDirListerTest::slotRefreshItems); // Arguably, the mtime change of "subdir" should lead to a refreshItem of subdir in the root dir. // So the next line shouldn't be necessary, if KDirLister did this correctly. This isn't what this test is about though. org::kde::KDirNotify::emitFilesChanged(QList() << QUrl::fromLocalFile(path)); QTRY_VERIFY(!m_refreshedItems.isEmpty()); QCOMPARE(m_dirLister.spyStarted.count(), 0); QCOMPARE(m_dirLister.spyCompleted.count(), 0); QCOMPARE(m_dirLister.spyCompletedQUrl.count(), 0); QCOMPARE(m_dirLister.spyCanceled.count(), 0); QCOMPARE(m_dirLister.spyCanceledQUrl.count(), 0); QCOMPARE(m_dirLister.spyClear.count(), 0); QCOMPARE(m_dirLister.spyClearQUrl.count(), 0); QCOMPARE(m_refreshedItems.count(), 1); QPair entry = m_refreshedItems.first(); QCOMPARE(entry.first.url().toLocalFile(), path); QCOMPARE(entry.first.name(), QString("subdir")); QCOMPARE(entry.second.url().toLocalFile(), path); QCOMPARE(entry.second.name(), QString("subdir")); QCOMPARE(m_refreshedItems2.count(), 1); entry = m_refreshedItems2.first(); QCOMPARE(entry.first.url().toLocalFile(), path); QCOMPARE(entry.second.url().toLocalFile(), path); // item name() doesn't matter here, it's the root item. m_refreshedItems.clear(); m_refreshedItems2.clear(); waitUntilMTimeChange(path); const QString directoryFile = path + "/.directory"; createSimpleFile(directoryFile); org::kde::KDirNotify::emitFilesAdded(QUrl::fromLocalFile(path)); QTest::qWait(200); // The order of these two is not deterministic org::kde::KDirNotify::emitFilesChanged(QList() << QUrl::fromLocalFile(directoryFile)); org::kde::KDirNotify::emitFilesChanged(QList() << QUrl::fromLocalFile(path)); QTRY_VERIFY(!m_refreshedItems.isEmpty()); QCOMPARE(m_refreshedItems.count(), 1); entry = m_refreshedItems.first(); QCOMPARE(entry.first.url().toLocalFile(), path); QCOMPARE(entry.second.url().toLocalFile(), path); m_refreshedItems.clear(); m_refreshedItems2.clear(); // Note: this test leaves the .directory file as a side effect. // Hidden though, shouldn't matter. } void KDirListerTest::testDeleteItem() { testOpenUrl(); // ensure m_items is uptodate const int origItemCount = m_items.count(); QCOMPARE(fileCount(), origItemCount); const QString path = m_tempDir.path() + '/'; //qDebug() << "Removing " << path+"toplevelfile_new"; QFile::remove(path + QString("toplevelfile_new")); // the remove() doesn't always trigger kdirwatch in stat mode, if this all happens in the same second KDirWatch::self()->setDirty(path); // The signal should be emited once with the deleted file QTRY_COMPARE(m_dirLister.spyItemsDeleted.count(), 1); // OK now kdirlister told us the file was deleted, let's try a re-listing m_items.clear(); connect(&m_dirLister, &KCoreDirLister::newItems, this, &KDirListerTest::slotNewItems); m_dirLister.openUrl(QUrl::fromLocalFile(path), KDirLister::NoFlags); QVERIFY(!m_dirLister.isFinished()); QTRY_COMPARE(m_items.count(), origItemCount - 1); QVERIFY(m_dirLister.isFinished()); disconnect(&m_dirLister, nullptr, this, nullptr); QCOMPARE(fileCount(), m_items.count()); } void KDirListerTest::testDeleteItems() { testOpenUrl(); // ensure m_items is uptodate const int origItemCount = m_items.count(); QCOMPARE(fileCount(), origItemCount); const QString path = m_tempDir.path() + '/'; qDebug() << "Removing 100 files from " << path; for (int i=0; i <= 100; ++i) { QFile::remove(path + QString("toplevelfile_new_%1").arg(i)); } // the remove() doesn't always trigger kdirwatch in stat mode, if this all happens in the same second KDirWatch::self()->setDirty(path); // The signal could be emited 1 time with all the deleted files or more times QTRY_VERIFY(m_dirLister.spyItemsDeleted.count() > 0); // OK now kdirlister told us the file was deleted, let's try a re-listing m_items.clear(); connect(&m_dirLister, &KCoreDirLister::newItems, this, &KDirListerTest::slotNewItems); m_dirLister.openUrl(QUrl::fromLocalFile(path), KDirLister::NoFlags); QTRY_COMPARE(m_items.count(), origItemCount - 100); QVERIFY(m_dirLister.isFinished()); disconnect(&m_dirLister, nullptr, this, nullptr); QCOMPARE(fileCount(), m_items.count()); } void KDirListerTest::testRenameItem() { m_refreshedItems2.clear(); const QString dirPath = m_tempDir.path() + '/'; connect(&m_dirLister, &KCoreDirLister::refreshItems, this, &KDirListerTest::slotRefreshItems2); const QString path = dirPath + "toplevelfile_2"; const QString newPath = dirPath + "toplevelfile_2.renamed.html"; KIO::SimpleJob *job = KIO::rename(QUrl::fromLocalFile(path), QUrl::fromLocalFile(newPath), KIO::HideProgressInfo); QVERIFY(job->exec()); QSignalSpy spyRefreshItems(&m_dirLister, SIGNAL(refreshItems(QList >))); QVERIFY(spyRefreshItems.wait(2000)); QTRY_COMPARE(m_refreshedItems2.count(), 1); QPair entry = m_refreshedItems2.first(); QCOMPARE(entry.first.url().toLocalFile(), path); QCOMPARE(entry.first.mimetype(), QString("application/octet-stream")); QCOMPARE(entry.second.url().toLocalFile(), newPath); QCOMPARE(entry.second.mimetype(), QString("text/html")); disconnect(&m_dirLister, nullptr, this, nullptr); // Let's see what KDirLister has in cache now KFileItem cachedItem = m_dirLister.findByUrl(QUrl::fromLocalFile(newPath)); QVERIFY(!cachedItem.isNull()); QCOMPARE(cachedItem.url().toLocalFile(), newPath); KFileItem oldCachedItem = m_dirLister.findByUrl(QUrl::fromLocalFile(path)); QVERIFY(oldCachedItem.isNull()); m_refreshedItems2.clear(); } void KDirListerTest::testRenameAndOverwrite() // has to be run after testRenameItem { // Rename toplevelfile_2.renamed.html to toplevelfile_2, overwriting it. const QString dirPath = m_tempDir.path() + '/'; const QString path = dirPath + "toplevelfile_2"; createTestFile(path); #if WORKAROUND_BROKEN_INOTIFY org::kde::KDirNotify::emitFilesAdded(dirPath); #endif KFileItem existingItem; while (existingItem.isNull()) { QTest::qWait(100); existingItem = m_dirLister.findByUrl(QUrl::fromLocalFile(path)); }; QCOMPARE(existingItem.url().toLocalFile(), path); m_refreshedItems.clear(); connect(&m_dirLister, &KCoreDirLister::refreshItems, this, &KDirListerTest::slotRefreshItems); const QString newPath = dirPath + "toplevelfile_2.renamed.html"; KIO::SimpleJob *job = KIO::rename(QUrl::fromLocalFile(newPath), QUrl::fromLocalFile(path), KIO::Overwrite | KIO::HideProgressInfo); bool ok = job->exec(); QVERIFY(ok); if (m_refreshedItems.isEmpty()) { QTRY_VERIFY(!m_refreshedItems.isEmpty()); // could come from KDirWatch or KDirNotify. } // Check that itemsDeleted was emitted -- preferably BEFORE refreshItems, // but we can't easily check that with QSignalSpy... QCOMPARE(m_dirLister.spyItemsDeleted.count(), 1); QCOMPARE(m_refreshedItems.count(), 1); QPair entry = m_refreshedItems.first(); QCOMPARE(entry.first.url().toLocalFile(), newPath); QCOMPARE(entry.second.url().toLocalFile(), path); disconnect(&m_dirLister, nullptr, this, nullptr); // Let's see what KDirLister has in cache now KFileItem cachedItem = m_dirLister.findByUrl(QUrl::fromLocalFile(path)); QCOMPARE(cachedItem.url().toLocalFile(), path); KFileItem oldCachedItem = m_dirLister.findByUrl(QUrl::fromLocalFile(newPath)); QVERIFY(oldCachedItem.isNull()); m_refreshedItems.clear(); } void KDirListerTest::testConcurrentListing() { const int origItemCount = m_items.count(); QCOMPARE(fileCount(), origItemCount); m_items.clear(); m_items2.clear(); MyDirLister dirLister2; const QString path = m_tempDir.path() + '/'; connect(&m_dirLister, &KCoreDirLister::newItems, this, &KDirListerTest::slotNewItems); connect(&dirLister2, &KCoreDirLister::newItems, this, &KDirListerTest::slotNewItems2); // Before dirLister2 has time to emit the items, let's make m_dirLister move to another dir. // This reproduces the use case "clicking on a folder in dolphin iconview, and dirlister2 // is the one used by the "folder panel". m_dirLister is going to list the subdir, // while dirLister2 wants to list the folder that m_dirLister has just left. dirLister2.stop(); // like dolphin does, noop. dirLister2.openUrl(QUrl::fromLocalFile(path), KDirLister::NoFlags); m_dirLister.openUrl(QUrl::fromLocalFile(path + "subdir"), KDirLister::NoFlags); QCOMPARE(m_dirLister.spyStarted.count(), 1); QCOMPARE(m_dirLister.spyCompleted.count(), 0); QCOMPARE(m_dirLister.spyCompletedQUrl.count(), 0); QCOMPARE(m_dirLister.spyCanceled.count(), 0); QCOMPARE(m_dirLister.spyCanceledQUrl.count(), 0); QCOMPARE(m_dirLister.spyClear.count(), 1); QCOMPARE(m_dirLister.spyClearQUrl.count(), 0); QCOMPARE(m_items.count(), 0); QCOMPARE(dirLister2.spyStarted.count(), 1); QCOMPARE(dirLister2.spyCompleted.count(), 0); QCOMPARE(dirLister2.spyCompletedQUrl.count(), 0); QCOMPARE(dirLister2.spyCanceled.count(), 0); QCOMPARE(dirLister2.spyCanceledQUrl.count(), 0); QCOMPARE(dirLister2.spyClear.count(), 1); QCOMPARE(dirLister2.spyClearQUrl.count(), 0); QCOMPARE(m_items2.count(), 0); QVERIFY(!m_dirLister.isFinished()); QVERIFY(!dirLister2.isFinished()); // then wait for completed qDebug("waiting for completed"); //QCOMPARE(m_dirLister.spyStarted.count(), 1); // 2 when subdir is already in cache. QTRY_COMPARE(m_dirLister.spyCompleted.count(), 1); QCOMPARE(m_dirLister.spyCompletedQUrl.count(), 1); QCOMPARE(m_dirLister.spyCanceled.count(), 0); QCOMPARE(m_dirLister.spyCanceledQUrl.count(), 0); QCOMPARE(m_dirLister.spyClear.count(), 1); QCOMPARE(m_dirLister.spyClearQUrl.count(), 0); QCOMPARE(m_items.count(), 3); QTRY_COMPARE(dirLister2.spyStarted.count(), 1); QTRY_COMPARE(dirLister2.spyCompleted.count(), 1); QCOMPARE(dirLister2.spyCompletedQUrl.count(), 1); QCOMPARE(dirLister2.spyCanceled.count(), 0); QCOMPARE(dirLister2.spyCanceledQUrl.count(), 0); QCOMPARE(dirLister2.spyClear.count(), 1); QCOMPARE(dirLister2.spyClearQUrl.count(), 0); QCOMPARE(m_items2.count(), origItemCount); if (!m_dirLister.isFinished()) { // false when an update is running because subdir is already in cache // TODO check why this fails QVERIFY(m_dirLister.spyCanceled.wait(1000)); QTest::qWait(1000); } disconnect(&m_dirLister, nullptr, this, nullptr); disconnect(&dirLister2, nullptr, this, nullptr); } void KDirListerTest::testConcurrentHoldingListing() { // #167851. // A dirlister holding the items, and a second dirlister does // openUrl(reload) (which triggers updateDirectory()) // and the first lister immediately does openUrl() (which emits cached items). testOpenUrl(); // ensure m_dirLister holds the items. const int origItemCount = m_items.count(); connect(&m_dirLister, &KCoreDirLister::newItems, this, &KDirListerTest::slotNewItems); m_items.clear(); m_items2.clear(); const QString path = m_tempDir.path() + '/'; MyDirLister dirLister2; connect(&dirLister2, &KCoreDirLister::newItems, this, &KDirListerTest::slotNewItems2); dirLister2.openUrl(QUrl::fromLocalFile(path), KDirLister::Reload); // will start a list job QCOMPARE(dirLister2.spyStarted.count(), 1); QCOMPARE(dirLister2.spyCompleted.count(), 0); QCOMPARE(m_items.count(), 0); QCOMPARE(m_items2.count(), 0); qDebug("calling m_dirLister.openUrl"); m_dirLister.openUrl(QUrl::fromLocalFile(path), KDirLister::NoFlags); // should emit cached items, and then "join" the running listjob QCOMPARE(m_dirLister.spyStarted.count(), 1); QCOMPARE(m_dirLister.spyCompleted.count(), 0); QCOMPARE(m_items.count(), 0); QCOMPARE(m_items2.count(), 0); qDebug("waiting for completed"); QTRY_COMPARE(dirLister2.spyStarted.count(), 1); QTRY_COMPARE(dirLister2.spyCompleted.count(), 1); QCOMPARE(dirLister2.spyCompletedQUrl.count(), 1); QCOMPARE(dirLister2.spyCanceled.count(), 0); QCOMPARE(dirLister2.spyCanceledQUrl.count(), 0); QCOMPARE(dirLister2.spyClear.count(), 1); QCOMPARE(dirLister2.spyClearQUrl.count(), 0); QCOMPARE(m_items2.count(), origItemCount); QTRY_COMPARE(m_dirLister.spyStarted.count(), 1); QTRY_COMPARE(m_dirLister.spyCompleted.count(), 1); QCOMPARE(m_dirLister.spyCompletedQUrl.count(), 1); QCOMPARE(m_dirLister.spyCanceled.count(), 0); QCOMPARE(m_dirLister.spyCanceledQUrl.count(), 0); QCOMPARE(m_dirLister.spyClear.count(), 1); QCOMPARE(m_dirLister.spyClearQUrl.count(), 0); QVERIFY(dirLister2.isFinished()); QVERIFY(m_dirLister.isFinished()); disconnect(&m_dirLister, nullptr, this, nullptr); QCOMPARE(m_items.count(), origItemCount); } void KDirListerTest::testConcurrentListingAndStop() { m_items.clear(); m_items2.clear(); MyDirLister dirLister2; // Use a new tempdir for this test, so that we don't use the cache at all. QTemporaryDir tempDir; const QString path = tempDir.path() + '/'; createTestFile(path + "file_1"); createTestFile(path + "file_2"); createTestFile(path + "file_3"); connect(&m_dirLister, &KCoreDirLister::newItems, this, &KDirListerTest::slotNewItems); connect(&dirLister2, &KCoreDirLister::newItems, this, &KDirListerTest::slotNewItems2); // Before m_dirLister has time to emit the items, let's make dirLister2 call stop(). // This should not stop the list job for m_dirLister (#267709). dirLister2.openUrl(QUrl::fromLocalFile(path), KDirLister::Reload); m_dirLister.openUrl(QUrl::fromLocalFile(path)/*, KDirLister::Reload*/); QCOMPARE(m_dirLister.spyStarted.count(), 1); QCOMPARE(m_dirLister.spyCompleted.count(), 0); QCOMPARE(m_dirLister.spyCompletedQUrl.count(), 0); QCOMPARE(m_dirLister.spyCanceled.count(), 0); QCOMPARE(m_dirLister.spyCanceledQUrl.count(), 0); QCOMPARE(m_dirLister.spyClear.count(), 1); QCOMPARE(m_dirLister.spyClearQUrl.count(), 0); QCOMPARE(m_items.count(), 0); QCOMPARE(dirLister2.spyStarted.count(), 1); QCOMPARE(dirLister2.spyCompleted.count(), 0); QCOMPARE(dirLister2.spyCompletedQUrl.count(), 0); QCOMPARE(dirLister2.spyCanceled.count(), 0); QCOMPARE(dirLister2.spyCanceledQUrl.count(), 0); QCOMPARE(dirLister2.spyClear.count(), 1); QCOMPARE(dirLister2.spyClearQUrl.count(), 0); QCOMPARE(m_items2.count(), 0); QVERIFY(!m_dirLister.isFinished()); QVERIFY(!dirLister2.isFinished()); dirLister2.stop(); QCOMPARE(dirLister2.spyStarted.count(), 1); QCOMPARE(dirLister2.spyCompleted.count(), 0); QCOMPARE(dirLister2.spyCompletedQUrl.count(), 0); QCOMPARE(dirLister2.spyCanceled.count(), 1); QCOMPARE(dirLister2.spyCanceledQUrl.count(), 1); QCOMPARE(dirLister2.spyClear.count(), 1); QCOMPARE(dirLister2.spyClearQUrl.count(), 0); QCOMPARE(m_items2.count(), 0); // then wait for completed qDebug("waiting for completed"); QTRY_COMPARE(m_items.count(), 3); QTRY_COMPARE(m_items2.count(), 0); //QCOMPARE(m_dirLister.spyStarted.count(), 1); // 2 when in cache QCOMPARE(m_dirLister.spyCompleted.count(), 1); QCOMPARE(m_dirLister.spyCompletedQUrl.count(), 1); QCOMPARE(m_dirLister.spyCanceled.count(), 0); QCOMPARE(m_dirLister.spyCanceledQUrl.count(), 0); QCOMPARE(m_dirLister.spyClear.count(), 1); QCOMPARE(m_dirLister.spyClearQUrl.count(), 0); disconnect(&m_dirLister, nullptr, this, nullptr); } void KDirListerTest::testDeleteListerEarly() { // Do the same again, it should behave the same, even with the items in the cache testOpenUrl(); // Start a second lister, it will get a cached items job, but delete it before the job can run //qDebug() << "=========================================="; { m_items.clear(); const QString path = m_tempDir.path() + '/'; MyDirLister secondDirLister; secondDirLister.openUrl(QUrl::fromLocalFile(path), KDirLister::NoFlags); QVERIFY(!secondDirLister.isFinished()); } //qDebug() << "=========================================="; // Check if we didn't keep the deleted dirlister in one of our lists. // I guess the best way to do that is to just list the same dir again. testOpenUrl(); } void KDirListerTest::testOpenUrlTwice() { // Calling openUrl(reload)+openUrl(normal) before listing even starts. const int origItemCount = m_items.count(); m_items.clear(); const QString path = m_tempDir.path() + '/'; MyDirLister secondDirLister; connect(&secondDirLister, &KCoreDirLister::newItems, this, &KDirListerTest::slotNewItems); secondDirLister.openUrl(QUrl::fromLocalFile(path), KDirLister::Reload); // will start QCOMPARE(secondDirLister.spyStarted.count(), 1); QCOMPARE(secondDirLister.spyCompleted.count(), 0); qDebug("calling openUrl again"); secondDirLister.openUrl(QUrl::fromLocalFile(path), KDirLister::NoFlags); // will stop + start qDebug("waiting for completed"); QTRY_COMPARE(secondDirLister.spyStarted.count(), 2); QTRY_COMPARE(secondDirLister.spyCompleted.count(), 1); QCOMPARE(secondDirLister.spyCompletedQUrl.count(), 1); QCOMPARE(secondDirLister.spyCanceled.count(), 0); // should not be emitted, see next test QCOMPARE(secondDirLister.spyCanceledQUrl.count(), 0); QCOMPARE(secondDirLister.spyClear.count(), 2); QCOMPARE(secondDirLister.spyClearQUrl.count(), 0); if (origItemCount) { // 0 if running this test separately QCOMPARE(m_items.count(), origItemCount); } QVERIFY(secondDirLister.isFinished()); disconnect(&secondDirLister, nullptr, this, nullptr); } void KDirListerTest::testOpenUrlTwiceWithKeep() { // Calling openUrl(reload)+openUrl(keep) on a new dir, // before listing even starts (#177387) // Well, in 177387 the second openUrl call was made from within slotCanceled // called by the first openUrl // (slotLoadingFinished -> setCurrentItem -> expandToUrl -> listDir), // which messed things up in kdirlister (unexpected reentrancy). m_items.clear(); const QString path = m_tempDir.path() + "/newsubdir"; QDir().mkdir(path); MyDirLister secondDirLister; connect(&secondDirLister, &KCoreDirLister::newItems, this, &KDirListerTest::slotNewItems); secondDirLister.openUrl(QUrl::fromLocalFile(path)); // will start a list job QCOMPARE(secondDirLister.spyStarted.count(), 1); QCOMPARE(secondDirLister.spyCanceled.count(), 0); QCOMPARE(secondDirLister.spyCompleted.count(), 0); qDebug("calling openUrl again"); secondDirLister.openUrl(QUrl::fromLocalFile(path), KDirLister::Keep); // stops and restarts the job qDebug("waiting for completed"); QTRY_COMPARE(secondDirLister.spyStarted.count(), 2); QTRY_COMPARE(secondDirLister.spyCompleted.count(), 1); QCOMPARE(secondDirLister.spyCompletedQUrl.count(), 1); QCOMPARE(secondDirLister.spyCanceled.count(), 0); // should not be emitted, it led to recursion QCOMPARE(secondDirLister.spyCanceledQUrl.count(), 0); QCOMPARE(secondDirLister.spyClear.count(), 1); QCOMPARE(secondDirLister.spyClearQUrl.count(), 1); QCOMPARE(m_items.count(), 0); QVERIFY(secondDirLister.isFinished()); disconnect(&secondDirLister, nullptr, this, nullptr); QDir().remove(path); } void KDirListerTest::testOpenAndStop() { m_items.clear(); const QString path = QStringLiteral("/"); // better not use a directory that we already listed! connect(&m_dirLister, &KCoreDirLister::newItems, this, &KDirListerTest::slotNewItems); m_dirLister.openUrl(QUrl::fromLocalFile(path), KDirLister::NoFlags); qDebug() << "Calling stop!"; m_dirLister.stop(); // we should also test stop(QUrl::fromLocalFile(path))... QCOMPARE(m_dirLister.spyStarted.count(), 1); // The call to openUrl itself, emits started QCOMPARE(m_dirLister.spyCompleted.count(), 0); // we had time to stop before the job even started QCOMPARE(m_dirLister.spyCompletedQUrl.count(), 0); QCOMPARE(m_dirLister.spyCanceled.count(), 1); QCOMPARE(m_dirLister.spyCanceledQUrl.count(), 1); QCOMPARE(m_dirLister.spyClear.count(), 1); QCOMPARE(m_dirLister.spyClearQUrl.count(), 0); QCOMPARE(m_items.count(), 0); // we had time to stop before the job even started QVERIFY(m_dirLister.isFinished()); disconnect(&m_dirLister, nullptr, this, nullptr); } // A bug in the decAutoUpdate/incAutoUpdate logic made KDirLister stop watching a directory for changes, // and never watch it again when opening it from the cache. void KDirListerTest::testBug211472() { m_items.clear(); QTemporaryDir newDir; const QString path = newDir.path() + "/newsubdir/"; QDir().mkdir(path); MyDirLister dirLister; connect(&dirLister, &KCoreDirLister::newItems, this, &KDirListerTest::slotNewItems); dirLister.openUrl(QUrl::fromLocalFile(path)); QSignalSpy spyCompleted(&dirLister, SIGNAL(completed())); QVERIFY(spyCompleted.wait(1000)); QVERIFY(dirLister.isFinished()); QVERIFY(m_items.isEmpty()); if (true) { // This block is required to trigger bug 211472. // Go 'up' to the parent of 'newsubdir'. dirLister.openUrl(QUrl::fromLocalFile(newDir.path())); QVERIFY(spyCompleted.wait(1000)); QTRY_VERIFY(dirLister.isFinished()); QTRY_VERIFY(!m_items.isEmpty()); m_items.clear(); // Create a file in 'newsubdir' while we are listing its parent dir. createTestFile(path + "newFile-1"); // At this point, newsubdir is not used, so it's moved to the cache. // This happens in checkUpdate, called when receiving a notification for the cached dir, // this is why this unittest needs to create a test file in the subdir. // wait a second and ensure the list is still empty afterwards QTest::qWait(1000); QTRY_VERIFY(m_items.isEmpty()); // Return to 'newsubdir'. It will be emitted from the cache, then an update will happen. dirLister.openUrl(QUrl::fromLocalFile(path)); // Check that completed is emited twice QVERIFY(spyCompleted.wait(1000)); QVERIFY(spyCompleted.wait(1000)); QTRY_VERIFY(dirLister.isFinished()); QTRY_COMPARE(m_items.count(), 1); m_items.clear(); } // Now try to create a second file in 'newsubdir' and verify that the // dir lister notices it. QTest::qWait(1000); // We need a 1s timestamp difference on the dir, otherwise FAM won't notice anything. createTestFile(path + "newFile-2"); QTRY_COMPARE(m_items.count(), 1); newDir.remove(); QSignalSpy spyClear(&dirLister, SIGNAL(clear())); QVERIFY(spyClear.wait(1000)); } void KDirListerTest::testRenameCurrentDir() // #294445 { m_items.clear(); const QString path = m_tempDir.path() + "/newsubdir-1"; QVERIFY(QDir().mkdir(path)); MyDirLister secondDirLister; connect(&secondDirLister, &KCoreDirLister::newItems, this, &KDirListerTest::slotNewItems); secondDirLister.openUrl(QUrl::fromLocalFile(path)); QSignalSpy spyCompleted(&secondDirLister, SIGNAL(completed())); QVERIFY(spyCompleted.wait(1000)); QVERIFY(secondDirLister.isFinished()); QVERIFY(m_items.empty()); QCOMPARE(secondDirLister.rootItem().url().toLocalFile(), path); const QString newPath = m_tempDir.path() + "/newsubdir-2"; QVERIFY(QDir().rename(path, newPath)); org::kde::KDirNotify::emitFileRenamed(QUrl::fromLocalFile(path), QUrl::fromLocalFile(newPath)); QSignalSpy spyRedirection(&secondDirLister, SIGNAL(redirection(QUrl,QUrl))); QVERIFY(spyRedirection.wait(1000)); // Check that the URL of the root item got updated QCOMPARE(secondDirLister.rootItem().url().toLocalFile(), newPath); disconnect(&secondDirLister, nullptr, this, nullptr); QDir().rmdir(newPath); } void KDirListerTest::slotOpenUrlOnRename(const QUrl &newUrl) { QVERIFY(m_dirLister.openUrl(newUrl)); } //This tests for a crash if you connect redirects to openUrl, due //to internal data being inconsistently exposed. //Matches usage in gwenview. void KDirListerTest::testRenameCurrentDirOpenUrl() { m_items.clear(); const QString path = m_tempDir.path() + "/newsubdir-1/"; QVERIFY(QDir().mkdir(path)); connect(&m_dirLister, &KCoreDirLister::newItems, this, &KDirListerTest::slotNewItems); m_dirLister.openUrl(QUrl::fromLocalFile(path)); QSignalSpy spyCompleted(&m_dirLister, SIGNAL(completed())); // Wait for the signal completed to be emited QVERIFY(spyCompleted.wait(1000)); QVERIFY(m_dirLister.isFinished()); const QString newPath = m_tempDir.path() + "/newsubdir-2"; QVERIFY(QDir().rename(path, newPath)); org::kde::KDirNotify::emitFileRenamed(QUrl::fromLocalFile(path), QUrl::fromLocalFile(newPath)); //Connect the redirection to openURL, so that on a rename the new location is opened. //This matches usage in gwenview, and crashes connect(&m_dirLister, QOverload::of(&KCoreDirLister::redirection), this, &KDirListerTest::slotOpenUrlOnRename); QTRY_VERIFY(m_dirLister.isFinished()); disconnect(&m_dirLister, nullptr, this, nullptr); QDir().rmdir(newPath); } void KDirListerTest::testRedirection() { m_items.clear(); const QUrl url(QStringLiteral("file://somemachine/")); if (!KProtocolInfo::isKnownProtocol(QStringLiteral("smb"))) { QSKIP("smb not installed"); } connect(&m_dirLister, &KCoreDirLister::newItems, this, &KDirListerTest::slotNewItems); // The call to openUrl itself, emits started m_dirLister.openUrl(url, KDirLister::NoFlags); QCOMPARE(m_dirLister.spyStarted.count(), 1); QCOMPARE(m_dirLister.spyCompleted.count(), 0); QCOMPARE(m_dirLister.spyCompletedQUrl.count(), 0); QCOMPARE(m_dirLister.spyCanceled.count(), 0); QCOMPARE(m_dirLister.spyCanceledQUrl.count(), 0); QCOMPARE(m_dirLister.spyClear.count(), 1); QCOMPARE(m_dirLister.spyClearQUrl.count(), 0); QCOMPARE(m_dirLister.spyRedirection.count(), 0); QCOMPARE(m_items.count(), 0); QVERIFY(!m_dirLister.isFinished()); // then wait for the redirection signal qDebug("waiting for redirection"); QTRY_COMPARE(m_dirLister.spyStarted.count(), 1); QCOMPARE(m_dirLister.spyCompleted.count(), 0); // we stopped before the listing. QCOMPARE(m_dirLister.spyCompletedQUrl.count(), 0); QCOMPARE(m_dirLister.spyCanceled.count(), 0); QCOMPARE(m_dirLister.spyCanceledQUrl.count(), 0); QTRY_COMPARE(m_dirLister.spyClear.count(), 2); // redirection cleared a second time (just in case...) QCOMPARE(m_dirLister.spyClearQUrl.count(), 0); QTRY_COMPARE(m_dirLister.spyRedirection.count(), 1); QVERIFY(m_items.isEmpty()); QVERIFY(!m_dirLister.isFinished()); m_dirLister.stop(url); QVERIFY(!m_dirLister.isFinished()); disconnect(&m_dirLister, nullptr, this, nullptr); } void KDirListerTest::testListEmptyDirFromCache() // #278431 { m_items.clear(); QTemporaryDir newDir; const QUrl url = QUrl::fromLocalFile(newDir.path()); // List and watch an empty dir connect(&m_dirLister, &KCoreDirLister::newItems, this, &KDirListerTest::slotNewItems); m_dirLister.openUrl(url); QSignalSpy spyCompleted(&m_dirLister, SIGNAL(completed())); QVERIFY(spyCompleted.wait(1000)); QVERIFY(m_dirLister.isFinished()); QVERIFY(m_items.isEmpty()); // List it with two more dirlisters (one will create a cached items job, the second should also benefit from it) MyDirLister secondDirLister; connect(&secondDirLister, &KCoreDirLister::newItems, this, &KDirListerTest::slotNewItems); secondDirLister.openUrl(url); MyDirLister thirdDirLister; connect(&thirdDirLister, &KCoreDirLister::newItems, this, &KDirListerTest::slotNewItems); thirdDirLister.openUrl(url); // The point of this test is that (with DEBUG_CACHE enabled) it used to assert here // with "HUH? Lister KDirLister(0x7ffd1f044260) is supposed to be listing, but has no job!" // due to the if (!itemU->lstItems.isEmpty()) check which is now removed. QVERIFY(!secondDirLister.isFinished()); // we didn't go to the event loop yet QSignalSpy spySecondCompleted(&secondDirLister, SIGNAL(completed())); QVERIFY(spySecondCompleted.wait(1000)); if (!thirdDirLister.isFinished()) { QSignalSpy spyThirdCompleted(&thirdDirLister, SIGNAL(completed())); QVERIFY(spyThirdCompleted.wait(1000)); } } void KDirListerTest::testWatchingAfterCopyJob() // #331582 { m_items.clear(); QTemporaryDir newDir; const QString path = newDir.path() + '/'; // List and watch an empty dir connect(&m_dirLister, &KCoreDirLister::newItems, this, &KDirListerTest::slotNewItems); m_dirLister.openUrl(QUrl::fromLocalFile(path)); QSignalSpy spyCompleted(&m_dirLister, SIGNAL(completed())); QVERIFY(spyCompleted.wait(1000)); QVERIFY(m_dirLister.isFinished()); QVERIFY(m_items.isEmpty()); // Create three subfolders. QVERIFY(QDir().mkdir(path + "New Folder")); QVERIFY(QDir().mkdir(path + "New Folder 1")); QVERIFY(QDir().mkdir(path + "New Folder 2")); QVERIFY(spyCompleted.wait(1000)); QTRY_VERIFY(m_dirLister.isFinished()); QTRY_COMPARE(m_items.count(), 3); // Create a new file and verify that the dir lister notices it. m_items.clear(); createTestFile(path + "a"); QVERIFY(spyCompleted.wait(1000)); QTRY_VERIFY(m_dirLister.isFinished()); QTRY_COMPARE(m_items.count(), 1); // Rename one of the subfolders. const QString oldPath = path + "New Folder 1"; const QString newPath = path + "New Folder 1a"; // NOTE: The following two lines are required to trigger the bug! KIO::Job *job = KIO::moveAs(QUrl::fromLocalFile(oldPath), QUrl::fromLocalFile(newPath), KIO::HideProgressInfo); job->exec(); // Now try to create a second new file and verify that the // dir lister notices it. m_items.clear(); createTestFile(path + "b"); // This should end up in "KCoreDirListerCache::slotFileDirty" QTRY_COMPARE(m_items.count(), 1); newDir.remove(); QSignalSpy clearSpy(&m_dirLister, SIGNAL(clear())); QVERIFY(clearSpy.wait(1000)); } void KDirListerTest::testRemoveWatchedDirectory() { m_items.clear(); QTemporaryDir newDir; const QString path = newDir.path() + '/'; // List and watch an empty dir connect(&m_dirLister, &KCoreDirLister::newItems, this, &KDirListerTest::slotNewItems); m_dirLister.openUrl(QUrl::fromLocalFile(path)); QSignalSpy spyCompleted(&m_dirLister, SIGNAL(completed())); QVERIFY(spyCompleted.wait(1000)); QTRY_VERIFY(m_dirLister.isFinished()); QTRY_VERIFY(m_items.isEmpty()); // Create a subfolder. const QString subDirPath = path + "abc"; QVERIFY(QDir().mkdir(subDirPath)); QVERIFY(spyCompleted.wait(1000)); QTRY_VERIFY(m_dirLister.isFinished()); QTRY_COMPARE(m_items.count(), 1); const KFileItem item = m_items.at(0); // Watch the subfolder for changes, independently. // This is what triggers the bug. // (Technically, this could become a KDirWatch unittest, but if one day we use QFSW, good to have the tests here) KDirWatch watcher; watcher.addDir(subDirPath); // Remove the subfolder. m_items.clear(); QVERIFY(QDir().rmdir(path + "abc")); // This should trigger an update. QVERIFY(spyCompleted.wait(1000)); QVERIFY(m_dirLister.isFinished()); QCOMPARE(m_items.count(), 0); QCOMPARE(m_dirLister.spyItemsDeleted.count(), 1); const KFileItem deletedItem = m_dirLister.spyItemsDeleted.at(0).at(0).value().at(0); QCOMPARE(item, deletedItem); } void KDirListerTest::testDirPermissionChange() { QTemporaryDir tempDir; const QString path = tempDir.path() + '/'; const QString subdir = path + QLatin1String("subdir"); QVERIFY(QDir().mkdir(subdir)); MyDirLister mylister; mylister.openUrl(QUrl::fromLocalFile(tempDir.path())); QSignalSpy spyCompleted(&mylister, SIGNAL(completed())); QVERIFY(spyCompleted.wait(1000)); KFileItemList list = mylister.items(); QVERIFY(mylister.isFinished()); QCOMPARE(list.count(), 1); QCOMPARE(mylister.rootItem().url().toLocalFile(), tempDir.path()); const mode_t permissions = (S_IRUSR | S_IWUSR | S_IXUSR); KIO::SimpleJob *job = KIO::chmod(list.first().url(), permissions); QVERIFY(job->exec()); QSignalSpy spyRefreshItems(&mylister, SIGNAL(refreshItems(QList >))); QVERIFY(spyRefreshItems.wait(2000)); list = mylister.items(); QCOMPARE(list.first().permissions(), permissions); QVERIFY(QDir().rmdir(subdir)); } void KDirListerTest::slotNewItems(const KFileItemList &lst) { m_items += lst; } void KDirListerTest::slotNewItems2(const KFileItemList &lst) { m_items2 += lst; } void KDirListerTest::slotRefreshItems(const QList > &lst) { m_refreshedItems += lst; emit refreshItemsReceived(); } void KDirListerTest::slotRefreshItems2(const QList > &lst) { m_refreshedItems2 += lst; } void KDirListerTest::testCopyAfterListingAndMove() // #353195 { const QString dirA = m_tempDir.path() + "/a"; QVERIFY(QDir().mkdir(dirA)); const QString dirB = m_tempDir.path() + "/b"; QVERIFY(QDir().mkdir(dirB)); // ensure m_dirLister holds the items. m_dirLister.openUrl(QUrl::fromLocalFile(path()), KDirLister::NoFlags); QSignalSpy spyCompleted(&m_dirLister, SIGNAL(completed())); QVERIFY(spyCompleted.wait()); // Move b into a KIO::Job *moveJob = KIO::move(QUrl::fromLocalFile(dirB), QUrl::fromLocalFile(dirA)); moveJob->setUiDelegate(nullptr); QVERIFY(moveJob->exec()); QVERIFY(QFileInfo(m_tempDir.path() + "/a/b").isDir()); // Give some time to processPendingUpdates QTest::qWait(1000); // Copy folder a elsewhere const QString dest = m_tempDir.path() + "/subdir"; KIO::Job *copyJob = KIO::copy(QUrl::fromLocalFile(dirA), QUrl::fromLocalFile(dest)); copyJob->setUiDelegate(nullptr); QVERIFY(copyJob->exec()); QVERIFY(QFileInfo(m_tempDir.path() + "/subdir/a/b").isDir()); } void KDirListerTest::testDeleteCurrentDir() { // ensure m_dirLister holds the items. m_dirLister.openUrl(QUrl::fromLocalFile(path()), KDirLister::NoFlags); m_dirLister.clearSpies(); KIO::DeleteJob *job = KIO::del(QUrl::fromLocalFile(path()), KIO::HideProgressInfo); bool ok = job->exec(); QVERIFY(ok); QTRY_COMPARE(m_dirLister.spyClear.count(), 1); QCOMPARE(m_dirLister.spyClearQUrl.count(), 0); QList deletedUrls; for (int i = 0; i < m_dirLister.spyItemsDeleted.count(); ++i) { deletedUrls += m_dirLister.spyItemsDeleted[i][0].value().urlList(); } //qDebug() << deletedUrls; QUrl currentDirUrl = QUrl::fromLocalFile(path()).adjusted(QUrl::StripTrailingSlash); // Sometimes I get ("current/subdir", "current") here, but that seems ok. QVERIFY(deletedUrls.contains(currentDirUrl)); } int KDirListerTest::fileCount() const { return QDir(path()).entryList(QDir::AllEntries | QDir::NoDotAndDotDot).count(); } void KDirListerTest::createSimpleFile(const QString &fileName) { QFile file(fileName); QVERIFY(file.open(QIODevice::WriteOnly)); file.write(QByteArray("foo")); file.close(); } void KDirListerTest::fillDirLister2(MyDirLister &lister, const QString &path) { m_items2.clear(); connect(&lister, &KCoreDirLister::newItems, this, &KDirListerTest::slotNewItems2); connect(&m_dirLister, &KCoreDirLister::refreshItems, this, &KDirListerTest::slotRefreshItems2); lister.openUrl(QUrl::fromLocalFile(path), KDirLister::NoFlags); QTRY_VERIFY(lister.isFinished()); } void KDirListerTest::waitUntilMTimeChange(const QString &path) { // Wait until the current second is more than the file's mtime // otherwise this change will go unnoticed QFileInfo fi(path); QVERIFY(fi.exists()); const QDateTime ctime = qMax(fi.lastModified(), fi.created()); waitUntilAfter(ctime); } void KDirListerTest::waitUntilAfter(const QDateTime &ctime) { int totalWait = 0; QDateTime now; Q_FOREVER { now = QDateTime::currentDateTime(); if (now.toTime_t() == ctime.toTime_t()) { // truncate milliseconds totalWait += 50; QTest::qWait(50); } else { QVERIFY(now > ctime); // can't go back in time ;) QTest::qWait(50); // be safe break; } } //if (totalWait > 0) qDebug() << "Waited" << totalWait << "ms so that now" << now.toString(Qt::ISODate) << "is >" << ctime.toString(Qt::ISODate); } #include "moc_kdirlistertest.cpp" diff --git a/autotests/kdirlistertest.h b/autotests/kdirlistertest.h index f01edda9..c77e6043 100644 --- a/autotests/kdirlistertest.h +++ b/autotests/kdirlistertest.h @@ -1,150 +1,151 @@ /* This file is part of the KDE project Copyright (C) 2007 David Faure This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KDIRLISTERTEST_H #define KDIRLISTERTEST_H #include #include #include #include #include #include Q_DECLARE_METATYPE(KFileItemList) class GlobalInits { public: GlobalInits() { // Must be done before the QSignalSpys connect qRegisterMetaType(); qRegisterMetaType(); } }; class MyDirLister : public KDirLister, GlobalInits { public: MyDirLister() : spyStarted(this, SIGNAL(started(QUrl))), spyClear(this, SIGNAL(clear())), spyClearQUrl(this, SIGNAL(clear(QUrl))), spyCompleted(this, SIGNAL(completed())), spyCompletedQUrl(this, SIGNAL(completed(QUrl))), spyCanceled(this, SIGNAL(canceled())), spyCanceledQUrl(this, SIGNAL(canceled(QUrl))), spyRedirection(this, SIGNAL(redirection(QUrl))), spyItemsDeleted(this, SIGNAL(itemsDeleted(KFileItemList))) {} void clearSpies() { spyStarted.clear(); spyClear.clear(); spyClearQUrl.clear(); spyCompleted.clear(); spyCompletedQUrl.clear(); spyCanceled.clear(); spyCanceledQUrl.clear(); spyRedirection.clear(); spyItemsDeleted.clear(); } QSignalSpy spyStarted; QSignalSpy spyClear; QSignalSpy spyClearQUrl; QSignalSpy spyCompleted; QSignalSpy spyCompletedQUrl; QSignalSpy spyCanceled; QSignalSpy spyCanceledQUrl; QSignalSpy spyRedirection; QSignalSpy spyItemsDeleted; protected: void handleError(KIO::Job *job) override; }; class KDirListerTest : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase(); void cleanup(); void testOpenUrl(); void testOpenUrlFromCache(); void testNewItem(); void testNewItems(); + void benchFindByUrl(); void testNewItemByCopy(); void testNewItemsInSymlink(); void testRefreshItems(); void testRefreshRootItem(); void testDeleteItem(); void testDeleteItems(); void testRenameItem(); void testRenameAndOverwrite(); void testConcurrentListing(); void testConcurrentHoldingListing(); void testConcurrentListingAndStop(); void testDeleteListerEarly(); void testOpenUrlTwice(); void testOpenUrlTwiceWithKeep(); void testOpenAndStop(); void testBug211472(); void testRenameCurrentDir(); void testRenameCurrentDirOpenUrl(); void testRedirection(); void testListEmptyDirFromCache(); void testWatchingAfterCopyJob(); void testRemoveWatchedDirectory(); void testDirPermissionChange(); void testCopyAfterListingAndMove(); // #353195 void testDeleteCurrentDir(); // must be last! protected Q_SLOTS: // 'more private than private slots' - i.e. not seen by qtestlib void slotNewItems(const KFileItemList &); void slotNewItems2(const KFileItemList &); void slotRefreshItems(const QList > &); void slotRefreshItems2(const QList > &); void slotOpenUrlOnRename(const QUrl &); Q_SIGNALS: void refreshItemsReceived(); private: int fileCount() const; QString path() const { return m_tempDir.path() + '/'; } void createSimpleFile(const QString &fileName); void fillDirLister2(MyDirLister &lister, const QString &path); void waitUntilMTimeChange(const QString &path); void waitUntilAfter(const QDateTime &ctime); private: int m_exitCount; QEventLoop m_eventLoop; QTemporaryDir m_tempDir; MyDirLister m_dirLister; KFileItemList m_items; KFileItemList m_items2; QList > m_refreshedItems, m_refreshedItems2; }; #endif diff --git a/src/core/kcoredirlister.cpp b/src/core/kcoredirlister.cpp index 82111273..5c3652ab 100644 --- a/src/core/kcoredirlister.cpp +++ b/src/core/kcoredirlister.cpp @@ -1,2844 +1,2809 @@ /* This file is part of the KDE project Copyright (C) 1998, 1999 Torben Weis 2000 Carsten Pfeiffer 2003-2005 David Faure 2001-2006 Michael Brade This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kcoredirlister.h" #include "kcoredirlister_p.h" #include #include #include "kprotocolmanager.h" #include "kmountpoint.h" #include "kiocoredebug.h" #include "../pathhelpers_p.h" #include #include #include #include #include #include #include #include Q_DECLARE_LOGGING_CATEGORY(KIO_CORE_DIRLISTER) Q_LOGGING_CATEGORY(KIO_CORE_DIRLISTER, "kf5.kio.core.dirlister", QtWarningMsg) // Enable this to get printDebug() called often, to see the contents of the cache //#define DEBUG_CACHE // Make really sure it doesn't get activated in the final build #ifdef NDEBUG #undef DEBUG_CACHE #endif Q_GLOBAL_STATIC(KCoreDirListerCache, kDirListerCache) KCoreDirListerCache::KCoreDirListerCache() : itemsCached(10), // keep the last 10 directories around m_cacheHiddenFiles(10) // keep the last 10 ".hidden" files around { qCDebug(KIO_CORE_DIRLISTER); connect(&pendingUpdateTimer, &QTimer::timeout, this, &KCoreDirListerCache::processPendingUpdates); pendingUpdateTimer.setSingleShot(true); connect(KDirWatch::self(), &KDirWatch::dirty, this, &KCoreDirListerCache::slotFileDirty); connect(KDirWatch::self(), &KDirWatch::created, this, &KCoreDirListerCache::slotFileCreated); connect(KDirWatch::self(), &KDirWatch::deleted, this, &KCoreDirListerCache::slotFileDeleted); kdirnotify = new org::kde::KDirNotify(QString(), QString(), QDBusConnection::sessionBus(), this); connect(kdirnotify, &org::kde::KDirNotify::FileRenamedWithLocalPath, this, &KCoreDirListerCache::slotFileRenamed); connect(kdirnotify, &org::kde::KDirNotify::FilesAdded , this, &KCoreDirListerCache::slotFilesAdded); connect(kdirnotify, &org::kde::KDirNotify::FilesChanged, this, &KCoreDirListerCache::slotFilesChanged); connect(kdirnotify, &org::kde::KDirNotify::FilesRemoved, this, QOverload::of(&KCoreDirListerCache::slotFilesRemoved)); // Probably not needed in KF5 anymore: // The use of KUrl::url() in ~DirItem (sendSignal) crashes if the static for QRegExpEngine got deleted already, // so we need to destroy the KCoreDirListerCache before that. //qAddPostRoutine(kDirListerCache.destroy); } KCoreDirListerCache::~KCoreDirListerCache() { qCDebug(KIO_CORE_DIRLISTER); qDeleteAll(itemsInUse); itemsInUse.clear(); itemsCached.clear(); directoryData.clear(); m_cacheHiddenFiles.clear(); if (KDirWatch::exists()) { KDirWatch::self()->disconnect(this); } } // setting _reload to true will emit the old files and // call updateDirectory bool KCoreDirListerCache::listDir(KCoreDirLister *lister, const QUrl &_u, bool _keep, bool _reload) { QUrl _url(_u); _url.setPath(QDir::cleanPath(_url.path())); // kill consecutive slashes if (!_url.host().isEmpty() && KProtocolInfo::protocolClass(_url.scheme()) == QLatin1String(":local") && _url.scheme() != QLatin1String("file")) { // ":local" protocols ignore the hostname, so strip it out preventively - #160057 // kio_file is special cased since it does honor the hostname (by redirecting to e.g. smb) _url.setHost(QString()); if (_keep == false) { emit lister->redirection(_url); } } // like this we don't have to worry about trailing slashes any further _url = _url.adjusted(QUrl::StripTrailingSlash); QString resolved; if (_url.isLocalFile()) { // Resolve symlinks (#213799) const QString local = _url.toLocalFile(); resolved = QFileInfo(local).canonicalFilePath(); if (local != resolved) { canonicalUrls[QUrl::fromLocalFile(resolved)].append(_url); } // TODO: remove entry from canonicalUrls again in forgetDirs // Note: this is why we use a QStringList value in there rather than a QSet: // we can just remove one entry and not have to worry about other dirlisters // (the non-unicity of the stringlist gives us the refcounting, basically). } if (!validUrl(lister, _url)) { qCDebug(KIO_CORE_DIRLISTER) << lister << "url=" << _url << "not a valid url"; return false; } qCDebug(KIO_CORE_DIRLISTER) << lister << "url=" << _url << "keep=" << _keep << "reload=" << _reload; #ifdef DEBUG_CACHE printDebug(); #endif if (!_keep) { // stop any running jobs for lister stop(lister, true /*silent*/); // clear our internal list for lister forgetDirs(lister); lister->d->rootFileItem = KFileItem(); } else if (lister->d->lstDirs.contains(_url)) { // stop the job listing _url for this lister stopListingUrl(lister, _url, true /*silent*/); // remove the _url as well, it will be added in a couple of lines again! // forgetDirs with three args does not do this // TODO: think about moving this into forgetDirs lister->d->lstDirs.removeAll(_url); // clear _url for lister forgetDirs(lister, _url, true); if (lister->d->url == _url) { lister->d->rootFileItem = KFileItem(); } } lister->d->complete = false; lister->d->lstDirs.append(_url); if (lister->d->url.isEmpty() || !_keep) { // set toplevel URL only if not set yet lister->d->url = _url; } DirItem *itemU = itemsInUse.value(_url); KCoreDirListerCacheDirectoryData &dirData = directoryData[_url]; // find or insert if (dirData.listersCurrentlyListing.isEmpty()) { // if there is an update running for _url already we get into // the following case - it will just be restarted by updateDirectory(). dirData.listersCurrentlyListing.append(lister); DirItem *itemFromCache = nullptr; if (itemU || (!_reload && (itemFromCache = itemsCached.take(_url)))) { if (itemU) { qCDebug(KIO_CORE_DIRLISTER) << "Entry already in use:" << _url; // if _reload is set, then we'll emit cached items and then updateDirectory. } else { qCDebug(KIO_CORE_DIRLISTER) << "Entry in cache:" << _url; itemsInUse.insert(_url, itemFromCache); itemU = itemFromCache; } if (lister->d->autoUpdate) { itemU->incAutoUpdate(); } if (itemFromCache && itemFromCache->watchedWhileInCache) { itemFromCache->watchedWhileInCache = false;; itemFromCache->decAutoUpdate(); } emit lister->started(_url); // List items from the cache in a delayed manner, just like things would happen // if we were not using the cache. new KCoreDirLister::Private::CachedItemsJob(lister, _url, _reload); } else { // dir not in cache or _reload is true if (_reload) { qCDebug(KIO_CORE_DIRLISTER) << "Reloading directory:" << _url; itemsCached.remove(_url); } else { qCDebug(KIO_CORE_DIRLISTER) << "Listing directory:" << _url; } itemU = new DirItem(_url, resolved); itemsInUse.insert(_url, itemU); if (lister->d->autoUpdate) { itemU->incAutoUpdate(); } // // we have a limit of MAX_JOBS_PER_LISTER concurrently running jobs // if ( lister->d->numJobs() >= MAX_JOBS_PER_LISTER ) // { // pendingUpdates.insert( _url ); // } // else { KIO::ListJob *job = KIO::listDir(_url, KIO::HideProgressInfo); runningListJobs.insert(job, KIO::UDSEntryList()); lister->jobStarted(job); lister->d->connectJob(job); connect(job, &KIO::ListJob::entries, this, &KCoreDirListerCache::slotEntries); connect(job, &KJob::result, this, &KCoreDirListerCache::slotResult); connect(job, &KIO::ListJob::redirection, this, &KCoreDirListerCache::slotRedirection); emit lister->started(_url); } qCDebug(KIO_CORE_DIRLISTER) << "Entry now being listed by" << dirData.listersCurrentlyListing; } } else { qCDebug(KIO_CORE_DIRLISTER) << "Entry currently being listed:" << _url << "by" << dirData.listersCurrentlyListing; #ifdef DEBUG_CACHE printDebug(); #endif emit lister->started(_url); // Maybe listersCurrentlyListing/listersCurrentlyHolding should be QSets? Q_ASSERT(!dirData.listersCurrentlyListing.contains(lister)); dirData.listersCurrentlyListing.append(lister); KIO::ListJob *job = jobForUrl(_url); // job will be 0 if we were listing from cache rather than listing from a kio job. if (job) { lister->jobStarted(job); lister->d->connectJob(job); } Q_ASSERT(itemU); // List existing items in a delayed manner, just like things would happen // if we were not using the cache. qCDebug(KIO_CORE_DIRLISTER) << "Listing" << itemU->lstItems.count() << "cached items soon"; KCoreDirLister::Private::CachedItemsJob* cachedItemsJob = new KCoreDirLister::Private::CachedItemsJob(lister, _url, _reload); if (job) { // The ListJob will take care of emitting completed. // ### If it finishes before the CachedItemsJob, then we'll emit cached items after completed(), not sure how bad this is. cachedItemsJob->setEmitCompleted(false); } #ifdef DEBUG_CACHE printDebug(); #endif } return true; } KCoreDirLister::Private::CachedItemsJob *KCoreDirLister::Private::cachedItemsJobForUrl(const QUrl &url) const { Q_FOREACH (CachedItemsJob *job, m_cachedItemsJobs) { if (job->url() == url) { return job; } } return nullptr; } KCoreDirLister::Private::CachedItemsJob::CachedItemsJob(KCoreDirLister *lister, const QUrl &url, bool reload) : KJob(lister), m_lister(lister), m_url(url), m_reload(reload), m_emitCompleted(true) { qCDebug(KIO_CORE_DIRLISTER) << "Creating CachedItemsJob" << this << "for lister" << lister << url; if (lister->d->cachedItemsJobForUrl(url)) { qCWarning(KIO_CORE) << "Lister" << lister << "has a cached items job already for" << url; } lister->d->m_cachedItemsJobs.append(this); setAutoDelete(true); start(); } // Called by start() via QueuedConnection void KCoreDirLister::Private::CachedItemsJob::done() { if (!m_lister) { // job was already killed, but waiting deletion due to deleteLater return; } kDirListerCache()->emitItemsFromCache(this, m_lister, m_url, m_reload, m_emitCompleted); emitResult(); } bool KCoreDirLister::Private::CachedItemsJob::doKill() { qCDebug(KIO_CORE_DIRLISTER) << this; kDirListerCache()->forgetCachedItemsJob(this, m_lister, m_url); if (!property("_kdlc_silent").toBool()) { emit m_lister->canceled(m_url); emit m_lister->canceled(); } m_lister = nullptr; return true; } void KCoreDirListerCache::emitItemsFromCache(KCoreDirLister::Private::CachedItemsJob *cachedItemsJob, KCoreDirLister *lister, const QUrl &_url, bool _reload, bool _emitCompleted) { KCoreDirLister::Private *kdl = lister->d; kdl->complete = false; DirItem *itemU = kDirListerCache()->itemsInUse.value(_url); if (!itemU) { qCWarning(KIO_CORE) << "Can't find item for directory" << _url << "anymore"; } else { - const NonMovableFileItemList items = itemU->lstItems; + const QList items = itemU->lstItems; const KFileItem rootItem = itemU->rootItem; _reload = _reload || !itemU->complete; if (kdl->rootFileItem.isNull() && !rootItem.isNull() && kdl->url == _url) { kdl->rootFileItem = rootItem; } if (!items.isEmpty()) { qCDebug(KIO_CORE_DIRLISTER) << "emitting" << items.count() << "for lister" << lister; kdl->addNewItems(_url, items); kdl->emitItems(); } } forgetCachedItemsJob(cachedItemsJob, lister, _url); // Emit completed, unless we were told not to, // or if listDir() was called while another directory listing for this dir was happening, // so we "joined" it. We detect that using jobForUrl to ensure it's a real ListJob, // not just a lister-specific CachedItemsJob (which wouldn't emit completed for us). if (_emitCompleted) { kdl->complete = true; emit lister->completed(_url); emit lister->completed(); if (_reload) { updateDirectory(_url); } } } void KCoreDirListerCache::forgetCachedItemsJob(KCoreDirLister::Private::CachedItemsJob *cachedItemsJob, KCoreDirLister *lister, const QUrl &_url) { // Modifications to data structures only below this point; // so that addNewItems is called with a consistent state lister->d->m_cachedItemsJobs.removeAll(cachedItemsJob); KCoreDirListerCacheDirectoryData &dirData = directoryData[_url]; Q_ASSERT(dirData.listersCurrentlyListing.contains(lister)); KIO::ListJob *listJob = jobForUrl(_url); if (!listJob) { Q_ASSERT(!dirData.listersCurrentlyHolding.contains(lister)); qCDebug(KIO_CORE_DIRLISTER) << "Moving from listing to holding, because no more job" << lister << _url; dirData.listersCurrentlyHolding.append(lister); dirData.listersCurrentlyListing.removeAll(lister); } else { qCDebug(KIO_CORE_DIRLISTER) << "Still having a listjob" << listJob << ", so not moving to currently-holding."; } } bool KCoreDirListerCache::validUrl(KCoreDirLister *lister, const QUrl &url) const { if (!url.isValid()) { qCWarning(KIO_CORE) << url.errorString(); lister->handleErrorMessage(i18n("Malformed URL\n%1", url.errorString())); return false; } if (!KProtocolManager::supportsListing(url)) { lister->handleErrorMessage(i18n("URL cannot be listed\n%1", url.toString())); return false; } return true; } void KCoreDirListerCache::stop(KCoreDirLister *lister, bool silent) { #ifdef DEBUG_CACHE //printDebug(); #endif qCDebug(KIO_CORE_DIRLISTER) << "lister:" << lister << "silent=" << silent; const QList urls = lister->d->lstDirs; Q_FOREACH (const QUrl &url, urls) { stopListingUrl(lister, url, silent); } #if 0 // test code QHash::iterator dirit = directoryData.begin(); const QHash::iterator dirend = directoryData.end(); for (; dirit != dirend; ++dirit) { KCoreDirListerCacheDirectoryData &dirData = dirit.value(); if (dirData.listersCurrentlyListing.contains(lister)) { qCDebug(KIO_CORE_DIRLISTER) << "ERROR: found lister" << lister << "in list - for" << dirit.key(); Q_ASSERT(false); } } #endif } void KCoreDirListerCache::stopListingUrl(KCoreDirLister *lister, const QUrl &_u, bool silent) { QUrl url(_u); url = url.adjusted(QUrl::StripTrailingSlash); KCoreDirLister::Private::CachedItemsJob *cachedItemsJob = lister->d->cachedItemsJobForUrl(url); if (cachedItemsJob) { if (silent) { cachedItemsJob->setProperty("_kdlc_silent", true); } cachedItemsJob->kill(); // removes job from list, too } // TODO: consider to stop all the "child jobs" of url as well qCDebug(KIO_CORE_DIRLISTER) << lister << " url=" << url; const auto dirit = directoryData.find(url); if (dirit == directoryData.end()) { return; } KCoreDirListerCacheDirectoryData &dirData = dirit.value(); if (dirData.listersCurrentlyListing.contains(lister)) { qCDebug(KIO_CORE_DIRLISTER) << " found lister" << lister << "in list - for" << url; if (dirData.listersCurrentlyListing.count() == 1) { // This was the only dirlister interested in the list job -> kill the job stopListJob(url, silent); } else { // Leave the job running for the other dirlisters, just unsubscribe us. dirData.listersCurrentlyListing.removeAll(lister); if (!silent) { emit lister->canceled(); emit lister->canceled(url); } } } } // Helper for stop() and stopListingUrl() void KCoreDirListerCache::stopListJob(const QUrl &url, bool silent) { // Old idea: if it's an update job, let's just leave the job running. // After all, update jobs do run for "listersCurrentlyHolding", // so there's no reason to kill them just because @p lister is now a holder. // However it could be a long-running non-local job (e.g. filenamesearch), which // the user wants to abort, and which will never be used for updating... // And in any case slotEntries/slotResult is not meant to be called by update jobs. // So, change of plan, let's kill it after all, in a way that triggers slotResult/slotUpdateResult. KIO::ListJob *job = jobForUrl(url); if (job) { qCDebug(KIO_CORE_DIRLISTER) << "Killing list job" << job << "for" << url; if (silent) { job->setProperty("_kdlc_silent", true); } job->kill(KJob::EmitResult); } } void KCoreDirListerCache::setAutoUpdate(KCoreDirLister *lister, bool enable) { // IMPORTANT: this method does not check for the current autoUpdate state! for (auto it = lister->d->lstDirs.constBegin(), cend = lister->d->lstDirs.constEnd(); it != cend; ++it) { DirItem *dirItem = itemsInUse.value(*it); Q_ASSERT(dirItem); if (enable) { dirItem->incAutoUpdate(); } else { dirItem->decAutoUpdate(); } } } void KCoreDirListerCache::forgetDirs(KCoreDirLister *lister) { qCDebug(KIO_CORE_DIRLISTER) << lister; emit lister->clear(); // clear lister->d->lstDirs before calling forgetDirs(), so that // it doesn't contain things that itemsInUse doesn't. When emitting // the canceled signals, lstDirs must not contain anything that // itemsInUse does not contain. (otherwise it might crash in findByName()). const QList lstDirsCopy = lister->d->lstDirs; lister->d->lstDirs.clear(); qCDebug(KIO_CORE_DIRLISTER) << "Iterating over dirs" << lstDirsCopy; for (QList::const_iterator it = lstDirsCopy.begin(); it != lstDirsCopy.end(); ++it) { forgetDirs(lister, *it, false); } } static bool manually_mounted(const QString &path, const KMountPoint::List &possibleMountPoints) { KMountPoint::Ptr mp = possibleMountPoints.findByPath(path); if (!mp) { // not listed in fstab -> yes, manually mounted if (possibleMountPoints.isEmpty()) { // no fstab at all -> don't assume anything return false; } return true; } // noauto -> manually mounted. Otherwise, mounted at boot time, won't be unmounted any time soon hopefully. return mp->mountOptions().contains(QStringLiteral("noauto")); } void KCoreDirListerCache::forgetDirs(KCoreDirLister *lister, const QUrl &_url, bool notify) { qCDebug(KIO_CORE_DIRLISTER) << lister << " _url: " << _url; const QUrl url = _url.adjusted(QUrl::StripTrailingSlash); DirectoryDataHash::iterator dit = directoryData.find(url); if (dit == directoryData.end()) { return; } KCoreDirListerCacheDirectoryData &dirData = *dit; dirData.listersCurrentlyHolding.removeAll(lister); // This lister doesn't care for updates running in anymore KIO::ListJob *job = jobForUrl(url); if (job) { lister->d->jobDone(job); } DirItem *item = itemsInUse.value(url); Q_ASSERT(item); bool insertIntoCache = false; if (dirData.listersCurrentlyHolding.isEmpty() && dirData.listersCurrentlyListing.isEmpty()) { // item not in use anymore -> move into cache if complete directoryData.erase(dit); itemsInUse.remove(url); // this job is a running update which nobody cares about anymore if (job) { killJob(job); qCDebug(KIO_CORE_DIRLISTER) << "Killing update job for " << url; // Well, the user of KCoreDirLister doesn't really care that we're stopping // a background-running job from a previous URL (in listDir) -> commented out. // stop() already emitted canceled. //emit lister->canceled( url ); if (lister->d->numJobs() == 0) { lister->d->complete = true; //emit lister->canceled(); } } if (notify) { lister->d->lstDirs.removeAll(url); emit lister->clear(url); } insertIntoCache = item->complete; if (insertIntoCache) { // TODO(afiestas): remove use of KMountPoint+manually_mounted and port to Solid: // 1) find Volume for the local path "item->url.toLocalFile()" (which could be anywhere // under the mount point) -- probably needs a new operator in libsolid query parser // 2) [**] becomes: if (Drive is hotpluggable or Volume is removable) "set to dirty" else "keep watch" const KMountPoint::List possibleMountPoints = KMountPoint::possibleMountPoints(KMountPoint::NeedMountOptions); // Should we forget the dir for good, or keep a watch on it? // Generally keep a watch, except when it would prevent // unmounting a removable device (#37780) const bool isLocal = item->url.isLocalFile(); bool isManuallyMounted = false; bool containsManuallyMounted = false; if (isLocal) { isManuallyMounted = manually_mounted(item->url.toLocalFile(), possibleMountPoints); if (!isManuallyMounted) { // Look for a manually-mounted directory inside // If there's one, we can't keep a watch either, FAM would prevent unmounting the CDROM // I hope this isn't too slow - NonMovableFileItemList::const_iterator kit = item->lstItems.constBegin(); - NonMovableFileItemList::const_iterator kend = item->lstItems.constEnd(); + auto kit = item->lstItems.constBegin(); + const auto kend = item->lstItems.constEnd(); for (; kit != kend && !containsManuallyMounted; ++kit) if ((*kit).isDir() && manually_mounted((*kit).url().toLocalFile(), possibleMountPoints)) { containsManuallyMounted = true; } } } if (isManuallyMounted || containsManuallyMounted) { // [**] qCDebug(KIO_CORE_DIRLISTER) << "Not adding a watch on " << item->url << " because it " << ( isManuallyMounted ? "is manually mounted" : "contains a manually mounted subdir" ); item->complete = false; // set to "dirty" } else { item->incAutoUpdate(); // keep watch item->watchedWhileInCache = true; } } else { delete item; item = nullptr; } } if (item && lister->d->autoUpdate) { item->decAutoUpdate(); } // Inserting into QCache must be done last, since it might delete the item if (item && insertIntoCache) { qCDebug(KIO_CORE_DIRLISTER) << lister << "item moved into cache:" << url; itemsCached.insert(url, item); } } void KCoreDirListerCache::updateDirectory(const QUrl &_dir) { qCDebug(KIO_CORE_DIRLISTER) << _dir; const QUrl dir = _dir.adjusted(QUrl::StripTrailingSlash); if (!checkUpdate(dir)) { - if (dir.isLocalFile() && findByUrl(nullptr, dir)) { + if (dir.isLocalFile() && !(findByUrl(nullptr, dir).isNull())) { pendingUpdates.insert(dir.toLocalFile()); if (!pendingUpdateTimer.isActive()) { pendingUpdateTimer.start(500); } } return; } // A job can be running to // - only list a new directory: the listers are in listersCurrentlyListing // - only update a directory: the listers are in listersCurrentlyHolding // - update a currently running listing: the listers are in both KCoreDirListerCacheDirectoryData &dirData = directoryData[dir]; QList listers = dirData.listersCurrentlyListing; QList holders = dirData.listersCurrentlyHolding; qCDebug(KIO_CORE_DIRLISTER) << dir << "listers=" << listers << "holders=" << holders; bool killed = false; KIO::ListJob *job = jobForUrl(dir); if (job) { // the job is running already, tell it to do another update at the end // (don't kill it, we would keep doing that during a long download to a slow sshfs mount) job->setProperty("need_another_update", true); return; } else { // Emit any cached items. // updateDirectory() is about the diff compared to the cached items... Q_FOREACH (KCoreDirLister *kdl, listers) { KCoreDirLister::Private::CachedItemsJob *cachedItemsJob = kdl->d->cachedItemsJobForUrl(dir); if (cachedItemsJob) { cachedItemsJob->setEmitCompleted(false); cachedItemsJob->done(); // removes from cachedItemsJobs list delete cachedItemsJob; killed = true; } } } qCDebug(KIO_CORE_DIRLISTER) << "Killed=" << killed; // we don't need to emit canceled signals since we only replaced the job, // the listing is continuing. if (!(listers.isEmpty() || killed)) { qCWarning(KIO_CORE) << "The unexpected happened."; qCWarning(KIO_CORE) << "listers for" << dir << "=" << listers; qCWarning(KIO_CORE) << "job=" << job; Q_FOREACH(KCoreDirLister *kdl, listers) { qCDebug(KIO_CORE_DIRLISTER) << "lister" << kdl << "m_cachedItemsJobs=" << kdl->d->m_cachedItemsJobs; } #ifndef NDEBUG printDebug(); #endif } Q_ASSERT(listers.isEmpty() || killed); job = KIO::listDir(dir, KIO::HideProgressInfo); runningListJobs.insert(job, KIO::UDSEntryList()); connect(job, &KIO::ListJob::entries, this, &KCoreDirListerCache::slotUpdateEntries); connect(job, &KJob::result, this, &KCoreDirListerCache::slotUpdateResult); qCDebug(KIO_CORE_DIRLISTER) << "update started in" << dir; foreach (KCoreDirLister *kdl, listers) { kdl->jobStarted(job); } if (!holders.isEmpty()) { if (!killed) { foreach (KCoreDirLister *kdl, holders) { kdl->jobStarted(job); emit kdl->started(dir); } } else { foreach (KCoreDirLister *kdl, holders) { kdl->jobStarted(job); } } } } bool KCoreDirListerCache::checkUpdate(const QUrl &_dir) { if (!itemsInUse.contains(_dir)) { DirItem *item = itemsCached[_dir]; if (item && item->complete) { item->complete = false; item->watchedWhileInCache = false; item->decAutoUpdate(); // Hmm, this debug output might include login/password from the _dir URL. qCDebug(KIO_CORE_DIRLISTER) << "directory " << _dir << " not in use, marked dirty."; } //else qCDebug(KIO_CORE_DIRLISTER) << "aborted, directory " << _dir << " not in cache."; return false; } else { return true; } } KFileItem KCoreDirListerCache::itemForUrl(const QUrl &url) const { - KFileItem *item = findByUrl(nullptr, url); - if (item) { - return *item; - } else { - return KFileItem(); - } + return findByUrl(nullptr, url); } KCoreDirListerCache::DirItem *KCoreDirListerCache::dirItemForUrl(const QUrl &dir) const { const QUrl url = dir.adjusted(QUrl::StripTrailingSlash); DirItem *item = itemsInUse.value(url); if (!item) { item = itemsCached[url]; } return item; } -NonMovableFileItemList *KCoreDirListerCache::itemsForDir(const QUrl &dir) const +QList *KCoreDirListerCache::itemsForDir(const QUrl &dir) const { DirItem *item = dirItemForUrl(dir); return item ? &item->lstItems : nullptr; } KFileItem KCoreDirListerCache::findByName(const KCoreDirLister *lister, const QString &_name) const { Q_ASSERT(lister); for (QList::const_iterator it = lister->d->lstDirs.constBegin(); it != lister->d->lstDirs.constEnd(); ++it) { DirItem *dirItem = itemsInUse.value(*it); Q_ASSERT(dirItem); - const KFileItem item = dirItem->lstItems.findByName(_name); - if (!item.isNull()) { - return item; + + auto lit = dirItem->lstItems.constBegin(); + const auto litend = dirItem->lstItems.constEnd(); + for (; lit != litend; ++lit) { + if ((*lit).name() == _name) { + return *lit; + } } } return KFileItem(); } -KFileItem *KCoreDirListerCache::findByUrl(const KCoreDirLister *lister, const QUrl &_u) const +KFileItem KCoreDirListerCache::findByUrl(const KCoreDirLister *lister, const QUrl &_u) const { QUrl url(_u); url = url.adjusted(QUrl::StripTrailingSlash); const QUrl parentDir = url.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash); DirItem *dirItem = dirItemForUrl(parentDir); if (dirItem) { // If lister is set, check that it contains this dir if (!lister || lister->d->lstDirs.contains(parentDir)) { - NonMovableFileItemList::iterator it = dirItem->lstItems.begin(); - const NonMovableFileItemList::iterator end = dirItem->lstItems.end(); - for (; it != end; ++it) { - if ((*it).url() == url) { - return &*it; - } + // Binary search + auto it = std::lower_bound(dirItem->lstItems.begin(), dirItem->lstItems.end(), url); + if (it != dirItem->lstItems.end() && it->url() == url) { + return *it; } } } // Maybe _u is a directory itself? (see KDirModelTest::testChmodDirectory) // We check this last, though, we prefer returning a kfileitem with an actual // name if possible (and we make it '.' for root items later). dirItem = dirItemForUrl(url); if (dirItem && !dirItem->rootItem.isNull() && dirItem->rootItem.url() == url) { // If lister is set, check that it contains this dir if (!lister || lister->d->lstDirs.contains(url)) { - return &dirItem->rootItem; + return dirItem->rootItem; } } - return nullptr; + return KFileItem(); } void KCoreDirListerCache::slotFilesAdded(const QString &dir /*url*/) // from KDirNotify signals { QUrl urlDir(dir); itemsAddedInDirectory(urlDir); } void KCoreDirListerCache::itemsAddedInDirectory(const QUrl &urlDir) { qCDebug(KIO_CORE_DIRLISTER) << urlDir; // output urls, not qstrings, since they might contain a password Q_FOREACH (const QUrl &u, directoriesForCanonicalPath(urlDir)) { updateDirectory(u); } } void KCoreDirListerCache::slotFilesRemoved(const QStringList &fileList) // from KDirNotify signals { // TODO: handling of symlinks-to-directories isn't done here, // because I'm not sure how to do it and keep the performance ok... slotFilesRemoved(QUrl::fromStringList(fileList)); } void KCoreDirListerCache::slotFilesRemoved(const QList &fileList) { qCDebug(KIO_CORE_DIRLISTER) << fileList.count(); // Group notifications by parent dirs (usually there would be only one parent dir) QMap removedItemsByDir; QList deletedSubdirs; for (auto it = fileList.cbegin(), cend = fileList.end(); it != cend; ++it) { QUrl url(*it); DirItem *dirItem = dirItemForUrl(url); // is it a listed directory? if (dirItem) { deletedSubdirs.append(url); if (!dirItem->rootItem.isNull()) { removedItemsByDir[url].append(dirItem->rootItem); } } const QUrl parentDir = url.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash); dirItem = dirItemForUrl(parentDir); if (!dirItem) { continue; } - for (NonMovableFileItemList::iterator fit = dirItem->lstItems.begin(), fend = dirItem->lstItems.end(); fit != fend; ++fit) { + for (auto fit = dirItem->lstItems.begin(), fend = dirItem->lstItems.end(); fit != fend; ++fit) { if ((*fit).url() == url) { const KFileItem fileitem = *fit; removedItemsByDir[parentDir].append(fileitem); // If we found a fileitem, we can test if it's a dir. If not, we'll go to deleteDir just in case. if (fileitem.isNull() || fileitem.isDir()) { deletedSubdirs.append(url); } dirItem->lstItems.erase(fit); // remove fileitem from list break; } } } for (auto rit = removedItemsByDir.constBegin(), cend = removedItemsByDir.constEnd(); rit != cend; ++rit) { // Tell the views about it before calling deleteDir. // They might need the subdirs' file items (see the dirtree). auto dit = directoryData.constFind(rit.key()); if (dit != directoryData.constEnd()) { itemsDeleted((*dit).listersCurrentlyHolding, rit.value()); } } Q_FOREACH (const QUrl &url, deletedSubdirs) { // in case of a dir, check if we have any known children, there's much to do in that case // (stopping jobs, removing dirs from cache etc.) deleteDir(url); } } void KCoreDirListerCache::slotFilesChanged(const QStringList &fileList) // from KDirNotify signals { qCDebug(KIO_CORE_DIRLISTER) << fileList; QList dirsToUpdate; QStringList::const_iterator it = fileList.begin(); for (; it != fileList.end(); ++it) { QUrl url(*it); - KFileItem *fileitem = findByUrl(nullptr, url); - if (!fileitem) { + const KFileItem &fileitem = findByUrl(nullptr, url); + if (fileitem.isNull()) { qCDebug(KIO_CORE_DIRLISTER) << "item not found for" << url; continue; } if (url.isLocalFile()) { pendingUpdates.insert(url.toLocalFile()); // delegate the work to processPendingUpdates } else { pendingRemoteUpdates.insert(fileitem); // For remote files, we won't be able to figure out the new information, // we have to do a update (directory listing) const QUrl dir = url.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash); if (!dirsToUpdate.contains(dir)) { dirsToUpdate.prepend(dir); } } } QList::const_iterator itdir = dirsToUpdate.constBegin(); for (; itdir != dirsToUpdate.constEnd(); ++itdir) { updateDirectory(*itdir); } // ## TODO problems with current jobs listing/updating that dir // ( see kde-2.2.2's kdirlister ) processPendingUpdates(); } void KCoreDirListerCache::slotFileRenamed(const QString &_src, const QString &_dst, const QString &dstPath) // from KDirNotify signals { QUrl src(_src); QUrl dst(_dst); qCDebug(KIO_CORE_DIRLISTER) << src << "->" << dst; #ifdef DEBUG_CACHE printDebug(); #endif QUrl oldurl = src.adjusted(QUrl::StripTrailingSlash); - KFileItem *fileitem = findByUrl(nullptr, oldurl); - if (!fileitem) { + KFileItem fileitem = findByUrl(nullptr, oldurl); + if (fileitem.isNull()) { qCDebug(KIO_CORE_DIRLISTER) << "Item not found:" << oldurl; return; } - const KFileItem oldItem = *fileitem; + const KFileItem oldItem = fileitem; // Dest already exists? Was overwritten then (testcase: #151851) // We better emit it as deleted -before- doing the renaming, otherwise // the "update" mechanism will emit the old one as deleted and // kdirmodel will delete the new (renamed) one! - KFileItem *existingDestItem = findByUrl(nullptr, dst); - if (existingDestItem) { + const KFileItem &existingDestItem = findByUrl(nullptr, dst); + if (!existingDestItem.isNull()) { qCDebug(KIO_CORE_DIRLISTER) << dst << "already existed, let's delete it"; slotFilesRemoved(QList() << dst); } // If the item had a UDS_URL as well as UDS_NAME set, the user probably wants // to be updating the name only (since they can't see the URL). // Check to see if a URL exists, and if so, if only the file part has changed, // only update the name and not the underlying URL. - bool nameOnly = !fileitem->entry().stringValue(KIO::UDSEntry::UDS_URL).isEmpty(); + bool nameOnly = !fileitem.entry().stringValue(KIO::UDSEntry::UDS_URL).isEmpty(); nameOnly = nameOnly && src.adjusted(QUrl::RemoveFilename) == dst.adjusted(QUrl::RemoveFilename); - if (!nameOnly && fileitem->isDir()) { + if (!nameOnly && fileitem.isDir()) { renameDir(oldurl, dst); // #172945 - if the fileitem was the root item of a DirItem that was just removed from the cache, // then it's a dangling pointer now... fileitem = findByUrl(nullptr, oldurl); - if (!fileitem) { //deleted from cache altogether, #188807 + if (fileitem.isNull()) { //deleted from cache altogether, #188807 return; } } // Now update the KFileItem representing that file or dir (not exclusive with the above!) if (!oldItem.isLocalFile() && !oldItem.localPath().isEmpty() && dstPath.isEmpty()) { // it uses UDS_LOCAL_PATH and we don't know the new path? needs an update then slotFilesChanged(QStringList() << src.toString()); } else { + const QUrl &itemOldUrl = fileitem.url(); if (nameOnly) { - fileitem->setName(dst.fileName()); + fileitem.setName(dst.fileName()); } else { - fileitem->setUrl(dst); + fileitem.setUrl(dst); } if (!dstPath.isEmpty()) { - fileitem->setLocalPath(dstPath); + fileitem.setLocalPath(dstPath); } - fileitem->refreshMimeType(); - fileitem->determineMimeType(); - QSet listers = emitRefreshItem(oldItem, *fileitem); + fileitem.refreshMimeType(); + fileitem.determineMimeType(); + reinsert(fileitem, itemOldUrl); + + QSet listers = emitRefreshItem(oldItem, fileitem); Q_FOREACH (KCoreDirLister *kdl, listers) { kdl->d->emitItems(); } } #ifdef DEBUG_CACHE printDebug(); #endif } QSet KCoreDirListerCache::emitRefreshItem(const KFileItem &oldItem, const KFileItem &fileitem) { qCDebug(KIO_CORE_DIRLISTER) << "old:" << oldItem.name() << oldItem.url() << "new:" << fileitem.name() << fileitem.url(); // Look whether this item was shown in any view, i.e. held by any dirlister const QUrl parentDir = oldItem.url().adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash); DirectoryDataHash::iterator dit = directoryData.find(parentDir); QList listers; // Also look in listersCurrentlyListing, in case the user manages to rename during a listing if (dit != directoryData.end()) { listers += (*dit).listersCurrentlyHolding + (*dit).listersCurrentlyListing; } if (oldItem.isDir()) { // For a directory, look for dirlisters where it's the root item. dit = directoryData.find(oldItem.url()); if (dit != directoryData.end()) { listers += (*dit).listersCurrentlyHolding + (*dit).listersCurrentlyListing; } } QSet listersToRefresh; Q_FOREACH (KCoreDirLister *kdl, listers) { // For a directory, look for dirlisters where it's the root item. QUrl directoryUrl(oldItem.url()); if (oldItem.isDir() && kdl->d->rootFileItem == oldItem) { const KFileItem oldRootItem = kdl->d->rootFileItem; kdl->d->rootFileItem = fileitem; kdl->d->addRefreshItem(directoryUrl, oldRootItem, fileitem); } else { directoryUrl = directoryUrl.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash); kdl->d->addRefreshItem(directoryUrl, oldItem, fileitem); } listersToRefresh.insert(kdl); } return listersToRefresh; } QList KCoreDirListerCache::directoriesForCanonicalPath(const QUrl &dir) const { QList dirs; dirs << dir; dirs << canonicalUrls.value(dir).toSet().toList(); /* make unique; there are faster ways, but this is really small anyway */ if (dirs.count() > 1) { qCDebug(KIO_CORE_DIRLISTER) << dir << "known as" << dirs; } return dirs; } // private slots // Called by KDirWatch - usually when a dir we're watching has been modified, // but it can also be called for a file. void KCoreDirListerCache::slotFileDirty(const QString &path) { qCDebug(KIO_CORE_DIRLISTER) << path; QUrl url = QUrl::fromLocalFile(path).adjusted(QUrl::StripTrailingSlash); // File or dir? bool isDir; const KFileItem item = itemForUrl(url); if (!item.isNull()) { isDir = item.isDir(); } else { QFileInfo info(path); if (!info.exists()) { return; // error } isDir = info.isDir(); } if (isDir) { Q_FOREACH (const QUrl &dir, directoriesForCanonicalPath(url)) { handleFileDirty(dir); // e.g. for permission changes handleDirDirty(dir); } } else { Q_FOREACH (const QUrl &dir, directoriesForCanonicalPath(url.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash))) { QUrl aliasUrl(dir); aliasUrl.setPath(concatPaths(aliasUrl.path(), url.fileName())); handleFileDirty(aliasUrl); } } } // Called by slotFileDirty void KCoreDirListerCache::handleDirDirty(const QUrl &url) { // A dir: launch an update job if anyone cares about it // This also means we can forget about pending updates to individual files in that dir const QString dir = url.toLocalFile(); QString dirPath = dir; if (!dirPath.endsWith('/')) { dirPath += '/'; } QMutableSetIterator pendingIt(pendingUpdates); while (pendingIt.hasNext()) { const QString updPath = pendingIt.next(); qCDebug(KIO_CORE_DIRLISTER) << "had pending update" << updPath; if (updPath.startsWith(dirPath) && updPath.indexOf('/', dirPath.length()) == -1) { // direct child item qCDebug(KIO_CORE_DIRLISTER) << "forgetting about individual update to" << updPath; pendingIt.remove(); } } if (checkUpdate(url) && !pendingDirectoryUpdates.contains(dir)) { pendingDirectoryUpdates.insert(dir); if (!pendingUpdateTimer.isActive()) { pendingUpdateTimer.start(200); } } } // Called by slotFileDirty void KCoreDirListerCache::handleFileDirty(const QUrl &url) { // A file: do we know about it already? - KFileItem *existingItem = findByUrl(nullptr, url); + const KFileItem &existingItem = findByUrl(nullptr, url); const QUrl dir = url.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash); QString filePath = url.toLocalFile(); - if (!existingItem) { + if (existingItem.isNull()) { // No - update the parent dir then handleDirDirty(dir); } // Delay updating the file, FAM is flooding us with events if (checkUpdate(dir) && !pendingUpdates.contains(filePath)) { pendingUpdates.insert(filePath); if (!pendingUpdateTimer.isActive()) { pendingUpdateTimer.start(200); } } } void KCoreDirListerCache::slotFileCreated(const QString &path) // from KDirWatch { qCDebug(KIO_CORE_DIRLISTER) << path; // XXX: how to avoid a complete rescan here? // We'd need to stat that one file separately and refresh the item(s) for it. QUrl fileUrl(QUrl::fromLocalFile(path)); itemsAddedInDirectory(fileUrl.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash)); } void KCoreDirListerCache::slotFileDeleted(const QString &path) // from KDirWatch { qCDebug(KIO_CORE_DIRLISTER) << path; const QString fileName = QFileInfo(path).fileName(); QUrl dirUrl(QUrl::fromLocalFile(path)); QStringList fileUrls; Q_FOREACH (const QUrl &url, directoriesForCanonicalPath(dirUrl.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash))) { QUrl urlInfo(url); urlInfo.setPath(concatPaths(urlInfo.path(), fileName)); fileUrls << urlInfo.toString(); } slotFilesRemoved(fileUrls); } void KCoreDirListerCache::slotEntries(KIO::Job *job, const KIO::UDSEntryList &entries) { QUrl url(joburl(static_cast(job))); url = url.adjusted(QUrl::StripTrailingSlash); qCDebug(KIO_CORE_DIRLISTER) << "new entries for " << url; DirItem *dir = itemsInUse.value(url); if (!dir) { qCWarning(KIO_CORE) << "Internal error: job is listing" << url << "but itemsInUse only knows about" << itemsInUse.keys(); Q_ASSERT(dir); return; } DirectoryDataHash::iterator dit = directoryData.find(url); if (dit == directoryData.end()) { qCWarning(KIO_CORE) << "Internal error: job is listing" << url << "but directoryData doesn't know about that url, only about:" << directoryData.keys(); Q_ASSERT(dit != directoryData.end()); return; } KCoreDirListerCacheDirectoryData &dirData = *dit; if (dirData.listersCurrentlyListing.isEmpty()) { qCWarning(KIO_CORE) << "Internal error: job is listing" << url << "but directoryData says no listers are currently listing " << url; #ifndef NDEBUG printDebug(); #endif Q_ASSERT(!dirData.listersCurrentlyListing.isEmpty()); return; } // check if anyone wants the mimetypes immediately bool delayedMimeTypes = true; foreach (KCoreDirLister *kdl, dirData.listersCurrentlyListing) { delayedMimeTypes &= kdl->d->delayedMimeTypes; } QSet filesToHide; bool dotHiddenChecked = false; KIO::UDSEntryList::const_iterator it = entries.begin(); const KIO::UDSEntryList::const_iterator end = entries.end(); for (; it != end; ++it) { const QString name = (*it).stringValue(KIO::UDSEntry::UDS_NAME); Q_ASSERT(!name.isEmpty()); if (name.isEmpty()) { continue; } if (name == QLatin1String(".")) { Q_ASSERT(dir->rootItem.isNull()); // Try to reuse an existing KFileItem (if we listed the parent dir) // rather than creating a new one. There are many reasons: // 1) renames and permission changes to the item would have to emit the signals // twice, otherwise, so that both views manage to recognize the item. // 2) with kio_ftp we can only know that something is a symlink when // listing the parent, so prefer that item, which has more info. // Note that it gives a funky name() to the root item, rather than "." ;) dir->rootItem = itemForUrl(url); if (dir->rootItem.isNull()) { dir->rootItem = KFileItem(*it, url, delayedMimeTypes, true); } foreach (KCoreDirLister *kdl, dirData.listersCurrentlyListing) if (kdl->d->rootFileItem.isNull() && kdl->d->url == url) { kdl->d->rootFileItem = dir->rootItem; } } else if (name != QLatin1String("..")) { KFileItem item(*it, url, delayedMimeTypes, true); // get the names of the files listed in ".hidden", if it exists and is a local file if (!dotHiddenChecked) { const QString localPath = item.localPath(); if (!localPath.isEmpty()) { const QString rootItemPath = QFileInfo(localPath).absolutePath(); filesToHide = filesInDotHiddenForDir(rootItemPath); } dotHiddenChecked = true; } // hide file if listed in ".hidden" if (filesToHide.contains(name)) { item.setHidden(); } qCDebug(KIO_CORE_DIRLISTER)<< "Adding item: " << item.url(); - dir->lstItems.append(item); + // Add the items sorted by url, needed by findByUrl + dir->insert(item); foreach (KCoreDirLister *kdl, dirData.listersCurrentlyListing) { kdl->d->addNewItem(url, item); } } } foreach (KCoreDirLister *kdl, dirData.listersCurrentlyListing) { kdl->d->emitItems(); } } void KCoreDirListerCache::slotResult(KJob *j) { #ifdef DEBUG_CACHE //printDebug(); #endif Q_ASSERT(j); KIO::ListJob *job = static_cast(j); runningListJobs.remove(job); QUrl jobUrl(joburl(job)); jobUrl = jobUrl.adjusted(QUrl::StripTrailingSlash); // need remove trailing slashes again, in case of redirections qCDebug(KIO_CORE_DIRLISTER) << "finished listing" << jobUrl; const auto dit = directoryData.find(jobUrl); if (dit == directoryData.end()) { qCWarning(KIO_CORE) << "Nothing found in directoryData for URL" << jobUrl; #ifndef NDEBUG printDebug(); #endif Q_ASSERT(dit != directoryData.end()); return; } KCoreDirListerCacheDirectoryData &dirData = *dit; if (dirData.listersCurrentlyListing.isEmpty()) { qCWarning(KIO_CORE) << "OOOOPS, nothing in directoryData.listersCurrentlyListing for" << jobUrl; // We're about to assert; dump the current state... #ifndef NDEBUG printDebug(); #endif Q_ASSERT(!dirData.listersCurrentlyListing.isEmpty()); } QList listers = dirData.listersCurrentlyListing; // move all listers to the holding list, do it before emitting // the signals to make sure it exists in KCoreDirListerCache in case someone // calls listDir during the signal emission Q_ASSERT(dirData.listersCurrentlyHolding.isEmpty()); dirData.moveListersWithoutCachedItemsJob(jobUrl); if (job->error()) { foreach (KCoreDirLister *kdl, listers) { kdl->d->jobDone(job); if (job->error() != KJob::KilledJobError) { kdl->handleError(job); } const bool silent = job->property("_kdlc_silent").toBool(); if (!silent) { emit kdl->canceled(jobUrl); } if (kdl->d->numJobs() == 0) { kdl->d->complete = true; if (!silent) { emit kdl->canceled(); } } } } else { DirItem *dir = itemsInUse.value(jobUrl); Q_ASSERT(dir); dir->complete = true; foreach (KCoreDirLister *kdl, listers) { kdl->d->jobDone(job); emit kdl->completed(jobUrl); if (kdl->d->numJobs() == 0) { kdl->d->complete = true; emit kdl->completed(); } } } // TODO: hmm, if there was an error and job is a parent of one or more // of the pending urls we should cancel it/them as well processPendingUpdates(); if (job->property("need_another_update").toBool()) { updateDirectory(jobUrl); } #ifdef DEBUG_CACHE printDebug(); #endif } void KCoreDirListerCache::slotRedirection(KIO::Job *j, const QUrl &url) { Q_ASSERT(j); KIO::ListJob *job = static_cast(j); QUrl oldUrl(job->url()); // here we really need the old url! QUrl newUrl(url); // strip trailing slashes oldUrl = oldUrl.adjusted(QUrl::StripTrailingSlash); newUrl = newUrl.adjusted(QUrl::StripTrailingSlash); if (oldUrl == newUrl) { qCDebug(KIO_CORE_DIRLISTER) << "New redirection url same as old, giving up."; return; } else if (newUrl.isEmpty()) { qCDebug(KIO_CORE_DIRLISTER) << "New redirection url is empty, giving up."; return; } qCDebug(KIO_CORE_DIRLISTER) << oldUrl << "->" << newUrl; #ifdef DEBUG_CACHE // Can't do that here. KCoreDirListerCache::joburl() will use the new url already, // while our data structures haven't been updated yet -> assert fail. //printDebug(); #endif // I don't think there can be dirItems that are children of oldUrl. // Am I wrong here? And even if so, we don't need to delete them, right? // DF: redirection happens before listDir emits any item. Makes little sense otherwise. // oldUrl cannot be in itemsCached because only completed items are moved there DirItem *dir = itemsInUse.take(oldUrl); Q_ASSERT(dir); DirectoryDataHash::iterator dit = directoryData.find(oldUrl); Q_ASSERT(dit != directoryData.end()); KCoreDirListerCacheDirectoryData oldDirData = *dit; directoryData.erase(dit); Q_ASSERT(!oldDirData.listersCurrentlyListing.isEmpty()); const QList listers = oldDirData.listersCurrentlyListing; Q_ASSERT(!listers.isEmpty()); foreach (KCoreDirLister *kdl, listers) { kdl->d->redirect(oldUrl, newUrl, false /*clear items*/); } // when a lister was stopped before the job emits the redirection signal, the old url will // also be in listersCurrentlyHolding const QList holders = oldDirData.listersCurrentlyHolding; foreach (KCoreDirLister *kdl, holders) { kdl->jobStarted(job); // do it like when starting a new list-job that will redirect later // TODO: maybe don't emit started if there's an update running for newUrl already? emit kdl->started(oldUrl); kdl->d->redirect(oldUrl, newUrl, false /*clear items*/); } DirItem *newDir = itemsInUse.value(newUrl); if (newDir) { qCDebug(KIO_CORE_DIRLISTER) << newUrl << "already in use"; // only in this case there can newUrl already be in listersCurrentlyListing or listersCurrentlyHolding delete dir; // get the job if one's running for newUrl already (can be a list-job or an update-job), but // do not return this 'job', which would happen because of the use of redirectionURL() KIO::ListJob *oldJob = jobForUrl(newUrl, job); // listers of newUrl with oldJob: forget about the oldJob and use the already running one // which will be converted to an updateJob KCoreDirListerCacheDirectoryData &newDirData = directoryData[newUrl]; QList &curListers = newDirData.listersCurrentlyListing; if (!curListers.isEmpty()) { qCDebug(KIO_CORE_DIRLISTER) << "and it is currently listed"; Q_ASSERT(oldJob); // ?! foreach (KCoreDirLister *kdl, curListers) { // listers of newUrl kdl->d->jobDone(oldJob); kdl->jobStarted(job); kdl->d->connectJob(job); } // append listers of oldUrl with newJob to listers of newUrl with oldJob foreach (KCoreDirLister *kdl, listers) { curListers.append(kdl); } } else { curListers = listers; } if (oldJob) { // kill the old job, be it a list-job or an update-job killJob(oldJob); } // holders of newUrl: use the already running job which will be converted to an updateJob QList &curHolders = newDirData.listersCurrentlyHolding; if (!curHolders.isEmpty()) { qCDebug(KIO_CORE_DIRLISTER) << "and it is currently held."; foreach (KCoreDirLister *kdl, curHolders) { // holders of newUrl kdl->jobStarted(job); emit kdl->started(newUrl); } // append holders of oldUrl to holders of newUrl foreach (KCoreDirLister *kdl, holders) { curHolders.append(kdl); } } else { curHolders = holders; } // emit old items: listers, holders. NOT: newUrlListers/newUrlHolders, they already have them listed // TODO: make this a separate method? foreach (KCoreDirLister *kdl, listers + holders) { if (kdl->d->rootFileItem.isNull() && kdl->d->url == newUrl) { kdl->d->rootFileItem = newDir->rootItem; } kdl->d->addNewItems(newUrl, newDir->lstItems); kdl->d->emitItems(); } } else if ((newDir = itemsCached.take(newUrl))) { qCDebug(KIO_CORE_DIRLISTER) << newUrl << "is unused, but already in the cache."; delete dir; itemsInUse.insert(newUrl, newDir); KCoreDirListerCacheDirectoryData &newDirData = directoryData[newUrl]; newDirData.listersCurrentlyListing = listers; newDirData.listersCurrentlyHolding = holders; // emit old items: listers, holders foreach (KCoreDirLister *kdl, listers + holders) { if (kdl->d->rootFileItem.isNull() && kdl->d->url == newUrl) { kdl->d->rootFileItem = newDir->rootItem; } kdl->d->addNewItems(newUrl, newDir->lstItems); kdl->d->emitItems(); } } else { qCDebug(KIO_CORE_DIRLISTER) << newUrl << "has not been listed yet."; dir->rootItem = KFileItem(); dir->lstItems.clear(); dir->redirect(newUrl); itemsInUse.insert(newUrl, dir); KCoreDirListerCacheDirectoryData &newDirData = directoryData[newUrl]; newDirData.listersCurrentlyListing = listers; newDirData.listersCurrentlyHolding = holders; if (holders.isEmpty()) { #ifdef DEBUG_CACHE printDebug(); #endif return; // only in this case the job doesn't need to be converted, } } // make the job an update job job->disconnect(this); connect(job, &KIO::ListJob::entries, this, &KCoreDirListerCache::slotUpdateEntries); connect(job, &KJob::result, this, &KCoreDirListerCache::slotUpdateResult); // FIXME: autoUpdate-Counts!! #ifdef DEBUG_CACHE printDebug(); #endif } struct KCoreDirListerCache::ItemInUseChange { ItemInUseChange(const QUrl &old, const QUrl &newU, DirItem *di) : oldUrl(old), newUrl(newU), dirItem(di) {} QUrl oldUrl; QUrl newUrl; DirItem *dirItem; }; void KCoreDirListerCache::renameDir(const QUrl &oldUrl, const QUrl &newUrl) { qCDebug(KIO_CORE_DIRLISTER) << oldUrl << "->" << newUrl; //const QString oldUrlStr = oldUrl.url(KUrl::RemoveTrailingSlash); //const QString newUrlStr = newUrl.url(KUrl::RemoveTrailingSlash); // Not enough. Also need to look at any child dir, even sub-sub-sub-dir. //DirItem *dir = itemsInUse.take( oldUrlStr ); //emitRedirections( oldUrl, url ); QLinkedList itemsToChange; QSet listers; // Look at all dirs being listed/shown for (auto itu = itemsInUse.begin(), ituend = itemsInUse.end(); itu != ituend; ++itu) { DirItem *dir = itu.value(); QUrl oldDirUrl(itu.key()); qCDebug(KIO_CORE_DIRLISTER) << "itemInUse:" << oldDirUrl; // Check if this dir is oldUrl, or a subfolder of it if (oldDirUrl == oldUrl || oldUrl.isParentOf(oldDirUrl)) { // TODO should use KUrl::cleanpath like isParentOf does QString relPath = oldDirUrl.path().mid(oldUrl.path().length()+1); QUrl newDirUrl(newUrl); // take new base if (!relPath.isEmpty()) { newDirUrl.setPath(concatPaths(newDirUrl.path(), relPath)); // add unchanged relative path } qCDebug(KIO_CORE_DIRLISTER) << "new url=" << newDirUrl; // Update URL in dir item and in itemsInUse dir->redirect(newDirUrl); itemsToChange.append(ItemInUseChange(oldDirUrl.adjusted(QUrl::StripTrailingSlash), newDirUrl.adjusted(QUrl::StripTrailingSlash), dir)); // Rename all items under that dir - for (NonMovableFileItemList::iterator kit = dir->lstItems.begin(), kend = dir->lstItems.end(); - kit != kend; ++kit) { + for (auto kit = dir->lstItems.begin(), kend = dir->lstItems.end(); kit != kend; ++kit) { const KFileItem oldItem = *kit; + KFileItem newItem = oldItem; - const QUrl oldItemUrl = (*kit).url().adjusted(QUrl::StripTrailingSlash); + const QUrl &oldItemUrl = oldItem.url(); QUrl newItemUrl(oldItemUrl); newItemUrl.setPath(concatPaths(newDirUrl.path(), oldItemUrl.fileName())); qCDebug(KIO_CORE_DIRLISTER) << "renaming" << oldItemUrl << "to" << newItemUrl; - (*kit).setUrl(newItemUrl); + newItem.setUrl(newItemUrl); + reinsert(newItem, oldItemUrl); - listers |= emitRefreshItem(oldItem, *kit); + listers |= emitRefreshItem(oldItem, newItem); } } } Q_FOREACH (KCoreDirLister *kdl, listers) { kdl->d->emitItems(); } // Do the changes to itemsInUse out of the loop to avoid messing up iterators, // and so that emitRefreshItem can find the stuff in the hash. foreach (const ItemInUseChange &i, itemsToChange) { itemsInUse.remove(i.oldUrl); itemsInUse.insert(i.newUrl, i.dirItem); } //Now that all the caches are updated and consistent, emit the redirection. foreach(const ItemInUseChange& i, itemsToChange) { emitRedirections(QUrl(i.oldUrl), QUrl(i.newUrl)); } // Is oldUrl a directory in the cache? // Remove any child of oldUrl from the cache - even if the renamed dir itself isn't in it! removeDirFromCache(oldUrl); // TODO rename, instead. } // helper for renameDir, not used for redirections from KIO::listDir(). void KCoreDirListerCache::emitRedirections(const QUrl &_oldUrl, const QUrl &_newUrl) { qCDebug(KIO_CORE_DIRLISTER) << _oldUrl << "->" << _newUrl; const QUrl oldUrl = _oldUrl.adjusted(QUrl::StripTrailingSlash); const QUrl newUrl = _newUrl.adjusted(QUrl::StripTrailingSlash); KIO::ListJob *job = jobForUrl(oldUrl); if (job) { killJob(job); } // Check if we were listing this dir. Need to abort and restart with new name in that case. DirectoryDataHash::iterator dit = directoryData.find(oldUrl); if (dit == directoryData.end()) { return; } const QList listers = (*dit).listersCurrentlyListing; const QList holders = (*dit).listersCurrentlyHolding; KCoreDirListerCacheDirectoryData &newDirData = directoryData[newUrl]; // Tell the world that the job listing the old url is dead. foreach (KCoreDirLister *kdl, listers) { if (job) { kdl->d->jobDone(job); } emit kdl->canceled(oldUrl); } newDirData.listersCurrentlyListing += listers; // Check if we are currently displaying this directory (odds opposite wrt above) foreach (KCoreDirLister *kdl, holders) { if (job) { kdl->d->jobDone(job); } } newDirData.listersCurrentlyHolding += holders; directoryData.erase(dit); if (!listers.isEmpty()) { updateDirectory(newUrl); // Tell the world about the new url foreach (KCoreDirLister *kdl, listers) { emit kdl->started(newUrl); } } // And notify the dirlisters of the redirection foreach (KCoreDirLister *kdl, holders) { kdl->d->redirect(oldUrl, newUrl, true /*keep items*/); } } void KCoreDirListerCache::removeDirFromCache(const QUrl &dir) { qCDebug(KIO_CORE_DIRLISTER) << dir; const QList cachedDirs = itemsCached.keys(); // seems slow, but there's no qcache iterator... foreach (const QUrl &cachedDir, cachedDirs) { if (dir == cachedDir || dir.isParentOf(cachedDir)) { itemsCached.remove(cachedDir); } } } void KCoreDirListerCache::slotUpdateEntries(KIO::Job *job, const KIO::UDSEntryList &list) { runningListJobs[static_cast(job)] += list; } void KCoreDirListerCache::slotUpdateResult(KJob *j) { Q_ASSERT(j); KIO::ListJob *job = static_cast(j); QUrl jobUrl(joburl(job)); jobUrl = jobUrl.adjusted(QUrl::StripTrailingSlash); // need remove trailing slashes again, in case of redirections qCDebug(KIO_CORE_DIRLISTER) << "finished update" << jobUrl; KCoreDirListerCacheDirectoryData &dirData = directoryData[jobUrl]; // Collect the dirlisters which were listing the URL using that ListJob // plus those that were already holding that URL - they all get updated. dirData.moveListersWithoutCachedItemsJob(jobUrl); QList listers = dirData.listersCurrentlyHolding; listers += dirData.listersCurrentlyListing; // once we are updating dirs that are only in the cache this will fail! Q_ASSERT(!listers.isEmpty()); if (job->error()) { foreach (KCoreDirLister *kdl, listers) { kdl->d->jobDone(job); //don't bother the user //kdl->handleError( job ); const bool silent = job->property("_kdlc_silent").toBool(); if (!silent) { emit kdl->canceled(jobUrl); } if (kdl->d->numJobs() == 0) { kdl->d->complete = true; if (!silent) { emit kdl->canceled(); } } } runningListJobs.remove(job); // TODO: if job is a parent of one or more // of the pending urls we should cancel them processPendingUpdates(); return; } DirItem *dir = itemsInUse.value(jobUrl, nullptr); if (!dir) { qCWarning(KIO_CORE) << "Internal error: itemsInUse did not contain" << jobUrl; #ifndef NDEBUG printDebug(); #endif Q_ASSERT(dir); } else { dir->complete = true; } // check if anyone wants the mimetypes immediately bool delayedMimeTypes = true; foreach (KCoreDirLister *kdl, listers) { delayedMimeTypes &= kdl->d->delayedMimeTypes; } - typedef QHash FileItemHash; // fileName -> KFileItem* + typedef QHash FileItemHash; // fileName -> KFileItem FileItemHash fileItems; // Fill the hash from the old list of items. We'll remove entries as we see them // in the new listing, and the resulting hash entries will be the deleted items. - for (NonMovableFileItemList::iterator kit = dir->lstItems.begin(), kend = dir->lstItems.end(); kit != kend; ++kit) { - fileItems.insert((*kit).name(), &*kit); + for (auto kit = dir->lstItems.begin(), kend = dir->lstItems.end(); kit != kend; ++kit) { + fileItems.insert((*kit).name(), *kit); } QSet filesToHide; bool dotHiddenChecked = false; const KIO::UDSEntryList &buf = runningListJobs.value(job); KIO::UDSEntryList::const_iterator it = buf.constBegin(); const KIO::UDSEntryList::const_iterator end = buf.constEnd(); for (; it != end; ++it) { // Form the complete url KFileItem item(*it, jobUrl, delayedMimeTypes, true); const QString name = item.name(); Q_ASSERT(!name.isEmpty()); // A kioslave setting an empty UDS_NAME is utterly broken, fix the kioslave! // we duplicate the check for dotdot here, to avoid iterating over // all items again and checking in matchesFilter() that way. if (name.isEmpty() || name == QLatin1String("..")) { continue; } if (name == QLatin1String(".")) { // if the update was started before finishing the original listing // there is no root item yet if (dir->rootItem.isNull()) { dir->rootItem = item; foreach (KCoreDirLister *kdl, listers) if (kdl->d->rootFileItem.isNull() && kdl->d->url == jobUrl) { kdl->d->rootFileItem = dir->rootItem; } } continue; } else { // get the names of the files listed in ".hidden", if it exists and is a local file if (!dotHiddenChecked) { const QString localPath = item.localPath(); if (!localPath.isEmpty()) { const QString rootItemPath = QFileInfo(localPath).absolutePath(); filesToHide = filesInDotHiddenForDir(rootItemPath); } dotHiddenChecked = true; } } // hide file if listed in ".hidden" if (filesToHide.contains(name)) { item.setHidden(); } // Find this item FileItemHash::iterator fiit = fileItems.find(item.name()); if (fiit != fileItems.end()) { - KFileItem* tmp = fiit.value(); - QSet::iterator pru_it = pendingRemoteUpdates.find(tmp); + const KFileItem tmp = fiit.value(); + QSet::iterator pru_it = pendingRemoteUpdates.find(tmp); const bool inPendingRemoteUpdates = (pru_it != pendingRemoteUpdates.end()); // check if something changed for this file, using KFileItem::cmp() - if (!tmp->cmp(item) || inPendingRemoteUpdates) { + if (!tmp.cmp(item) || inPendingRemoteUpdates) { if (inPendingRemoteUpdates) { pendingRemoteUpdates.erase(pru_it); } - qCDebug(KIO_CORE_DIRLISTER) << "file changed:" << tmp->name(); + qCDebug(KIO_CORE_DIRLISTER) << "file changed:" << tmp.name(); - const KFileItem oldItem = *tmp; - *tmp = item; + reinsert(item, tmp.url()); foreach (KCoreDirLister *kdl, listers) { - kdl->d->addRefreshItem(jobUrl, oldItem, *tmp); + kdl->d->addRefreshItem(jobUrl, tmp, item); } } // Seen, remove fileItems.erase(fiit); } else { // this is a new file qCDebug(KIO_CORE_DIRLISTER) << "new file:" << name; - - dir->lstItems.append(item); + dir->insert(item); foreach (KCoreDirLister *kdl, listers) { kdl->d->addNewItem(jobUrl, item); } } } runningListJobs.remove(job); if (!fileItems.isEmpty()) { deleteUnmarkedItems(listers, dir->lstItems, fileItems); } foreach (KCoreDirLister *kdl, listers) { kdl->d->emitItems(); kdl->d->jobDone(job); emit kdl->completed(jobUrl); if (kdl->d->numJobs() == 0) { kdl->d->complete = true; emit kdl->completed(); } } // TODO: hmm, if there was an error and job is a parent of one or more // of the pending urls we should cancel it/them as well processPendingUpdates(); if (job->property("need_another_update").toBool()) { updateDirectory(jobUrl); } } // private KIO::ListJob *KCoreDirListerCache::jobForUrl(const QUrl &url, KIO::ListJob *not_job) { auto it = runningListJobs.constBegin(); while (it != runningListJobs.constEnd()) { KIO::ListJob *job = it.key(); const QUrl jobUrl = joburl(job).adjusted(QUrl::StripTrailingSlash); if (jobUrl == url && job != not_job) { return job; } ++it; } return nullptr; } const QUrl &KCoreDirListerCache::joburl(KIO::ListJob *job) { if (job->redirectionUrl().isValid()) { return job->redirectionUrl(); } else { return job->url(); } } void KCoreDirListerCache::killJob(KIO::ListJob *job) { runningListJobs.remove(job); job->disconnect(this); job->kill(); } -void KCoreDirListerCache::deleteUnmarkedItems(const QList &listers, NonMovableFileItemList &lstItems, const QHash &itemsToDelete) +void KCoreDirListerCache::deleteUnmarkedItems(const QList &listers, QList &lstItems, const QHash &itemsToDelete) { // Make list of deleted items (for emitting) KFileItemList deletedItems; - QHashIterator kit(itemsToDelete); + QHashIterator kit(itemsToDelete); while (kit.hasNext()) { - const KFileItem& item = *kit.next().value(); + const KFileItem item = kit.next().value(); deletedItems.append(item); - qCDebug(KIO_CORE_DIRLISTER) << "deleted:" << item.name() << &item; + qCDebug(KIO_CORE_DIRLISTER) << "deleted:" << item.name() << item; } // Delete all remaining items - QMutableListIterator it(lstItems); + QMutableListIterator it(lstItems); while (it.hasNext()) { if (itemsToDelete.contains(it.next().name())) { it.remove(); } } itemsDeleted(listers, deletedItems); } void KCoreDirListerCache::itemsDeleted(const QList &listers, const KFileItemList &deletedItems) { Q_FOREACH (KCoreDirLister *kdl, listers) { kdl->d->emitItemsDeleted(deletedItems); } Q_FOREACH (const KFileItem &item, deletedItems) { if (item.isDir()) { deleteDir(item.url()); } } } void KCoreDirListerCache::deleteDir(const QUrl &_dirUrl) { qCDebug(KIO_CORE_DIRLISTER) << _dirUrl; // unregister and remove the children of the deleted item. // Idea: tell all the KCoreDirListers that they should forget the dir // and then remove it from the cache. QUrl dirUrl(_dirUrl.adjusted(QUrl::StripTrailingSlash)); // Separate itemsInUse iteration and calls to forgetDirs (which modify itemsInUse) QList affectedItems; auto itu = itemsInUse.begin(); const auto ituend = itemsInUse.end(); for (; itu != ituend; ++itu) { const QUrl deletedUrl(itu.key()); if (dirUrl == deletedUrl || dirUrl.isParentOf(deletedUrl)) { affectedItems.append(deletedUrl); } } foreach (const QUrl &deletedUrl, affectedItems) { // stop all jobs for deletedUrlStr DirectoryDataHash::iterator dit = directoryData.find(deletedUrl); if (dit != directoryData.end()) { // we need a copy because stop modifies the list QList listers = (*dit).listersCurrentlyListing; foreach (KCoreDirLister *kdl, listers) { stopListingUrl(kdl, deletedUrl); } // tell listers holding deletedUrl to forget about it // this will stop running updates for deletedUrl as well // we need a copy because forgetDirs modifies the list QList holders = (*dit).listersCurrentlyHolding; foreach (KCoreDirLister *kdl, holders) { // lister's root is the deleted item if (kdl->d->url == deletedUrl) { // tell the view first. It might need the subdirs' items (which forgetDirs will delete) if (!kdl->d->rootFileItem.isNull()) { emit kdl->itemsDeleted(KFileItemList() << kdl->d->rootFileItem); } forgetDirs(kdl); kdl->d->rootFileItem = KFileItem(); } else { const bool treeview = kdl->d->lstDirs.count() > 1; if (!treeview) { emit kdl->clear(); kdl->d->lstDirs.clear(); } else { kdl->d->lstDirs.removeAll(deletedUrl); } forgetDirs(kdl, deletedUrl, treeview); } } } // delete the entry for deletedUrl - should not be needed, it's in // items cached now int count = itemsInUse.remove(deletedUrl); Q_ASSERT(count == 0); Q_UNUSED(count); //keep gcc "unused variable" complaining quiet when in release mode } // remove the children from the cache removeDirFromCache(dirUrl); } // delayed updating of files, FAM is flooding us with events void KCoreDirListerCache::processPendingUpdates() { QSet listers; foreach (const QString &file, pendingUpdates) { // always a local path qCDebug(KIO_CORE_DIRLISTER) << file; QUrl u = QUrl::fromLocalFile(file); - KFileItem *item = findByUrl(nullptr, u); // search all items - if (item) { + KFileItem item = findByUrl(nullptr, u); // search all items + if (!item.isNull()) { // we need to refresh the item, because e.g. the permissions can have changed. - KFileItem oldItem = *item; - item->refresh(); - if (!oldItem.cmp(*item)) { - listers |= emitRefreshItem(oldItem, *item); + KFileItem oldItem = item; + item.refresh(); + + if (!oldItem.cmp(item)) { + reinsert(item, oldItem.url()); + listers |= emitRefreshItem(oldItem, item); } } } pendingUpdates.clear(); Q_FOREACH (KCoreDirLister *kdl, listers) { kdl->d->emitItems(); } // Directories in need of updating foreach (const QString &dir, pendingDirectoryUpdates) { updateDirectory(QUrl::fromLocalFile(dir)); } pendingDirectoryUpdates.clear(); } #ifndef NDEBUG void KCoreDirListerCache::printDebug() { qCDebug(KIO_CORE_DIRLISTER) << "Items in use:"; auto itu = itemsInUse.constBegin(); const auto ituend = itemsInUse.constEnd(); for (; itu != ituend; ++itu) { qCDebug(KIO_CORE_DIRLISTER) << " " << itu.key() << "URL:" << itu.value()->url << "rootItem:" << (!itu.value()->rootItem.isNull() ? itu.value()->rootItem.url() : QUrl()) << "autoUpdates refcount:" << itu.value()->autoUpdates << "complete:" << itu.value()->complete << QStringLiteral("with %1 items.").arg(itu.value()->lstItems.count()); } QList listersWithoutJob; qCDebug(KIO_CORE_DIRLISTER) << "Directory data:"; DirectoryDataHash::const_iterator dit = directoryData.constBegin(); for (; dit != directoryData.constEnd(); ++dit) { QString list; foreach (KCoreDirLister *listit, (*dit).listersCurrentlyListing) { list += " 0x" + QString::number(reinterpret_cast(listit), 16); } qCDebug(KIO_CORE_DIRLISTER) << " " << dit.key() << (*dit).listersCurrentlyListing.count() << "listers:" << list; foreach (KCoreDirLister *listit, (*dit).listersCurrentlyListing) { if (!listit->d->m_cachedItemsJobs.isEmpty()) { qCDebug(KIO_CORE_DIRLISTER) << " Lister" << listit << "has CachedItemsJobs" << listit->d->m_cachedItemsJobs; } else if (KIO::ListJob *listJob = jobForUrl(dit.key())) { qCDebug(KIO_CORE_DIRLISTER) << " Lister" << listit << "has ListJob" << listJob; } else { listersWithoutJob.append(listit); } } list.clear(); foreach (KCoreDirLister *listit, (*dit).listersCurrentlyHolding) { list += " 0x" + QString::number(reinterpret_cast(listit), 16); } qCDebug(KIO_CORE_DIRLISTER) << " " << dit.key() << (*dit).listersCurrentlyHolding.count() << "holders:" << list; } QMap< KIO::ListJob *, KIO::UDSEntryList >::Iterator jit = runningListJobs.begin(); qCDebug(KIO_CORE_DIRLISTER) << "Jobs:"; for (; jit != runningListJobs.end(); ++jit) { qCDebug(KIO_CORE_DIRLISTER) << " " << jit.key() << "listing" << joburl(jit.key()) << ":" << (*jit).count() << "entries."; } qCDebug(KIO_CORE_DIRLISTER) << "Items in cache:"; const QList cachedDirs = itemsCached.keys(); foreach (const QUrl &cachedDir, cachedDirs) { DirItem *dirItem = itemsCached.object(cachedDir); qCDebug(KIO_CORE_DIRLISTER) << " " << cachedDir << "rootItem:" << (!dirItem->rootItem.isNull() ? dirItem->rootItem.url().toString() : QStringLiteral("NULL")) << "with" << dirItem->lstItems.count() << "items."; } // Abort on listers without jobs -after- showing the full dump. Easier debugging. Q_FOREACH (KCoreDirLister *listit, listersWithoutJob) { qCWarning(KIO_CORE) << "Fatal Error: HUH? Lister" << listit << "is supposed to be listing, but has no job!"; abort(); } } #endif KCoreDirLister::KCoreDirLister(QObject *parent) : QObject(parent), d(new Private(this)) { qCDebug(KIO_CORE_DIRLISTER) << "+KCoreDirLister"; d->complete = true; setAutoUpdate(true); setDirOnlyMode(false); setShowingDotFiles(false); } KCoreDirLister::~KCoreDirLister() { qCDebug(KIO_CORE_DIRLISTER) << "~KCoreDirLister" << this; // Stop all running jobs, remove lister from lists if (!kDirListerCache.isDestroyed()) { stop(); kDirListerCache()->forgetDirs(this); } delete d; } bool KCoreDirLister::openUrl(const QUrl &_url, OpenUrlFlags _flags) { // emit the current changes made to avoid an inconsistent treeview if (d->hasPendingChanges && (_flags & Keep)) { emitChanges(); } d->hasPendingChanges = false; return kDirListerCache()->listDir(this, _url, _flags & Keep, _flags & Reload); } void KCoreDirLister::stop() { kDirListerCache()->stop(this); } void KCoreDirLister::stop(const QUrl &_url) { kDirListerCache()->stopListingUrl(this, _url); } bool KCoreDirLister::autoUpdate() const { return d->autoUpdate; } void KCoreDirLister::setAutoUpdate(bool _enable) { if (d->autoUpdate == _enable) { return; } d->autoUpdate = _enable; kDirListerCache()->setAutoUpdate(this, _enable); } bool KCoreDirLister::showingDotFiles() const { return d->settings.isShowingDotFiles; } void KCoreDirLister::setShowingDotFiles(bool _showDotFiles) { if (d->settings.isShowingDotFiles == _showDotFiles) { return; } d->prepareForSettingsChange(); d->settings.isShowingDotFiles = _showDotFiles; } bool KCoreDirLister::dirOnlyMode() const { return d->settings.dirOnlyMode; } void KCoreDirLister::setDirOnlyMode(bool _dirsOnly) { if (d->settings.dirOnlyMode == _dirsOnly) { return; } d->prepareForSettingsChange(); d->settings.dirOnlyMode = _dirsOnly; } QUrl KCoreDirLister::url() const { return d->url; } QList KCoreDirLister::directories() const { return d->lstDirs; } void KCoreDirLister::emitChanges() { d->emitChanges(); } void KCoreDirLister::Private::emitChanges() { if (!hasPendingChanges) { return; } // reset 'hasPendingChanges' now, in case of recursion // (testcase: enabling recursive scan in ktorrent, #174920) hasPendingChanges = false; const Private::FilterSettings newSettings = settings; settings = oldSettings; // temporarily // Fill hash with all items that are currently visible QSet oldVisibleItems; Q_FOREACH (const QUrl &dir, lstDirs) { - NonMovableFileItemList *itemList = kDirListerCache()->itemsForDir(dir); + QList *itemList = kDirListerCache()->itemsForDir(dir); if (!itemList) { continue; } foreach (const KFileItem &item, *itemList) { if (isItemVisible(item) && m_parent->matchesMimeFilter(item)) { oldVisibleItems.insert(item.name()); } } } settings = newSettings; Q_FOREACH (const QUrl &dir, lstDirs) { KFileItemList deletedItems; - NonMovableFileItemList *itemList = kDirListerCache()->itemsForDir(dir); + QList *itemList = kDirListerCache()->itemsForDir(dir); if (!itemList) { continue; } - NonMovableFileItemList::iterator kit = itemList->begin(); - const NonMovableFileItemList::iterator kend = itemList->end(); + auto kit = itemList->begin(); + const auto kend = itemList->end(); for (; kit != kend; ++kit) { KFileItem &item = *kit; const QString text = item.text(); if (text == QLatin1String(".") || text == QLatin1String("..")) { continue; } const bool wasVisible = oldVisibleItems.contains(item.name()); const bool nowVisible = isItemVisible(item) && m_parent->matchesMimeFilter(item); if (nowVisible && !wasVisible) { addNewItem(dir, item); // takes care of emitting newItem or itemsFilteredByMime } else if (!nowVisible && wasVisible) { deletedItems.append(*kit); } } if (!deletedItems.isEmpty()) { emit m_parent->itemsDeleted(deletedItems); } emitItems(); } oldSettings = settings; } void KCoreDirLister::updateDirectory(const QUrl &_u) { kDirListerCache()->updateDirectory(_u); } bool KCoreDirLister::isFinished() const { return d->complete; } KFileItem KCoreDirLister::rootItem() const { return d->rootFileItem; } KFileItem KCoreDirLister::findByUrl(const QUrl &_url) const { - KFileItem *item = kDirListerCache()->findByUrl(this, _url); - if (item) { - return *item; - } else { - return KFileItem(); - } + return kDirListerCache()->findByUrl(this, _url); } KFileItem KCoreDirLister::findByName(const QString &_name) const { return kDirListerCache()->findByName(this, _name); } // ================ public filter methods ================ // void KCoreDirLister::setNameFilter(const QString &nameFilter) { if (d->nameFilter == nameFilter) { return; } d->prepareForSettingsChange(); d->settings.lstFilters.clear(); d->nameFilter = nameFilter; // Split on white space const QStringList list = nameFilter.split(' ', QString::SkipEmptyParts); for (QStringList::const_iterator it = list.begin(); it != list.end(); ++it) { d->settings.lstFilters.append(QRegExp(*it, Qt::CaseInsensitive, QRegExp::Wildcard)); } } QString KCoreDirLister::nameFilter() const { return d->nameFilter; } void KCoreDirLister::setMimeFilter(const QStringList &mimeFilter) { if (d->settings.mimeFilter == mimeFilter) { return; } d->prepareForSettingsChange(); if (mimeFilter.contains(QStringLiteral("application/octet-stream")) || mimeFilter.contains(QStringLiteral("all/allfiles"))) { // all files d->settings.mimeFilter.clear(); } else { d->settings.mimeFilter = mimeFilter; } } void KCoreDirLister::setMimeExcludeFilter(const QStringList &mimeExcludeFilter) { if (d->settings.mimeExcludeFilter == mimeExcludeFilter) { return; } d->prepareForSettingsChange(); d->settings.mimeExcludeFilter = mimeExcludeFilter; } void KCoreDirLister::clearMimeFilter() { d->prepareForSettingsChange(); d->settings.mimeFilter.clear(); d->settings.mimeExcludeFilter.clear(); } QStringList KCoreDirLister::mimeFilters() const { return d->settings.mimeFilter; } bool KCoreDirLister::matchesFilter(const QString &name) const { return doNameFilter(name, d->settings.lstFilters); } bool KCoreDirLister::matchesMimeFilter(const QString &mime) const { return doMimeFilter(mime, d->settings.mimeFilter) && d->doMimeExcludeFilter(mime, d->settings.mimeExcludeFilter); } // ================ protected methods ================ // bool KCoreDirLister::matchesFilter(const KFileItem &item) const { Q_ASSERT(!item.isNull()); if (item.text() == QLatin1String("..")) { return false; } if (!d->settings.isShowingDotFiles && item.isHidden()) { return false; } if (item.isDir() || d->settings.lstFilters.isEmpty()) { return true; } return matchesFilter(item.text()); } bool KCoreDirLister::matchesMimeFilter(const KFileItem &item) const { Q_ASSERT(!item.isNull()); // Don't lose time determining the mimetype if there is no filter if (d->settings.mimeFilter.isEmpty() && d->settings.mimeExcludeFilter.isEmpty()) { return true; } return matchesMimeFilter(item.mimetype()); } bool KCoreDirLister::doNameFilter(const QString &name, const QList &filters) const { for (QList::const_iterator it = filters.begin(); it != filters.end(); ++it) if ((*it).exactMatch(name)) { return true; } return false; } bool KCoreDirLister::doMimeFilter(const QString &mime, const QStringList &filters) const { if (filters.isEmpty()) { return true; } QMimeDatabase db; const QMimeType mimeptr = db.mimeTypeForName(mime); if (!mimeptr.isValid()) { return false; } qCDebug(KIO_CORE_DIRLISTER) << "doMimeFilter: investigating:" << mimeptr.name(); QStringList::const_iterator it = filters.begin(); for (; it != filters.end(); ++it) if (mimeptr.inherits(*it)) { return true; } //else qCDebug(KIO_CORE_DIRLISTER) << "doMimeFilter: compared without result to "<<*it; return false; } bool KCoreDirLister::Private::doMimeExcludeFilter(const QString &mime, const QStringList &filters) const { if (filters.isEmpty()) { return true; } QStringList::const_iterator it = filters.begin(); for (; it != filters.end(); ++it) if ((*it) == mime) { return false; } return true; } void KCoreDirLister::handleError(KIO::Job *job) { qCWarning(KIO_CORE) << job->errorString(); } void KCoreDirLister::handleErrorMessage(const QString &message) { qCWarning(KIO_CORE) << message; } // ================= private methods ================= // void KCoreDirLister::Private::addNewItem(const QUrl &directoryUrl, const KFileItem &item) { if (!isItemVisible(item)) { return; // No reason to continue... bailing out here prevents a mimetype scan. } qCDebug(KIO_CORE_DIRLISTER) << "in" << directoryUrl << "item:" << item.url(); if (m_parent->matchesMimeFilter(item)) { - if (!lstNewItems) { - lstNewItems = new NewItemsHash; - } - Q_ASSERT(!item.isNull()); - (*lstNewItems)[directoryUrl].append(item); // items not filtered + lstNewItems[directoryUrl].append(item); // items not filtered } else { - if (!lstMimeFilteredItems) { - lstMimeFilteredItems = new KFileItemList; - } - Q_ASSERT(!item.isNull()); - lstMimeFilteredItems->append(item); // only filtered by mime + lstMimeFilteredItems.append(item); // only filtered by mime } } -void KCoreDirLister::Private::addNewItems(const QUrl &directoryUrl, const NonMovableFileItemList &items) +void KCoreDirLister::Private::addNewItems(const QUrl &directoryUrl, const QList &items) { // TODO: make this faster - test if we have a filter at all first // DF: was this profiled? The matchesFoo() functions should be fast, w/o filters... // Of course if there is no filter and we can do a range-insertion instead of a loop, that might be good. - NonMovableFileItemList::const_iterator kit = items.begin(); - const NonMovableFileItemList::const_iterator kend = items.end(); + auto kit = items.cbegin(); + const auto kend = items.cend(); for (; kit != kend; ++kit) { addNewItem(directoryUrl, *kit); } } void KCoreDirLister::Private::addRefreshItem(const QUrl &directoryUrl, const KFileItem &oldItem, const KFileItem &item) { const bool refreshItemWasFiltered = !isItemVisible(oldItem) || !m_parent->matchesMimeFilter(oldItem); if (isItemVisible(item) && m_parent->matchesMimeFilter(item)) { if (refreshItemWasFiltered) { - if (!lstNewItems) { - lstNewItems = new NewItemsHash; - } - Q_ASSERT(!item.isNull()); - (*lstNewItems)[directoryUrl].append(item); + lstNewItems[directoryUrl].append(item); } else { - if (!lstRefreshItems) { - lstRefreshItems = new QList >; - } - Q_ASSERT(!item.isNull()); - lstRefreshItems->append(qMakePair(oldItem, item)); + lstRefreshItems.append(qMakePair(oldItem, item)); } } else if (!refreshItemWasFiltered) { - if (!lstRemoveItems) { - lstRemoveItems = new KFileItemList; - } - // notify the user that the mimetype of a file changed that doesn't match // a filter or does match an exclude filter // This also happens when renaming foo to .foo and dot files are hidden (#174721) Q_ASSERT(!oldItem.isNull()); - lstRemoveItems->append(oldItem); + lstRemoveItems.append(oldItem); } } void KCoreDirLister::Private::emitItems() { - NewItemsHash *tmpNew = lstNewItems; - lstNewItems = nullptr; - - KFileItemList *tmpMime = lstMimeFilteredItems; - lstMimeFilteredItems = nullptr; - - QList > *tmpRefresh = lstRefreshItems; - lstRefreshItems = nullptr; - - KFileItemList *tmpRemove = lstRemoveItems; - lstRemoveItems = nullptr; - - if (tmpNew) { - QHashIterator it(*tmpNew); + if (!lstNewItems.empty()) { + QHashIterator it(lstNewItems); while (it.hasNext()) { it.next(); emit m_parent->itemsAdded(it.key(), it.value()); emit m_parent->newItems(it.value()); // compat } - delete tmpNew; + lstNewItems.clear(); } - if (tmpMime) { - emit m_parent->itemsFilteredByMime(*tmpMime); - delete tmpMime; + if (!lstMimeFilteredItems.empty()) { + emit m_parent->itemsFilteredByMime(lstMimeFilteredItems); + lstMimeFilteredItems.clear(); } - if (tmpRefresh) { - emit m_parent->refreshItems(*tmpRefresh); - delete tmpRefresh; + if (!lstRefreshItems.empty()) { + emit m_parent->refreshItems(lstRefreshItems); + lstRefreshItems.clear(); } - if (tmpRemove) { - emit m_parent->itemsDeleted(*tmpRemove); - delete tmpRemove; + if (!lstRemoveItems.empty()) { + emit m_parent->itemsDeleted(lstRemoveItems); + lstRemoveItems.clear(); } } bool KCoreDirLister::Private::isItemVisible(const KFileItem &item) const { // Note that this doesn't include mime filters, because // of the itemsFilteredByMime signal. Filtered-by-mime items are // considered "visible", they are just visible via a different signal... return (!settings.dirOnlyMode || item.isDir()) && m_parent->matchesFilter(item); } void KCoreDirLister::Private::emitItemsDeleted(const KFileItemList &_items) { KFileItemList items = _items; QMutableListIterator it(items); while (it.hasNext()) { const KFileItem &item = it.next(); if (!isItemVisible(item) || !m_parent->matchesMimeFilter(item)) { it.remove(); } } if (!items.isEmpty()) { emit m_parent->itemsDeleted(items); } } // ================ private slots ================ // void KCoreDirLister::Private::_k_slotInfoMessage(KJob *, const QString &message) { emit m_parent->infoMessage(message); } void KCoreDirLister::Private::_k_slotPercent(KJob *job, unsigned long pcnt) { jobData[static_cast(job)].percent = pcnt; int result = 0; KIO::filesize_t size = 0; QMap< KIO::ListJob *, Private::JobData >::Iterator dataIt = jobData.begin(); while (dataIt != jobData.end()) { result += (*dataIt).percent * (*dataIt).totalSize; size += (*dataIt).totalSize; ++dataIt; } if (size != 0) { result /= size; } else { result = 100; } emit m_parent->percent(result); } void KCoreDirLister::Private::_k_slotTotalSize(KJob *job, qulonglong size) { jobData[static_cast(job)].totalSize = size; KIO::filesize_t result = 0; QMap< KIO::ListJob *, Private::JobData >::Iterator dataIt = jobData.begin(); while (dataIt != jobData.end()) { result += (*dataIt).totalSize; ++dataIt; } emit m_parent->totalSize(result); } void KCoreDirLister::Private::_k_slotProcessedSize(KJob *job, qulonglong size) { jobData[static_cast(job)].processedSize = size; KIO::filesize_t result = 0; QMap< KIO::ListJob *, Private::JobData >::Iterator dataIt = jobData.begin(); while (dataIt != jobData.end()) { result += (*dataIt).processedSize; ++dataIt; } emit m_parent->processedSize(result); } void KCoreDirLister::Private::_k_slotSpeed(KJob *job, unsigned long spd) { jobData[static_cast(job)].speed = spd; int result = 0; QMap< KIO::ListJob *, Private::JobData >::Iterator dataIt = jobData.begin(); while (dataIt != jobData.end()) { result += (*dataIt).speed; ++dataIt; } emit m_parent->speed(result); } uint KCoreDirLister::Private::numJobs() { #ifdef DEBUG_CACHE // This code helps detecting stale entries in the jobData map. qCDebug(KIO_CORE_DIRLISTER) << m_parent << "numJobs:" << jobData.count(); QMapIterator it(jobData); while (it.hasNext()) { it.next(); qCDebug(KIO_CORE_DIRLISTER) << (void*)it.key(); qCDebug(KIO_CORE_DIRLISTER) << it.key(); } #endif return jobData.count(); } void KCoreDirLister::Private::jobDone(KIO::ListJob *job) { jobData.remove(job); } void KCoreDirLister::jobStarted(KIO::ListJob *job) { Private::JobData data; data.speed = 0; data.percent = 0; data.processedSize = 0; data.totalSize = 0; d->jobData.insert(job, data); d->complete = false; } void KCoreDirLister::Private::connectJob(KIO::ListJob *job) { m_parent->connect(job, &KJob::infoMessage, m_parent, [this](KJob *job, const QString &plain){ _k_slotInfoMessage(job, plain); }); m_parent->connect(job, QOverload::of(&KJob::percent), m_parent, [this](KJob *job, ulong _percent){ _k_slotPercent(job, _percent); }); m_parent->connect(job, &KJob::totalSize, m_parent, [this](KJob *job, qulonglong _size){ _k_slotTotalSize(job, _size); }); m_parent->connect(job, &KJob::processedSize, m_parent, [this](KJob *job, qulonglong _psize){ _k_slotProcessedSize(job, _psize); }); m_parent->connect(job, &KJob::speed, m_parent, [this](KJob *job, qulonglong _speed){ _k_slotSpeed(job, _speed); }); } KFileItemList KCoreDirLister::items(WhichItems which) const { return itemsForDir(url(), which); } KFileItemList KCoreDirLister::itemsForDir(const QUrl &dir, WhichItems which) const { - NonMovableFileItemList *allItems = kDirListerCache()->itemsForDir(dir); + QList *allItems = kDirListerCache()->itemsForDir(dir); + KFileItemList result; if (!allItems) { - return KFileItemList(); + return result; } if (which == AllItems) { - return allItems->toKFileItemList(); + return KFileItemList(*allItems); } else { // only items passing the filters - KFileItemList result; - NonMovableFileItemList::const_iterator kit = allItems->constBegin(); - const NonMovableFileItemList::const_iterator kend = allItems->constEnd(); + auto kit = allItems->constBegin(); + const auto kend = allItems->constEnd(); for (; kit != kend; ++kit) { const KFileItem &item = *kit; if (d->isItemVisible(item) && matchesMimeFilter(item)) { result.append(item); } } - return result; } + return result; } bool KCoreDirLister::delayedMimeTypes() const { return d->delayedMimeTypes; } void KCoreDirLister::setDelayedMimeTypes(bool delayedMimeTypes) { d->delayedMimeTypes = delayedMimeTypes; } // called by KCoreDirListerCache::slotRedirection void KCoreDirLister::Private::redirect(const QUrl &oldUrl, const QUrl &newUrl, bool keepItems) { if (url.matches(oldUrl, QUrl::StripTrailingSlash)) { if (!keepItems) { rootFileItem = KFileItem(); } else { rootFileItem.setUrl(newUrl); } url = newUrl; } const int idx = lstDirs.indexOf(oldUrl); if (idx == -1) { qCWarning(KIO_CORE) << "Unexpected redirection from" << oldUrl << "to" << newUrl << "but this dirlister is currently listing/holding" << lstDirs; } else { lstDirs[ idx ] = newUrl; } if (lstDirs.count() == 1) { if (!keepItems) { emit m_parent->clear(); } emit m_parent->redirection(newUrl); } else { if (!keepItems) { emit m_parent->clear(oldUrl); } } emit m_parent->redirection(oldUrl, newUrl); } void KCoreDirListerCacheDirectoryData::moveListersWithoutCachedItemsJob(const QUrl &url) { // Move dirlisters from listersCurrentlyListing to listersCurrentlyHolding, // but not those that are still waiting on a CachedItemsJob... // Unit-testing note: // Run kdirmodeltest in valgrind to hit the case where an update // is triggered while a lister has a CachedItemsJob (different timing...) QMutableListIterator lister_it(listersCurrentlyListing); while (lister_it.hasNext()) { KCoreDirLister *kdl = lister_it.next(); if (!kdl->d->cachedItemsJobForUrl(url)) { // OK, move this lister from "currently listing" to "currently holding". // Huh? The KCoreDirLister was present twice in listersCurrentlyListing, or was in both lists? Q_ASSERT(!listersCurrentlyHolding.contains(kdl)); if (!listersCurrentlyHolding.contains(kdl)) { listersCurrentlyHolding.append(kdl); } lister_it.remove(); } else { qCDebug(KIO_CORE_DIRLISTER) << "Not moving" << kdl << "to listersCurrentlyHolding because it still has job" << kdl->d->m_cachedItemsJobs; } } } KFileItem KCoreDirLister::cachedItemForUrl(const QUrl &url) { if (kDirListerCache.exists()) { return kDirListerCache()->itemForUrl(url); } else { return {}; } } QSet KCoreDirListerCache::filesInDotHiddenForDir(const QString& dir) { const QString path = dir + "/.hidden"; QFile dotHiddenFile(path); if (dotHiddenFile.exists()) { const QDateTime mtime = QFileInfo(dotHiddenFile).lastModified(); const CacheHiddenFile *cachedDotHiddenFile = m_cacheHiddenFiles.object(path); if (cachedDotHiddenFile && mtime <= cachedDotHiddenFile->mtime) { // ".hidden" is in cache and still valid (the file was not modified since then), // so return it return cachedDotHiddenFile->listedFiles; } else { // read the ".hidden" file, then cache it and return it if (dotHiddenFile.open(QIODevice::ReadOnly | QIODevice::Text)) { QSet filesToHide; QTextStream stream(&dotHiddenFile); while (!stream.atEnd()) { QString name = stream.readLine(); if (!name.isEmpty()) { filesToHide.insert(name); } } m_cacheHiddenFiles.insert(path, new CacheHiddenFile(mtime, filesToHide)); return filesToHide; } } } return QSet(); } #include "moc_kcoredirlister.cpp" #include "moc_kcoredirlister_p.cpp" diff --git a/src/core/kcoredirlister_p.h b/src/core/kcoredirlister_p.h index 422f79b1..db013e70 100644 --- a/src/core/kcoredirlister_p.h +++ b/src/core/kcoredirlister_p.h @@ -1,586 +1,551 @@ /* This file is part of the KDE project Copyright (C) 2002-2006 Michael Brade This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef kdirlister_p_h #define kdirlister_p_h #include "kfileitem.h" #include #include #include #include #include #include #include #include #include #include -/** - * KCoreDirListerCache stores pointers to KFileItems internally and expects that - * these pointers remain valid, even if the number of items in the list 'lstItems' - * in KCoreDirLister::DirItem changes. Since such changes in a QList - * may change the internal memory layout of the QList, pointers to KFileItems in a - * QList might become invalid. - * - * Therefore, we make 'lstItems' a QList, where - * NonMovableFileItem is a class that inherits KFileItem, but is not declared as a - * Q_MOVABLE_TYPE. This forces QList to never move these items in memory, which is - * achieved by allocating space for each individual item, and store pointers to - * these items in the contiguous region in memory. - * - * TODO: Try to get rid of the raw KFileItem pointers in KCoreDirListerCache, and - * replace all occurrences of NonMovableFileItem(List) with KFileItem(List). - */ -class NonMovableFileItem : public KFileItem -{ -public: - NonMovableFileItem(const KFileItem &item) : - KFileItem(item) - {} -}; - -class NonMovableFileItemList : public QList -{ -public: - NonMovableFileItemList() - {} - - KFileItem findByName(const QString &fileName) const - { - const_iterator it = begin(); - const const_iterator itend = end(); - for (; it != itend; ++it) { - if ((*it).name() == fileName) { - return *it; - } - } - return KFileItem(); - } - - KFileItemList toKFileItemList() const - { - KFileItemList result; - result.reserve(count()); - - foreach (const NonMovableFileItem &item, *this) { - result.append(item); - } - - return result; - } -}; - class KCoreDirLister; namespace KIO { class Job; class ListJob; } class OrgKdeKDirNotifyInterface; struct KCoreDirListerCacheDirectoryData; class Q_DECL_HIDDEN KCoreDirLister::Private { public: Private(KCoreDirLister *parent) : m_parent(parent) { complete = false; autoUpdate = false; delayedMimeTypes = false; rootFileItem = KFileItem(); - lstNewItems = nullptr; - lstRefreshItems = nullptr; - lstMimeFilteredItems = nullptr; - lstRemoveItems = nullptr; - hasPendingChanges = false; } void _k_emitCachedItems(const QUrl &, bool, bool); void _k_slotInfoMessage(KJob *, const QString &); void _k_slotPercent(KJob *, unsigned long); void _k_slotTotalSize(KJob *, qulonglong); void _k_slotProcessedSize(KJob *, qulonglong); void _k_slotSpeed(KJob *, unsigned long); bool doMimeExcludeFilter(const QString &mimeExclude, const QStringList &filters) const; void connectJob(KIO::ListJob *); void jobDone(KIO::ListJob *); uint numJobs(); void addNewItem(const QUrl &directoryUrl, const KFileItem &item); - void addNewItems(const QUrl &directoryUrl, const NonMovableFileItemList &items); + void addNewItems(const QUrl &directoryUrl, const QList &items); void addRefreshItem(const QUrl &directoryUrl, const KFileItem &oldItem, const KFileItem &item); void emitItems(); void emitItemsDeleted(const KFileItemList &items); /** * Redirect this dirlister from oldUrl to newUrl. * @param keepItems if true, keep the fileitems (e.g. when renaming an existing dir); * if false, clear out everything (e.g. when redirecting during listing). */ void redirect(const QUrl &oldUrl, const QUrl &newUrl, bool keepItems); /** * Should this item be visible according to the current filter settings? */ bool isItemVisible(const KFileItem &item) const; void prepareForSettingsChange() { if (!hasPendingChanges) { hasPendingChanges = true; oldSettings = settings; } } void emitChanges(); class CachedItemsJob; CachedItemsJob *cachedItemsJobForUrl(const QUrl &url) const; KCoreDirLister *m_parent; /** * List of dirs handled by this dirlister. The first entry is the base URL. * For a tree view, it contains all the dirs shown. */ QList lstDirs; // toplevel URL QUrl url; bool complete: 1; bool autoUpdate: 1; bool delayedMimeTypes: 1; bool hasPendingChanges: 1; // i.e. settings != oldSettings struct JobData { long unsigned int percent, speed; KIO::filesize_t processedSize, totalSize; }; QMap jobData; // file item for the root itself (".") KFileItem rootFileItem; typedef QHash NewItemsHash; - NewItemsHash *lstNewItems; - QList > *lstRefreshItems; - KFileItemList *lstMimeFilteredItems, *lstRemoveItems; + NewItemsHash lstNewItems; + QList > lstRefreshItems; + KFileItemList lstMimeFilteredItems, lstRemoveItems; QList m_cachedItemsJobs; QString nameFilter; // parsed into lstFilters struct FilterSettings { FilterSettings() : isShowingDotFiles(false), dirOnlyMode(false) {} bool isShowingDotFiles; bool dirOnlyMode; QList lstFilters; QStringList mimeFilter; QStringList mimeExcludeFilter; }; FilterSettings settings; FilterSettings oldSettings; friend class KCoreDirListerCache; }; /** * Design of the cache: * There is a single KCoreDirListerCache for the whole process. * It holds all the items used by the dir listers (itemsInUse) * as well as a cache of the recently used items (itemsCached). * Those items are grouped by directory (a DirItem represents a whole directory). * * KCoreDirListerCache also runs all the jobs for listing directories, whether they are for * normal listing or for updates. * For faster lookups, it also stores a hash table, which gives for a directory URL: * - the dirlisters holding that URL (listersCurrentlyHolding) * - the dirlisters currently listing that URL (listersCurrentlyListing) */ class KCoreDirListerCache : public QObject { Q_OBJECT public: KCoreDirListerCache(); // only called by K_GLOBAL_STATIC ~KCoreDirListerCache(); void updateDirectory(const QUrl &dir); KFileItem itemForUrl(const QUrl &url) const; - NonMovableFileItemList *itemsForDir(const QUrl &dir) const; + QList *itemsForDir(const QUrl &dir) const; bool listDir(KCoreDirLister *lister, const QUrl &_url, bool _keep, bool _reload); // stop all running jobs for lister void stop(KCoreDirLister *lister, bool silent = false); // stop just the job listing url for lister void stopListingUrl(KCoreDirLister *lister, const QUrl &_url, bool silent = false); void setAutoUpdate(KCoreDirLister *lister, bool enable); void forgetDirs(KCoreDirLister *lister); void forgetDirs(KCoreDirLister *lister, const QUrl &_url, bool notify); KFileItem findByName(const KCoreDirLister *lister, const QString &_name) const; // findByUrl returns a pointer so that it's possible to modify the item. // See itemForUrl for the version that returns a readonly kfileitem. // @param lister can be 0. If set, it is checked that the url is held by the lister - KFileItem *findByUrl(const KCoreDirLister *lister, const QUrl &url) const; + KFileItem findByUrl(const KCoreDirLister *lister, const QUrl &url) const; // Called by CachedItemsJob: // Emits the cached items, for this lister and this url void emitItemsFromCache(KCoreDirLister::Private::CachedItemsJob *job, KCoreDirLister *lister, const QUrl &_url, bool _reload, bool _emitCompleted); // Called by CachedItemsJob: void forgetCachedItemsJob(KCoreDirLister::Private::CachedItemsJob *job, KCoreDirLister *lister, const QUrl &url); public Q_SLOTS: /** * Notify that files have been added in @p directory * The receiver will list that directory again to find * the new items (since it needs more than just the names anyway). * Connected to the DBus signal from the KDirNotify interface. */ void slotFilesAdded(const QString &urlDirectory); /** * Notify that files have been deleted. * This call passes the exact urls of the deleted files * so that any view showing them can simply remove them * or be closed (if its current dir was deleted) * Connected to the DBus signal from the KDirNotify interface. */ void slotFilesRemoved(const QStringList &fileList); /** * Notify that files have been changed. * At the moment, this is only used for new icon, but it could be * used for size etc. as well. * Connected to the DBus signal from the KDirNotify interface. */ void slotFilesChanged(const QStringList &fileList); void slotFileRenamed(const QString &srcUrl, const QString &dstUrl, const QString &dstPath); private Q_SLOTS: void slotFileDirty(const QString &_file); void slotFileCreated(const QString &_file); void slotFileDeleted(const QString &_file); void slotEntries(KIO::Job *job, const KIO::UDSEntryList &entries); void slotResult(KJob *j); void slotRedirection(KIO::Job *job, const QUrl &url); void slotUpdateEntries(KIO::Job *job, const KIO::UDSEntryList &entries); void slotUpdateResult(KJob *job); void processPendingUpdates(); private: void itemsAddedInDirectory(const QUrl &url); class DirItem; DirItem *dirItemForUrl(const QUrl &dir) const; bool validUrl(KCoreDirLister *lister, const QUrl &_url) const; void stopListJob(const QUrl &url, bool silent); KIO::ListJob *jobForUrl(const QUrl &url, KIO::ListJob *not_job = nullptr); const QUrl &joburl(KIO::ListJob *job); void killJob(KIO::ListJob *job); // Called when something tells us that the directory @p url has changed. // Returns true if @p url is held by some lister (meaning: do the update now) // otherwise mark the cached item as not-up-to-date for later and return false bool checkUpdate(const QUrl &url); // Helper method for slotFileDirty void handleFileDirty(const QUrl &url); void handleDirDirty(const QUrl &url); // when there were items deleted from the filesystem all the listers holding // the parent directory need to be notified, the items have to be deleted // and removed from the cache including all the children. - void deleteUnmarkedItems(const QList&, NonMovableFileItemList &lstItems, const QHash &itemsToDelete); + void deleteUnmarkedItems(const QList&, QList &lstItems, const QHash &itemsToDelete); // Helper method called when we know that a list of items was deleted void itemsDeleted(const QList &listers, const KFileItemList &deletedItems); void slotFilesRemoved(const QList &urls); // common for slotRedirection and slotFileRenamed void renameDir(const QUrl &oldUrl, const QUrl &url); // common for deleteUnmarkedItems and slotFilesRemoved void deleteDir(const QUrl &dirUrl); // remove directory from cache (itemsCached), including all child dirs void removeDirFromCache(const QUrl &dir); // helper for renameDir void emitRedirections(const QUrl &oldUrl, const QUrl &url); /** * Emits refreshItem() in the directories that cared for oldItem. * The caller has to remember to call emitItems in the set of dirlisters returned * (but this allows to buffer change notifications) */ QSet emitRefreshItem(const KFileItem &oldItem, const KFileItem &fileitem); + /** + * Remove the item from the sorted by url list matching @p oldUrl, + * that is in the wrong place (because its url has changed) and insert @p item in the right place. + * @param oldUrl the previous url of the @p item + * @param item the modified item to be inserted + */ + void reinsert(const KFileItem &item, const QUrl &oldUrl) + { + const QUrl parentDir = oldUrl.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash); + DirItem *dirItem = dirItemForUrl(parentDir); + if (dirItem) { + auto it = std::lower_bound(dirItem->lstItems.begin(), dirItem->lstItems.end(), oldUrl); + Q_ASSERT(it != dirItem->lstItems.end()); + dirItem->lstItems.erase(it); + dirItem->insert(item); + } + } + /** * When KDirWatch tells us that something changed in "dir", we need to * also notify the dirlisters that are listing a symlink to "dir" (#213799) */ QList directoriesForCanonicalPath(const QUrl &dir) const; /** * Returns the names listed in dir's ".hidden" file, if it exists. * If a file named ".hidden" exists in the @p dir directory, this method * returns all the file names listed in that file. If it doesn't exist, an * empty set is returned. * @param dir path to the target directory. * @return names listed in the directory's ".hidden" file (empty if it doesn't exist). */ QSet filesInDotHiddenForDir(const QString& dir); #ifndef NDEBUG void printDebug(); #endif class DirItem { public: DirItem(const QUrl &dir, const QString &canonicalPath) : url(dir), m_canonicalPath(canonicalPath) { autoUpdates = 0; complete = false; watchedWhileInCache = false; } ~DirItem() { if (autoUpdates) { if (KDirWatch::exists() && url.isLocalFile()) { KDirWatch::self()->removeDir(m_canonicalPath); } // Since sendSignal goes through D-Bus, QCoreApplication has to be available // which might not be the case anymore from a global static dtor like the // lister cache if (QCoreApplication::instance()) { sendSignal(false, url); } } lstItems.clear(); } void sendSignal(bool entering, const QUrl &url) { // Note that "entering" means "start watching", and "leaving" means "stop watching" // (i.e. it's not when the user leaves the directory, it's when the directory is removed from the cache) if (entering) { org::kde::KDirNotify::emitEnteredDirectory(url); } else { org::kde::KDirNotify::emitLeftDirectory(url); } } void redirect(const QUrl &newUrl) { if (autoUpdates) { if (url.isLocalFile()) { KDirWatch::self()->removeDir(m_canonicalPath); } sendSignal(false, url); if (newUrl.isLocalFile()) { m_canonicalPath = QFileInfo(newUrl.toLocalFile()).canonicalFilePath(); KDirWatch::self()->addDir(m_canonicalPath); } sendSignal(true, newUrl); } url = newUrl; if (!rootItem.isNull()) { rootItem.setUrl(newUrl); } } void incAutoUpdate() { if (autoUpdates++ == 0) { if (url.isLocalFile()) { KDirWatch::self()->addDir(m_canonicalPath); } sendSignal(true, url); } } void decAutoUpdate() { if (--autoUpdates == 0) { if (url.isLocalFile()) { KDirWatch::self()->removeDir(m_canonicalPath); } sendSignal(false, url); } else if (autoUpdates < 0) { autoUpdates = 0; } } + // Insert the item in the sorted list + void insert(const KFileItem &item) + { + auto it = std::lower_bound(lstItems.begin(), lstItems.end(), item.url()); + lstItems.insert(it, item); + } + // number of KCoreDirListers using autoUpdate for this dir short autoUpdates; // this directory is up-to-date bool complete; // the directory is watched while being in the cache (useful for proper incAutoUpdate/decAutoUpdate count) bool watchedWhileInCache; // the complete url of this directory QUrl url; // the local path, with symlinks resolved, so that KDirWatch works QString m_canonicalPath; // KFileItem representing the root of this directory. // Remember that this is optional. FTP sites don't return '.' in // the list, so they give no root item KFileItem rootItem; - NonMovableFileItemList lstItems; + QList lstItems; }; // definition of the cache of ".hidden" files struct CacheHiddenFile { CacheHiddenFile(const QDateTime& mtime, const QSet& listedFiles) : mtime(mtime), listedFiles(listedFiles) { } QDateTime mtime; QSet listedFiles; }; //static const unsigned short MAX_JOBS_PER_LISTER; QMap runningListJobs; // an item is a complete directory QHash itemsInUse; QCache itemsCached; // cache of ".hidden" files QCache