diff --git a/kio/kio/kdirlister.cpp b/kio/kio/kdirlister.cpp index 94b1d1d781..a72167353a 100644 --- a/kio/kio/kdirlister.cpp +++ b/kio/kio/kdirlister.cpp @@ -1,2773 +1,2779 @@ /* 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 "kdirlister.h" #include "kdirlister_p.h" #include #include #include #include #include #include #include #include "kprotocolmanager.h" #include "kmountpoint.h" #include // 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 K_GLOBAL_STATIC(KDirListerCache, kDirListerCache) KDirListerCache::KDirListerCache() : itemsCached( 10 ) // keep the last 10 directories around { //kDebug(7004); connect( &pendingUpdateTimer, SIGNAL(timeout()), this, SLOT(processPendingUpdates()) ); pendingUpdateTimer.setSingleShot( true ); connect( KDirWatch::self(), SIGNAL(dirty(QString)), this, SLOT(slotFileDirty(QString)) ); connect( KDirWatch::self(), SIGNAL(created(QString)), this, SLOT(slotFileCreated(QString)) ); connect( KDirWatch::self(), SIGNAL(deleted(QString)), this, SLOT(slotFileDeleted(QString)) ); kdirnotify = new org::kde::KDirNotify(QString(), QString(), QDBusConnection::sessionBus(), this); connect(kdirnotify, SIGNAL(FileRenamed(QString,QString)), SLOT(slotFileRenamed(QString,QString))); connect(kdirnotify, SIGNAL(FilesAdded(QString)), SLOT(slotFilesAdded(QString))); connect(kdirnotify, SIGNAL(FilesChanged(QStringList)), SLOT(slotFilesChanged(QStringList))); connect(kdirnotify, SIGNAL(FilesRemoved(QStringList)), SLOT(slotFilesRemoved(QStringList))); // The use of KUrl::url() in ~DirItem (sendSignal) crashes if the static for QRegExpEngine got deleted already, // so we need to destroy the KDirListerCache before that. qAddPostRoutine(kDirListerCache.destroy); } KDirListerCache::~KDirListerCache() { //kDebug(7004); qDeleteAll(itemsInUse); itemsInUse.clear(); itemsCached.clear(); directoryData.clear(); if ( KDirWatch::exists() ) KDirWatch::self()->disconnect( this ); } // setting _reload to true will emit the old files and // call updateDirectory bool KDirListerCache::listDir( KDirLister *lister, const KUrl& _u, bool _keep, bool _reload ) { KUrl _url(_u); _url.cleanPath(); // kill consecutive slashes if (!_url.host().isEmpty() && KProtocolInfo::protocolClass(_url.protocol()) == ":local" && _url.protocol() != "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.adjustPath(KUrl::RemoveTrailingSlash); const QString urlStr = _url.url(); QString resolved; if (_url.isLocalFile()) { // Resolve symlinks (#213799) const QString local = _url.toLocalFile(); resolved = QFileInfo(local).canonicalFilePath(); if (local != resolved) canonicalUrls[resolved].append(urlStr); // 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)) { kDebug(7004) << lister << "url=" << _url << "not a valid url"; return false; } //kDebug(7004) << 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(urlStr); KDirListerCacheDirectoryData& dirData = directoryData[urlStr]; // 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 = 0; if (itemU || (!_reload && (itemFromCache = itemsCached.take(urlStr)) ) ) { if (itemU) { kDebug(7004) << "Entry already in use:" << _url; // if _reload is set, then we'll emit cached items and then updateDirectory. } else { kDebug(7004) << "Entry in cache:" << _url; itemsInUse.insert(urlStr, 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 KDirLister::Private::CachedItemsJob(lister, _url, _reload); } else { // dir not in cache or _reload is true if (_reload) { kDebug(7004) << "Reloading directory:" << _url; itemsCached.remove(urlStr); } else { kDebug(7004) << "Listing directory:" << _url; } itemU = new DirItem(_url, resolved); itemsInUse.insert(urlStr, 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->d->jobStarted(job); lister->d->connectJob(job); if (lister->d->window) job->ui()->setWindow(lister->d->window); connect(job, SIGNAL(entries(KIO::Job*,KIO::UDSEntryList)), this, SLOT(slotEntries(KIO::Job*,KIO::UDSEntryList))); connect(job, SIGNAL(result(KJob*)), this, SLOT(slotResult(KJob*))); connect(job, SIGNAL(redirection(KIO::Job*,KUrl)), this, SLOT(slotRedirection(KIO::Job*,KUrl))); emit lister->started(_url); } //kDebug(7004) << "Entry now being listed by" << dirData.listersCurrentlyListing; } } else { kDebug(7004) << "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( urlStr ); // job will be 0 if we were listing from cache rather than listing from a kio job. if( job ) { lister->d->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. if (!itemU->lstItems.isEmpty()) { kDebug() << "Listing" << itemU->lstItems.count() << "cached items soon"; new KDirLister::Private::CachedItemsJob(lister, _url, _reload); } else { // The other lister hasn't emitted anything yet. Good, we'll just listen to it. // One problem could be if we have _reload=true and the existing job doesn't, though. } #ifdef DEBUG_CACHE printDebug(); #endif } return true; } KDirLister::Private::CachedItemsJob* KDirLister::Private::cachedItemsJobForUrl(const KUrl& url) const { Q_FOREACH(CachedItemsJob* job, m_cachedItemsJobs) { if (job->url() == url) return job; } return 0; } KDirLister::Private::CachedItemsJob::CachedItemsJob(KDirLister* lister, const KUrl& url, bool reload) : KJob(lister), m_lister(lister), m_url(url), m_reload(reload), m_emitCompleted(true) { //kDebug() << "Creating CachedItemsJob" << this << "for lister" << lister << url; if (lister->d->cachedItemsJobForUrl(url)) { kWarning(7004) << "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 KDirLister::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 KDirLister::Private::CachedItemsJob::doKill() { //kDebug(7004) << 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 = 0; return true; } void KDirListerCache::emitItemsFromCache(KDirLister::Private::CachedItemsJob* cachedItemsJob, KDirLister* lister, const KUrl& _url, bool _reload, bool _emitCompleted) { const QString urlStr = _url.url(); KDirLister::Private* kdl = lister->d; kdl->complete = false; DirItem *itemU = kDirListerCache->itemsInUse.value(urlStr); if (!itemU) { kWarning(7004) << "Can't find item for directory" << urlStr << "anymore"; } else { const KFileItemList 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()) { //kDebug(7004) << "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 KDirListerCache::forgetCachedItemsJob(KDirLister::Private::CachedItemsJob* cachedItemsJob, KDirLister* lister, const KUrl& _url) { // Modifications to data structures only below this point; // so that addNewItems is called with a consistent state const QString urlStr = _url.url(); lister->d->m_cachedItemsJobs.removeAll(cachedItemsJob); KDirListerCacheDirectoryData& dirData = directoryData[urlStr]; Q_ASSERT(dirData.listersCurrentlyListing.contains(lister)); KIO::ListJob *listJob = jobForUrl(urlStr); if (!listJob) { Q_ASSERT(!dirData.listersCurrentlyHolding.contains(lister)); //kDebug(7004) << "Moving from listing to holding, because no more job" << lister << urlStr; dirData.listersCurrentlyHolding.append( lister ); dirData.listersCurrentlyListing.removeAll( lister ); } else { //kDebug(7004) << "Still having a listjob" << listJob << ", so not moving to currently-holding."; } } bool KDirListerCache::validUrl( const KDirLister *lister, const KUrl& url ) const { if ( !url.isValid() ) { if ( lister->d->autoErrorHandling ) { QString tmp = i18n("Malformed URL\n%1", url.prettyUrl() ); KMessageBox::error( lister->d->errorParent, tmp ); } return false; } if ( !KProtocolManager::supportsListing( url ) ) { if ( lister->d->autoErrorHandling ) { QString tmp = i18n("URL cannot be listed\n%1", url.prettyUrl() ); KMessageBox::error( lister->d->errorParent, tmp ); } return false; } return true; } void KDirListerCache::stop( KDirLister *lister, bool silent ) { #ifdef DEBUG_CACHE //printDebug(); #endif //kDebug(7004) << "lister:" << lister << "silent=" << silent; const KUrl::List urls = lister->d->lstDirs; Q_FOREACH(const KUrl& 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 ) { KDirListerCacheDirectoryData& dirData = dirit.value(); if (dirData.listersCurrentlyListing.contains(lister)) { kDebug(7004) << "ERROR: found lister" << lister << "in list - for" << dirit.key(); Q_ASSERT(false); } } #endif } void KDirListerCache::stopListingUrl(KDirLister *lister, const KUrl& _u, bool silent) { KUrl url(_u); url.adjustPath( KUrl::RemoveTrailingSlash ); const QString urlStr = url.url(); KDirLister::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 kDebug(7004) << lister << " url=" << url; QHash::iterator dirit = directoryData.find(urlStr); if (dirit == directoryData.end()) return; KDirListerCacheDirectoryData& dirData = dirit.value(); if (dirData.listersCurrentlyListing.contains(lister)) { //kDebug(7004) << " found lister" << lister << "in list - for" << urlStr; if (dirData.listersCurrentlyListing.count() == 1) { // This was the only dirlister interested in the list job -> kill the job stopListJob(urlStr, 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 KDirListerCache::stopListJob(const QString& 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) { //kDebug() << "Killing list job" << job << "for" << url; if (silent) { job->setProperty("_kdlc_silent", true); } job->kill(KJob::EmitResult); } } void KDirListerCache::setAutoUpdate( KDirLister *lister, bool enable ) { // IMPORTANT: this method does not check for the current autoUpdate state! for ( KUrl::List::const_iterator it = lister->d->lstDirs.constBegin(); it != lister->d->lstDirs.constEnd(); ++it ) { DirItem* dirItem = itemsInUse.value((*it).url()); Q_ASSERT(dirItem); if ( enable ) dirItem->incAutoUpdate(); else dirItem->decAutoUpdate(); } } void KDirListerCache::forgetDirs( KDirLister *lister ) { //kDebug(7004) << 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 KUrl::List lstDirsCopy = lister->d->lstDirs; lister->d->lstDirs.clear(); //kDebug() << "Iterating over dirs" << lstDirsCopy; for ( KUrl::List::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; } const bool supermount = mp->mountType() == "supermount"; if (supermount) { return true; } // noauto -> manually mounted. Otherwise, mounted at boot time, won't be unmounted any time soon hopefully. return mp->mountOptions().contains("noauto"); } void KDirListerCache::forgetDirs( KDirLister *lister, const KUrl& _url, bool notify ) { //kDebug(7004) << lister << " _url: " << _url; KUrl url( _url ); url.adjustPath( KUrl::RemoveTrailingSlash ); const QString urlStr = url.url(); DirectoryDataHash::iterator dit = directoryData.find(urlStr); if (dit == directoryData.end()) return; KDirListerCacheDirectoryData& dirData = *dit; dirData.listersCurrentlyHolding.removeAll(lister); // This lister doesn't care for updates running in anymore KIO::ListJob *job = jobForUrl(urlStr); if (job) lister->d->jobDone(job); DirItem *item = itemsInUse.value(urlStr); 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( urlStr ); // this job is a running update which nobody cares about anymore if ( job ) { killJob( job ); kDebug(7004) << "Killing update job for " << urlStr; // Well, the user of KDirLister 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 KFileItemList::const_iterator kit = item->lstItems.constBegin(); KFileItemList::const_iterator kend = item->lstItems.constEnd(); for ( ; kit != kend && !containsManuallyMounted; ++kit ) if ( (*kit).isDir() && manually_mounted((*kit).url().toLocalFile(), possibleMountPoints) ) containsManuallyMounted = true; } } if ( isManuallyMounted || containsManuallyMounted ) // [**] { kDebug(7004) << "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 = 0; } } if ( item && lister->d->autoUpdate ) item->decAutoUpdate(); // Inserting into QCache must be done last, since it might delete the item if (item && insertIntoCache) { kDebug(7004) << lister << "item moved into cache:" << url; itemsCached.insert(urlStr, item); } } void KDirListerCache::updateDirectory( const KUrl& _dir ) { kDebug(7004) << _dir; QString urlStr = _dir.url(KUrl::RemoveTrailingSlash); - if ( !checkUpdate( urlStr ) ) + if (!checkUpdate(urlStr)) { + if (_dir.isLocalFile() && findByUrl(0, _dir)) { + 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 KDirListerCacheDirectoryData& dirData = directoryData[urlStr]; QList listers = dirData.listersCurrentlyListing; QList holders = dirData.listersCurrentlyHolding; //kDebug(7004) << urlStr << "listers=" << listers << "holders=" << holders; // restart the job for _dir if it is running already bool killed = false; QWidget *window = 0; KIO::ListJob *job = jobForUrl( urlStr ); if (job) { window = job->ui()->window(); killJob( job ); killed = true; foreach ( KDirLister *kdl, listers ) kdl->d->jobDone( job ); foreach ( KDirLister *kdl, holders ) kdl->d->jobDone( job ); } else { // Emit any cached items. // updateDirectory() is about the diff compared to the cached items... Q_FOREACH(KDirLister *kdl, listers) { KDirLister::Private::CachedItemsJob* cachedItemsJob = kdl->d->cachedItemsJobForUrl(_dir); if (cachedItemsJob) { cachedItemsJob->setEmitCompleted(false); cachedItemsJob->done(); // removes from cachedItemsJobs list delete cachedItemsJob; killed = true; } } } //kDebug(7004) << "Killed=" << killed; // we don't need to emit canceled signals since we only replaced the job, // the listing is continuing. if (!(listers.isEmpty() || killed)) { kWarning() << "The unexpected happened."; kWarning() << "listers for" << _dir << "=" << listers; kWarning() << "job=" << job; Q_FOREACH(KDirLister *kdl, listers) { kDebug() << "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, SIGNAL(entries(KIO::Job*,KIO::UDSEntryList)), this, SLOT(slotUpdateEntries(KIO::Job*,KIO::UDSEntryList)) ); connect( job, SIGNAL(result(KJob*)), this, SLOT(slotUpdateResult(KJob*)) ); kDebug(7004) << "update started in" << _dir; foreach ( KDirLister *kdl, listers ) { kdl->d->jobStarted( job ); } if ( !holders.isEmpty() ) { if ( !killed ) { bool first = true; foreach ( KDirLister *kdl, holders ) { kdl->d->jobStarted( job ); if ( first && kdl->d->window ) { first = false; job->ui()->setWindow( kdl->d->window ); } emit kdl->started( _dir ); } } else { job->ui()->setWindow( window ); foreach ( KDirLister *kdl, holders ) { kdl->d->jobStarted( job ); } } } } bool KDirListerCache::checkUpdate( const QString& _dir ) { if ( !itemsInUse.contains(_dir) ) { DirItem *item = itemsCached[_dir]; if ( item && item->complete ) { // Stop watching items once they are only in the cache and not used anymore. // We'll trigger an update when listing that dir again later. item->complete = false; item->watchedWhileInCache = false; item->decAutoUpdate(); // Hmm, this debug output might include login/password from the _dir URL. //kDebug(7004) << "directory " << _dir << " not in use, marked dirty."; } //else //kDebug(7004) << "aborted, directory " << _dir << " not in cache."; return false; } else return true; } KFileItem KDirListerCache::itemForUrl( const KUrl& url ) const { KFileItem *item = findByUrl( 0, url ); if (item) { return *item; } else { return KFileItem(); } } KDirListerCache::DirItem *KDirListerCache::dirItemForUrl(const KUrl& dir) const { const QString urlStr = dir.url(KUrl::RemoveTrailingSlash); DirItem *item = itemsInUse.value(urlStr); if ( !item ) item = itemsCached[urlStr]; return item; } KFileItemList *KDirListerCache::itemsForDir(const KUrl& dir) const { DirItem *item = dirItemForUrl(dir); return item ? &item->lstItems : 0; } KFileItem KDirListerCache::findByName( const KDirLister *lister, const QString& _name ) const { Q_ASSERT(lister); for (KUrl::List::const_iterator it = lister->d->lstDirs.constBegin(); it != lister->d->lstDirs.constEnd(); ++it) { DirItem* dirItem = itemsInUse.value((*it).url()); Q_ASSERT(dirItem); const KFileItem item = dirItem->lstItems.findByName(_name); if (!item.isNull()) return item; } return KFileItem(); } KFileItem *KDirListerCache::findByUrl( const KDirLister *lister, const KUrl& _u ) const { KUrl url(_u); url.adjustPath(KUrl::RemoveTrailingSlash); KUrl parentDir(url); parentDir.setPath( parentDir.directory() ); DirItem* dirItem = dirItemForUrl(parentDir); if (dirItem) { // If lister is set, check that it contains this dir if (!lister || lister->d->lstDirs.contains(parentDir)) { KFileItemList::iterator it = dirItem->lstItems.begin(); const KFileItemList::iterator end = dirItem->lstItems.end(); for (; it != end ; ++it) { if ((*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 0; } void KDirListerCache::slotFilesAdded( const QString &dir /*url*/ ) // from KDirNotify signals { KUrl urlDir(dir); kDebug(7004) << urlDir; // output urls, not qstrings, since they might contain a password if (urlDir.isLocalFile()) { Q_FOREACH(const QString& u, directoriesForCanonicalPath(urlDir.toLocalFile())) { updateDirectory(KUrl(u)); } } else { updateDirectory(urlDir); } } void KDirListerCache::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(KUrl::List(fileList)); } void KDirListerCache::slotFilesRemoved(const KUrl::List& fileList) { //kDebug(7004) << fileList.count(); // Group notifications by parent dirs (usually there would be only one parent dir) QMap removedItemsByDir; KUrl::List deletedSubdirs; for (KUrl::List::const_iterator it = fileList.begin(); it != fileList.end() ; ++it) { const KUrl url(*it); DirItem* dirItem = dirItemForUrl(url); // is it a listed directory? if (dirItem) { deletedSubdirs.append(url); if (!dirItem->rootItem.isNull()) { removedItemsByDir[url.url()].append(dirItem->rootItem); } } KUrl parentDir(url); parentDir.setPath(parentDir.directory()); dirItem = dirItemForUrl(parentDir); if (!dirItem) continue; for (KFileItemList::iterator fit = dirItem->lstItems.begin(), fend = dirItem->lstItems.end(); fit != fend ; ++fit) { if ((*fit).url() == url) { const KFileItem fileitem = *fit; removedItemsByDir[parentDir.url()].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; } } } QMap::const_iterator rit = removedItemsByDir.constBegin(); for(; rit != removedItemsByDir.constEnd(); ++rit) { // Tell the views about it before calling deleteDir. // They might need the subdirs' file items (see the dirtree). DirectoryDataHash::const_iterator dit = directoryData.constFind(rit.key()); if (dit != directoryData.constEnd()) { itemsDeleted((*dit).listersCurrentlyHolding, rit.value()); } } Q_FOREACH(const KUrl& 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 KDirListerCache::slotFilesChanged( const QStringList &fileList ) // from KDirNotify signals { //kDebug(7004) << fileList; KUrl::List dirsToUpdate; QStringList::const_iterator it = fileList.begin(); for (; it != fileList.end() ; ++it) { KUrl url( *it ); KFileItem *fileitem = findByUrl(0, url); if (!fileitem) { kDebug(7004) << "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) KUrl dir(url); dir.setPath(dir.directory()); if (!dirsToUpdate.contains(dir)) dirsToUpdate.prepend(dir); } } KUrl::List::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 KDirListerCache::slotFileRenamed( const QString &_src, const QString &_dst ) // from KDirNotify signals { KUrl src( _src ); KUrl dst( _dst ); kDebug(7004) << src << "->" << dst; #ifdef DEBUG_CACHE printDebug(); #endif KUrl oldurl(src); oldurl.adjustPath( KUrl::RemoveTrailingSlash ); KFileItem *fileitem = findByUrl(0, oldurl); if (!fileitem) { kDebug(7004) << "Item not found:" << oldurl; return; } 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(0, dst); if (existingDestItem) { //kDebug() << dst << "already existed, let's delete it"; slotFilesRemoved(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(); nameOnly &= src.directory( KUrl::IgnoreTrailingSlash | KUrl::AppendTrailingSlash ) == dst.directory( KUrl::IgnoreTrailingSlash | KUrl::AppendTrailingSlash ); if (!nameOnly && fileitem->isDir()) { renameDir( src, 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(0, oldurl); if (!fileitem) //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()) { // it uses UDS_LOCAL_PATH? ouch, needs an update then slotFilesChanged( QStringList() << src.url() ); } else { if( nameOnly ) fileitem->setName( dst.fileName() ); else fileitem->setUrl( dst ); fileitem->refreshMimeType(); fileitem->determineMimeType(); QSet listers = emitRefreshItem( oldItem, *fileitem ); Q_FOREACH(KDirLister * kdl, listers) { kdl->d->emitItems(); } } #ifdef DEBUG_CACHE printDebug(); #endif } QSet KDirListerCache::emitRefreshItem(const KFileItem& oldItem, const KFileItem& fileitem) { //kDebug(7004) << "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 KUrl parentDir( oldItem.url() ); parentDir.setPath( parentDir.directory() ); const QString parentDirURL = parentDir.url(); DirectoryDataHash::iterator dit = directoryData.find(parentDirURL); 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().url()); if (dit != directoryData.end()) listers += (*dit).listersCurrentlyHolding + (*dit).listersCurrentlyListing; } QSet listersToRefresh; Q_FOREACH(KDirLister *kdl, listers) { // For a directory, look for dirlisters where it's the root item. KUrl 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.setPath(directoryUrl.directory()); kdl->d->addRefreshItem(directoryUrl, oldItem, fileitem); } listersToRefresh.insert(kdl); } return listersToRefresh; } QStringList KDirListerCache::directoriesForCanonicalPath(const QString& dir) const { QStringList 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) kDebug() << 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 KDirListerCache::slotFileDirty( const QString& path ) { kDebug(7004) << path; // File or dir? KDE_struct_stat buff; if ( KDE::stat( path, &buff ) != 0 ) return; // error const bool isDir = S_ISDIR(buff.st_mode); KUrl url(path); url.adjustPath(KUrl::RemoveTrailingSlash); if (isDir) { Q_FOREACH(const QString& dir, directoriesForCanonicalPath(url.toLocalFile())) { handleDirDirty(dir); } } else { Q_FOREACH(const QString& dir, directoriesForCanonicalPath(url.directory())) { KUrl aliasUrl(dir); aliasUrl.addPath(url.fileName()); handleFileDirty(aliasUrl); } } } // Called by slotFileDirty void KDirListerCache::handleDirDirty(const KUrl& 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 dirPath = url.toLocalFile(KUrl::AddTrailingSlash); QMutableSetIterator pendingIt(pendingUpdates); while (pendingIt.hasNext()) { const QString updPath = pendingIt.next(); //kDebug(7004) << "had pending update" << updPath; if (updPath.startsWith(dirPath) && updPath.indexOf('/', dirPath.length()) == -1) { // direct child item kDebug(7004) << "forgetting about individual update to" << updPath; pendingIt.remove(); } } updateDirectory(url); } // Called by slotFileDirty void KDirListerCache::handleFileDirty(const KUrl& url) { // A file: do we know about it already? KFileItem* existingItem = findByUrl(0, url); if (!existingItem) { // No - update the parent dir then KUrl dir(url); dir.setPath(url.directory()); updateDirectory(dir); } else { // A known file: delay updating it, FAM is flooding us with events const QString filePath = url.toLocalFile(); if (!pendingUpdates.contains(filePath)) { KUrl dir(url); dir.setPath(dir.directory()); if (checkUpdate(dir.url())) { pendingUpdates.insert(filePath); if (!pendingUpdateTimer.isActive()) pendingUpdateTimer.start(500); } } } } void KDirListerCache::slotFileCreated( const QString& path ) // from KDirWatch { kDebug(7004) << 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. KUrl fileUrl(path); slotFilesAdded(fileUrl.directory()); } void KDirListerCache::slotFileDeleted( const QString& path ) // from KDirWatch { kDebug(7004) << path; KUrl u( path ); QStringList fileUrls; Q_FOREACH(KUrl url, directoriesForCanonicalPath(u.directory())) { url.addPath(u.fileName()); fileUrls << url.url(); } slotFilesRemoved(fileUrls); } void KDirListerCache::slotEntries( KIO::Job *job, const KIO::UDSEntryList &entries ) { KUrl url(joburl( static_cast(job) )); url.adjustPath(KUrl::RemoveTrailingSlash); QString urlStr = url.url(); //kDebug(7004) << "new entries for " << url; DirItem *dir = itemsInUse.value(urlStr); if (!dir) { kError(7004) << "Internal error: job is listing" << url << "but itemsInUse only knows about" << itemsInUse.keys(); Q_ASSERT( dir ); return; } DirectoryDataHash::iterator dit = directoryData.find(urlStr); if (dit == directoryData.end()) { kError(7004) << "Internal error: job is listing" << url << "but directoryData doesn't know about that url, only about:" << directoryData.keys(); Q_ASSERT(dit != directoryData.end()); return; } KDirListerCacheDirectoryData& dirData = *dit; if (dirData.listersCurrentlyListing.isEmpty()) { kError(7004) << "Internal error: job is listing" << url << "but directoryData says no listers are currently listing " << urlStr; #ifndef NDEBUG printDebug(); #endif Q_ASSERT( !dirData.listersCurrentlyListing.isEmpty() ); return; } // check if anyone wants the mimetypes immediately bool delayedMimeTypes = true; foreach ( KDirLister *kdl, dirData.listersCurrentlyListing ) delayedMimeTypes &= kdl->d->delayedMimeTypes; 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 == "." ) { 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 ( KDirLister *kdl, dirData.listersCurrentlyListing ) if ( kdl->d->rootFileItem.isNull() && kdl->d->url == url ) kdl->d->rootFileItem = dir->rootItem; } else if ( name != ".." ) { KFileItem item( *it, url, delayedMimeTypes, true ); //kDebug(7004)<< "Adding item: " << item.url(); dir->lstItems.append( item ); foreach ( KDirLister *kdl, dirData.listersCurrentlyListing ) kdl->d->addNewItem(url, item); } } foreach ( KDirLister *kdl, dirData.listersCurrentlyListing ) kdl->d->emitItems(); } void KDirListerCache::slotResult( KJob *j ) { #ifdef DEBUG_CACHE //printDebug(); #endif Q_ASSERT( j ); KIO::ListJob *job = static_cast( j ); runningListJobs.remove( job ); KUrl jobUrl(joburl( job )); jobUrl.adjustPath(KUrl::RemoveTrailingSlash); // need remove trailing slashes again, in case of redirections QString jobUrlStr = jobUrl.url(); kDebug(7004) << "finished listing" << jobUrl; DirectoryDataHash::iterator dit = directoryData.find(jobUrlStr); if (dit == directoryData.end()) { kError() << "Nothing found in directoryData for URL" << jobUrlStr; #ifndef NDEBUG printDebug(); #endif Q_ASSERT(dit != directoryData.end()); return; } KDirListerCacheDirectoryData& dirData = *dit; if ( dirData.listersCurrentlyListing.isEmpty() ) { kError() << "OOOOPS, nothing in directoryData.listersCurrentlyListing for" << jobUrlStr; // 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 KDirListerCache in case someone // calls listDir during the signal emission Q_ASSERT( dirData.listersCurrentlyHolding.isEmpty() ); dirData.moveListersWithoutCachedItemsJob(jobUrl); if ( job->error() ) { foreach ( KDirLister *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(jobUrlStr); Q_ASSERT( dir ); dir->complete = true; foreach ( KDirLister* 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(); #ifdef DEBUG_CACHE printDebug(); #endif } void KDirListerCache::slotRedirection( KIO::Job *j, const KUrl& url ) { Q_ASSERT( j ); KIO::ListJob *job = static_cast( j ); KUrl oldUrl(job->url()); // here we really need the old url! KUrl newUrl(url); // strip trailing slashes oldUrl.adjustPath(KUrl::RemoveTrailingSlash); newUrl.adjustPath(KUrl::RemoveTrailingSlash); if ( oldUrl == newUrl ) { kDebug(7004) << "New redirection url same as old, giving up."; return; } else if (newUrl.isEmpty()) { kDebug(7004) << "New redirection url is empty, giving up."; return; } const QString oldUrlStr = oldUrl.url(); const QString newUrlStr = newUrl.url(); kDebug(7004) << oldUrl << "->" << newUrl; #ifdef DEBUG_CACHE // Can't do that here. KDirListerCache::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(oldUrlStr); Q_ASSERT( dir ); DirectoryDataHash::iterator dit = directoryData.find(oldUrlStr); Q_ASSERT(dit != directoryData.end()); KDirListerCacheDirectoryData oldDirData = *dit; directoryData.erase(dit); Q_ASSERT( !oldDirData.listersCurrentlyListing.isEmpty() ); const QList listers = oldDirData.listersCurrentlyListing; Q_ASSERT( !listers.isEmpty() ); foreach ( KDirLister *kdl, listers ) { kdl->d->redirect(oldUrlStr, 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 ( KDirLister *kdl, holders ) { kdl->d->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(newUrlStr); if ( newDir ) { kDebug(7004) << 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( newUrlStr, job ); // listers of newUrl with oldJob: forget about the oldJob and use the already running one // which will be converted to an updateJob KDirListerCacheDirectoryData& newDirData = directoryData[newUrlStr]; QList& curListers = newDirData.listersCurrentlyListing; if ( !curListers.isEmpty() ) { kDebug(7004) << "and it is currently listed"; Q_ASSERT( oldJob ); // ?! foreach ( KDirLister *kdl, curListers ) { // listers of newUrl kdl->d->jobDone( oldJob ); kdl->d->jobStarted( job ); kdl->d->connectJob( job ); } // append listers of oldUrl with newJob to listers of newUrl with oldJob foreach ( KDirLister *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() ) { kDebug(7004) << "and it is currently held."; foreach ( KDirLister *kdl, curHolders ) { // holders of newUrl kdl->d->jobStarted( job ); emit kdl->started( newUrl ); } // append holders of oldUrl to holders of newUrl foreach ( KDirLister *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 ( KDirLister *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( newUrlStr )) ) { kDebug(7004) << newUrl << "is unused, but already in the cache."; delete dir; itemsInUse.insert( newUrlStr, newDir ); KDirListerCacheDirectoryData& newDirData = directoryData[newUrlStr]; newDirData.listersCurrentlyListing = listers; newDirData.listersCurrentlyHolding = holders; // emit old items: listers, holders foreach ( KDirLister *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 { kDebug(7004) << newUrl << "has not been listed yet."; dir->rootItem = KFileItem(); dir->lstItems.clear(); dir->redirect( newUrl ); itemsInUse.insert( newUrlStr, dir ); KDirListerCacheDirectoryData& newDirData = directoryData[newUrlStr]; 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, SIGNAL(entries(KIO::Job*,KIO::UDSEntryList)), this, SLOT(slotUpdateEntries(KIO::Job*,KIO::UDSEntryList)) ); connect( job, SIGNAL(result(KJob*)), this, SLOT(slotUpdateResult(KJob*)) ); // FIXME: autoUpdate-Counts!! #ifdef DEBUG_CACHE printDebug(); #endif } struct KDirListerCache::ItemInUseChange { ItemInUseChange(const QString& old, const QString& newU, DirItem* di) : oldUrl(old), newUrl(newU), dirItem(di) {} QString oldUrl; QString newUrl; DirItem* dirItem; }; void KDirListerCache::renameDir( const KUrl &oldUrl, const KUrl &newUrl ) { kDebug(7004) << 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 QHash::iterator itu = itemsInUse.begin(); const QHash::iterator ituend = itemsInUse.end(); for (; itu != ituend ; ++itu) { DirItem *dir = itu.value(); KUrl oldDirUrl ( itu.key() ); //kDebug(7004) << "itemInUse:" << oldDirUrl; // Check if this dir is oldUrl, or a subfolder of it if ( oldUrl.isParentOf( oldDirUrl ) ) { // TODO should use KUrl::cleanpath like isParentOf does QString relPath = oldDirUrl.path().mid( oldUrl.path().length() ); KUrl newDirUrl( newUrl ); // take new base if ( !relPath.isEmpty() ) newDirUrl.addPath( relPath ); // add unchanged relative path //kDebug(7004) << "new url=" << newDirUrl; // Update URL in dir item and in itemsInUse dir->redirect( newDirUrl ); itemsToChange.append(ItemInUseChange(oldDirUrl.url(KUrl::RemoveTrailingSlash), newDirUrl.url(KUrl::RemoveTrailingSlash), dir)); // Rename all items under that dir for ( KFileItemList::iterator kit = dir->lstItems.begin(), kend = dir->lstItems.end(); kit != kend ; ++kit ) { const KFileItem oldItem = *kit; const KUrl oldItemUrl ((*kit).url()); const QString oldItemUrlStr( oldItemUrl.url(KUrl::RemoveTrailingSlash) ); KUrl newItemUrl( oldItemUrl ); newItemUrl.setPath( newDirUrl.path() ); newItemUrl.addPath( oldItemUrl.fileName() ); kDebug(7004) << "renaming" << oldItemUrl << "to" << newItemUrl; (*kit).setUrl(newItemUrl); listers |= emitRefreshItem(oldItem, *kit); } emitRedirections( oldDirUrl, newDirUrl ); } } Q_FOREACH(KDirLister * 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); } // 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 KDirListerCache::emitRedirections( const KUrl &oldUrl, const KUrl &newUrl ) { kDebug(7004) << oldUrl << "->" << newUrl; const QString oldUrlStr = oldUrl.url(KUrl::RemoveTrailingSlash); const QString newUrlStr = newUrl.url(KUrl::RemoveTrailingSlash); KIO::ListJob *job = jobForUrl( oldUrlStr ); 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(oldUrlStr); if ( dit == directoryData.end() ) return; const QList listers = (*dit).listersCurrentlyListing; const QList holders = (*dit).listersCurrentlyHolding; KDirListerCacheDirectoryData& newDirData = directoryData[newUrlStr]; // Tell the world that the job listing the old url is dead. foreach ( KDirLister *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 ( KDirLister *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 ( KDirLister *kdl, listers ) emit kdl->started( newUrl ); } // And notify the dirlisters of the redirection foreach ( KDirLister *kdl, holders ) { kdl->d->redirect(oldUrl, newUrl, true /*keep items*/); } } void KDirListerCache::removeDirFromCache( const KUrl& dir ) { kDebug(7004) << dir; const QList cachedDirs = itemsCached.keys(); // seems slow, but there's no qcache iterator... foreach(const QString& cachedDir, cachedDirs) { if ( dir.isParentOf( KUrl( cachedDir ) ) ) itemsCached.remove( cachedDir ); } } void KDirListerCache::slotUpdateEntries( KIO::Job* job, const KIO::UDSEntryList& list ) { runningListJobs[static_cast(job)] += list; } void KDirListerCache::slotUpdateResult( KJob * j ) { Q_ASSERT( j ); KIO::ListJob *job = static_cast( j ); KUrl jobUrl (joburl( job )); jobUrl.adjustPath(KUrl::RemoveTrailingSlash); // need remove trailing slashes again, in case of redirections QString jobUrlStr (jobUrl.url()); kDebug(7004) << "finished update" << jobUrl; KDirListerCacheDirectoryData& dirData = directoryData[jobUrlStr]; // 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 ( KDirLister* 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(jobUrlStr, 0); if (!dir) { kError(7004) << "Internal error: itemsInUse did not contain" << jobUrlStr; #ifndef NDEBUG printDebug(); #endif Q_ASSERT(dir); } else { dir->complete = true; } // check if anyone wants the mimetypes immediately bool delayedMimeTypes = true; foreach ( KDirLister *kdl, listers ) delayedMimeTypes &= kdl->d->delayedMimeTypes; QHash fileItems; // fileName -> KFileItem* // Unmark all items in url for ( KFileItemList::iterator kit = dir->lstItems.begin(), kend = dir->lstItems.end() ; kit != kend ; ++kit ) { (*kit).unmark(); fileItems.insert( (*kit).name(), &*kit ); } 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() ); // we duplicate the check for dotdot here, to avoid iterating over // all items again and checking in matchesFilter() that way. if ( name.isEmpty() || name == ".." ) continue; if ( name == "." ) { // if the update was started before finishing the original listing // there is no root item yet if ( dir->rootItem.isNull() ) { dir->rootItem = item; foreach ( KDirLister *kdl, listers ) if ( kdl->d->rootFileItem.isNull() && kdl->d->url == jobUrl ) kdl->d->rootFileItem = dir->rootItem; } continue; } // Find this item if (KFileItem* tmp = fileItems.value(item.name())) { 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 (inPendingRemoteUpdates) { pendingRemoteUpdates.erase(pru_it); } //kDebug(7004) << "file changed:" << tmp->name(); const KFileItem oldItem = *tmp; *tmp = item; foreach ( KDirLister *kdl, listers ) kdl->d->addRefreshItem(jobUrl, oldItem, *tmp); } //kDebug(7004) << "marking" << tmp; tmp->mark(); } else // this is a new file { //kDebug(7004) << "new file:" << name; KFileItem pitem(item); pitem.mark(); dir->lstItems.append( pitem ); foreach ( KDirLister *kdl, listers ) kdl->d->addNewItem(jobUrl, pitem); } } runningListJobs.remove( job ); deleteUnmarkedItems( listers, dir->lstItems ); foreach ( KDirLister *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(); } // private KIO::ListJob *KDirListerCache::jobForUrl( const QString& url, KIO::ListJob *not_job ) { QMap< KIO::ListJob *, KIO::UDSEntryList >::const_iterator it = runningListJobs.constBegin(); while ( it != runningListJobs.constEnd() ) { KIO::ListJob *job = it.key(); if ( joburl( job ).url(KUrl::RemoveTrailingSlash) == url && job != not_job ) return job; ++it; } return 0; } const KUrl& KDirListerCache::joburl( KIO::ListJob *job ) { if ( job->redirectionUrl().isValid() ) return job->redirectionUrl(); else return job->url(); } void KDirListerCache::killJob( KIO::ListJob *job ) { runningListJobs.remove( job ); job->disconnect( this ); job->kill(); } void KDirListerCache::deleteUnmarkedItems( const QList& listers, KFileItemList &lstItems ) { KFileItemList deletedItems; // Find all unmarked items and delete them QMutableListIterator kit(lstItems); while (kit.hasNext()) { const KFileItem& item = kit.next(); if (!item.isMarked()) { //kDebug(7004) << "deleted:" << item.name() << &item; deletedItems.append(item); kit.remove(); } } if (!deletedItems.isEmpty()) itemsDeleted(listers, deletedItems); } void KDirListerCache::itemsDeleted(const QList& listers, const KFileItemList& deletedItems) { Q_FOREACH(KDirLister *kdl, listers) { kdl->d->emitItemsDeleted(deletedItems); } Q_FOREACH(const KFileItem& item, deletedItems) { if (item.isDir()) deleteDir(item.url()); } } void KDirListerCache::deleteDir( const KUrl& dirUrl ) { //kDebug() << dirUrl; // unregister and remove the children of the deleted item. // Idea: tell all the KDirListers that they should forget the dir // and then remove it from the cache. // Separate itemsInUse iteration and calls to forgetDirs (which modify itemsInUse) KUrl::List affectedItems; QHash::iterator itu = itemsInUse.begin(); const QHash::iterator ituend = itemsInUse.end(); for ( ; itu != ituend; ++itu ) { const KUrl deletedUrl( itu.key() ); if ( dirUrl.isParentOf( deletedUrl ) ) { affectedItems.append(deletedUrl); } } foreach(const KUrl& deletedUrl, affectedItems) { const QString deletedUrlStr = deletedUrl.url(); // stop all jobs for deletedUrlStr DirectoryDataHash::iterator dit = directoryData.find(deletedUrlStr); if (dit != directoryData.end()) { // we need a copy because stop modifies the list QList listers = (*dit).listersCurrentlyListing; foreach ( KDirLister *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 ( KDirLister *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->deleteItem( kdl->d->rootFileItem ); 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( deletedUrlStr ); 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 KDirListerCache::processPendingUpdates() { QSet listers; foreach(const QString& file, pendingUpdates) { // always a local path kDebug(7004) << file; KUrl u(file); KFileItem *item = findByUrl( 0, u ); // search all items if ( item ) { // we need to refresh the item, because e.g. the permissions can have changed. KFileItem oldItem = *item; item->refresh(); listers |= emitRefreshItem( oldItem, *item ); } } pendingUpdates.clear(); Q_FOREACH(KDirLister * kdl, listers) { kdl->d->emitItems(); } } #ifndef NDEBUG void KDirListerCache::printDebug() { kDebug(7004) << "Items in use:"; QHash::const_iterator itu = itemsInUse.constBegin(); const QHash::const_iterator ituend = itemsInUse.constEnd(); for ( ; itu != ituend ; ++itu ) { kDebug(7004) << " " << itu.key() << "URL:" << itu.value()->url << "rootItem:" << ( !itu.value()->rootItem.isNull() ? itu.value()->rootItem.url() : KUrl() ) << "autoUpdates refcount:" << itu.value()->autoUpdates << "complete:" << itu.value()->complete << QString("with %1 items.").arg(itu.value()->lstItems.count()); } QList listersWithoutJob; kDebug(7004) << "Directory data:"; DirectoryDataHash::const_iterator dit = directoryData.constBegin(); for ( ; dit != directoryData.constEnd(); ++dit ) { QString list; foreach ( KDirLister* listit, (*dit).listersCurrentlyListing ) list += " 0x" + QString::number( (qlonglong)listit, 16 ); kDebug(7004) << " " << dit.key() << (*dit).listersCurrentlyListing.count() << "listers:" << list; foreach ( KDirLister* listit, (*dit).listersCurrentlyListing ) { if (!listit->d->m_cachedItemsJobs.isEmpty()) { kDebug(7004) << " Lister" << listit << "has CachedItemsJobs" << listit->d->m_cachedItemsJobs; } else if (KIO::ListJob* listJob = jobForUrl(dit.key())) { kDebug(7004) << " Lister" << listit << "has ListJob" << listJob; } else { listersWithoutJob.append(listit); } } list.clear(); foreach ( KDirLister* listit, (*dit).listersCurrentlyHolding ) list += " 0x" + QString::number( (qlonglong)listit, 16 ); kDebug(7004) << " " << dit.key() << (*dit).listersCurrentlyHolding.count() << "holders:" << list; } QMap< KIO::ListJob *, KIO::UDSEntryList >::Iterator jit = runningListJobs.begin(); kDebug(7004) << "Jobs:"; for ( ; jit != runningListJobs.end() ; ++jit ) kDebug(7004) << " " << jit.key() << "listing" << joburl( jit.key() ) << ":" << (*jit).count() << "entries."; kDebug(7004) << "Items in cache:"; const QList cachedDirs = itemsCached.keys(); foreach(const QString& cachedDir, cachedDirs) { DirItem* dirItem = itemsCached.object(cachedDir); kDebug(7004) << " " << cachedDir << "rootItem:" << (!dirItem->rootItem.isNull() ? dirItem->rootItem.url().prettyUrl() : QString("NULL") ) << "with" << dirItem->lstItems.count() << "items."; } // Abort on listers without jobs -after- showing the full dump. Easier debugging. Q_FOREACH(KDirLister* listit, listersWithoutJob) { kFatal() << "HUH? Lister" << listit << "is supposed to be listing, but has no job!"; } } #endif KDirLister::KDirLister( QObject* parent ) : QObject(parent), d(new Private(this)) { //kDebug(7003) << "+KDirLister"; d->complete = true; setAutoUpdate( true ); setDirOnlyMode( false ); setShowingDotFiles( false ); setAutoErrorHandlingEnabled( true, 0 ); } KDirLister::~KDirLister() { //kDebug(7003) << "~KDirLister" << this; // Stop all running jobs, remove lister from lists if (!kDirListerCache.isDestroyed()) { stop(); kDirListerCache->forgetDirs( this ); } delete d; } bool KDirLister::openUrl( const KUrl& _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 KDirLister::stop() { kDirListerCache->stop( this ); } void KDirLister::stop( const KUrl& _url ) { kDirListerCache->stopListingUrl( this, _url ); } bool KDirLister::autoUpdate() const { return d->autoUpdate; } void KDirLister::setAutoUpdate( bool _enable ) { if ( d->autoUpdate == _enable ) return; d->autoUpdate = _enable; kDirListerCache->setAutoUpdate( this, _enable ); } bool KDirLister::showingDotFiles() const { return d->settings.isShowingDotFiles; } void KDirLister::setShowingDotFiles( bool _showDotFiles ) { if ( d->settings.isShowingDotFiles == _showDotFiles ) return; d->prepareForSettingsChange(); d->settings.isShowingDotFiles = _showDotFiles; } bool KDirLister::dirOnlyMode() const { return d->settings.dirOnlyMode; } void KDirLister::setDirOnlyMode( bool _dirsOnly ) { if ( d->settings.dirOnlyMode == _dirsOnly ) return; d->prepareForSettingsChange(); d->settings.dirOnlyMode = _dirsOnly; } bool KDirLister::autoErrorHandlingEnabled() const { return d->autoErrorHandling; } void KDirLister::setAutoErrorHandlingEnabled( bool enable, QWidget* parent ) { d->autoErrorHandling = enable; d->errorParent = parent; } KUrl KDirLister::url() const { return d->url; } KUrl::List KDirLister::directories() const { return d->lstDirs; } void KDirLister::emitChanges() { d->emitChanges(); } void KDirLister::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 // Mark all items that are currently visible Q_FOREACH(const KUrl& dir, lstDirs) { KFileItemList* itemList = kDirListerCache->itemsForDir(dir); if (!itemList) { continue; } KFileItemList::iterator kit = itemList->begin(); const KFileItemList::iterator kend = itemList->end(); for (; kit != kend; ++kit) { if (isItemVisible(*kit) && m_parent->matchesMimeFilter(*kit)) (*kit).mark(); else (*kit).unmark(); } } settings = newSettings; Q_FOREACH(const KUrl& dir, lstDirs) { KFileItemList deletedItems; KFileItemList* itemList = kDirListerCache->itemsForDir(dir); if (!itemList) { continue; } KFileItemList::iterator kit = itemList->begin(); const KFileItemList::iterator kend = itemList->end(); for (; kit != kend; ++kit) { KFileItem& item = *kit; const QString text = item.text(); if (text == "." || text == "..") continue; const bool nowVisible = isItemVisible(item) && m_parent->matchesMimeFilter(item); if (nowVisible && !item.isMarked()) addNewItem(dir, item); // takes care of emitting newItem or itemsFilteredByMime else if (!nowVisible && item.isMarked()) deletedItems.append(*kit); } if (!deletedItems.isEmpty()) { emit m_parent->itemsDeleted(deletedItems); // for compat Q_FOREACH(const KFileItem& item, deletedItems) emit m_parent->deleteItem(item); } emitItems(); } oldSettings = settings; } void KDirLister::updateDirectory( const KUrl& _u ) { kDirListerCache->updateDirectory( _u ); } bool KDirLister::isFinished() const { return d->complete; } KFileItem KDirLister::rootItem() const { return d->rootFileItem; } KFileItem KDirLister::findByUrl( const KUrl& _url ) const { KFileItem *item = kDirListerCache->findByUrl( this, _url ); if (item) { return *item; } else { return KFileItem(); } } KFileItem KDirLister::findByName( const QString& _name ) const { return kDirListerCache->findByName( this, _name ); } // ================ public filter methods ================ // void KDirLister::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 KDirLister::nameFilter() const { return d->nameFilter; } void KDirLister::setMimeFilter( const QStringList& mimeFilter ) { if (d->settings.mimeFilter == mimeFilter) return; d->prepareForSettingsChange(); if (mimeFilter.contains(QLatin1String("application/octet-stream")) || mimeFilter.contains(QLatin1String("all/allfiles"))) // all files d->settings.mimeFilter.clear(); else d->settings.mimeFilter = mimeFilter; } void KDirLister::setMimeExcludeFilter( const QStringList& mimeExcludeFilter ) { if (d->settings.mimeExcludeFilter == mimeExcludeFilter) return; d->prepareForSettingsChange(); d->settings.mimeExcludeFilter = mimeExcludeFilter; } void KDirLister::clearMimeFilter() { d->prepareForSettingsChange(); d->settings.mimeFilter.clear(); d->settings.mimeExcludeFilter.clear(); } QStringList KDirLister::mimeFilters() const { return d->settings.mimeFilter; } bool KDirLister::matchesFilter( const QString& name ) const { return doNameFilter(name, d->settings.lstFilters); } bool KDirLister::matchesMimeFilter( const QString& mime ) const { return doMimeFilter(mime, d->settings.mimeFilter) && d->doMimeExcludeFilter(mime, d->settings.mimeExcludeFilter); } // ================ protected methods ================ // bool KDirLister::matchesFilter( const KFileItem& item ) const { Q_ASSERT( !item.isNull() ); if ( item.text() == ".." ) return false; if ( !d->settings.isShowingDotFiles && item.isHidden() ) return false; if ( item.isDir() || d->settings.lstFilters.isEmpty() ) return true; return matchesFilter( item.text() ); } bool KDirLister::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 KDirLister::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 KDirLister::doMimeFilter( const QString& mime, const QStringList& filters ) const { if ( filters.isEmpty() ) return true; const KMimeType::Ptr mimeptr = KMimeType::mimeType(mime); if ( !mimeptr ) return false; //kDebug(7004) << "doMimeFilter: investigating: "<name(); QStringList::const_iterator it = filters.begin(); for ( ; it != filters.end(); ++it ) if ( mimeptr->is(*it) ) return true; //else kDebug(7004) << "doMimeFilter: compared without result to "<<*it; return false; } bool KDirLister::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 KDirLister::handleError( KIO::Job *job ) { if ( d->autoErrorHandling ) job->uiDelegate()->showErrorMessage(); } // ================= private methods ================= // void KDirLister::Private::addNewItem(const KUrl& directoryUrl, const KFileItem &item) { if (!isItemVisible(item)) return; // No reason to continue... bailing out here prevents a mimetype scan. //kDebug(7004) << "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 } else { if ( !lstMimeFilteredItems ) { lstMimeFilteredItems = new KFileItemList; } Q_ASSERT( !item.isNull() ); lstMimeFilteredItems->append( item ); // only filtered by mime } } void KDirLister::Private::addNewItems(const KUrl& directoryUrl, const KFileItemList& 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. KFileItemList::const_iterator kit = items.begin(); const KFileItemList::const_iterator kend = items.end(); for ( ; kit != kend; ++kit ) addNewItem(directoryUrl, *kit); } void KDirLister::Private::addRefreshItem(const KUrl& 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 ); } else { if ( !lstRefreshItems ) { lstRefreshItems = new QList >; } Q_ASSERT( !item.isNull() ); 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); } } void KDirLister::Private::emitItems() { NewItemsHash *tmpNew = lstNewItems; lstNewItems = 0; KFileItemList *tmpMime = lstMimeFilteredItems; lstMimeFilteredItems = 0; QList > *tmpRefresh = lstRefreshItems; lstRefreshItems = 0; KFileItemList *tmpRemove = lstRemoveItems; lstRemoveItems = 0; if (tmpNew) { QHashIterator it(*tmpNew); while (it.hasNext()) { it.next(); emit m_parent->itemsAdded(it.key(), it.value()); emit m_parent->newItems(it.value()); // compat } delete tmpNew; } if ( tmpMime ) { emit m_parent->itemsFilteredByMime( *tmpMime ); delete tmpMime; } if ( tmpRefresh ) { emit m_parent->refreshItems( *tmpRefresh ); delete tmpRefresh; } if ( tmpRemove ) { emit m_parent->itemsDeleted( *tmpRemove ); delete tmpRemove; } } bool KDirLister::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 KDirLister::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)) { // for compat emit m_parent->deleteItem(item); } else { it.remove(); } } if (!items.isEmpty()) emit m_parent->itemsDeleted(items); } // ================ private slots ================ // void KDirLister::Private::_k_slotInfoMessage( KJob *, const QString& message ) { emit m_parent->infoMessage( message ); } void KDirLister::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 KDirLister::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 KDirLister::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 KDirLister::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 KDirLister::Private::numJobs() { #ifdef DEBUG_CACHE // This code helps detecting stale entries in the jobData map. qDebug() << m_parent << "numJobs:" << jobData.count(); QMapIterator it(jobData); while (it.hasNext()) { it.next(); qDebug() << (void*)it.key(); qDebug() << it.key(); } #endif return jobData.count(); } void KDirLister::Private::jobDone( KIO::ListJob *job ) { jobData.remove( job ); } void KDirLister::Private::jobStarted( KIO::ListJob *job ) { Private::JobData data; data.speed = 0; data.percent = 0; data.processedSize = 0; data.totalSize = 0; jobData.insert( job, data ); complete = false; } void KDirLister::Private::connectJob( KIO::ListJob *job ) { m_parent->connect( job, SIGNAL(infoMessage(KJob*,QString,QString)), m_parent, SLOT(_k_slotInfoMessage(KJob*,QString)) ); m_parent->connect( job, SIGNAL(percent(KJob*,ulong)), m_parent, SLOT(_k_slotPercent(KJob*,ulong)) ); m_parent->connect( job, SIGNAL(totalSize(KJob*,qulonglong)), m_parent, SLOT(_k_slotTotalSize(KJob*,qulonglong)) ); m_parent->connect( job, SIGNAL(processedSize(KJob*,qulonglong)), m_parent, SLOT(_k_slotProcessedSize(KJob*,qulonglong)) ); m_parent->connect( job, SIGNAL(speed(KJob*,ulong)), m_parent, SLOT(_k_slotSpeed(KJob*,ulong)) ); } void KDirLister::setMainWindow( QWidget *window ) { d->window = window; } QWidget *KDirLister::mainWindow() { return d->window; } KFileItemList KDirLister::items( WhichItems which ) const { return itemsForDir( url(), which ); } KFileItemList KDirLister::itemsForDir( const KUrl& dir, WhichItems which ) const { KFileItemList *allItems = kDirListerCache->itemsForDir( dir ); if ( !allItems ) return KFileItemList(); if ( which == AllItems ) return *allItems; else // only items passing the filters { KFileItemList result; KFileItemList::const_iterator kit = allItems->constBegin(); const KFileItemList::const_iterator kend = allItems->constEnd(); for ( ; kit != kend; ++kit ) { const KFileItem& item = *kit; if (d->isItemVisible(item) && matchesMimeFilter(item)) { result.append(item); } } return result; } } bool KDirLister::delayedMimeTypes() const { return d->delayedMimeTypes; } void KDirLister::setDelayedMimeTypes( bool delayedMimeTypes ) { d->delayedMimeTypes = delayedMimeTypes; } // called by KDirListerCache::slotRedirection void KDirLister::Private::redirect(const KUrl& oldUrl, const KUrl& newUrl, bool keepItems) { if ( url.equals( oldUrl, KUrl::CompareWithoutTrailingSlash ) ) { if (!keepItems) { rootFileItem = KFileItem(); } else { rootFileItem.setUrl(newUrl); } url = newUrl; } const int idx = lstDirs.indexOf( oldUrl ); if (idx == -1) { kWarning(7004) << "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 KDirListerCacheDirectoryData::moveListersWithoutCachedItemsJob(const KUrl& 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()) { KDirLister* kdl = lister_it.next(); if (!kdl->d->cachedItemsJobForUrl(url)) { // OK, move this lister from "currently listing" to "currently holding". // Huh? The KDirLister 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 { //kDebug(7004) << "Not moving" << kdl << "to listersCurrentlyHolding because it still has job" << kdl->d->m_cachedItemsJobs; } } } KFileItem KDirLister::cachedItemForUrl(const KUrl& url) { return kDirListerCache->itemForUrl(url); } #include "kdirlister.moc" #include "kdirlister_p.moc" diff --git a/kio/tests/kdirlistertest.cpp b/kio/tests/kdirlistertest.cpp index a0b296d703..9b809e8fc3 100644 --- a/kio/tests/kdirlistertest.cpp +++ b/kio/tests/kdirlistertest.cpp @@ -1,1197 +1,1223 @@ /* 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 "kdirlistertest.moc" #include #include QTEST_KDEMAIN( KDirListerTest, NoGUI ) #include #include "kiotesthelper.h" #include #include #include #include #define WORKAROUND_BROKEN_INOTIFY 0 void MyDirLister::handleError(KIO::Job* job) { // Currently we don't expect any errors. kFatal() << "KDirLister called handleError!" << job << job->error() << job->errorString(); } void KDirListerTest::initTestCase() { 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.name(); createTestFile(path+"toplevelfile_1"); createTestFile(path+"toplevelfile_2"); createTestFile(path+"toplevelfile_3"); createTestDirectory(path+"subdir"); createTestDirectory(path+"subdir/subsubdir"); } void KDirListerTest::cleanup() { m_dirLister.clearSpies(); disconnect(&m_dirLister, 0, this, 0); } void KDirListerTest::testOpenUrl() { m_items.clear(); const QString path = m_tempDir.name(); connect(&m_dirLister, SIGNAL(newItems(KFileItemList)), this, SLOT(slotNewItems(KFileItemList))); // The call to openUrl itself, emits started m_dirLister.openUrl(KUrl(path), KDirLister::NoFlags); QCOMPARE(m_dirLister.spyStarted.count(), 1); QCOMPARE(m_dirLister.spyCompleted.count(), 0); QCOMPARE(m_dirLister.spyCompletedKUrl.count(), 0); QCOMPARE(m_dirLister.spyCanceled.count(), 0); QCOMPARE(m_dirLister.spyCanceledKUrl.count(), 0); QCOMPARE(m_dirLister.spyClear.count(), 1); QCOMPARE(m_dirLister.spyClearKUrl.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"); connect(&m_dirLister, SIGNAL(completed()), this, SLOT(exitLoop())); enterLoop(); QCOMPARE(m_dirLister.spyStarted.count(), 1); QCOMPARE(m_dirLister.spyCompleted.count(), 1); QCOMPARE(m_dirLister.spyCompletedKUrl.count(), 1); QCOMPARE(m_dirLister.spyCanceled.count(), 0); QCOMPARE(m_dirLister.spyCanceledKUrl.count(), 0); QCOMPARE(m_dirLister.spyClear.count(), 1); QCOMPARE(m_dirLister.spyClearKUrl.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, 0, this, 0); const QString fileName = "toplevelfile_3"; const KUrl itemUrl(path + fileName); KFileItem byName = m_dirLister.findByName(fileName); QVERIFY(!byName.isNull()); QCOMPARE(byName.url().url(), itemUrl.url()); QCOMPARE(byName.entry().stringValue(KIO::UDSEntry::UDS_NAME), fileName); KFileItem byUrl = m_dirLister.findByUrl(itemUrl); QVERIFY(!byUrl.isNull()); QCOMPARE(byUrl.url().url(), itemUrl.url()); QCOMPARE(byUrl.entry().stringValue(KIO::UDSEntry::UDS_NAME), fileName); KFileItem itemForUrl = KDirLister::cachedItemForUrl(itemUrl); QVERIFY(!itemForUrl.isNull()); QCOMPARE(itemForUrl.url().url(), itemUrl.url()); QCOMPARE(itemForUrl.entry().stringValue(KIO::UDSEntry::UDS_NAME), fileName); KFileItem rootByUrl = m_dirLister.findByUrl(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.name(); MyDirLister secondDirLister; connect(&secondDirLister, SIGNAL(newItems(KFileItemList)), this, SLOT(slotNewItems(KFileItemList))); secondDirLister.openUrl(KUrl(path), KDirLister::NoFlags); QCOMPARE(secondDirLister.spyStarted.count(), 1); QCOMPARE(secondDirLister.spyCompleted.count(), 0); QCOMPARE(secondDirLister.spyCompletedKUrl.count(), 0); QCOMPARE(secondDirLister.spyCanceled.count(), 0); QCOMPARE(secondDirLister.spyCanceledKUrl.count(), 0); QCOMPARE(secondDirLister.spyClear.count(), 1); QCOMPARE(secondDirLister.spyClearKUrl.count(), 0); QCOMPARE(m_items.count(), 0); QVERIFY(!secondDirLister.isFinished()); // then wait for completed qDebug("waiting for completed"); connect(&secondDirLister, SIGNAL(completed()), this, SLOT(exitLoop())); enterLoop(); QCOMPARE(secondDirLister.spyStarted.count(), 1); QCOMPARE(secondDirLister.spyCompleted.count(), 1); QCOMPARE(secondDirLister.spyCompletedKUrl.count(), 1); QCOMPARE(secondDirLister.spyCanceled.count(), 0); QCOMPARE(secondDirLister.spyCanceledKUrl.count(), 0); QCOMPARE(secondDirLister.spyClear.count(), 1); QCOMPARE(secondDirLister.spyClearKUrl.count(), 0); QCOMPARE(m_items.count(), 4); QVERIFY(secondDirLister.isFinished()); } disconnect(&m_dirLister, 0, this, 0); } // This test assumes testOpenUrl was run before. So m_dirLister is holding the items already. void KDirListerTest::testNewItems() { QCOMPARE(m_items.count(), 4); const QString path = m_tempDir.name(); connect(&m_dirLister, SIGNAL(newItems(KFileItemList)), this, SLOT(slotNewItems(KFileItemList))); QTest::qWait(1000); // We need a 1s timestamp difference on the dir, otherwise FAM won't notice anything. kDebug() << "Creating new file"; const QString fileName = "toplevelfile_new"; createSimpleFile(path + fileName); int numTries = 0; // Give time for KDirWatch to notify us while (m_items.count() == 4) { QVERIFY(++numTries < 10); QTest::qWait(200); } //kDebug() << "numTries=" << numTries; QCOMPARE(m_items.count(), 5); QCOMPARE(m_dirLister.spyStarted.count(), 1); // Updates call started QCOMPARE(m_dirLister.spyCompleted.count(), 1); // and completed QCOMPARE(m_dirLister.spyCompletedKUrl.count(), 1); QCOMPARE(m_dirLister.spyCanceled.count(), 0); QCOMPARE(m_dirLister.spyCanceledKUrl.count(), 0); QCOMPARE(m_dirLister.spyClear.count(), 0); QCOMPARE(m_dirLister.spyClearKUrl.count(), 0); const KUrl itemUrl(path + fileName); KFileItem itemForUrl = KDirLister::cachedItemForUrl(itemUrl); QVERIFY(!itemForUrl.isNull()); QCOMPARE(itemForUrl.url().url(), itemUrl.url()); QCOMPARE(itemForUrl.entry().stringValue(KIO::UDSEntry::UDS_NAME), fileName); } 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.name(); connect(&m_dirLister, SIGNAL(newItems(KFileItemList)), this, SLOT(slotNewItems(KFileItemList))); QTest::qWait(1000); // We need a 1s timestamp difference on the dir, otherwise FAM won't notice anything. const QString fileName = "toplevelfile_copy"; const KUrl itemUrl(path + fileName); KIO::CopyJob* job = KIO::copyAs(QString(path+"toplevelfile_3"), itemUrl, KIO::HideProgressInfo); job->exec(); int numTries = 0; // Give time for KDirWatch/KDirNotify to notify us while (m_items.count() == origItemCount) { QVERIFY(++numTries < 10); QTest::qWait(200); } //kDebug() << "numTries=" << numTries; QCOMPARE(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.spyCompletedKUrl.count(), 1); QCOMPARE(m_dirLister.spyCanceled.count(), 0); QCOMPARE(m_dirLister.spyCanceledKUrl.count(), 0); QCOMPARE(m_dirLister.spyClear.count(), 0); QCOMPARE(m_dirLister.spyClearKUrl.count(), 0); // Give some time to KDirWatch QTest::qWait(1000); KFileItem itemForUrl = KDirLister::cachedItemForUrl(itemUrl); QVERIFY(!itemForUrl.isNull()); QCOMPARE(itemForUrl.url().url(), itemUrl.url()); 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.name(); KTemporaryFile tempFile; QVERIFY(tempFile.open()); const QString symPath = tempFile.fileName() + "_link"; tempFile.close(); bool symlinkOk = ::symlink(QFile::encodeName(path), QFile::encodeName(symPath)) == 0; QVERIFY(symlinkOk); MyDirLister dirLister2; m_items2.clear(); connect(&dirLister2, SIGNAL(newItems(KFileItemList)), this, SLOT(slotNewItems2(KFileItemList))); connect(&m_dirLister, SIGNAL(newItems(KFileItemList)), this, SLOT(slotNewItems(KFileItemList))); // The initial listing dirLister2.openUrl(KUrl(symPath), KDirLister::NoFlags); connect(&dirLister2, SIGNAL(completed()), this, SLOT(exitLoop())); enterLoop(); QCOMPARE(m_items2.count(), origItemCount); QVERIFY(dirLister2.isFinished()); QTest::qWait(1000); // We need a 1s timestamp difference on the dir, otherwise FAM won't notice anything. kDebug() << "Creating new file"; const QString fileName = "toplevelfile_newinlink"; createSimpleFile(path + fileName); #if WORKAROUND_BROKEN_INOTIFY org::kde::KDirNotify::emitFilesAdded(path); #endif int numTries = 0; // Give time for KDirWatch to notify us while (m_items2.count() == origItemCount) { QVERIFY(++numTries < 10); QTest::qWait(200); } //kDebug() << "numTries=" << numTries; QCOMPARE(m_items2.count(), origItemCount+1); QCOMPARE(m_items.count(), origItemCount+1); // Now create an item using the symlink-path const QString fileName2 = "toplevelfile_newinlink2"; { createSimpleFile(path + fileName2); int numTries = 0; // Give time for KDirWatch to notify us while (m_items2.count() == origItemCount + 1) { QVERIFY(++numTries < 10); QTest::qWait(200); } QCOMPARE(m_items2.count(), origItemCount+2); QCOMPARE(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); while (dirLister2.spyDeleteItem.count() == 0) { QVERIFY(++numTries < 10); QTest::qWait(200); } QCOMPARE(dirLister2.spyDeleteItem.count(), 1); const KFileItem item = dirLister2.spyDeleteItem[0][0].value(); QCOMPARE(item.url().toLocalFile(), QString(symPath + '/' + fileName)); } // TODO: test file update. disconnect(&m_dirLister, 0, this, 0); } // This test assumes testOpenUrl was run before. So m_dirLister is holding the items already. void KDirListerTest::testRefreshItems() { m_refreshedItems.clear(); const QString path = m_tempDir.name(); const QString fileName = path+"toplevelfile_1"; KFileItem cachedItem = m_dirLister.findByUrl(KUrl(fileName)); QVERIFY(!cachedItem.isNull()); QCOMPARE(cachedItem.mimetype(), QString("application/octet-stream")); connect(&m_dirLister, SIGNAL(refreshItems(QList >)), this, SLOT(slotRefreshItems(QList >))); QFile file(fileName); QVERIFY(file.open(QIODevice::Append)); file.write(QByteArray("")); file.close(); QCOMPARE(QFileInfo(fileName).size(), 11LL /*Hello world*/ + 6 /**/); waitForRefreshedItems(); QCOMPARE(m_dirLister.spyStarted.count(), 0); // fast path: no directory listing needed QCOMPARE(m_dirLister.spyCompleted.count(), 0); QCOMPARE(m_dirLister.spyCompletedKUrl.count(), 0); QCOMPARE(m_dirLister.spyCanceled.count(), 0); QCOMPARE(m_dirLister.spyCanceledKUrl.count(), 0); QCOMPARE(m_dirLister.spyClear.count(), 0); QCOMPARE(m_dirLister.spyClearKUrl.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(KUrl(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.name() + "subdir"; MyDirLister dirLister2; fillDirLister2(dirLister2, path); connect(&m_dirLister, SIGNAL(refreshItems(QList >)), this, SLOT(slotRefreshItems(QList >))); org::kde::KDirNotify::emitFilesChanged(QStringList() << KUrl(path).url()); waitForRefreshedItems(); QCOMPARE(m_dirLister.spyStarted.count(), 0); QCOMPARE(m_dirLister.spyCompleted.count(), 0); QCOMPARE(m_dirLister.spyCompletedKUrl.count(), 0); QCOMPARE(m_dirLister.spyCanceled.count(), 0); QCOMPARE(m_dirLister.spyCanceledKUrl.count(), 0); QCOMPARE(m_dirLister.spyClear.count(), 0); QCOMPARE(m_dirLister.spyClearKUrl.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(); const QString directoryFile = path + "/.directory"; createSimpleFile(directoryFile); org::kde::KDirNotify::emitFilesAdded(KUrl(path).url()); QTest::qWait(200); org::kde::KDirNotify::emitFilesChanged(QStringList() << KUrl(directoryFile).url()); QCOMPARE(m_refreshedItems.count(), 0); org::kde::KDirNotify::emitFilesChanged(QStringList() << KUrl(path).url()); waitForRefreshedItems(); 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.name(); connect(&m_dirLister, SIGNAL(deleteItem(KFileItem)), this, SLOT(exitLoop())); //kDebug() << "Removing " << path+"toplevelfile_1"; QFile::remove(path+"toplevelfile_1"); // the remove() doesn't always trigger kdirwatch in stat mode, if this all happens in the same second KDirWatch::self()->setDirty(path); if (m_dirLister.spyDeleteItem.count() == 0) { qDebug("waiting for deleteItem"); enterLoop(); } QCOMPARE(m_dirLister.spyDeleteItem.count(), 1); QCOMPARE(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, SIGNAL(newItems(KFileItemList)), this, SLOT(slotNewItems(KFileItemList))); m_dirLister.openUrl(KUrl(path), KDirLister::NoFlags); QVERIFY(!m_dirLister.isFinished()); connect(&m_dirLister, SIGNAL(completed()), this, SLOT(exitLoop())); enterLoop(); QVERIFY(m_dirLister.isFinished()); QCOMPARE(m_items.count(), origItemCount-1); disconnect(&m_dirLister, 0, this, 0); QCOMPARE(fileCount(), m_items.count()); } void KDirListerTest::testRenameItem() { - m_refreshedItems.clear(); + m_refreshedItems2.clear(); const QString dirPath = m_tempDir.name(); connect(&m_dirLister, SIGNAL(refreshItems(QList >)), - this, SLOT(slotRefreshItems(QList >))); + this, SLOT(slotRefreshItems2(QList >))); const QString path = dirPath+"toplevelfile_2"; const QString newPath = dirPath+"toplevelfile_2.renamed.html"; KIO::SimpleJob* job = KIO::rename(path, newPath, KIO::HideProgressInfo); - bool ok = job->exec(); - QVERIFY(ok); + QVERIFY(job->exec()); - if (m_refreshedItems.isEmpty()) { - waitForRefreshedItems(); // refreshItems could come from KDirWatch or KDirNotify. - } + QVERIFY(QTest::kWaitForSignal(&m_dirLister, SIGNAL(refreshItems(QList >)), 2000)); - QCOMPARE(m_refreshedItems.count(), 1); - QPair entry = m_refreshedItems.first(); + QCOMPARE(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, 0, this, 0); // Let's see what KDirLister has in cache now KFileItem cachedItem = m_dirLister.findByUrl(KUrl(newPath)); QVERIFY(!cachedItem.isNull()); QCOMPARE(cachedItem.url().toLocalFile(), newPath); KFileItem oldCachedItem = m_dirLister.findByUrl(KUrl(path)); QVERIFY(oldCachedItem.isNull()); - m_refreshedItems.clear(); + 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.name(); 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(KUrl(path)); }; QCOMPARE(existingItem.url().toLocalFile(), path); m_refreshedItems.clear(); connect(&m_dirLister, SIGNAL(refreshItems(QList >)), this, SLOT(slotRefreshItems(QList >))); const QString newPath = dirPath+"toplevelfile_2.renamed.html"; KIO::SimpleJob* job = KIO::rename(newPath, path, KIO::Overwrite | KIO::HideProgressInfo); bool ok = job->exec(); QVERIFY(ok); if (m_refreshedItems.isEmpty()) { waitForRefreshedItems(); // refreshItems could come from KDirWatch or KDirNotify. } // Check that itemsDeleted was emitted -- preferrably 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, 0, this, 0); // Let's see what KDirLister has in cache now KFileItem cachedItem = m_dirLister.findByUrl(KUrl(path)); QCOMPARE(cachedItem.url().toLocalFile(), path); KFileItem oldCachedItem = m_dirLister.findByUrl(KUrl(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.name(); connect(&m_dirLister, SIGNAL(newItems(KFileItemList)), this, SLOT(slotNewItems(KFileItemList))); connect(&dirLister2, SIGNAL(newItems(KFileItemList)), this, SLOT(slotNewItems2(KFileItemList))); // 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(KUrl(path), KDirLister::NoFlags); m_dirLister.openUrl(KUrl(path+"subdir"), KDirLister::NoFlags); QCOMPARE(m_dirLister.spyStarted.count(), 1); QCOMPARE(m_dirLister.spyCompleted.count(), 0); QCOMPARE(m_dirLister.spyCompletedKUrl.count(), 0); QCOMPARE(m_dirLister.spyCanceled.count(), 0); QCOMPARE(m_dirLister.spyCanceledKUrl.count(), 0); QCOMPARE(m_dirLister.spyClear.count(), 1); QCOMPARE(m_dirLister.spyClearKUrl.count(), 0); QCOMPARE(m_items.count(), 0); QCOMPARE(dirLister2.spyStarted.count(), 1); QCOMPARE(dirLister2.spyCompleted.count(), 0); QCOMPARE(dirLister2.spyCompletedKUrl.count(), 0); QCOMPARE(dirLister2.spyCanceled.count(), 0); QCOMPARE(dirLister2.spyCanceledKUrl.count(), 0); QCOMPARE(dirLister2.spyClear.count(), 1); QCOMPARE(dirLister2.spyClearKUrl.count(), 0); QCOMPARE(m_items2.count(), 0); QVERIFY(!m_dirLister.isFinished()); QVERIFY(!dirLister2.isFinished()); // then wait for completed qDebug("waiting for completed"); connect(&m_dirLister, SIGNAL(completed()), this, SLOT(exitLoop())); connect(&dirLister2, SIGNAL(completed()), this, SLOT(exitLoop())); enterLoop(2); //QCOMPARE(m_dirLister.spyStarted.count(), 1); // 2 when subdir is already in cache. QCOMPARE(m_dirLister.spyCompleted.count(), 1); QCOMPARE(m_dirLister.spyCompletedKUrl.count(), 1); QCOMPARE(m_dirLister.spyCanceled.count(), 0); QCOMPARE(m_dirLister.spyCanceledKUrl.count(), 0); QCOMPARE(m_dirLister.spyClear.count(), 1); QCOMPARE(m_dirLister.spyClearKUrl.count(), 0); QCOMPARE(m_items.count(), 3); QCOMPARE(dirLister2.spyStarted.count(), 1); QCOMPARE(dirLister2.spyCompleted.count(), 1); QCOMPARE(dirLister2.spyCompletedKUrl.count(), 1); QCOMPARE(dirLister2.spyCanceled.count(), 0); QCOMPARE(dirLister2.spyCanceledKUrl.count(), 0); QCOMPARE(dirLister2.spyClear.count(), 1); QCOMPARE(dirLister2.spyClearKUrl.count(), 0); QCOMPARE(m_items2.count(), origItemCount); if (!m_dirLister.isFinished()) { // false when an update is running because subdir is already in cache QTest::kWaitForSignal(&m_dirLister, SIGNAL(canceled()), 1000); } disconnect(&m_dirLister, 0, this, 0); disconnect(&dirLister2, 0, this, 0); } 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, SIGNAL(newItems(KFileItemList)), this, SLOT(slotNewItems(KFileItemList))); m_items.clear(); m_items2.clear(); const QString path = m_tempDir.name(); MyDirLister dirLister2; connect(&dirLister2, SIGNAL(newItems(KFileItemList)), this, SLOT(slotNewItems2(KFileItemList))); dirLister2.openUrl(KUrl(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(KUrl(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"); connect(&dirLister2, SIGNAL(completed()), this, SLOT(exitLoop())); enterLoop(); QCOMPARE(dirLister2.spyStarted.count(), 1); QCOMPARE(dirLister2.spyCompleted.count(), 1); QCOMPARE(dirLister2.spyCompletedKUrl.count(), 1); QCOMPARE(dirLister2.spyCanceled.count(), 0); QCOMPARE(dirLister2.spyCanceledKUrl.count(), 0); QCOMPARE(dirLister2.spyClear.count(), 1); QCOMPARE(dirLister2.spyClearKUrl.count(), 0); QCOMPARE(m_items2.count(), origItemCount); if (m_dirLister.spyCompleted.isEmpty()) { connect(&m_dirLister, SIGNAL(completed()), this, SLOT(exitLoop())); enterLoop(); } QCOMPARE(m_dirLister.spyStarted.count(), 1); QCOMPARE(m_dirLister.spyCompleted.count(), 1); QCOMPARE(m_dirLister.spyCompletedKUrl.count(), 1); QCOMPARE(m_dirLister.spyCanceled.count(), 0); QCOMPARE(m_dirLister.spyCanceledKUrl.count(), 0); QCOMPARE(m_dirLister.spyClear.count(), 1); QCOMPARE(m_dirLister.spyClearKUrl.count(), 0); QVERIFY(dirLister2.isFinished()); QVERIFY(m_dirLister.isFinished()); disconnect(&m_dirLister, 0, this, 0); 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. KTempDir tempDir; const QString path = tempDir.name(); createTestFile(path+"file_1"); createTestFile(path+"file_2"); createTestFile(path+"file_3"); connect(&m_dirLister, SIGNAL(newItems(KFileItemList)), this, SLOT(slotNewItems(KFileItemList))); connect(&dirLister2, SIGNAL(newItems(KFileItemList)), this, SLOT(slotNewItems2(KFileItemList))); // 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(KUrl(path), KDirLister::Reload); m_dirLister.openUrl(KUrl(path)/*, KDirLister::Reload*/); QCOMPARE(m_dirLister.spyStarted.count(), 1); QCOMPARE(m_dirLister.spyCompleted.count(), 0); QCOMPARE(m_dirLister.spyCompletedKUrl.count(), 0); QCOMPARE(m_dirLister.spyCanceled.count(), 0); QCOMPARE(m_dirLister.spyCanceledKUrl.count(), 0); QCOMPARE(m_dirLister.spyClear.count(), 1); QCOMPARE(m_dirLister.spyClearKUrl.count(), 0); QCOMPARE(m_items.count(), 0); QCOMPARE(dirLister2.spyStarted.count(), 1); QCOMPARE(dirLister2.spyCompleted.count(), 0); QCOMPARE(dirLister2.spyCompletedKUrl.count(), 0); QCOMPARE(dirLister2.spyCanceled.count(), 0); QCOMPARE(dirLister2.spyCanceledKUrl.count(), 0); QCOMPARE(dirLister2.spyClear.count(), 1); QCOMPARE(dirLister2.spyClearKUrl.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.spyCompletedKUrl.count(), 0); QCOMPARE(dirLister2.spyCanceled.count(), 1); QCOMPARE(dirLister2.spyCanceledKUrl.count(), 1); QCOMPARE(dirLister2.spyClear.count(), 1); QCOMPARE(dirLister2.spyClearKUrl.count(), 0); QCOMPARE(m_items2.count(), 0); // then wait for completed qDebug("waiting for completed"); connect(&m_dirLister, SIGNAL(completed()), this, SLOT(exitLoop())); enterLoop(); QCOMPARE(m_items.count(), 3); QCOMPARE(m_items2.count(), 0); //QCOMPARE(m_dirLister.spyStarted.count(), 1); // 2 when in cache QCOMPARE(m_dirLister.spyCompleted.count(), 1); QCOMPARE(m_dirLister.spyCompletedKUrl.count(), 1); QCOMPARE(m_dirLister.spyCanceled.count(), 0); QCOMPARE(m_dirLister.spyCanceledKUrl.count(), 0); QCOMPARE(m_dirLister.spyClear.count(), 1); QCOMPARE(m_dirLister.spyClearKUrl.count(), 0); disconnect(&m_dirLister, 0, this, 0); } 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 //kDebug() << "=========================================="; { m_items.clear(); const QString path = m_tempDir.name(); MyDirLister secondDirLister; secondDirLister.openUrl(KUrl(path), KDirLister::NoFlags); QVERIFY(!secondDirLister.isFinished()); } //kDebug() << "=========================================="; // 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.name(); MyDirLister secondDirLister; connect(&secondDirLister, SIGNAL(newItems(KFileItemList)), this, SLOT(slotNewItems(KFileItemList))); secondDirLister.openUrl(KUrl(path), KDirLister::Reload); // will start QCOMPARE(secondDirLister.spyStarted.count(), 1); QCOMPARE(secondDirLister.spyCompleted.count(), 0); qDebug("calling openUrl again"); secondDirLister.openUrl(KUrl(path), KDirLister::NoFlags); // will stop + start qDebug("waiting for completed"); connect(&secondDirLister, SIGNAL(completed()), this, SLOT(exitLoop())); enterLoop(); QCOMPARE(secondDirLister.spyStarted.count(), 2); QCOMPARE(secondDirLister.spyCompleted.count(), 1); QCOMPARE(secondDirLister.spyCompletedKUrl.count(), 1); QCOMPARE(secondDirLister.spyCanceled.count(), 0); // should not be emitted, see next test QCOMPARE(secondDirLister.spyCanceledKUrl.count(), 0); QCOMPARE(secondDirLister.spyClear.count(), 2); QCOMPARE(secondDirLister.spyClearKUrl.count(), 0); if (origItemCount) { // 0 if running this test separately QCOMPARE(m_items.count(), origItemCount); } QVERIFY(secondDirLister.isFinished()); disconnect(&secondDirLister, 0, this, 0); } 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.name() + "/newsubdir"; QDir().mkdir(path); MyDirLister secondDirLister; connect(&secondDirLister, SIGNAL(newItems(KFileItemList)), this, SLOT(slotNewItems(KFileItemList))); secondDirLister.openUrl(KUrl(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(KUrl(path), KDirLister::Keep); // stops and restarts the job qDebug("waiting for completed"); connect(&secondDirLister, SIGNAL(completed()), this, SLOT(exitLoop())); enterLoop(); QCOMPARE(secondDirLister.spyStarted.count(), 2); QCOMPARE(secondDirLister.spyCompleted.count(), 1); QCOMPARE(secondDirLister.spyCompletedKUrl.count(), 1); QCOMPARE(secondDirLister.spyCanceled.count(), 0); // should not be emitted, it led to recursion QCOMPARE(secondDirLister.spyCanceledKUrl.count(), 0); QCOMPARE(secondDirLister.spyClear.count(), 1); QCOMPARE(secondDirLister.spyClearKUrl.count(), 1); QCOMPARE(m_items.count(), 0); QVERIFY(secondDirLister.isFinished()); disconnect(&secondDirLister, 0, this, 0); QDir().remove(path); } void KDirListerTest::testOpenAndStop() { m_items.clear(); const QString path = "/"; // better not use a directory that we already listed! connect(&m_dirLister, SIGNAL(newItems(KFileItemList)), this, SLOT(slotNewItems(KFileItemList))); m_dirLister.openUrl(KUrl(path), KDirLister::NoFlags); kDebug() << "Calling stop!"; m_dirLister.stop(); // we should also test stop(KUrl(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.spyCompletedKUrl.count(), 0); QCOMPARE(m_dirLister.spyCanceled.count(), 1); QCOMPARE(m_dirLister.spyCanceledKUrl.count(), 1); QCOMPARE(m_dirLister.spyClear.count(), 1); QCOMPARE(m_dirLister.spyClearKUrl.count(), 0); QCOMPARE(m_items.count(), 0); // we had time to stop before the job even started QVERIFY(m_dirLister.isFinished()); disconnect(&m_dirLister, 0, this, 0); } // 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(); KTempDir newDir; const QString path = newDir.name() + "newsubdir/"; QDir().mkdir(path); MyDirLister dirLister; connect(&dirLister, SIGNAL(newItems(KFileItemList)), this, SLOT(slotNewItems(KFileItemList))); dirLister.openUrl(KUrl(path)); QVERIFY(QTest::kWaitForSignal(&dirLister, SIGNAL(completed()), 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(KUrl(newDir.name())); QVERIFY(QTest::kWaitForSignal(&dirLister, SIGNAL(completed()), 1000)); QVERIFY(dirLister.isFinished()); QVERIFY(!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. QTest::qWait(1000); QVERIFY(m_items.isEmpty()); // Return to 'newsubdir'. It will be emitted from the cache, then an update will happen. dirLister.openUrl(KUrl(path)); QVERIFY(QTest::kWaitForSignal(&dirLister, SIGNAL(completed()), 1000)); QVERIFY(QTest::kWaitForSignal(&dirLister, SIGNAL(completed()), 1000)); QVERIFY(dirLister.isFinished()); QCOMPARE(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"); int numTries = 0; // Give time for KDirWatch to notify us while (m_items.isEmpty()) { QVERIFY(++numTries < 10); QTest::qWait(200); } QCOMPARE(m_items.count(), 1); newDir.unlink(); QVERIFY(QTest::kWaitForSignal(&dirLister, SIGNAL(clear()), 1000)); } void KDirListerTest::testRenameCurrentDir() // #294445 { m_items.clear(); const QString path = m_tempDir.name() + "newsubdir-1"; QVERIFY(QDir().mkdir(path)); MyDirLister secondDirLister; connect(&secondDirLister, SIGNAL(newItems(KFileItemList)), this, SLOT(slotNewItems(KFileItemList))); secondDirLister.openUrl(KUrl(path)); QVERIFY(QTest::kWaitForSignal(&secondDirLister, SIGNAL(completed()), 1000)); QVERIFY(secondDirLister.isFinished()); QVERIFY(m_items.empty()); QCOMPARE(secondDirLister.rootItem().url().toLocalFile(), path); const QString newPath = m_tempDir.name() + "newsubdir-2"; QVERIFY(QDir().rename(path, newPath)); org::kde::KDirNotify::emitFileRenamed(KUrl(path).url(), KUrl(newPath).url()); QVERIFY(QTest::kWaitForSignal(&secondDirLister, SIGNAL(redirection(KUrl,KUrl)), 1000)); // Check that the URL of the root item got updated QCOMPARE(secondDirLister.rootItem().url().toLocalFile(), newPath); disconnect(&secondDirLister, 0, this, 0); QDir().remove(newPath); } void KDirListerTest::testRedirection() { m_items.clear(); const KUrl url("file://somemachine/"); connect(&m_dirLister, SIGNAL(newItems(KFileItemList)), this, SLOT(slotNewItems(KFileItemList))); // 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.spyCompletedKUrl.count(), 0); QCOMPARE(m_dirLister.spyCanceled.count(), 0); QCOMPARE(m_dirLister.spyCanceledKUrl.count(), 0); QCOMPARE(m_dirLister.spyClear.count(), 1); QCOMPARE(m_dirLister.spyClearKUrl.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"); connect(&m_dirLister, SIGNAL(redirection(KUrl,KUrl)), this, SLOT(exitLoop())); enterLoop(); QCOMPARE(m_dirLister.spyStarted.count(), 1); QCOMPARE(m_dirLister.spyCompleted.count(), 0); // we stopped before the listing. QCOMPARE(m_dirLister.spyCompletedKUrl.count(), 0); QCOMPARE(m_dirLister.spyCanceled.count(), 0); QCOMPARE(m_dirLister.spyCanceledKUrl.count(), 0); QCOMPARE(m_dirLister.spyClear.count(), 2); // redirection cleared a second time (just in case...) QCOMPARE(m_dirLister.spyClearKUrl.count(), 0); QCOMPARE(m_dirLister.spyRedirection.count(), 1); QVERIFY(m_items.isEmpty()); QVERIFY(!m_dirLister.isFinished()); m_dirLister.stop(url); QVERIFY(!m_dirLister.isFinished()); disconnect(&m_dirLister, 0, this, 0); } void KDirListerTest::testWatchingAfterCopyJob() // #331582 { m_items.clear(); KTempDir newDir; const QString path = newDir.name(); // List and watch an empty dir connect(&m_dirLister, SIGNAL(newItems(KFileItemList)), this, SLOT(slotNewItems(KFileItemList))); m_dirLister.openUrl(KUrl(path)); QVERIFY(QTest::kWaitForSignal(&m_dirLister, SIGNAL(completed()), 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(QTest::kWaitForSignal(&m_dirLister, SIGNAL(completed()), 1000)); QVERIFY(m_dirLister.isFinished()); QCOMPARE(m_items.count(), 3); // Create a new file and verify that the dir lister notices it. m_items.clear(); createTestFile(path + "a"); QVERIFY(QTest::kWaitForSignal(&m_dirLister, SIGNAL(completed()), 1000)); QVERIFY(m_dirLister.isFinished()); QCOMPARE(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(KUrl(oldPath), KUrl(newPath), KIO::HideProgressInfo); job->exec(); // Now try to create a second new file in and verify that the // dir lister notices it. m_items.clear(); createTestFile(path + "b"); int numTries = 0; // Give time for KDirWatch to notify us // This should end up in "KDirListerCache::slotFileDirty" while (m_items.isEmpty()) { QVERIFY(++numTries < 10); QTest::qWait(200); } QCOMPARE(m_items.count(), 1); newDir.unlink(); QVERIFY(QTest::kWaitForSignal(&m_dirLister, SIGNAL(clear()), 1000)); } void KDirListerTest::testRemoveWatchedDirectory() { m_items.clear(); KTempDir newDir; const QString path = newDir.name(); // List and watch an empty dir connect(&m_dirLister, SIGNAL(newItems(KFileItemList)), this, SLOT(slotNewItems(KFileItemList))); m_dirLister.openUrl(KUrl(path)); QVERIFY(QTest::kWaitForSignal(&m_dirLister, SIGNAL(completed()), 1000)); QVERIFY(m_dirLister.isFinished()); QVERIFY(m_items.isEmpty()); // Create a subfolder. const QString subDirPath = path + "abc"; QVERIFY(QDir().mkdir(subDirPath)); QVERIFY(QTest::kWaitForSignal(&m_dirLister, SIGNAL(completed()), 1000)); QVERIFY(m_dirLister.isFinished()); QCOMPARE(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(QTest::kWaitForSignal(&m_dirLister, SIGNAL(completed()), 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() +{ + KTempDir tempDir; + tempDir.setAutoRemove(false); + + const QString path = tempDir.name(); + const QString subdir = path + QLatin1String("subdir"); + QVERIFY(QDir().mkdir(subdir)); + + MyDirLister mylister; + mylister.openUrl(KUrl::fromPath(tempDir.name())); + QVERIFY(QTest::kWaitForSignal(&mylister, SIGNAL(completed()), 1000)); + + KFileItemList list = mylister.items(); + QVERIFY(mylister.isFinished()); + QCOMPARE(list.count(), 1); + QCOMPARE(mylister.rootItem().url().toLocalFile(KUrl::AddTrailingSlash), path); + + const quint32 permissions = (S_IRUSR | S_IWUSR | S_IXUSR); + KIO::SimpleJob* job = KIO::chmod(list.first().url(), permissions); + QVERIFY(job->exec()); + + QVERIFY(QTest::kWaitForSignal(&mylister, SIGNAL(refreshItems(QList >)), 2000)); + + list = mylister.items(); + QCOMPARE(list.first().permissions(), permissions); + QVERIFY(QDir().rmdir(subdir)); +} + void KDirListerTest::enterLoop(int exitCount) { //qDebug("enterLoop"); m_exitCount = exitCount; m_eventLoop.exec(QEventLoop::ExcludeUserInputEvents); } void KDirListerTest::exitLoop() { //qDebug("exitLoop"); --m_exitCount; if (m_exitCount <= 0) { m_eventLoop.quit(); } } 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::testDeleteCurrentDir() { // ensure m_dirLister holds the items. m_dirLister.openUrl(KUrl(path()), KDirLister::NoFlags); connect(&m_dirLister, SIGNAL(completed()), this, SLOT(exitLoop())); enterLoop(); disconnect(&m_dirLister, SIGNAL(completed()), this, SLOT(exitLoop())); m_dirLister.clearSpies(); connect(&m_dirLister, SIGNAL(clear()), &m_eventLoop, SLOT(quit())); KIO::DeleteJob* job = KIO::del(path(), KIO::HideProgressInfo); bool ok = job->exec(); QVERIFY(ok); enterLoop(); QCOMPARE(m_dirLister.spyClear.count(), 1); QCOMPARE(m_dirLister.spyClearKUrl.count(), 0); KUrl::List deletedUrls; for (int i = 0; i < m_dirLister.spyItemsDeleted.count(); ++i) deletedUrls += m_dirLister.spyItemsDeleted[i][0].value().urlList(); //kDebug() << deletedUrls; KUrl currentDirUrl = QUrl::fromLocalFile(path()); currentDirUrl.adjustPath(KUrl::RemoveTrailingSlash); // 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::waitForRefreshedItems() { int numTries = 0; // Give time for KDirWatch to notify us while (m_refreshedItems.isEmpty()) { QVERIFY(++numTries < 10); QTest::qWait(200); } } 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, SIGNAL(newItems(KFileItemList)), this, SLOT(slotNewItems2(KFileItemList))); connect(&lister, SIGNAL(refreshItems(QList >)), this, SLOT(slotRefreshItems2(QList >))); lister.openUrl(KUrl(path), KDirLister::NoFlags); connect(&lister, SIGNAL(completed()), this, SLOT(exitLoop())); enterLoop(); QVERIFY(lister.isFinished()); } diff --git a/kio/tests/kdirlistertest.h b/kio/tests/kdirlistertest.h index 1b117bbbf1..e8094a1e2e 100644 --- a/kio/tests/kdirlistertest.h +++ b/kio/tests/kdirlistertest.h @@ -1,145 +1,146 @@ /* 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(); qRegisterMetaType(); } }; class MyDirLister : public KDirLister, GlobalInits { public: MyDirLister() : spyStarted(this, SIGNAL(started(KUrl))), spyClear(this, SIGNAL(clear())), spyClearKUrl(this, SIGNAL(clear(KUrl))), spyCompleted(this, SIGNAL(completed())), spyCompletedKUrl(this, SIGNAL(completed(KUrl))), spyCanceled(this, SIGNAL(canceled())), spyCanceledKUrl(this, SIGNAL(canceled(KUrl))), spyRedirection(this, SIGNAL(redirection(KUrl))), spyDeleteItem(this, SIGNAL(deleteItem(KFileItem))), spyItemsDeleted(this, SIGNAL(itemsDeleted(KFileItemList))) {} void clearSpies() { spyStarted.clear(); spyClear.clear(); spyClearKUrl.clear(); spyCompleted.clear(); spyCompletedKUrl.clear(); spyCanceled.clear(); spyCanceledKUrl.clear(); spyRedirection.clear(); spyDeleteItem.clear(); spyItemsDeleted.clear(); } QSignalSpy spyStarted; QSignalSpy spyClear; QSignalSpy spyClearKUrl; QSignalSpy spyCompleted; QSignalSpy spyCompletedKUrl; QSignalSpy spyCanceled; QSignalSpy spyCanceledKUrl; QSignalSpy spyRedirection; QSignalSpy spyDeleteItem; QSignalSpy spyItemsDeleted; protected: virtual void handleError(KIO::Job* job); }; class KDirListerTest : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase(); void cleanup(); void testOpenUrl(); void testOpenUrlFromCache(); void testNewItems(); void testNewItemByCopy(); void testNewItemsInSymlink(); void testRefreshItems(); void testRefreshRootItem(); void testDeleteItem(); void testRenameItem(); void testRenameAndOverwrite(); void testConcurrentListing(); void testConcurrentHoldingListing(); void testConcurrentListingAndStop(); void testDeleteListerEarly(); void testOpenUrlTwice(); void testOpenUrlTwiceWithKeep(); void testOpenAndStop(); void testBug211472(); void testRenameCurrentDir(); void testRedirection(); void testWatchingAfterCopyJob(); void testRemoveWatchedDirectory(); + void testDirPermissionChange(); void testDeleteCurrentDir(); // must be last! protected Q_SLOTS: // 'more private than private slots' - i.e. not seen by qtestlib void exitLoop(); void slotNewItems(const KFileItemList&); void slotNewItems2(const KFileItemList&); void slotRefreshItems(const QList >&); void slotRefreshItems2(const QList >&); Q_SIGNALS: void refreshItemsReceived(); private: void enterLoop(int exitCount = 1); int fileCount() const; QString path() const { return m_tempDir.name(); } void waitForRefreshedItems(); void createSimpleFile(const QString& fileName); void fillDirLister2(MyDirLister& lister, const QString& path); private: int m_exitCount; QEventLoop m_eventLoop; KTempDir m_tempDir; MyDirLister m_dirLister; KFileItemList m_items; KFileItemList m_items2; QList > m_refreshedItems, m_refreshedItems2; }; #endif