diff --git a/kio/kio/chmodjob.cpp b/kio/kio/chmodjob.cpp index 5ba9785301..bb75cf585d 100644 --- a/kio/kio/chmodjob.cpp +++ b/kio/kio/chmodjob.cpp @@ -1,292 +1,293 @@ /* This file is part of the KDE libraries Copyright (C) 2000 Stephan Kulow David Faure Waldo Bastian 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 "chmodjob.h" #include "job.h" #include "jobuidelegate.h" #include #include #include #include +#include #include #include #include #include #include #include "job_p.h" namespace KIO { struct ChmodInfo { - KUrl url; + QUrl url; int permissions; }; enum ChmodJobState { CHMODJOB_STATE_LISTING, CHMODJOB_STATE_CHMODING }; class ChmodJobPrivate: public KIO::JobPrivate { public: ChmodJobPrivate(const KFileItemList& lstItems, int permissions, int mask, int newOwner, int newGroup, bool recursive) : state( CHMODJOB_STATE_LISTING ) , m_permissions( permissions ) , m_mask( mask ) , m_newOwner( newOwner ) , m_newGroup( newGroup ) , m_recursive( recursive ) , m_lstItems( lstItems ) { } ChmodJobState state; int m_permissions; int m_mask; int m_newOwner; int m_newGroup; bool m_recursive; KFileItemList m_lstItems; QLinkedList m_infos; // linkedlist since we keep removing the first item void chmodNextFile(); void _k_slotEntries( KIO::Job * , const KIO::UDSEntryList & ); void _k_processList(); Q_DECLARE_PUBLIC(ChmodJob) static inline ChmodJob *newJob(const KFileItemList& lstItems, int permissions, int mask, int newOwner, int newGroup, bool recursive, JobFlags flags) { ChmodJob *job = new ChmodJob(*new ChmodJobPrivate(lstItems,permissions,mask, newOwner,newGroup,recursive)); job->setUiDelegate(new JobUiDelegate()); if (!(flags & HideProgressInfo)) KIO::getJobTracker()->registerJob(job); return job; } }; } // namespace KIO using namespace KIO; ChmodJob::ChmodJob(ChmodJobPrivate &dd) : KIO::Job(dd) { QMetaObject::invokeMethod( this, "_k_processList", Qt::QueuedConnection ); } ChmodJob::~ChmodJob() { } void ChmodJobPrivate::_k_processList() { Q_Q(ChmodJob); while ( !m_lstItems.isEmpty() ) { const KFileItem item = m_lstItems.first(); if ( !item.isLink() ) // don't do anything with symlinks { // File or directory -> remember to chmod ChmodInfo info; info.url = item.url(); // This is a toplevel file, we apply changes directly (no +X emulation here) const mode_t permissions = item.permissions() & 0777; // get rid of "set gid" and other special flags info.permissions = ( m_permissions & m_mask ) | ( permissions & ~m_mask ); /*kDebug(7007) << "toplevel url:" << info.url << "\n current permissions=" << QString::number(permissions,8) << "\n wanted permission=" << QString::number(m_permissions,8) << "\n with mask=" << QString::number(m_mask,8) << "\n with ~mask (mask bits we keep) =" << QString::number((uint)~m_mask,8) << "\n bits we keep =" << QString::number(permissions & ~m_mask,8) << "\n new permissions = " << QString::number(info.permissions,8);*/ m_infos.prepend( info ); //kDebug(7007) << "processList : Adding info for " << info.url; // Directory and recursive -> list if ( item.isDir() && m_recursive ) { //kDebug(7007) << "ChmodJob::processList dir -> listing"; KIO::ListJob * listJob = KIO::listRecursive( item.url(), KIO::HideProgressInfo ); q->connect( listJob, SIGNAL(entries( KIO::Job *, const KIO::UDSEntryList& )), SLOT(_k_slotEntries(KIO::Job*,KIO::UDSEntryList))); q->addSubjob( listJob ); return; // we'll come back later, when this one's finished } } m_lstItems.removeFirst(); } kDebug(7007) << "ChmodJob::processList -> going to STATE_CHMODING"; // We have finished, move on state = CHMODJOB_STATE_CHMODING; chmodNextFile(); } void ChmodJobPrivate::_k_slotEntries( KIO::Job*, const KIO::UDSEntryList & list ) { KIO::UDSEntryList::ConstIterator it = list.begin(); KIO::UDSEntryList::ConstIterator end = list.end(); for (; it != end; ++it) { const KIO::UDSEntry& entry = *it; const bool isLink = !entry.stringValue( KIO::UDSEntry::UDS_LINK_DEST ).isEmpty(); const QString relativePath = entry.stringValue( KIO::UDSEntry::UDS_NAME ); if ( !isLink && relativePath != ".." ) { const mode_t permissions = entry.numberValue( KIO::UDSEntry::UDS_ACCESS ) & 0777; // get rid of "set gid" and other special flags ChmodInfo info; info.url = m_lstItems.first().url(); // base directory - info.url.addPath( relativePath ); + info.url = QUrlPathInfo::addPathToUrl(info.url, relativePath); int mask = m_mask; // Emulate -X: only give +x to files that had a +x bit already // So the check is the opposite : if the file had no x bit, don't touch x bits // For dirs this doesn't apply if ( !entry.isDir() ) { int newPerms = m_permissions & mask; if ( (newPerms & 0111) && !(permissions & 0111) ) { // don't interfere with mandatory file locking if ( newPerms & 02000 ) mask = mask & ~0101; else mask = mask & ~0111; } } info.permissions = ( m_permissions & mask ) | ( permissions & ~mask ); /*kDebug(7007) << info.url << "\n current permissions=" << QString::number(permissions,8) << "\n wanted permission=" << QString::number(m_permissions,8) << "\n with mask=" << QString::number(mask,8) << "\n with ~mask (mask bits we keep) =" << QString::number((uint)~mask,8) << "\n bits we keep =" << QString::number(permissions & ~mask,8) << "\n new permissions = " << QString::number(info.permissions,8);*/ // Prepend this info in our todo list. // This way, the toplevel dirs are done last. m_infos.prepend( info ); } } } void ChmodJobPrivate::chmodNextFile() { Q_Q(ChmodJob); if ( !m_infos.isEmpty() ) { ChmodInfo info = m_infos.takeFirst(); // First update group / owner (if local file) // (permissions have to set after, in case of suid and sgid) if ( info.url.isLocalFile() && ( m_newOwner != -1 || m_newGroup != -1 ) ) { QString path = info.url.toLocalFile(); if ( chown( QFile::encodeName(path), m_newOwner, m_newGroup ) != 0 ) { int answer = KMessageBox::warningContinueCancel( 0, i18n( "Could not modify the ownership of file %1. You have insufficient access to the file to perform the change." , path), QString(), KGuiItem(i18n("&Skip File")) ); if (answer == KMessageBox::Cancel) { q->setError( ERR_USER_CANCELED ); q->emitResult(); return; } } } kDebug(7007) << "chmod'ing" << info.url << "to" << QString::number(info.permissions,8); KIO::SimpleJob * job = KIO::chmod( info.url, info.permissions ); // copy the metadata for acl and default acl const QString aclString = q->queryMetaData( QLatin1String("ACL_STRING") ); const QString defaultAclString = q->queryMetaData( QLatin1String("DEFAULT_ACL_STRING") ); if ( !aclString.isEmpty() ) job->addMetaData( QLatin1String("ACL_STRING"), aclString ); if ( !defaultAclString.isEmpty() ) job->addMetaData( QLatin1String("DEFAULT_ACL_STRING"), defaultAclString ); q->addSubjob(job); } else // We have finished q->emitResult(); } void ChmodJob::slotResult( KJob * job ) { Q_D(ChmodJob); removeSubjob(job); if ( job->error() ) { setError( job->error() ); setErrorText( job->errorText() ); emitResult(); return; } //kDebug(7007) << "d->m_lstItems:" << d->m_lstItems.count(); switch ( d->state ) { case CHMODJOB_STATE_LISTING: d->m_lstItems.removeFirst(); kDebug(7007) << "-> processList"; d->_k_processList(); return; case CHMODJOB_STATE_CHMODING: kDebug(7007) << "-> chmodNextFile"; d->chmodNextFile(); return; default: assert(0); return; } } ChmodJob *KIO::chmod( const KFileItemList& lstItems, int permissions, int mask, const QString& owner, const QString& group, bool recursive, JobFlags flags ) { uid_t newOwnerID = uid_t(-1); // chown(2) : -1 means no change if ( !owner.isEmpty() ) { struct passwd* pw = getpwnam(QFile::encodeName(owner)); if ( pw == 0L ) kError(250) << " ERROR: No user" << owner; else newOwnerID = pw->pw_uid; } gid_t newGroupID = gid_t(-1); // chown(2) : -1 means no change if ( !group.isEmpty() ) { struct group* g = getgrnam(QFile::encodeName(group)); if ( g == 0L ) kError(250) << " ERROR: No group" << group; else newGroupID = g->gr_gid; } return ChmodJobPrivate::newJob(lstItems, permissions, mask, newOwnerID, newGroupID, recursive, flags); } #include "moc_chmodjob.cpp" diff --git a/kio/kio/copyjob.cpp b/kio/kio/copyjob.cpp index a044357284..19ef7868e3 100644 --- a/kio/kio/copyjob.cpp +++ b/kio/kio/copyjob.cpp @@ -1,2246 +1,2249 @@ /* This file is part of the KDE libraries Copyright 2000 Stephan Kulow Copyright 2000-2006 David Faure Copyright 2000 Waldo Bastian 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 "copyjob.h" #include #include "kdirlister.h" #include "kfileitem.h" #include "mkdirjob.h" #include "deletejob.h" #include #include #include #include #include "slave.h" #include "scheduler.h" #include "kdirwatch.h" #include "kprotocolmanager.h" #include "jobuidelegate.h" #include #ifdef Q_OS_UNIX #include #endif #include #include #include #include #include // mode_t #include #include #include "job_p.h" #include #include // Porting helpers. Qt 5: remove #if QT_VERSION < QT_VERSION_CHECK(5, 0, 0) #define toDisplayString toString #endif using namespace KIO; //this will update the report dialog with 5 Hz, I think this is fast enough, aleXXX #define REPORT_TIMEOUT 200 enum DestinationState { DEST_NOT_STATED, DEST_IS_DIR, DEST_IS_FILE, DEST_DOESNT_EXIST }; /** * States: * STATE_STATING for the dest * statCurrentSrc then does, for each src url: * STATE_RENAMING if direct rename looks possible * (on already exists, and user chooses rename, TODO: go to STATE_RENAMING again) * STATE_STATING * and then, if dir -> STATE_LISTING (filling 'd->dirs' and 'd->files') * STATE_CREATING_DIRS (createNextDir, iterating over 'd->dirs') * if conflict: STATE_CONFLICT_CREATING_DIRS * STATE_COPYING_FILES (copyNextFile, iterating over 'd->files') * if conflict: STATE_CONFLICT_COPYING_FILES * STATE_DELETING_DIRS (deleteNextDir) (if moving) * STATE_SETTING_DIR_ATTRIBUTES (setNextDirAttribute, iterating over d->m_directoriesCopied) * done. */ enum CopyJobState { STATE_STATING, STATE_RENAMING, STATE_LISTING, STATE_CREATING_DIRS, STATE_CONFLICT_CREATING_DIRS, STATE_COPYING_FILES, STATE_CONFLICT_COPYING_FILES, STATE_DELETING_DIRS, STATE_SETTING_DIR_ATTRIBUTES }; /** @internal */ class KIO::CopyJobPrivate: public KIO::JobPrivate { public: CopyJobPrivate(const QList& src, const QUrl& dest, CopyJob::CopyMode mode, bool asMethod) : m_globalDest(dest) , m_globalDestinationState(DEST_NOT_STATED) , m_defaultPermissions(false) , m_bURLDirty(false) , m_mode(mode) , m_asMethod(asMethod) , destinationState(DEST_NOT_STATED) , state(STATE_STATING) , m_freeSpace(-1) , m_totalSize(0) , m_processedSize(0) , m_fileProcessedSize(0) , m_processedFiles(0) , m_processedDirs(0) , m_srcList(src) , m_currentStatSrc(m_srcList.constBegin()) , m_bCurrentOperationIsLink(false) , m_bSingleFileCopy(false) , m_bOnlyRenames(mode==CopyJob::Move) , m_dest(dest) , m_bAutoRenameFiles(false) , m_bAutoRenameDirs(false) , m_bAutoSkipFiles( false ) , m_bAutoSkipDirs( false ) , m_bOverwriteAllFiles( false ) , m_bOverwriteAllDirs( false ) , m_conflictError(0) , m_reportTimer(0) { } // This is the dest URL that was initially given to CopyJob // It is copied into m_dest, which can be changed for a given src URL // (when using the RENAME dialog in slotResult), // and which will be reset for the next src URL. - KUrl m_globalDest; + QUrl m_globalDest; // The state info about that global dest DestinationState m_globalDestinationState; // See setDefaultPermissions bool m_defaultPermissions; // Whether URLs changed (and need to be emitted by the next slotReport call) bool m_bURLDirty; // Used after copying all the files into the dirs, to set mtime (TODO: and permissions?) // after the copy is done QLinkedList m_directoriesCopied; QLinkedList::const_iterator m_directoriesCopiedIterator; CopyJob::CopyMode m_mode; bool m_asMethod; DestinationState destinationState; CopyJobState state; KIO::filesize_t m_freeSpace; KIO::filesize_t m_totalSize; KIO::filesize_t m_processedSize; KIO::filesize_t m_fileProcessedSize; int m_processedFiles; int m_processedDirs; QList files; QList dirs; QList dirsToRemove; QList m_srcList; QList m_successSrcList; // Entries in m_srcList that have successfully been moved QList::const_iterator m_currentStatSrc; bool m_bCurrentSrcIsDir; bool m_bCurrentOperationIsLink; bool m_bSingleFileCopy; bool m_bOnlyRenames; - KUrl m_dest; - KUrl m_currentDest; // set during listing, used by slotEntries + QUrl m_dest; + QUrl m_currentDest; // set during listing, used by slotEntries // QStringList m_skipList; QSet m_overwriteList; bool m_bAutoRenameFiles; bool m_bAutoRenameDirs; bool m_bAutoSkipFiles; bool m_bAutoSkipDirs; bool m_bOverwriteAllFiles; bool m_bOverwriteAllDirs; int m_conflictError; QTimer *m_reportTimer; // The current src url being stat'ed or copied // During the stat phase, this is initially equal to *m_currentStatSrc but it can be resolved to a local file equivalent (#188903). - KUrl m_currentSrcURL; - KUrl m_currentDestURL; + QUrl m_currentSrcURL; + QUrl m_currentDestURL; QSet m_parentDirs; void statCurrentSrc(); void statNextSrc(); // Those aren't slots but submethods for slotResult. void slotResultStating( KJob * job ); void startListing( const QUrl & src ); void slotResultCreatingDirs( KJob * job ); void slotResultConflictCreatingDirs( KJob * job ); void createNextDir(); void slotResultCopyingFiles( KJob * job ); void slotResultConflictCopyingFiles( KJob * job ); // KIO::Job* linkNextFile( const QUrl& uSource, const QUrl& uDest, bool overwrite ); KIO::Job* linkNextFile( const QUrl& uSource, const QUrl& uDest, JobFlags flags ); void copyNextFile(); void slotResultDeletingDirs( KJob * job ); void deleteNextDir(); void sourceStated(const UDSEntry& entry, const QUrl& sourceUrl); void skip(const QUrl & sourceURL, bool isDir); void slotResultRenaming( KJob * job ); void slotResultSettingDirAttributes( KJob * job ); void setNextDirAttribute(); void startRenameJob(const QUrl &slave_url); bool shouldOverwriteDir( const QString& path ) const; bool shouldOverwriteFile( const QString& path ) const; bool shouldSkip( const QString& path ) const; void skipSrc(bool isDir); void slotStart(); void slotEntries( KIO::Job*, const KIO::UDSEntryList& list ); void slotSubError(KIO::ListJob* job, KIO::ListJob *subJob); void addCopyInfoFromUDSEntry(const UDSEntry& entry, const QUrl& srcUrl, bool srcIsDir, const QUrl& currentDest); /** * Forward signal from subjob */ void slotProcessedSize( KJob*, qulonglong data_size ); /** * Forward signal from subjob * @param size the total size */ void slotTotalSize( KJob*, qulonglong size ); void slotReport(); Q_DECLARE_PUBLIC(CopyJob) static inline CopyJob *newJob(const QList& src, const QUrl& dest, CopyJob::CopyMode mode, bool asMethod, JobFlags flags) { CopyJob *job = new CopyJob(*new CopyJobPrivate(src,dest,mode,asMethod)); job->setUiDelegate(new JobUiDelegate); if (!(flags & HideProgressInfo)) KIO::getJobTracker()->registerJob(job); if (flags & KIO::Overwrite) { job->d_func()->m_bOverwriteAllDirs = true; job->d_func()->m_bOverwriteAllFiles = true; } return job; } }; CopyJob::CopyJob(CopyJobPrivate &dd) : Job(dd) { - setProperty("destUrl", d_func()->m_dest.url()); + setProperty("destUrl", d_func()->m_dest.toString()); QTimer::singleShot(0, this, SLOT(slotStart())); } CopyJob::~CopyJob() { } QList CopyJob::srcUrls() const { return d_func()->m_srcList; } QUrl CopyJob::destUrl() const { return d_func()->m_dest; } void CopyJobPrivate::slotStart() { Q_Q(CopyJob); /** We call the functions directly instead of using signals. Calling a function via a signal takes approx. 65 times the time compared to calling it directly (at least on my machine). aleXXX */ m_reportTimer = new QTimer(q); q->connect(m_reportTimer,SIGNAL(timeout()),q,SLOT(slotReport())); m_reportTimer->start(REPORT_TIMEOUT); // Stat the dest KIO::Job * job = KIO::stat( m_dest, StatJob::DestinationSide, 2, KIO::HideProgressInfo ); //kDebug(7007) << "CopyJob:stating the dest " << m_dest; q->addSubjob(job); } // For unit test purposes KIO_EXPORT bool kio_resolve_local_urls = true; void CopyJobPrivate::slotResultStating( KJob *job ) { Q_Q(CopyJob); //kDebug(7007); // Was there an error while stating the src ? if (job->error() && destinationState != DEST_NOT_STATED ) { const QUrl srcurl = static_cast(job)->url(); if ( !srcurl.isLocalFile() ) { // Probably : src doesn't exist. Well, over some protocols (e.g. FTP) // this info isn't really reliable (thanks to MS FTP servers). // We'll assume a file, and try to download anyway. kDebug(7007) << "Error while stating source. Activating hack"; q->removeSubjob( job ); assert ( !q->hasSubjobs() ); // We should have only one job at a time ... struct CopyInfo info; info.permissions = (mode_t) -1; info.mtime = (time_t) -1; info.ctime = (time_t) -1; info.size = (KIO::filesize_t)-1; info.uSource = srcurl; info.uDest = m_dest; // Append filename or dirname to destination URL, if allowed if (destinationState == DEST_IS_DIR && !m_asMethod) { info.uDest = QUrlPathInfo::addPathToUrl(info.uDest, QUrlPathInfo(srcurl).fileName()); } files.append( info ); statNextSrc(); return; } // Local file. If stat fails, the file definitely doesn't exist. // yes, q->Job::, because we don't want to call our override q->Job::slotResult( job ); // will set the error and emit result(this) return; } // Keep copy of the stat result const UDSEntry entry = static_cast(job)->statResult(); if ( destinationState == DEST_NOT_STATED ) { if ( m_dest.isLocalFile() ) { //works for dirs as well QString path = m_dest.toLocalFile(); if (m_asMethod) { // In copy-as mode, we want to check the directory to which we're // copying. The target file or directory does not exist yet, which // might confuse KDiskFreeSpaceInfo. path = QFileInfo(path).absolutePath(); } KFileSystemType::Type fsType = KFileSystemType::fileSystemType( path ); if ( fsType != KFileSystemType::Nfs && fsType != KFileSystemType::Smb ) { m_freeSpace = KDiskFreeSpaceInfo::freeSpaceInfo( path ).available(); } //TODO actually preliminary check is even more valuable for slow NFS/SMB mounts, //but we need to find a way to report connection errors to user } const bool isGlobalDest = m_dest == m_globalDest; const bool isDir = entry.isDir(); // we were stating the dest if (job->error()) { destinationState = DEST_DOESNT_EXIST; //kDebug(7007) << "dest does not exist"; } else { // Treat symlinks to dirs as dirs here, so no test on isLink destinationState = isDir ? DEST_IS_DIR : DEST_IS_FILE; //kDebug(7007) << "dest is dir:" << isDir; const QString sLocalPath = entry.stringValue( KIO::UDSEntry::UDS_LOCAL_PATH ); if ( !sLocalPath.isEmpty() && kio_resolve_local_urls && destinationState != DEST_DOESNT_EXIST ) { m_dest = QUrl::fromLocalFile(sLocalPath); if ( isGlobalDest ) m_globalDest = m_dest; } } if ( isGlobalDest ) m_globalDestinationState = destinationState; q->removeSubjob( job ); assert ( !q->hasSubjobs() ); // After knowing what the dest is, we can start stat'ing the first src. statCurrentSrc(); } else { sourceStated(entry, static_cast(job)->url()); q->removeSubjob( job ); } } void CopyJobPrivate::sourceStated(const UDSEntry& entry, const QUrl& sourceUrl) { const QString sLocalPath = entry.stringValue( KIO::UDSEntry::UDS_LOCAL_PATH ); const bool isDir = entry.isDir(); // We were stating the current source URL // Is it a file or a dir ? // There 6 cases, and all end up calling addCopyInfoFromUDSEntry first : // 1 - src is a dir, destination is a directory, // slotEntries will append the source-dir-name to the destination // 2 - src is a dir, destination is a file -- will offer to overwrite, later on. // 3 - src is a dir, destination doesn't exist, then it's the destination dirname, // so slotEntries will use it as destination. // 4 - src is a file, destination is a directory, // slotEntries will append the filename to the destination. // 5 - src is a file, destination is a file, m_dest is the exact destination name // 6 - src is a file, destination doesn't exist, m_dest is the exact destination name QUrl srcurl; if (!sLocalPath.isEmpty() && destinationState != DEST_DOESNT_EXIST) { kDebug() << "Using sLocalPath. destinationState=" << destinationState; // Prefer the local path -- but only if we were able to stat() the dest. // Otherwise, renaming a desktop:/ url would copy from src=file to dest=desktop (#218719) srcurl = QUrl::fromLocalFile(sLocalPath); } else { srcurl = sourceUrl; } addCopyInfoFromUDSEntry(entry, srcurl, false, m_dest); QUrlPathInfo srcurlInfo(srcurl); m_currentDest = m_dest; m_bCurrentSrcIsDir = false; if ( isDir // treat symlinks as files (no recursion) && !entry.isLink() && m_mode != CopyJob::Link ) // No recursion in Link mode either. { //kDebug(7007) << "Source is a directory"; if (srcurl.isLocalFile()) { const QString parentDir = srcurlInfo.localPath(QUrlPathInfo::StripTrailingSlash); m_parentDirs.insert(parentDir); } m_bCurrentSrcIsDir = true; // used by slotEntries if ( destinationState == DEST_IS_DIR ) // (case 1) { if ( !m_asMethod ) { // Use / as destination, from now on QString directory = srcurlInfo.fileName(); const QString sName = entry.stringValue( KIO::UDSEntry::UDS_NAME ); KProtocolInfo::FileNameUsedForCopying fnu = KProtocolManager::fileNameUsedForCopying(srcurl); if (fnu == KProtocolInfo::Name) { if (!sName.isEmpty()) directory = sName; } else if (fnu == KProtocolInfo::DisplayName) { const QString dispName = entry.stringValue( KIO::UDSEntry::UDS_DISPLAY_NAME ); if (!dispName.isEmpty()) directory = dispName; else if (!sName.isEmpty()) directory = sName; } m_currentDest = QUrlPathInfo::addPathToUrl(m_currentDest, directory); } } else // (case 3) { // otherwise dest is new name for toplevel dir // so the destination exists, in fact, from now on. // (This even works with other src urls in the list, since the // dir has effectively been created) destinationState = DEST_IS_DIR; if ( m_dest == m_globalDest ) m_globalDestinationState = destinationState; } startListing( srcurl ); } else { //kDebug(7007) << "Source is a file (or a symlink), or we are linking -> no recursive listing"; if (srcurl.isLocalFile()) { const QString parentDir = srcurlInfo.directory(); m_parentDirs.insert(parentDir); } statNextSrc(); } } bool CopyJob::doSuspend() { Q_D(CopyJob); d->slotReport(); return Job::doSuspend(); } void CopyJobPrivate::slotReport() { Q_Q(CopyJob); if ( q->isSuspended() ) return; // If showProgressInfo was set, progressId() is > 0. switch (state) { case STATE_RENAMING: q->setTotalAmount(KJob::Files, m_srcList.count()); // fall-through intended case STATE_COPYING_FILES: q->setProcessedAmount( KJob::Files, m_processedFiles ); if (m_bURLDirty) { // Only emit urls when they changed. This saves time, and fixes #66281 m_bURLDirty = false; if (m_mode==CopyJob::Move) { emitMoving(q, m_currentSrcURL, m_currentDestURL); emit q->moving( q, m_currentSrcURL, m_currentDestURL); } else if (m_mode==CopyJob::Link) { emitCopying( q, m_currentSrcURL, m_currentDestURL ); // we don't have a delegate->linking emit q->linking( q, m_currentSrcURL.path(), m_currentDestURL ); } else { emitCopying( q, m_currentSrcURL, m_currentDestURL ); emit q->copying( q, m_currentSrcURL, m_currentDestURL ); } } break; case STATE_CREATING_DIRS: q->setProcessedAmount( KJob::Directories, m_processedDirs ); if (m_bURLDirty) { m_bURLDirty = false; emit q->creatingDir( q, m_currentDestURL ); emitCreatingDir( q, m_currentDestURL ); } break; case STATE_STATING: case STATE_LISTING: if (m_bURLDirty) { m_bURLDirty = false; if (m_mode==CopyJob::Move) { emitMoving( q, m_currentSrcURL, m_currentDestURL ); } else { emitCopying( q, m_currentSrcURL, m_currentDestURL ); } } q->setTotalAmount(KJob::Bytes, m_totalSize); q->setTotalAmount(KJob::Files, files.count()); q->setTotalAmount(KJob::Directories, dirs.count()); break; default: break; } } void CopyJobPrivate::slotEntries(KIO::Job* job, const UDSEntryList& list) { //Q_Q(CopyJob); UDSEntryList::ConstIterator it = list.constBegin(); UDSEntryList::ConstIterator end = list.constEnd(); for (; it != end; ++it) { const UDSEntry& entry = *it; addCopyInfoFromUDSEntry(entry, static_cast(job)->url(), m_bCurrentSrcIsDir, m_currentDest); } } void CopyJobPrivate::slotSubError(ListJob* job, ListJob* subJob) { const KUrl url = subJob->url(); kWarning() << url << subJob->errorString(); Q_Q(CopyJob); emit q->warning(job, subJob->errorString(), QString()); skip(url, true); } void CopyJobPrivate::addCopyInfoFromUDSEntry(const UDSEntry& entry, const QUrl& srcUrl, bool srcIsDir, const QUrl& currentDest) { struct CopyInfo info; info.permissions = entry.numberValue(KIO::UDSEntry::UDS_ACCESS, -1); info.mtime = (time_t) entry.numberValue(KIO::UDSEntry::UDS_MODIFICATION_TIME, -1); info.ctime = (time_t) entry.numberValue(KIO::UDSEntry::UDS_CREATION_TIME, -1); info.size = (KIO::filesize_t) entry.numberValue(KIO::UDSEntry::UDS_SIZE, -1); if (info.size != (KIO::filesize_t) -1) m_totalSize += info.size; // recursive listing, displayName can be a/b/c/d const QString fileName = entry.stringValue(KIO::UDSEntry::UDS_NAME); const QString urlStr = entry.stringValue(KIO::UDSEntry::UDS_URL); QUrl url; if (!urlStr.isEmpty()) url = QUrl(urlStr); QString localPath = entry.stringValue(KIO::UDSEntry::UDS_LOCAL_PATH); const bool isDir = entry.isDir(); info.linkDest = entry.stringValue(KIO::UDSEntry::UDS_LINK_DEST); if (fileName != QLatin1String("..") && fileName != QLatin1String(".")) { const bool hasCustomURL = !url.isEmpty() || !localPath.isEmpty(); if (!hasCustomURL) { // Make URL from displayName url = srcUrl; if (srcIsDir) { // Only if src is a directory. Otherwise uSource is fine as is //kDebug(7007) << "adding path" << displayName; url = QUrlPathInfo::addPathToUrl(url, fileName); } } //kDebug(7007) << "displayName=" << displayName << "url=" << url; if (!localPath.isEmpty() && kio_resolve_local_urls && destinationState != DEST_DOESNT_EXIST) { url = QUrl::fromLocalFile(localPath); } info.uSource = url; info.uDest = currentDest; //kDebug(7007) << "uSource=" << info.uSource << "uDest(1)=" << info.uDest; // Append filename or dirname to destination URL, if allowed if (destinationState == DEST_IS_DIR && // "copy/move as " means 'foo' is the dest for the base srcurl // (passed here during stating) but not its children (during listing) (! (m_asMethod && state == STATE_STATING))) { QString destFileName; KProtocolInfo::FileNameUsedForCopying fnu = KProtocolManager::fileNameUsedForCopying(url); if (hasCustomURL && fnu == KProtocolInfo::FromUrl) { //destFileName = url.fileName(); // Doesn't work for recursive listing // Count the number of prefixes used by the recursive listjob int numberOfSlashes = fileName.count('/'); // don't make this a find()! QString path = url.path(); int pos = 0; for (int n = 0; n < numberOfSlashes + 1; ++n) { pos = path.lastIndexOf('/', pos - 1); if (pos == -1) { // error kWarning(7007) << "kioslave bug: not enough slashes in UDS_URL" << path << "- looking for" << numberOfSlashes << "slashes"; break; } } if (pos >= 0) { destFileName = path.mid(pos + 1); } } else if ( fnu == KProtocolInfo::Name ) { // destination filename taken from UDS_NAME destFileName = fileName; } else { // from display name (with fallback to name) const QString displayName = entry.stringValue(KIO::UDSEntry::UDS_DISPLAY_NAME); destFileName = displayName.isEmpty() ? fileName : displayName; } // Here we _really_ have to add some filename to the dest. // Otherwise, we end up with e.g. dest=..../Desktop/ itself. // (This can happen when dropping a link to a webpage with no path) if (destFileName.isEmpty()) { #if QT_VERSION < QT_VERSION_CHECK(5, 0, 0) destFileName = KIO::encodeFileName(info.uSource.toString()); #else destFileName = KIO::encodeFileName(info.uSource.toDisplayString()); #endif } //kDebug(7007) << " adding destFileName=" << destFileName; info.uDest = QUrlPathInfo::addPathToUrl(info.uDest, destFileName); } //kDebug(7007) << " uDest(2)=" << info.uDest; //kDebug(7007) << " " << info.uSource << "->" << info.uDest; if (info.linkDest.isEmpty() && isDir && m_mode != CopyJob::Link) { // Dir dirs.append(info); // Directories if (m_mode == CopyJob::Move) { dirsToRemove.append(info.uSource); } } else { files.append(info); // Files and any symlinks } } } void CopyJobPrivate::skipSrc(bool isDir) { m_dest = m_globalDest; destinationState = m_globalDestinationState; skip(*m_currentStatSrc, isDir); ++m_currentStatSrc; statCurrentSrc(); } void CopyJobPrivate::statNextSrc() { /* Revert to the global destination, the one that applies to all source urls. * Imagine you copy the items a b and c into /d, but /d/b exists so the user uses "Rename" to put it in /foo/b instead. * d->m_dest is /foo/b for b, but we have to revert to /d for item c and following. */ m_dest = m_globalDest; destinationState = m_globalDestinationState; ++m_currentStatSrc; statCurrentSrc(); } void CopyJobPrivate::statCurrentSrc() { Q_Q(CopyJob); if (m_currentStatSrc != m_srcList.constEnd()) { m_currentSrcURL = (*m_currentStatSrc); m_bURLDirty = true; if (m_mode == CopyJob::Link) { // Skip the "stating the source" stage, we don't need it for linking m_currentDest = m_dest; struct CopyInfo info; info.permissions = -1; info.mtime = (time_t) -1; info.ctime = (time_t) -1; info.size = (KIO::filesize_t)-1; info.uSource = m_currentSrcURL; info.uDest = m_currentDest; // Append filename or dirname to destination URL, if allowed if (destinationState == DEST_IS_DIR && !m_asMethod) { if ( (m_currentSrcURL.scheme() == info.uDest.scheme()) && (m_currentSrcURL.host() == info.uDest.host()) && (m_currentSrcURL.port() == info.uDest.port()) && (m_currentSrcURL.userName() == info.uDest.userName()) && (m_currentSrcURL.password() == info.uDest.password()) ) { // This is the case of creating a real symlink - info.uDest = QUrlPathInfo::addPathToUrl(info.uDest, m_currentSrcURL.fileName()); + info.uDest = QUrlPathInfo::addPathToUrl(info.uDest, QUrlPathInfo(m_currentSrcURL).fileName()); } else { // Different protocols, we'll create a .desktop file // We have to change the extension anyway, so while we're at it, // name the file like the URL - info.uDest = QUrlPathInfo::addPathToUrl(info.uDest, KIO::encodeFileName(m_currentSrcURL.prettyUrl()) + ".desktop"); + info.uDest = QUrlPathInfo::addPathToUrl(info.uDest, KIO::encodeFileName(m_currentSrcURL.toDisplayString()) + ".desktop"); } } files.append( info ); // Files and any symlinks statNextSrc(); // we could use a loop instead of a recursive call :) return; } // Let's see if we can skip stat'ing, for the case where a directory view has the info already const KFileItem cachedItem = KDirLister::cachedItemForUrl(m_currentSrcURL); KIO::UDSEntry entry; if (!cachedItem.isNull()) { entry = cachedItem.entry(); if (destinationState != DEST_DOESNT_EXIST) { // only resolve src if we could resolve dest (#218719) bool dummyIsLocal; m_currentSrcURL = cachedItem.mostLocalUrl(dummyIsLocal); // #183585 } } if (m_mode == CopyJob::Move && ( // Don't go renaming right away if we need a stat() to find out the destination filename KProtocolManager::fileNameUsedForCopying(m_currentSrcURL) == KProtocolInfo::FromUrl || destinationState != DEST_IS_DIR || m_asMethod) ) { // If moving, before going for the full stat+[list+]copy+del thing, try to rename // The logic is pretty similar to FileCopyJobPrivate::slotStart() if ( (m_currentSrcURL.scheme() == m_dest.scheme()) && (m_currentSrcURL.host() == m_dest.host()) && (m_currentSrcURL.port() == m_dest.port()) && - (m_currentSrcURL.user() == m_dest.user()) && - (m_currentSrcURL.pass() == m_dest.pass()) ) + (m_currentSrcURL.userName() == m_dest.userName()) && + (m_currentSrcURL.password() == m_dest.password()) ) { startRenameJob( m_currentSrcURL ); return; } else if ( m_currentSrcURL.isLocalFile() && KProtocolManager::canRenameFromFile( m_dest ) ) { startRenameJob( m_dest ); return; } else if ( m_dest.isLocalFile() && KProtocolManager::canRenameToFile( m_currentSrcURL ) ) { startRenameJob( m_currentSrcURL ); return; } } // if the file system doesn't support deleting, we do not even stat if (m_mode == CopyJob::Move && !KProtocolManager::supportsDeleting(m_currentSrcURL)) { QPointer that = q; - emit q->warning( q, buildErrorString(ERR_CANNOT_DELETE, m_currentSrcURL.prettyUrl()) ); + emit q->warning( q, buildErrorString(ERR_CANNOT_DELETE, m_currentSrcURL.toDisplayString()) ); if (that) statNextSrc(); // we could use a loop instead of a recursive call :) return; } m_bOnlyRenames = false; // Testing for entry.count()>0 here is not good enough; KFileItem inserts // entries for UDS_USER and UDS_GROUP even on initially empty UDSEntries (#192185) if (entry.contains(KIO::UDSEntry::UDS_NAME)) { kDebug(7007) << "fast path! found info about" << m_currentSrcURL << "in KDirLister"; sourceStated(entry, m_currentSrcURL); return; } // Stat the next src url Job * job = KIO::stat( m_currentSrcURL, StatJob::SourceSide, 2, KIO::HideProgressInfo ); //kDebug(7007) << "KIO::stat on" << m_currentSrcURL; state = STATE_STATING; q->addSubjob(job); m_currentDestURL = m_dest; m_bURLDirty = true; } else { // Finished the stat'ing phase // First make sure that the totals were correctly emitted state = STATE_STATING; m_bURLDirty = true; slotReport(); kDebug(7007)<<"Stating finished. To copy:"<aboutToCreate( q, dirs ); if (!files.isEmpty()) emit q->aboutToCreate( q, files ); // Check if we are copying a single file m_bSingleFileCopy = ( files.count() == 1 && dirs.isEmpty() ); // Then start copying things state = STATE_CREATING_DIRS; createNextDir(); } } void CopyJobPrivate::startRenameJob( const QUrl& slave_url ) { Q_Q(CopyJob); // Silence KDirWatch notifications, otherwise performance is horrible if (m_currentSrcURL.isLocalFile()) { - const QString parentDir = m_currentSrcURL.directory(KUrl::ObeyTrailingSlash); + const QString parentDir = QUrlPathInfo(m_currentSrcURL).directory(); if (!m_parentDirs.contains(parentDir)) { KDirWatch::self()->stopDirScan(parentDir); m_parentDirs.insert(parentDir); } } KUrl dest = m_dest; // Append filename or dirname to destination URL, if allowed if ( destinationState == DEST_IS_DIR && !m_asMethod ) - dest = QUrlPathInfo::addPathToUrl(dest, m_currentSrcURL.fileName()); + dest = QUrlPathInfo::addPathToUrl(dest, QUrlPathInfo(m_currentSrcURL).fileName()); m_currentDestURL = dest; kDebug(7007) << m_currentSrcURL << "->" << dest << "trying direct rename first"; state = STATE_RENAMING; struct CopyInfo info; info.permissions = -1; info.mtime = (time_t) -1; info.ctime = (time_t) -1; info.size = (KIO::filesize_t)-1; info.uSource = m_currentSrcURL; info.uDest = dest; QList files; files.append(info); emit q->aboutToCreate( q, files ); KIO_ARGS << m_currentSrcURL << dest << (qint8) false /*no overwrite*/; SimpleJob * newJob = SimpleJobPrivate::newJobNoUi(slave_url, CMD_RENAME, packedArgs); Scheduler::setJobPriority(newJob, 1); q->addSubjob( newJob ); - if ( m_currentSrcURL.directory() != dest.directory() ) // For the user, moving isn't renaming. Only renaming is. + if (QUrlPathInfo(m_currentSrcURL).directory() != QUrlPathInfo(dest).directory()) // For the user, moving isn't renaming. Only renaming is. m_bOnlyRenames = false; } void CopyJobPrivate::startListing( const QUrl & src ) { Q_Q(CopyJob); state = STATE_LISTING; m_bURLDirty = true; ListJob * newjob = listRecursive(src, KIO::HideProgressInfo); newjob->setUnrestricted(true); q->connect(newjob, SIGNAL(entries(KIO::Job*,KIO::UDSEntryList)), SLOT(slotEntries(KIO::Job*,KIO::UDSEntryList))); q->connect(newjob, SIGNAL(subError(KIO::ListJob*,KIO::ListJob*)), SLOT(slotSubError(KIO::ListJob*,KIO::ListJob*))); q->addSubjob( newjob ); } void CopyJobPrivate::skip(const QUrl & sourceUrl, bool isDir) { QUrlPathInfo dir(sourceUrl); if (!isDir) { // Skipping a file: make sure not to delete the parent dir (#208418) dir.setPath(dir.directory()); } while (dirsToRemove.removeAll(dir.url()) > 0) { // Do not rely on rmdir() on the parent directories aborting. // Exclude the parent dirs explicitly. dir.setPath(dir.directory()); } } bool CopyJobPrivate::shouldOverwriteDir( const QString& path ) const { if ( m_bOverwriteAllDirs ) return true; return m_overwriteList.contains(path); } bool CopyJobPrivate::shouldOverwriteFile( const QString& path ) const { if ( m_bOverwriteAllFiles ) return true; return m_overwriteList.contains(path); } bool CopyJobPrivate::shouldSkip( const QString& path ) const { Q_FOREACH(const QString& skipPath, m_skipList) { if ( path.startsWith(skipPath) ) return true; } return false; } void CopyJobPrivate::slotResultCreatingDirs( KJob * job ) { Q_Q(CopyJob); // The dir we are trying to create: QList::Iterator it = dirs.begin(); // Was there an error creating a dir ? if ( job->error() ) { m_conflictError = job->error(); if ( (m_conflictError == ERR_DIR_ALREADY_EXIST) || (m_conflictError == ERR_FILE_ALREADY_EXIST) ) // can't happen? { KUrl oldURL = ((SimpleJob*)job)->url(); // Should we skip automatically ? if ( m_bAutoSkipDirs ) { // We don't want to copy files in this directory, so we put it on the skip list m_skipList.append( oldURL.path( KUrl::AddTrailingSlash ) ); skip(oldURL, true); dirs.erase( it ); // Move on to next dir } else { // Did the user choose to overwrite already? const QString destDir = (*it).uDest.path(); if ( shouldOverwriteDir( destDir ) ) { // overwrite => just skip emit q->copyingDone( q, (*it).uSource, (*it).uDest, (*it).mtime, true /* directory */, false /* renamed */ ); dirs.erase( it ); // Move on to next dir } else { if (m_bAutoRenameDirs) { QUrlPathInfo destInfo((*it).uDest); QString oldPath = destInfo.path(QUrlPathInfo::AppendTrailingSlash); QUrl destDirectory = QUrlPathInfo((*it).uDest).directoryUrl(); QString newName = KIO::RenameDialog::suggestName(destDirectory, destInfo.fileName()); KUrl newUrl((*it).uDest); newUrl.setFileName(newName); emit q->renamed(q, (*it).uDest, newUrl); // for e.g. kpropsdlg // Change the current one and strip the trailing '/' (*it).uDest.setPath(newUrl.path(KUrl::RemoveTrailingSlash)); QString newPath = newUrl.path(KUrl::AddTrailingSlash); // With trailing slash QList::Iterator renamedirit = it; ++renamedirit; // Change the name of subdirectories inside the directory for(; renamedirit != dirs.end() ; ++renamedirit) { QString path = (*renamedirit).uDest.path(); if (path.startsWith(oldPath)) { QString n = path; n.replace(0, oldPath.length(), newPath); kDebug(7007) << "dirs list:" << (*renamedirit).uSource.path() << "was going to be" << path << ", changed into" << n; (*renamedirit).uDest.setPath(n); } } // Change filenames inside the directory QList::Iterator renamefileit = files.begin(); for(; renamefileit != files.end() ; ++renamefileit) { QString path = (*renamefileit).uDest.path(); if (path.startsWith(oldPath)) { QString n = path; n.replace(0, oldPath.length(), newPath); kDebug(7007) << "files list:" << (*renamefileit).uSource.path() << "was going to be" << path << ", changed into" << n; (*renamefileit).uDest.setPath(n); } } if (!dirs.isEmpty()) { emit q->aboutToCreate(q, dirs); } if (!files.isEmpty()) { emit q->aboutToCreate(q, files); } } else { if (!q->isInteractive()) { q->Job::slotResult(job); // will set the error and emit result(this) return; } Q_ASSERT(((SimpleJob*)job)->url() == (*it).uDest); q->removeSubjob(job); assert (!q->hasSubjobs()); // We should have only one job at a time ... // We need to stat the existing dir, to get its last-modification time KUrl existingDest((*it).uDest); SimpleJob * newJob = KIO::stat(existingDest, StatJob::DestinationSide, 2, KIO::HideProgressInfo); Scheduler::setJobPriority(newJob, 1); kDebug(7007) << "KIO::stat for resolving conflict on " << existingDest; state = STATE_CONFLICT_CREATING_DIRS; q->addSubjob(newJob); return; // Don't move to next dir yet ! } } } } else { // Severe error, abort q->Job::slotResult( job ); // will set the error and emit result(this) return; } } else // no error : remove from list, to move on to next dir { //this is required for the undo feature emit q->copyingDone( q, (*it).uSource, (*it).uDest, (*it).mtime, true, false ); m_directoriesCopied.append( *it ); dirs.erase( it ); } m_processedDirs++; //emit processedAmount( this, KJob::Directories, m_processedDirs ); q->removeSubjob( job ); assert( !q->hasSubjobs() ); // We should have only one job at a time ... createNextDir(); } void CopyJobPrivate::slotResultConflictCreatingDirs( KJob * job ) { Q_Q(CopyJob); // We come here after a conflict has been detected and we've stated the existing dir // The dir we were trying to create: QList::Iterator it = dirs.begin(); const UDSEntry entry = ((KIO::StatJob*)job)->statResult(); // Its modification time: const time_t destmtime = (time_t) entry.numberValue( KIO::UDSEntry::UDS_MODIFICATION_TIME, -1 ); const time_t destctime = (time_t) entry.numberValue( KIO::UDSEntry::UDS_CREATION_TIME, -1 ); const KIO::filesize_t destsize = entry.numberValue( KIO::UDSEntry::UDS_SIZE ); const QString linkDest = entry.stringValue( KIO::UDSEntry::UDS_LINK_DEST ); q->removeSubjob( job ); assert ( !q->hasSubjobs() ); // We should have only one job at a time ... const QUrlPathInfo destInfo((*it).uDest); const QUrlPathInfo sourceInfo((*it).uSource); // Always multi and skip (since there are files after that) RenameDialog_Mode mode = (RenameDialog_Mode)( M_MULTI | M_SKIP | M_ISDIR ); // Overwrite only if the existing thing is a dir (no chance with a file) if ( m_conflictError == ERR_DIR_ALREADY_EXIST ) { if( (*it).uSource == (*it).uDest || ((*it).uSource.scheme() == (*it).uDest.scheme() && sourceInfo.path(QUrlPathInfo::StripTrailingSlash) == linkDest)) mode = (RenameDialog_Mode)( mode | M_OVERWRITE_ITSELF); else mode = (RenameDialog_Mode)( mode | M_OVERWRITE ); } const QString existingDest = destInfo.path(); QString newPath; if (m_reportTimer) m_reportTimer->stop(); RenameDialog_Result r = q->ui()->askFileRename( q, i18n("Folder Already Exists"), (*it).uSource, (*it).uDest, mode, newPath, (*it).size, destsize, (*it).ctime, destctime, (*it).mtime, destmtime ); if (m_reportTimer) m_reportTimer->start(REPORT_TIMEOUT); switch ( r ) { case R_CANCEL: q->setError( ERR_USER_CANCELED ); q->emitResult(); return; case R_AUTO_RENAME: m_bAutoRenameDirs = true; // fall through case R_RENAME: { const QString oldPath = destInfo.path(QUrlPathInfo::StripTrailingSlash); QUrlPathInfo newUrl((*it).uDest); newUrl.setPath(newPath); emit q->renamed(q, (*it).uDest, newUrl.url()); // for e.g. kpropsdlg // Change the current one and strip the trailing '/' (*it).uDest.setPath(newUrl.path(QUrlPathInfo::StripTrailingSlash)); newPath = newUrl.path(QUrlPathInfo::AppendTrailingSlash); // With trailing slash QList::Iterator renamedirit = it; ++renamedirit; // Change the name of subdirectories inside the directory for( ; renamedirit != dirs.end() ; ++renamedirit ) { QString path = (*renamedirit).uDest.path(); if ( path.startsWith( oldPath ) ) { QString n = path; n.replace( 0, oldPath.length(), newPath ); kDebug(7007) << "dirs list:" << (*renamedirit).uSource.path() << "was going to be" << path << ", changed into" << n; (*renamedirit).uDest.setPath( n ); } } // Change filenames inside the directory QList::Iterator renamefileit = files.begin(); for( ; renamefileit != files.end() ; ++renamefileit ) { QString path = (*renamefileit).uDest.path(); if ( path.startsWith( oldPath ) ) { QString n = path; n.replace( 0, oldPath.length(), newPath ); kDebug(7007) << "files list:" << (*renamefileit).uSource.path() << "was going to be" << path << ", changed into" << n; (*renamefileit).uDest.setPath( n ); } } if (!dirs.isEmpty()) emit q->aboutToCreate( q, dirs ); if (!files.isEmpty()) emit q->aboutToCreate( q, files ); } break; case R_AUTO_SKIP: m_bAutoSkipDirs = true; // fall through case R_SKIP: m_skipList.append( existingDest ); skip((*it).uSource, true); // Move on to next dir dirs.erase( it ); m_processedDirs++; break; case R_OVERWRITE: m_overwriteList.insert( existingDest ); emit q->copyingDone( q, (*it).uSource, (*it).uDest, (*it).mtime, true /* directory */, false /* renamed */ ); // Move on to next dir dirs.erase( it ); m_processedDirs++; break; case R_OVERWRITE_ALL: m_bOverwriteAllDirs = true; emit q->copyingDone( q, (*it).uSource, (*it).uDest, (*it).mtime, true /* directory */, false /* renamed */ ); // Move on to next dir dirs.erase( it ); m_processedDirs++; break; default: assert( 0 ); } state = STATE_CREATING_DIRS; //emit processedAmount( this, KJob::Directories, m_processedDirs ); createNextDir(); } void CopyJobPrivate::createNextDir() { Q_Q(CopyJob); KUrl udir; if ( !dirs.isEmpty() ) { // Take first dir to create out of list QList::Iterator it = dirs.begin(); // Is this URL on the skip list or the overwrite list ? while( it != dirs.end() && udir.isEmpty() ) { const QString dir = (*it).uDest.path(); if ( shouldSkip( dir ) ) { dirs.erase( it ); it = dirs.begin(); } else udir = (*it).uDest; } } if ( !udir.isEmpty() ) // any dir to create, finally ? { // Create the directory - with default permissions so that we can put files into it // TODO : change permissions once all is finished; but for stuff coming from CDROM it sucks... KIO::SimpleJob *newjob = KIO::mkdir( udir, -1 ); Scheduler::setJobPriority(newjob, 1); if (shouldOverwriteFile(udir.path())) { // if we are overwriting an existing file or symlink newjob->addMetaData("overwrite", "true"); } m_currentDestURL = udir; m_bURLDirty = true; q->addSubjob(newjob); return; } else // we have finished creating dirs { q->setProcessedAmount( KJob::Directories, m_processedDirs ); // make sure final number appears if (m_mode == CopyJob::Move) { // Now we know which dirs hold the files we're going to delete. // To speed things up and prevent double-notification, we disable KDirWatch // on those dirs temporarily (using KDirWatch::self, that's the instanced // used by e.g. kdirlister). for ( QSet::const_iterator it = m_parentDirs.constBegin() ; it != m_parentDirs.constEnd() ; ++it ) KDirWatch::self()->stopDirScan( *it ); } state = STATE_COPYING_FILES; m_processedFiles++; // Ralf wants it to start at 1, not 0 copyNextFile(); } } void CopyJobPrivate::slotResultCopyingFiles( KJob * job ) { Q_Q(CopyJob); // The file we were trying to copy: QList::Iterator it = files.begin(); if ( job->error() ) { // Should we skip automatically ? if ( m_bAutoSkipFiles ) { skip((*it).uSource, false); m_fileProcessedSize = (*it).size; files.erase( it ); // Move on to next file } else { m_conflictError = job->error(); // save for later // Existing dest ? if ( ( m_conflictError == ERR_FILE_ALREADY_EXIST ) || ( m_conflictError == ERR_DIR_ALREADY_EXIST ) || ( m_conflictError == ERR_IDENTICAL_FILES ) ) { if (m_bAutoRenameFiles) { QUrl destDirectory = QUrlPathInfo((*it).uDest).directoryUrl(); QUrlPathInfo destInfo((*it).uDest); const QString newName = KIO::RenameDialog::suggestName(destDirectory, destInfo.fileName()); destInfo.setFileName(newName); emit q->renamed(q, (*it).uDest, destInfo.url()); // for e.g. kpropsdlg (*it).uDest = destInfo.url(); QList files; files.append(*it); emit q->aboutToCreate(q, files); } else { if ( !q->isInteractive() ) { q->Job::slotResult( job ); // will set the error and emit result(this) return; } q->removeSubjob(job); assert (!q->hasSubjobs()); // We need to stat the existing file, to get its last-modification time KUrl existingFile((*it).uDest); SimpleJob * newJob = KIO::stat(existingFile, StatJob::DestinationSide, 2, KIO::HideProgressInfo); Scheduler::setJobPriority(newJob, 1); kDebug(7007) << "KIO::stat for resolving conflict on " << existingFile; state = STATE_CONFLICT_COPYING_FILES; q->addSubjob(newJob); return; // Don't move to next file yet ! } } else { if ( m_bCurrentOperationIsLink && qobject_cast( job ) ) { // Very special case, see a few lines below // We are deleting the source of a symlink we successfully moved... ignore error m_fileProcessedSize = (*it).size; files.erase( it ); } else { if ( !q->isInteractive() ) { q->Job::slotResult( job ); // will set the error and emit result(this) return; } // Go directly to the conflict resolution, there is nothing to stat slotResultConflictCopyingFiles( job ); return; } } } } else // no error { // Special case for moving links. That operation needs two jobs, unlike others. if ( m_bCurrentOperationIsLink && m_mode == CopyJob::Move && !qobject_cast( job ) // Deleting source not already done ) { q->removeSubjob( job ); assert ( !q->hasSubjobs() ); // The only problem with this trick is that the error handling for this del operation // is not going to be right... see 'Very special case' above. KIO::Job * newjob = KIO::del( (*it).uSource, HideProgressInfo ); q->addSubjob( newjob ); return; // Don't move to next file yet ! } if ( m_bCurrentOperationIsLink ) { QString target = ( m_mode == CopyJob::Link ? (*it).uSource.path() : (*it).linkDest ); //required for the undo feature emit q->copyingLinkDone( q, (*it).uSource, target, (*it).uDest ); } else { //required for the undo feature emit q->copyingDone( q, (*it).uSource, (*it).uDest, (*it).mtime, false, false ); if (m_mode == CopyJob::Move) { org::kde::KDirNotify::emitFileMoved((*it).uSource, (*it).uDest); } m_successSrcList.append((*it).uSource); if (m_freeSpace != (KIO::filesize_t)-1 && (*it).size != (KIO::filesize_t)-1) { m_freeSpace -= (*it).size; } } // remove from list, to move on to next file files.erase( it ); } m_processedFiles++; // clear processed size for last file and add it to overall processed size m_processedSize += m_fileProcessedSize; m_fileProcessedSize = 0; //kDebug(7007) << files.count() << "files remaining"; // Merge metadata from subjob KIO::Job* kiojob = dynamic_cast(job); Q_ASSERT(kiojob); m_incomingMetaData += kiojob->metaData(); q->removeSubjob( job ); assert( !q->hasSubjobs() ); // We should have only one job at a time ... copyNextFile(); } void CopyJobPrivate::slotResultConflictCopyingFiles( KJob * job ) { Q_Q(CopyJob); // We come here after a conflict has been detected and we've stated the existing file // The file we were trying to create: QList::Iterator it = files.begin(); RenameDialog_Result res; QString newPath; if (m_reportTimer) m_reportTimer->stop(); if ( ( m_conflictError == ERR_FILE_ALREADY_EXIST ) || ( m_conflictError == ERR_DIR_ALREADY_EXIST ) || ( m_conflictError == ERR_IDENTICAL_FILES ) ) { // Its modification time: const UDSEntry entry = static_cast(job)->statResult(); const time_t destmtime = (time_t) entry.numberValue( KIO::UDSEntry::UDS_MODIFICATION_TIME, -1 ); const time_t destctime = (time_t) entry.numberValue( KIO::UDSEntry::UDS_CREATION_TIME, -1 ); const KIO::filesize_t destsize = entry.numberValue( KIO::UDSEntry::UDS_SIZE ); const QString linkDest = entry.stringValue( KIO::UDSEntry::UDS_LINK_DEST ); // Offer overwrite only if the existing thing is a file // If src==dest, use "overwrite-itself" RenameDialog_Mode mode; bool isDir = true; QUrlPathInfo sourceInfo((*it).uSource); if( m_conflictError == ERR_DIR_ALREADY_EXIST ) { mode = M_ISDIR; } else { if ( (*it).uSource == (*it).uDest || ((*it).uSource.scheme() == (*it).uDest.scheme() && sourceInfo.path(QUrlPathInfo::StripTrailingSlash) == linkDest)) mode = M_OVERWRITE_ITSELF; else mode = M_OVERWRITE; isDir = false; } if ( !m_bSingleFileCopy ) mode = (RenameDialog_Mode) ( mode | M_MULTI | M_SKIP ); res = q->ui()->askFileRename( q, !isDir ? i18n("File Already Exists") : i18n("Already Exists as Folder"), (*it).uSource, (*it).uDest, mode, newPath, (*it).size, destsize, (*it).ctime, destctime, (*it).mtime, destmtime ); } else { if ( job->error() == ERR_USER_CANCELED ) res = R_CANCEL; else if ( !q->isInteractive() ) { q->Job::slotResult( job ); // will set the error and emit result(this) return; } else { SkipDialog_Result skipResult = q->ui()->askSkip( q, files.count() > 1, job->errorString() ); // Convert the return code from SkipDialog into a RenameDialog code res = ( skipResult == S_SKIP ) ? R_SKIP : ( skipResult == S_AUTO_SKIP ) ? R_AUTO_SKIP : R_CANCEL; } } if (m_reportTimer) m_reportTimer->start(REPORT_TIMEOUT); q->removeSubjob( job ); assert ( !q->hasSubjobs() ); switch ( res ) { case R_CANCEL: q->setError( ERR_USER_CANCELED ); q->emitResult(); return; case R_AUTO_RENAME: m_bAutoRenameFiles = true; // fall through case R_RENAME: { QUrl newUrl( (*it).uDest ); newUrl.setPath( newPath ); emit q->renamed( q, (*it).uDest, newUrl ); // for e.g. kpropsdlg (*it).uDest = newUrl; QList files; files.append(*it); emit q->aboutToCreate( q, files ); } break; case R_AUTO_SKIP: m_bAutoSkipFiles = true; // fall through case R_SKIP: // Move on to next file skip((*it).uSource, false); m_processedSize += (*it).size; files.erase( it ); m_processedFiles++; break; case R_OVERWRITE_ALL: m_bOverwriteAllFiles = true; break; case R_OVERWRITE: // Add to overwrite list, so that copyNextFile knows to overwrite m_overwriteList.insert( (*it).uDest.path() ); break; default: assert( 0 ); } state = STATE_COPYING_FILES; copyNextFile(); } KIO::Job* CopyJobPrivate::linkNextFile( const QUrl& uSource, const QUrl& uDest, JobFlags flags ) { //kDebug(7007) << "Linking"; if ( (uSource.scheme() == uDest.scheme()) && (uSource.host() == uDest.host()) && (uSource.port() == uDest.port()) && (uSource.userName() == uDest.userName()) && (uSource.password() == uDest.password()) ) { // This is the case of creating a real symlink KIO::SimpleJob *newJob = KIO::symlink( uSource.path(), uDest, flags|HideProgressInfo /*no GUI*/ ); Scheduler::setJobPriority(newJob, 1); //kDebug(7007) << "Linking target=" << uSource.path() << "link=" << uDest; //emit linking( this, uSource.path(), uDest ); m_bCurrentOperationIsLink = true; m_currentSrcURL=uSource; m_currentDestURL=uDest; m_bURLDirty = true; //Observer::self()->slotCopying( this, uSource, uDest ); // should be slotLinking perhaps return newJob; } else { Q_Q(CopyJob); //kDebug(7007) << "Linking URL=" << uSource << "link=" << uDest; if ( uDest.isLocalFile() ) { // if the source is a devices url, handle it a littlebit special QString path = uDest.toLocalFile(); //kDebug(7007) << "path=" << path; QFile f( path ); if ( f.open( QIODevice::ReadWrite ) ) { f.close(); KDesktopFile desktopFile( path ); KConfigGroup config = desktopFile.desktopGroup(); KUrl url = uSource; url.setPass( "" ); config.writePathEntry( "URL", url.url() ); config.writeEntry( "Name", url.url() ); config.writeEntry( "Type", QString::fromLatin1("Link") ); QString protocol = uSource.scheme(); if ( protocol == QLatin1String("ftp") ) config.writeEntry( "Icon", QString::fromLatin1("folder-remote") ); else if ( protocol == QLatin1String("http") ) config.writeEntry( "Icon", QString::fromLatin1("text-html") ); else if ( protocol == QLatin1String("info") ) config.writeEntry( "Icon", QString::fromLatin1("text-x-texinfo") ); else if ( protocol == QLatin1String("mailto") ) // sven: config.writeEntry( "Icon", QString::fromLatin1("internet-mail") ); // added mailto: support else config.writeEntry( "Icon", QString::fromLatin1("unknown") ); config.sync(); files.erase( files.begin() ); // done with this one, move on m_processedFiles++; //emit processedAmount( this, KJob::Files, m_processedFiles ); copyNextFile(); return 0; } else { kDebug(7007) << "ERR_CANNOT_OPEN_FOR_WRITING"; q->setError( ERR_CANNOT_OPEN_FOR_WRITING ); q->setErrorText( uDest.toLocalFile() ); q->emitResult(); return 0; } } else { // Todo: not show "link" on remote dirs if the src urls are not from the same protocol+host+... q->setError( ERR_CANNOT_SYMLINK ); q->setErrorText( uDest.toDisplayString() ); q->emitResult(); return 0; } } } void CopyJobPrivate::copyNextFile() { Q_Q(CopyJob); bool bCopyFile = false; //kDebug(7007); // Take the first file in the list QList::Iterator it = files.begin(); // Is this URL on the skip list ? while (it != files.end() && !bCopyFile) { const QString destFile = (*it).uDest.path(); bCopyFile = !shouldSkip( destFile ); if ( !bCopyFile ) { files.erase( it ); it = files.begin(); } } if (bCopyFile) // any file to create, finally ? { //kDebug()<<"preparing to copy"<<(*it).uSource<<(*it).size<setError( ERR_DISK_FULL ); q->emitResult(); return; } //TODO check if dst mount is msdos and (*it).size exceeds it's limits } const KUrl& uSource = (*it).uSource; const KUrl& uDest = (*it).uDest; // Do we set overwrite ? bool bOverwrite; const QString destFile = uDest.path(); // kDebug(7007) << "copying" << destFile; if ( uDest == uSource ) bOverwrite = false; else bOverwrite = shouldOverwriteFile( destFile ); m_bCurrentOperationIsLink = false; KIO::Job * newjob = 0; if ( m_mode == CopyJob::Link ) { // User requested that a symlink be made const JobFlags flags = bOverwrite ? Overwrite : DefaultFlags; newjob = linkNextFile(uSource, uDest, flags); if (!newjob) return; } else if ( !(*it).linkDest.isEmpty() && (uSource.scheme() == uDest.scheme()) && (uSource.host() == uDest.host()) && (uSource.port() == uDest.port()) && (uSource.user() == uDest.user()) && (uSource.pass() == uDest.pass())) // Copying a symlink - only on the same protocol/host/etc. (#5601, downloading an FTP file through its link), { const JobFlags flags = bOverwrite ? Overwrite : DefaultFlags; KIO::SimpleJob *newJob = KIO::symlink( (*it).linkDest, uDest, flags | HideProgressInfo /*no GUI*/ ); Scheduler::setJobPriority(newJob, 1); newjob = newJob; //kDebug(7007) << "Linking target=" << (*it).linkDest << "link=" << uDest; m_currentSrcURL = KUrl( (*it).linkDest ); m_currentDestURL = uDest; m_bURLDirty = true; //emit linking( this, (*it).linkDest, uDest ); //Observer::self()->slotCopying( this, m_currentSrcURL, uDest ); // should be slotLinking perhaps m_bCurrentOperationIsLink = true; // NOTE: if we are moving stuff, the deletion of the source will be done in slotResultCopyingFiles } else if (m_mode == CopyJob::Move) // Moving a file { JobFlags flags = bOverwrite ? Overwrite : DefaultFlags; KIO::FileCopyJob * moveJob = KIO::file_move( uSource, uDest, (*it).permissions, flags | HideProgressInfo/*no GUI*/ ); moveJob->setSourceSize( (*it).size ); if ((*it).mtime != -1) { moveJob->setModificationTime( QDateTime::fromTime_t( (*it).mtime ) ); // #55804 } newjob = moveJob; //kDebug(7007) << "Moving" << uSource << "to" << uDest; //emit moving( this, uSource, uDest ); m_currentSrcURL=uSource; m_currentDestURL=uDest; m_bURLDirty = true; //Observer::self()->slotMoving( this, uSource, uDest ); } else // Copying a file { // If source isn't local and target is local, we ignore the original permissions // Otherwise, files downloaded from HTTP end up with -r--r--r-- bool remoteSource = !KProtocolManager::supportsListing(uSource); int permissions = (*it).permissions; if ( m_defaultPermissions || ( remoteSource && uDest.isLocalFile() ) ) permissions = -1; JobFlags flags = bOverwrite ? Overwrite : DefaultFlags; KIO::FileCopyJob * copyJob = KIO::file_copy( uSource, uDest, permissions, flags | HideProgressInfo/*no GUI*/ ); copyJob->setParentJob( q ); // in case of rename dialog copyJob->setSourceSize( (*it).size ); if ((*it).mtime != -1) { copyJob->setModificationTime( QDateTime::fromTime_t( (*it).mtime ) ); } newjob = copyJob; //kDebug(7007) << "Copying" << uSource << "to" << uDest; m_currentSrcURL=uSource; m_currentDestURL=uDest; m_bURLDirty = true; } q->addSubjob(newjob); q->connect( newjob, SIGNAL(processedSize(KJob*,qulonglong)), SLOT(slotProcessedSize(KJob*,qulonglong)) ); q->connect( newjob, SIGNAL(totalSize(KJob*,qulonglong)), SLOT(slotTotalSize(KJob*,qulonglong)) ); } else { // We're done //kDebug(7007) << "copyNextFile finished"; deleteNextDir(); } } void CopyJobPrivate::deleteNextDir() { Q_Q(CopyJob); if ( m_mode == CopyJob::Move && !dirsToRemove.isEmpty() ) // some dirs to delete ? { state = STATE_DELETING_DIRS; m_bURLDirty = true; // Take first dir to delete out of list - last ones first ! QList::Iterator it = --dirsToRemove.end(); SimpleJob *job = KIO::rmdir( *it ); Scheduler::setJobPriority(job, 1); dirsToRemove.erase(it); q->addSubjob( job ); } else { // This step is done, move on state = STATE_SETTING_DIR_ATTRIBUTES; m_directoriesCopiedIterator = m_directoriesCopied.constBegin(); setNextDirAttribute(); } } void CopyJobPrivate::setNextDirAttribute() { Q_Q(CopyJob); while (m_directoriesCopiedIterator != m_directoriesCopied.constEnd() && (*m_directoriesCopiedIterator).mtime == -1) { ++m_directoriesCopiedIterator; } if ( m_directoriesCopiedIterator != m_directoriesCopied.constEnd() ) { const QUrl url = (*m_directoriesCopiedIterator).uDest; const time_t mtime = (*m_directoriesCopiedIterator).mtime; const QDateTime dt = QDateTime::fromTime_t(mtime); ++m_directoriesCopiedIterator; KIO::SimpleJob *job = KIO::setModificationTime( url, dt ); Scheduler::setJobPriority(job, 1); q->addSubjob( job ); #if 0 // ifdef Q_OS_UNIX // TODO: can be removed now. Or reintroduced as a fast path for local files // if launching even more jobs as done above is a performance problem. // QLinkedList::const_iterator it = m_directoriesCopied.constBegin(); for ( ; it != m_directoriesCopied.constEnd() ; ++it ) { const QUrl& url = (*it).uDest; if ( url.isLocalFile() && (*it).mtime != (time_t)-1 ) { KDE_struct_stat statbuf; if (KDE::lstat(url.path(), &statbuf) == 0) { struct utimbuf utbuf; utbuf.actime = statbuf.st_atime; // access time, unchanged utbuf.modtime = (*it).mtime; // modification time utime( path, &utbuf ); } } } m_directoriesCopied.clear(); // but then we need to jump to the else part below. Maybe with a recursive call? #endif } else { if (m_reportTimer) m_reportTimer->stop(); --m_processedFiles; // undo the "start at 1" hack slotReport(); // display final numbers, important if progress dialog stays up q->emitResult(); } } void CopyJob::emitResult() { Q_D(CopyJob); // Before we go, tell the world about the changes that were made. // Even if some error made us abort midway, we might still have done // part of the job so we better update the views! (#118583) if (!d->m_bOnlyRenames) { QUrl url(d->m_globalDest); if (d->m_globalDestinationState != DEST_IS_DIR || d->m_asMethod) url = QUrlPathInfo(url).directoryUrl(); //kDebug(7007) << "KDirNotify'ing FilesAdded" << url; org::kde::KDirNotify::emitFilesAdded(url); if (d->m_mode == CopyJob::Move && !d->m_successSrcList.isEmpty()) { kDebug(7007) << "KDirNotify'ing FilesRemoved" << d->m_successSrcList; org::kde::KDirNotify::emitFilesRemoved(d->m_successSrcList); } // Re-enable watching on the dirs that held the deleted files if (d->m_mode == CopyJob::Move) { for (QSet::const_iterator it = d->m_parentDirs.constBegin() ; it != d->m_parentDirs.constEnd() ; ++it) KDirWatch::self()->restartDirScan( *it ); } } Job::emitResult(); } void CopyJobPrivate::slotProcessedSize( KJob*, qulonglong data_size ) { Q_Q(CopyJob); //kDebug(7007) << data_size; m_fileProcessedSize = data_size; q->setProcessedAmount(KJob::Bytes, m_processedSize + m_fileProcessedSize); if ( m_processedSize + m_fileProcessedSize > m_totalSize ) { // Example: download any attachment from bugs.kde.org m_totalSize = m_processedSize + m_fileProcessedSize; //kDebug(7007) << "Adjusting m_totalSize to" << m_totalSize; q->setTotalAmount(KJob::Bytes, m_totalSize); // safety } //kDebug(7007) << "emit processedSize" << (unsigned long) (m_processedSize + m_fileProcessedSize); q->setProcessedAmount(KJob::Bytes, m_processedSize + m_fileProcessedSize); } void CopyJobPrivate::slotTotalSize( KJob*, qulonglong size ) { Q_Q(CopyJob); //kDebug(7007) << size; // Special case for copying a single file // This is because some protocols don't implement stat properly // (e.g. HTTP), and don't give us a size in some cases (redirection) // so we'd rather rely on the size given for the transfer if ( m_bSingleFileCopy && size != m_totalSize) { //kDebug(7007) << "slotTotalSize: updating totalsize to" << size; m_totalSize = size; q->setTotalAmount(KJob::Bytes, size); } } void CopyJobPrivate::slotResultDeletingDirs( KJob * job ) { Q_Q(CopyJob); if (job->error()) { // Couldn't remove directory. Well, perhaps it's not empty // because the user pressed Skip for a given file in it. // Let's not display "Could not remove dir ..." for each of those dir ! } else { m_successSrcList.append(static_cast(job)->url()); } q->removeSubjob( job ); assert( !q->hasSubjobs() ); deleteNextDir(); } void CopyJobPrivate::slotResultSettingDirAttributes( KJob * job ) { Q_Q(CopyJob); if (job->error()) { // Couldn't set directory attributes. Ignore the error, it can happen // with inferior file systems like VFAT. // Let's not display warnings for each dir like "cp -a" does. } q->removeSubjob( job ); assert( !q->hasSubjobs() ); setNextDirAttribute(); } // We were trying to do a direct renaming, before even stat'ing void CopyJobPrivate::slotResultRenaming( KJob* job ) { Q_Q(CopyJob); int err = job->error(); const QString errText = job->errorText(); // Merge metadata from subjob KIO::Job* kiojob = dynamic_cast(job); Q_ASSERT(kiojob); m_incomingMetaData += kiojob->metaData(); q->removeSubjob( job ); assert ( !q->hasSubjobs() ); // Determine dest again KUrl dest = m_dest; if ( destinationState == DEST_IS_DIR && !m_asMethod ) - dest = QUrlPathInfo::addPathToUrl(dest, m_currentSrcURL.fileName()); + dest = QUrlPathInfo::addPathToUrl(dest, QUrlPathInfo(m_currentSrcURL).fileName()); if ( err ) { // Direct renaming didn't work. Try renaming to a temp name, // this can help e.g. when renaming 'a' to 'A' on a VFAT partition. // In that case it's the _same_ dir, we don't want to copy+del (data loss!) - if ( m_currentSrcURL.isLocalFile() && m_currentSrcURL.url(KUrl::RemoveTrailingSlash) != dest.url(KUrl::RemoveTrailingSlash) && - m_currentSrcURL.url(KUrl::RemoveTrailingSlash).toLower() == dest.url(KUrl::RemoveTrailingSlash).toLower() && + if ( m_currentSrcURL.isLocalFile() && dest.isLocalFile() && + !QUrlPathInfo(m_currentSrcURL).equals(dest, QUrlPathInfo::CompareWithoutTrailingSlash) && + QUrlPathInfo(m_currentSrcURL).equals(dest, QUrlPathInfo::CompareWithoutTrailingSlash | QUrlPathInfo::ComparePathsCaseInsensitively) && ( err == ERR_FILE_ALREADY_EXIST || err == ERR_DIR_ALREADY_EXIST || err == ERR_IDENTICAL_FILES ) ) { kDebug(7007) << "Couldn't rename directly, dest already exists. Detected special case of lower/uppercase renaming in same dir, try with 2 rename calls"; const QString _src( m_currentSrcURL.toLocalFile() ); const QString _dest( dest.toLocalFile() ); - const QString _tmpPrefix = m_currentSrcURL.directory(KUrl::ObeyTrailingSlash|KUrl::AppendTrailingSlash); - QTemporaryFile tmpFile(_tmpPrefix + "kio_XXXXXX"); + const QString srcDir = QFileInfo(_src).absolutePath(); + QTemporaryFile tmpFile(srcDir + "kio_XXXXXX"); const bool openOk = tmpFile.open(); if (!openOk) { - kWarning(7007) << "Couldn't open temp file in" << _tmpPrefix; + kWarning(7007) << "Couldn't open temp file in" << srcDir; } else { const QString _tmp( tmpFile.fileName() ); tmpFile.close(); tmpFile.remove(); kDebug(7007) << "QTemporaryFile using" << _tmp << "as intermediary"; if (KDE::rename( _src, _tmp ) == 0) { //kDebug(7007) << "Renaming" << _src << "to" << _tmp << "succeeded"; if (!QFile::exists( _dest ) && KDE::rename(_tmp, _dest) == 0) { err = 0; org::kde::KDirNotify::emitFileRenamed(m_currentSrcURL, dest); } else { kDebug(7007) << "Didn't manage to rename" << _tmp << "to" << _dest << ", reverting"; // Revert back to original name! if (KDE::rename( _tmp, _src ) != 0) { kError(7007) << "Couldn't rename" << _tmp << "back to" << _src << '!'; // Severe error, abort q->Job::slotResult(job); // will set the error and emit result(this) return; } } } else { kDebug(7007) << "mv" << _src << _tmp << "failed:" << strerror(errno); } } } } if ( err ) { // This code is similar to CopyJobPrivate::slotResultConflictCopyingFiles // but here it's about the base src url being moved/renamed // (m_currentSrcURL) and its dest (m_dest), not about a single file. // It also means we already stated the dest, here. // On the other hand we haven't stated the src yet (we skipped doing it // to save time, since it's not necessary to rename directly!)... // Existing dest? if ( err == ERR_DIR_ALREADY_EXIST || err == ERR_FILE_ALREADY_EXIST || err == ERR_IDENTICAL_FILES ) { // Should we skip automatically ? bool isDir = (err == ERR_DIR_ALREADY_EXIST); // ## technically, isDir means "source is dir", not "dest is dir" ####### if ((isDir && m_bAutoSkipDirs) || (!isDir && m_bAutoSkipFiles)) { // Move on to next source url skipSrc(isDir); return; } else if ((isDir && m_bOverwriteAllDirs) || (!isDir && m_bOverwriteAllFiles)) { ; // nothing to do, stat+copy+del will overwrite } else if ((isDir && m_bAutoRenameDirs) || (!isDir && m_bAutoRenameFiles)) { QUrl destDirectory = QUrlPathInfo(m_currentDestURL).directoryUrl(); // m_currendDestURL includes filename - const QString newName = KIO::RenameDialog::suggestName(destDirectory, m_currentDestURL.fileName()); + const QString newName = KIO::RenameDialog::suggestName(destDirectory, QUrlPathInfo(m_currentDestURL).fileName()); m_dest.setPath(m_currentDestURL.path()); - m_dest.setFileName(newName); + QUrlPathInfo destInfo(m_dest); + destInfo.setFileName(newName); + m_dest = destInfo.url(); KIO::Job* job = KIO::stat(m_dest, StatJob::DestinationSide, 2, KIO::HideProgressInfo); state = STATE_STATING; destinationState = DEST_NOT_STATED; q->addSubjob(job); return; } else if ( q->isInteractive() ) { QString newPath; // we lack mtime info for both the src (not stated) // and the dest (stated but this info wasn't stored) // Let's do it for local files, at least KIO::filesize_t sizeSrc = (KIO::filesize_t) -1; KIO::filesize_t sizeDest = (KIO::filesize_t) -1; time_t ctimeSrc = (time_t) -1; time_t ctimeDest = (time_t) -1; time_t mtimeSrc = (time_t) -1; time_t mtimeDest = (time_t) -1; bool destIsDir = err == ERR_DIR_ALREADY_EXIST; // ## TODO we need to stat the source using KIO::stat // so that this code is properly network-transparent. KDE_struct_stat stat_buf; if ( m_currentSrcURL.isLocalFile() && KDE::stat(m_currentSrcURL.toLocalFile(), &stat_buf) == 0 ) { sizeSrc = stat_buf.st_size; ctimeSrc = stat_buf.st_ctime; mtimeSrc = stat_buf.st_mtime; isDir = S_ISDIR(stat_buf.st_mode); } if ( dest.isLocalFile() && KDE::stat(dest.toLocalFile(), &stat_buf) == 0 ) { sizeDest = stat_buf.st_size; ctimeDest = stat_buf.st_ctime; mtimeDest = stat_buf.st_mtime; destIsDir = S_ISDIR(stat_buf.st_mode); } // If src==dest, use "overwrite-itself" RenameDialog_Mode mode = ( m_currentSrcURL == dest ) ? M_OVERWRITE_ITSELF : M_OVERWRITE; if (!isDir && destIsDir) { // We can't overwrite a dir with a file. mode = (RenameDialog_Mode) 0; } if ( m_srcList.count() > 1 ) mode = (RenameDialog_Mode) ( mode | M_MULTI | M_SKIP ); if (destIsDir) mode = (RenameDialog_Mode) ( mode | M_ISDIR ); if (m_reportTimer) m_reportTimer->stop(); RenameDialog_Result r = q->ui()->askFileRename( q, err != ERR_DIR_ALREADY_EXIST ? i18n("File Already Exists") : i18n("Already Exists as Folder"), m_currentSrcURL, dest, mode, newPath, sizeSrc, sizeDest, ctimeSrc, ctimeDest, mtimeSrc, mtimeDest ); if (m_reportTimer) m_reportTimer->start(REPORT_TIMEOUT); switch ( r ) { case R_CANCEL: { q->setError( ERR_USER_CANCELED ); q->emitResult(); return; } case R_AUTO_RENAME: if (isDir) { m_bAutoRenameDirs = true; } else { m_bAutoRenameFiles = true; } // fall through case R_RENAME: { // Set m_dest to the chosen destination // This is only for this src url; the next one will revert to m_globalDest m_dest.setPath( newPath ); KIO::Job* job = KIO::stat( m_dest, StatJob::DestinationSide, 2, KIO::HideProgressInfo ); state = STATE_STATING; destinationState = DEST_NOT_STATED; q->addSubjob(job); return; } case R_AUTO_SKIP: if (isDir) m_bAutoSkipDirs = true; else m_bAutoSkipFiles = true; // fall through case R_SKIP: // Move on to next url skipSrc(isDir); return; case R_OVERWRITE_ALL: if (destIsDir) m_bOverwriteAllDirs = true; else m_bOverwriteAllFiles = true; break; case R_OVERWRITE: // Add to overwrite list // Note that we add dest, not m_dest. // This ensures that when moving several urls into a dir (m_dest), // we only overwrite for the current one, not for all. // When renaming a single file (m_asMethod), it makes no difference. kDebug(7007) << "adding to overwrite list: " << dest.path(); m_overwriteList.insert( dest.path() ); break; default: //assert( 0 ); break; } } else if ( err != KIO::ERR_UNSUPPORTED_ACTION ) { // Dest already exists, and job is not interactive -> abort with error q->setError( err ); q->setErrorText( errText ); q->emitResult(); return; } } else if ( err != KIO::ERR_UNSUPPORTED_ACTION ) { kDebug(7007) << "Couldn't rename" << m_currentSrcURL << "to" << dest << ", aborting"; q->setError( err ); q->setErrorText( errText ); q->emitResult(); return; } kDebug(7007) << "Couldn't rename" << m_currentSrcURL << "to" << dest << ", reverting to normal way, starting with stat"; //kDebug(7007) << "KIO::stat on" << m_currentSrcURL; KIO::Job* job = KIO::stat( m_currentSrcURL, StatJob::SourceSide, 2, KIO::HideProgressInfo ); state = STATE_STATING; q->addSubjob(job); m_bOnlyRenames = false; } else { kDebug(7007) << "Renaming succeeded, move on"; ++m_processedFiles; emit q->copyingDone( q, *m_currentStatSrc, dest, -1 /*mtime unknown, and not needed*/, true, true ); m_successSrcList.append(*m_currentStatSrc); statNextSrc(); } } void CopyJob::slotResult( KJob *job ) { Q_D(CopyJob); //kDebug(7007) << "d->state=" << (int) d->state; // In each case, what we have to do is : // 1 - check for errors and treat them // 2 - removeSubjob(job); // 3 - decide what to do next switch ( d->state ) { case STATE_STATING: // We were trying to stat a src url or the dest d->slotResultStating( job ); break; case STATE_RENAMING: // We were trying to do a direct renaming, before even stat'ing { d->slotResultRenaming( job ); break; } case STATE_LISTING: // recursive listing finished //kDebug(7007) << "totalSize:" << (unsigned int) d->m_totalSize << "files:" << d->files.count() << "d->dirs:" << d->dirs.count(); // Was there an error ? if (job->error()) { Job::slotResult( job ); // will set the error and emit result(this) return; } removeSubjob( job ); assert ( !hasSubjobs() ); d->statNextSrc(); break; case STATE_CREATING_DIRS: d->slotResultCreatingDirs( job ); break; case STATE_CONFLICT_CREATING_DIRS: d->slotResultConflictCreatingDirs( job ); break; case STATE_COPYING_FILES: d->slotResultCopyingFiles( job ); break; case STATE_CONFLICT_COPYING_FILES: d->slotResultConflictCopyingFiles( job ); break; case STATE_DELETING_DIRS: d->slotResultDeletingDirs( job ); break; case STATE_SETTING_DIR_ATTRIBUTES: d->slotResultSettingDirAttributes( job ); break; default: assert( 0 ); } } void KIO::CopyJob::setDefaultPermissions( bool b ) { d_func()->m_defaultPermissions = b; } KIO::CopyJob::CopyMode KIO::CopyJob::operationMode() const { return d_func()->m_mode; } void KIO::CopyJob::setAutoSkip(bool autoSkip) { d_func()->m_bAutoSkipFiles = autoSkip; d_func()->m_bAutoSkipDirs = autoSkip; } void KIO::CopyJob::setAutoRename(bool autoRename) { d_func()->m_bAutoRenameFiles = autoRename; d_func()->m_bAutoRenameDirs = autoRename; } void KIO::CopyJob::setWriteIntoExistingDirectories(bool overwriteAll) // #65926 { d_func()->m_bOverwriteAllDirs = overwriteAll; } CopyJob *KIO::copy(const QUrl& src, const QUrl& dest, JobFlags flags) { //kDebug(7007) << "src=" << src << "dest=" << dest; QList srcList; srcList.append( src ); return CopyJobPrivate::newJob(srcList, dest, CopyJob::Copy, false, flags); } CopyJob *KIO::copyAs(const QUrl& src, const QUrl& dest, JobFlags flags) { //kDebug(7007) << "src=" << src << "dest=" << dest; QList srcList; srcList.append( src ); return CopyJobPrivate::newJob(srcList, dest, CopyJob::Copy, true, flags); } CopyJob *KIO::copy( const QList& src, const QUrl& dest, JobFlags flags ) { //kDebug(7007) << src << dest; return CopyJobPrivate::newJob(src, dest, CopyJob::Copy, false, flags); } CopyJob *KIO::move(const QUrl& src, const QUrl& dest, JobFlags flags) { //kDebug(7007) << src << dest; QList srcList; srcList.append( src ); return CopyJobPrivate::newJob(srcList, dest, CopyJob::Move, false, flags); } CopyJob *KIO::moveAs(const QUrl& src, const QUrl& dest, JobFlags flags) { //kDebug(7007) << src << dest; QList srcList; srcList.append( src ); return CopyJobPrivate::newJob(srcList, dest, CopyJob::Move, true, flags); } CopyJob *KIO::move( const QList& src, const QUrl& dest, JobFlags flags) { //kDebug(7007) << src << dest; return CopyJobPrivate::newJob(src, dest, CopyJob::Move, false, flags); } CopyJob *KIO::link(const QUrl& src, const QUrl& destDir, JobFlags flags) { QList srcList; srcList.append( src ); return CopyJobPrivate::newJob(srcList, destDir, CopyJob::Link, false, flags); } CopyJob *KIO::link(const QList& srcList, const QUrl& destDir, JobFlags flags) { return CopyJobPrivate::newJob(srcList, destDir, CopyJob::Link, false, flags); } CopyJob *KIO::linkAs(const QUrl& src, const QUrl& destDir, JobFlags flags ) { QList srcList; srcList.append( src ); return CopyJobPrivate::newJob(srcList, destDir, CopyJob::Link, false, flags); } CopyJob *KIO::trash(const QUrl& src, JobFlags flags) { QList srcList; srcList.append( src ); return CopyJobPrivate::newJob(srcList, QUrl("trash:/"), CopyJob::Move, false, flags); } CopyJob *KIO::trash(const QList& srcList, JobFlags flags) { return CopyJobPrivate::newJob(srcList, QUrl("trash:/"), CopyJob::Move, false, flags); } #include "moc_copyjob.cpp" diff --git a/kio/kio/kdirlister.cpp b/kio/kio/kdirlister.cpp index f3c5bac7fa..babf15c38d 100644 --- a/kio/kio/kdirlister.cpp +++ b/kio/kio/kdirlister.cpp @@ -1,2768 +1,2768 @@ /* 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 #include #include #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 // TODO Qt 5.1 uses .destroy 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))); // Probably not needed in KF5 anymore: // The use of KUrl::url() in ~DirItem (sendSignal) crashes if the static for QRegExpEngine got deleted already, // so we need to destroy the 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 QUrl& _u, bool _keep, bool _reload ) { KUrl _url(_u); _url.cleanPath(); // kill consecutive slashes if (!_url.host().isEmpty() && KProtocolInfo::protocolClass(_url.scheme()) == ":local" && _url.scheme() != "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[QUrl::fromLocalFile(resolved)].append(_url); // TODO: remove entry from canonicalUrls again in forgetDirs // Note: this is why we use a QStringList value in there rather than a QSet: // we can just remove one entry and not have to worry about other dirlisters // (the non-unicity of the stringlist gives us the refcounting, basically). } if (!validUrl(lister, _url)) { 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; 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. if (lister->d->autoUpdate) itemU->incAutoUpdate(); } else { kDebug(7004) << "Entry in cache:" << _url; // In this code path, the itemsFromCache->decAutoUpdate + itemU->incAutoUpdate is optimized out itemsInUse.insert(urlStr, itemFromCache); itemU = itemFromCache; } 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*,QUrl)), this, SLOT(slotRedirection(KIO::Job*,QUrl))); 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 QUrl& url) const { Q_FOREACH(CachedItemsJob* job, m_cachedItemsJobs) { if (job->url() == url) return job; } return 0; } KDirLister::Private::CachedItemsJob::CachedItemsJob(KDirLister* lister, const QUrl& 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 QUrl& _url, bool _reload, bool _emitCompleted) { const QString urlStr = _url.toString(); 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 QUrl& _url) { // Modifications to data structures only below this point; // so that addNewItems is called with a consistent state const QString urlStr = _url.toString(); 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 QUrl& url ) const { if ( !url.isValid() ) { if ( lister->d->autoErrorHandling ) { QString tmp = i18n("Malformed URL\n%1", url.toString() ); 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.toString() ); 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 QList urls = lister->d->lstDirs; Q_FOREACH(const QUrl& url, urls) { stopListingUrl(lister, url, silent); } #if 0 // test code QHash::iterator dirit = directoryData.begin(); const QHash::iterator dirend = directoryData.end(); for( ; dirit != dirend ; ++dirit ) { 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 QUrl& _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 ( QList::const_iterator it = lister->d->lstDirs.constBegin(); it != lister->d->lstDirs.constEnd(); ++it ) { DirItem* dirItem = itemsInUse.value((*it).toString()); 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 QList lstDirsCopy = lister->d->lstDirs; lister->d->lstDirs.clear(); //kDebug() << "Iterating over dirs" << lstDirsCopy; for ( QList::const_iterator it = lstDirsCopy.begin(); it != lstDirsCopy.end(); ++it ) { forgetDirs( lister, *it, false ); } } static bool manually_mounted(const QString& path, const KMountPoint::List& possibleMountPoints) { KMountPoint::Ptr mp = possibleMountPoints.findByPath(path); if (!mp) // not listed in fstab -> yes, manually mounted 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 QUrl& _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 } 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 QUrl& _dir ) { kDebug(7004) << _dir; QUrlPathInfo dirInfo(_dir); dirInfo.adjustPath(QUrlPathInfo::StripTrailingSlash); if (!checkUpdate(dirInfo.url())) 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 QString urlStr = dirInfo.url().toString(); 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 QUrl& _dir) { QString dir = _dir.toString(); if (!itemsInUse.contains(dir)) { DirItem *item = itemsCached[dir]; if (item && item->complete) { item->complete = 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 QUrl& url ) const { KFileItem *item = findByUrl( 0, url ); if (item) { return *item; } else { return KFileItem(); } } KDirListerCache::DirItem *KDirListerCache::dirItemForUrl(const QUrl& dir) const { const QString urlStr = dir.toString(QUrl::StripTrailingSlash); DirItem *item = itemsInUse.value(urlStr); if ( !item ) item = itemsCached[urlStr]; return item; } KFileItemList *KDirListerCache::itemsForDir(const QUrl& 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 (QList::const_iterator it = lister->d->lstDirs.constBegin(); it != lister->d->lstDirs.constEnd(); ++it) { DirItem* dirItem = itemsInUse.value((*it).toString()); Q_ASSERT(dirItem); const KFileItem item = dirItem->lstItems.findByName(_name); if (!item.isNull()) return item; } return KFileItem(); } KFileItem *KDirListerCache::findByUrl( const KDirLister *lister, const QUrl& _u ) const { KUrl url(_u); url.adjustPath(KUrl::RemoveTrailingSlash); const QUrl parentDir = QUrlPathInfo(url).directoryUrl(); 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 { QUrl urlDir(dir); kDebug(7004) << urlDir; // output urls, not qstrings, since they might contain a password Q_FOREACH(const QUrl& u, directoriesForCanonicalPath(urlDir)) { updateDirectory(u); } } 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... // Qt5 TODO: use QUrl::fromStringList QList urls; Q_FOREACH(const QString& file, fileList) urls.append(QUrl(file)); slotFilesRemoved(urls); } void KDirListerCache::slotFilesRemoved(const QList& fileList) { //kDebug(7004) << fileList.count(); // Group notifications by parent dirs (usually there would be only one parent dir) QMap removedItemsByDir; QList deletedSubdirs; for (QList::const_iterator it = fileList.begin(); it != fileList.end() ; ++it) { QUrl url(*it); DirItem* dirItem = dirItemForUrl(url); // is it a listed directory? if (dirItem) { deletedSubdirs.append(url); if (!dirItem->rootItem.isNull()) { removedItemsByDir[url.toString()].append(dirItem->rootItem); } } const QUrl parentDir = QUrlPathInfo(url).directoryUrl(); 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.toString()].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) { + Q_FOREACH(const QUrl& url, deletedSubdirs) { // in case of a dir, check if we have any known children, there's much to do in that case // (stopping jobs, removing dirs from cache etc.) deleteDir(url); } } void KDirListerCache::slotFilesChanged( const QStringList &fileList ) // from KDirNotify signals { //kDebug(7004) << fileList; QList dirsToUpdate; QStringList::const_iterator it = fileList.begin(); for (; it != fileList.end() ; ++it) { - KUrl url( *it ); + QUrl url(*it); KFileItem *fileitem = findByUrl(0, url); if (!fileitem) { kDebug(7004) << "item not found for" << url; continue; } if (url.isLocalFile()) { pendingUpdates.insert(*it); // 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 = QUrlPathInfo(url).directoryUrl(); + const QUrl dir = QUrlPathInfo(url).directoryUrl(); if (!dirsToUpdate.contains(dir)) dirsToUpdate.prepend(dir); } } QList::const_iterator itdir = dirsToUpdate.constBegin(); for (; itdir != dirsToUpdate.constEnd() ; ++itdir) updateDirectory( *itdir ); // ## TODO problems with current jobs listing/updating that dir // ( see kde-2.2.2's kdirlister ) processPendingUpdates(); } void KDirListerCache::slotFileRenamed( const QString &_src, const QString &_dst ) // from KDirNotify signals { QUrl src(_src); QUrl dst(_dst); kDebug(7004) << src << "->" << dst; #ifdef DEBUG_CACHE printDebug(); #endif QUrlPathInfo oldurl(src); oldurl.adjustPath(QUrlPathInfo::StripTrailingSlash); KFileItem *fileitem = findByUrl(0, oldurl.url()); if (!fileitem) { kDebug(7004) << "Item not found:" << oldurl.url(); 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(QList() << dst); } // If the item had a UDS_URL as well as UDS_NAME set, the user probably wants // to be updating the name only (since they can't see the URL). // Check to see if a URL exists, and if so, if only the file part has changed, // only update the name and not the underlying URL. bool nameOnly = !fileitem->entry().stringValue( KIO::UDSEntry::UDS_URL ).isEmpty(); nameOnly &= QUrlPathInfo(src).directory() == QUrlPathInfo(dst).directory(); if (!nameOnly && fileitem->isDir()) { renameDir( oldurl.url(), 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.url()); 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.toString()); } else { if( nameOnly ) fileitem->setName(QUrlPathInfo(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 - const KUrl parentDir = QUrlPathInfo(oldItem.url()).directoryUrl(); - const QString parentDirURL = parentDir.url(); + const QUrl parentDir = QUrlPathInfo(oldItem.url()).directoryUrl(); + const QString parentDirURL = parentDir.toString(); 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().toString()); 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 = QUrlPathInfo(directoryUrl).directoryUrl(); kdl->d->addRefreshItem(directoryUrl, oldItem, fileitem); } listersToRefresh.insert(kdl); } return listersToRefresh; } QList KDirListerCache::directoriesForCanonicalPath(const QUrl& dir) const { QList dirs; dirs << dir; dirs << canonicalUrls.value(dir).toSet().toList(); /* make unique; there are faster ways, but this is really small anyway */ if (dirs.count() > 1) 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); QUrlPathInfo urlInfo(QUrl::fromLocalFile(path)); urlInfo.adjustPath(QUrlPathInfo::StripTrailingSlash); if (isDir) { Q_FOREACH(const QUrl& dir, directoriesForCanonicalPath(urlInfo.url())) { handleDirDirty(dir); } } else { Q_FOREACH(const QUrl& dir, directoriesForCanonicalPath(urlInfo.directoryUrl())) { KUrl aliasUrl(dir); aliasUrl.addPath(urlInfo.fileName()); handleFileDirty(aliasUrl); } } } // Called by slotFileDirty void KDirListerCache::handleDirDirty(const QUrl& url) { // A dir: launch an update job if anyone cares about it // This also means we can forget about pending updates to individual files in that dir const QString dirPath = QUrlPathInfo(url).localPath(QUrlPathInfo::AppendTrailingSlash); 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 QUrl& url) { // A file: do we know about it already? KFileItem* existingItem = findByUrl(0, url); if (!existingItem) { // No - update the parent dir then const QUrl dir = QUrlPathInfo(url).directoryUrl(); updateDirectory(dir); } else { // A known file: delay updating it, FAM is flooding us with events const QString filePath = url.toLocalFile(); if (!pendingUpdates.contains(filePath)) { const QUrl dir = QUrlPathInfo(url).directoryUrl(); if (checkUpdate(dir)) { 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; QUrlPathInfo dirUrlInfo(QUrl::fromLocalFile(path)); QStringList fileUrls; Q_FOREACH(const QUrl& url, directoriesForCanonicalPath(dirUrlInfo.directoryUrl())) { QUrlPathInfo urlInfo(url); urlInfo.addPath(dirUrlInfo.fileName()); fileUrls << urlInfo.url().toString(); } 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 QUrl& 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(oldUrl, newUrl, false /*clear items*/); } // when a lister was stopped before the job emits the redirection signal, the old url will // also be in listersCurrentlyHolding const QList holders = oldDirData.listersCurrentlyHolding; foreach ( 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 QUrl &oldUrl, const QUrl &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 ( oldDirUrl == oldUrl || 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 QUrl &oldUrl, const QUrl &newUrl ) { kDebug(7004) << oldUrl << "->" << newUrl; const QString oldUrlStr = oldUrl.toString(QUrl::StripTrailingSlash); const QString newUrlStr = newUrl.toString(QUrl::StripTrailingSlash); 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 QUrl& dir ) { kDebug(7004) << dir; const QList cachedDirs = itemsCached.keys(); // seems slow, but there's no qcache iterator... foreach(const QString& cachedDir, cachedDirs) { const QUrl cachedDirUrl(cachedDir); if (dir == cachedDirUrl || dir.isParentOf(cachedDirUrl)) 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 QT_VERSION < QT_VERSION_CHECK(5, 0, 0) // QUrl::toString(QUrl::StripTrailingSlash) is broken in Qt-4.x, it turns file:/// into file:. // This breaks KDirListerTest::testOpenAndStop(), so use KUrl for now. if (KUrl(joburl(job)).url(KUrl::RemoveTrailingSlash) == url && job != not_job) #else if (joburl(job).toString(QUrl::StripTrailingSlash) == url && job != not_job) #endif return job; ++it; } return 0; } const QUrl& 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 QUrl& _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. QUrl dirUrl(QUrlPathInfo(_dirUrl).url(QUrlPathInfo::StripTrailingSlash)); // Separate itemsInUse iteration and calls to forgetDirs (which modify itemsInUse) QList affectedItems; QHash::iterator itu = itemsInUse.begin(); const QHash::iterator ituend = itemsInUse.end(); for ( ; itu != ituend; ++itu ) { const KUrl deletedUrl( itu.key() ); if (dirUrl == deletedUrl || 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() : QUrl() ) << "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().toString() : 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 QUrl& _url, OpenUrlFlags _flags ) { // emit the current changes made to avoid an inconsistent treeview if (d->hasPendingChanges && (_flags & Keep)) emitChanges(); d->hasPendingChanges = false; return kDirListerCache->listDir( this, _url, _flags & Keep, _flags & Reload ); } void KDirLister::stop() { kDirListerCache->stop( this ); } void KDirLister::stop( const QUrl& _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; } QUrl KDirLister::url() const { return d->url; } QList 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 QUrl& _u ) { kDirListerCache->updateDirectory( _u ); } bool KDirLister::isFinished() const { return d->complete; } KFileItem KDirLister::rootItem() const { return d->rootFileItem; } KFileItem KDirLister::findByUrl( const QUrl& _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; QMimeDatabase db; const QMimeType mimeptr = db.mimeTypeForName(mime); if (!mimeptr.isValid()) return false; //kDebug(7004) << "doMimeFilter: investigating: "<name(); QStringList::const_iterator it = filters.begin(); for ( ; it != filters.end(); ++it ) if (mimeptr.inherits(*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 QUrl& 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 QUrl& 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 QUrl& directoryUrl, const KFileItem& oldItem, const KFileItem& item) { const bool refreshItemWasFiltered = !isItemVisible(oldItem) || !m_parent->matchesMimeFilter(oldItem); if (isItemVisible(item) && m_parent->matchesMimeFilter(item)) { if ( refreshItemWasFiltered ) { if ( !lstNewItems ) { lstNewItems = new NewItemsHash; } Q_ASSERT( !item.isNull() ); (*lstNewItems)[directoryUrl].append( item ); } 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); + 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 QUrl& 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 QUrl& oldUrl, const QUrl& newUrl, bool keepItems) { - if ( url.equals( oldUrl, KUrl::CompareWithoutTrailingSlash ) ) { + if (QUrlPathInfo(url).equals(oldUrl, QUrlPathInfo::CompareWithoutTrailingSlash)) { if (!keepItems) rootFileItem = KFileItem(); 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 QUrl& url) { // Move dirlisters from listersCurrentlyListing to listersCurrentlyHolding, // but not those that are still waiting on a CachedItemsJob... // Unit-testing note: // Run kdirmodeltest in valgrind to hit the case where an update // is triggered while a lister has a CachedItemsJob (different timing...) QMutableListIterator lister_it(listersCurrentlyListing); while (lister_it.hasNext()) { 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 QUrl& url) { return kDirListerCache->itemForUrl(url); } #include "moc_kdirlister.cpp" #include "moc_kdirlister_p.cpp" diff --git a/kio/kio/kdirlister_p.h b/kio/kio/kdirlister_p.h index 9d0266b457..7a4bfafd73 100644 --- a/kio/kio/kdirlister_p.h +++ b/kio/kio/kdirlister_p.h @@ -1,491 +1,491 @@ /* This file is part of the KDE project Copyright (C) 2002-2006 Michael Brade This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef kdirlister_p_h #define kdirlister_p_h #include "kfileitem.h" #include #include #include #include #include #include #include #include #include #include class KDirLister; namespace KIO { class Job; class ListJob; } class OrgKdeKDirNotifyInterface; struct KDirListerCacheDirectoryData; class KDirLister::Private { public: Private(KDirLister *parent) : m_parent(parent) { complete = false; autoUpdate = false; autoErrorHandling = false; errorParent = 0; delayedMimeTypes = false; rootFileItem = KFileItem(); lstNewItems = 0; lstRefreshItems = 0; lstMimeFilteredItems = 0; lstRemoveItems = 0; hasPendingChanges = false; window = 0; } void _k_emitCachedItems(const QUrl&, bool, bool); void _k_slotInfoMessage( KJob*, const QString& ); void _k_slotPercent( KJob*, unsigned long ); void _k_slotTotalSize( KJob*, qulonglong ); void _k_slotProcessedSize( KJob*, qulonglong ); void _k_slotSpeed( KJob*, unsigned long ); bool doMimeExcludeFilter( const QString& mimeExclude, const QStringList& filters ) const; void jobStarted( KIO::ListJob * ); void connectJob( KIO::ListJob * ); void jobDone( KIO::ListJob * ); uint numJobs(); void addNewItem(const QUrl& directoryUrl, const KFileItem& item); void addNewItems(const QUrl& directoryUrl, const KFileItemList& items); void addRefreshItem(const QUrl& directoryUrl, const KFileItem& oldItem, const KFileItem& item); void emitItems(); void emitItemsDeleted(const KFileItemList &items); /** * Redirect this dirlister from oldUrl to newUrl. * @param keepItems if true, keep the fileitems (e.g. when renaming an existing dir); * if false, clear out everything (e.g. when redirecting during listing). */ void redirect(const QUrl& oldUrl, const QUrl& newUrl, bool keepItems); /** * Should this item be visible according to the current filter settings? */ bool isItemVisible(const KFileItem& item) const; void prepareForSettingsChange() { if (!hasPendingChanges) { hasPendingChanges = true; oldSettings = settings; } } void emitChanges(); class CachedItemsJob; CachedItemsJob* cachedItemsJobForUrl(const QUrl& url) const; KDirLister *m_parent; /** * List of dirs handled by this dirlister. The first entry is the base URL. * For a tree view, it contains all the dirs shown. */ QList lstDirs; // toplevel URL - KUrl url; + QUrl url; bool complete:1; bool autoUpdate:1; bool delayedMimeTypes:1; bool hasPendingChanges:1; // i.e. settings != oldSettings bool autoErrorHandling:2; QWidget *errorParent; struct JobData { long unsigned int percent, speed; KIO::filesize_t processedSize, totalSize; }; QMap jobData; // file item for the root itself (".") KFileItem rootFileItem; - typedef QHash NewItemsHash; + typedef QHash NewItemsHash; NewItemsHash *lstNewItems; QList > *lstRefreshItems; KFileItemList *lstMimeFilteredItems, *lstRemoveItems; QWidget *window; // Main window this lister is associated with QList m_cachedItemsJobs; QString nameFilter; // parsed into lstFilters struct FilterSettings { FilterSettings() : isShowingDotFiles(false), dirOnlyMode(false) {} bool isShowingDotFiles; bool dirOnlyMode; QList lstFilters; QStringList mimeFilter; QStringList mimeExcludeFilter; }; FilterSettings settings; FilterSettings oldSettings; friend class KDirListerCache; }; /** * Design of the cache: * There is a single KDirListerCache for the whole process. * It holds all the items used by the dir listers (itemsInUse) * as well as a cache of the recently used items (itemsCached). * Those items are grouped by directory (a DirItem represents a whole directory). * * KDirListerCache also runs all the jobs for listing directories, whether they are for * normal listing or for updates. * For faster lookups, it also stores a hash table, which gives for a directory URL: * - the dirlisters holding that URL (listersCurrentlyHolding) * - the dirlisters currently listing that URL (listersCurrentlyListing) */ class KDirListerCache : public QObject { Q_OBJECT public: KDirListerCache(); // only called by K_GLOBAL_STATIC ~KDirListerCache(); void updateDirectory( const QUrl& dir ); KFileItem itemForUrl( const QUrl& url ) const; KFileItemList *itemsForDir(const QUrl& dir) const; bool listDir( KDirLister *lister, const QUrl& _url, bool _keep, bool _reload ); // stop all running jobs for lister void stop( KDirLister *lister, bool silent = false ); // stop just the job listing url for lister void stopListingUrl( KDirLister *lister, const QUrl &_url, bool silent = false ); void setAutoUpdate( KDirLister *lister, bool enable ); void forgetDirs( KDirLister *lister ); void forgetDirs( KDirLister *lister, const QUrl &_url, bool notify ); KFileItem findByName( const KDirLister *lister, const QString &_name ) const; // findByUrl returns a pointer so that it's possible to modify the item. // See itemForUrl for the version that returns a readonly kfileitem. // @param lister can be 0. If set, it is checked that the url is held by the lister KFileItem *findByUrl(const KDirLister *lister, const QUrl &url) const; // Called by CachedItemsJob: // Emits the cached items, for this lister and this url void emitItemsFromCache(KDirLister::Private::CachedItemsJob* job, KDirLister* lister, const QUrl& _url, bool _reload, bool _emitCompleted); // Called by CachedItemsJob: void forgetCachedItemsJob(KDirLister::Private::CachedItemsJob* job, KDirLister* lister, const QUrl& url); public Q_SLOTS: /** * Notify that files have been added in @p directory * The receiver will list that directory again to find * the new items (since it needs more than just the names anyway). * Connected to the DBus signal from the KDirNotify interface. */ void slotFilesAdded( const QString& urlDirectory ); /** * Notify that files have been deleted. * This call passes the exact urls of the deleted files * so that any view showing them can simply remove them * or be closed (if its current dir was deleted) * Connected to the DBus signal from the KDirNotify interface. */ void slotFilesRemoved( const QStringList& fileList ); /** * Notify that files have been changed. * At the moment, this is only used for new icon, but it could be * used for size etc. as well. * Connected to the DBus signal from the KDirNotify interface. */ void slotFilesChanged( const QStringList& fileList ); void slotFileRenamed( const QString& srcUrl, const QString& dstUrl ); private Q_SLOTS: void slotFileDirty( const QString &_file ); void slotFileCreated( const QString &_file ); void slotFileDeleted( const QString &_file ); void slotEntries( KIO::Job *job, const KIO::UDSEntryList &entries ); void slotResult( KJob *j ); void slotRedirection( KIO::Job *job, const QUrl &url ); void slotUpdateEntries( KIO::Job *job, const KIO::UDSEntryList &entries ); void slotUpdateResult( KJob *job ); void processPendingUpdates(); private: class DirItem; DirItem* dirItemForUrl(const QUrl& dir) const; bool validUrl( const KDirLister *lister, const QUrl& _url ) const; void stopListJob(const QString& url, bool silent); KIO::ListJob *jobForUrl( const QString& url, KIO::ListJob *not_job = 0 ); const QUrl& joburl( KIO::ListJob *job ); void killJob( KIO::ListJob *job ); // Called when something tells us that the directory @p url has changed. // Returns true if @p url is held by some lister (meaning: do the update now) // otherwise mark the cached item as not-up-to-date for later and return false bool checkUpdate(const QUrl& url); // Helper method for slotFileDirty void handleFileDirty(const QUrl& url); void handleDirDirty(const QUrl& url); // when there were items deleted from the filesystem all the listers holding // the parent directory need to be notified, the unmarked items have to be deleted // and removed from the cache including all the children. void deleteUnmarkedItems( const QList&, KFileItemList & ); // Helper method called when we know that a list of items was deleted void itemsDeleted(const QList& listers, const KFileItemList& deletedItems); void slotFilesRemoved(const QList& urls); // common for slotRedirection and slotFileRenamed void renameDir( const QUrl &oldUrl, const QUrl &url ); // common for deleteUnmarkedItems and slotFilesRemoved void deleteDir( const QUrl& dirUrl ); // remove directory from cache (itemsCached), including all child dirs void removeDirFromCache( const QUrl& dir ); // helper for renameDir void emitRedirections( const QUrl &oldUrl, const QUrl &url ); /** * Emits refreshItem() in the directories that cared for oldItem. * The caller has to remember to call emitItems in the set of dirlisters returned * (but this allows to buffer change notifications) */ QSet emitRefreshItem(const KFileItem& oldItem, const KFileItem& fileitem); /** * When KDirWatch tells us that something changed in "dir", we need to * also notify the dirlisters that are listing a symlink to "dir" (#213799) */ QList directoriesForCanonicalPath(const QUrl& dir) const; #ifndef NDEBUG void printDebug(); #endif class DirItem { public: DirItem(const QUrl &dir, const QString& canonicalPath) : url(dir), m_canonicalPath(canonicalPath) { autoUpdates = 0; complete = false; } ~DirItem() { if ( autoUpdates ) { if ( KDirWatch::exists() && url.isLocalFile() ) KDirWatch::self()->removeDir(m_canonicalPath); sendSignal( false, url ); } lstItems.clear(); } void sendSignal( bool entering, const QUrl& url ) { // Note that "entering" means "start watching", and "leaving" means "stop watching" // (i.e. it's not when the user leaves the directory, it's when the directory is removed from the cache) if (entering) org::kde::KDirNotify::emitEnteredDirectory(url); else org::kde::KDirNotify::emitLeftDirectory(url); } void redirect( const QUrl& newUrl ) { if ( autoUpdates ) { if ( url.isLocalFile() ) KDirWatch::self()->removeDir(m_canonicalPath); sendSignal( false, url ); if (newUrl.isLocalFile()) { m_canonicalPath = QFileInfo(newUrl.toLocalFile()).canonicalFilePath(); KDirWatch::self()->addDir(m_canonicalPath); } sendSignal( true, newUrl ); } url = newUrl; if ( !rootItem.isNull() ) rootItem.setUrl( newUrl ); } void incAutoUpdate() { if ( autoUpdates++ == 0 ) { if ( url.isLocalFile() ) KDirWatch::self()->addDir(m_canonicalPath); sendSignal( true, url ); } } void decAutoUpdate() { if ( --autoUpdates == 0 ) { if ( url.isLocalFile() ) KDirWatch::self()->removeDir(m_canonicalPath); sendSignal( false, url ); } else if ( autoUpdates < 0 ) autoUpdates = 0; } // number of KDirListers using autoUpdate for this dir short autoUpdates; // this directory is up-to-date bool complete; // the complete url of this directory - KUrl url; + QUrl url; // the local path, with symlinks resolved, so that KDirWatch works QString m_canonicalPath; // KFileItem representing the root of this directory. // Remember that this is optional. FTP sites don't return '.' in // the list, so they give no root item KFileItem rootItem; KFileItemList lstItems; }; //static const unsigned short MAX_JOBS_PER_LISTER; QMap runningListJobs; // an item is a complete directory QHash itemsInUse; QCache itemsCached; typedef QHash DirectoryDataHash; DirectoryDataHash directoryData; // Symlink-to-directories are registered here so that we can // find the url that changed, when kdirwatch tells us about // changes in the canonical url. (#213799) QHash /*dirlister urls*/> canonicalUrls; // Set of local files that we have changed recently (according to KDirWatch) // We temporize the notifications by keeping them 500ms in this list. QSet pendingUpdates; // The timer for doing the delayed updates QTimer pendingUpdateTimer; // Set of remote files that have changed recently -- but we can't emit those // changes yet, we need to wait for the "update" directory listing. // The cmp() call can't differ mimetypes since they are determined on demand, // this is why we need to remember those files here. QSet pendingRemoteUpdates; // the KDirNotify signals OrgKdeKDirNotifyInterface *kdirnotify; struct ItemInUseChange; }; // Data associated with a directory url // This could be in DirItem but only in the itemsInUse dict... struct KDirListerCacheDirectoryData { // A lister can be EITHER in listersCurrentlyListing OR listersCurrentlyHolding // but NOT in both at the same time. // But both lists can have different listers at the same time; this // happens if more listers are requesting url at the same time and // one lister was stopped during the listing of files. // Listers that are currently listing this url QList listersCurrentlyListing; // Listers that are currently holding this url QList listersCurrentlyHolding; void moveListersWithoutCachedItemsJob(const QUrl& url); }; //const unsigned short KDirListerCache::MAX_JOBS_PER_LISTER = 5; // This job tells KDirListerCache to emit cached items asynchronously from listDir() // to give the KDirLister user enough time for connecting to its signals, and so // that KDirListerCache behaves just like when a real KIO::Job is used: nothing // is emitted during the openUrl call itself. class KDirLister::Private::CachedItemsJob : public KJob { Q_OBJECT public: CachedItemsJob(KDirLister* lister, const QUrl& url, bool reload); /*reimp*/ void start() { QMetaObject::invokeMethod(this, "done", Qt::QueuedConnection); } // For updateDirectory() to cancel m_emitCompleted; void setEmitCompleted(bool b) { m_emitCompleted = b; } QUrl url() const { return m_url; } protected: virtual bool doKill(); public Q_SLOTS: void done(); private: KDirLister* m_lister; - KUrl m_url; + QUrl m_url; bool m_reload; bool m_emitCompleted; }; #endif diff --git a/kio/kio/previewjob.cpp b/kio/kio/previewjob.cpp index 27e3e04877..f9823f5027 100644 --- a/kio/kio/previewjob.cpp +++ b/kio/kio/previewjob.cpp @@ -1,789 +1,789 @@ // -*- c++ -*- // vim: ts=4 sw=4 et /* This file is part of the KDE libraries Copyright (C) 2000 David Faure 2000 Carsten Pfeiffer 2001 Malte Starostik 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 "previewjob.h" #include #include #include #ifdef Q_OS_UNIX #include #include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "jobuidelegate.h" #include "job_p.h" namespace KIO { struct PreviewItem; } using namespace KIO; struct KIO::PreviewItem { KFileItem item; KService::Ptr plugin; }; class KIO::PreviewJobPrivate: public KIO::JobPrivate { public: PreviewJobPrivate(const KFileItemList &items, const QSize &size) : initialItems(items), tOrig(0), width(size.width()), height(size.height()), cacheWidth(width), cacheHeight(height), bScale(true), bSave(true), ignoreMaximumSize(false), sequenceIndex(0), succeeded(false), maximumLocalSize(0), maximumRemoteSize(0), iconSize(0), iconAlpha(70), shmid(-1), shmaddr(0) { // http://specifications.freedesktop.org/thumbnail-spec/thumbnail-spec-latest.html#DIRECTORY thumbRoot = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + QLatin1String("/thumbnails/"); } enum { STATE_STATORIG, // if the thumbnail exists STATE_GETORIG, // if we create it STATE_CREATETHUMB // thumbnail:/ slave } state; KFileItemList initialItems; QStringList enabledPlugins; // Some plugins support remote URLs, QHash m_remoteProtocolPlugins; // Our todo list :) // We remove the first item at every step, so use QLinkedList QLinkedList items; // The current item PreviewItem currentItem; // The modification time of that URL time_t tOrig; // Path to thumbnail cache for the current size QString thumbPath; // Original URL of current item in TMS format // (file:///path/to/file instead of file:/path/to/file) QString origName; // Thumbnail file name for current item QString thumbName; // Size of thumbnail int width; int height; // Unscaled size of thumbnail (128 or 256 if cache is enabled) int cacheWidth; int cacheHeight; // Whether the thumbnail should be scaled bool bScale; // Whether we should save the thumbnail bool bSave; bool ignoreMaximumSize; int sequenceIndex; bool succeeded; // If the file to create a thumb for was a temp file, this is its name QString tempName; KIO::filesize_t maximumLocalSize; KIO::filesize_t maximumRemoteSize; // the size for the icon overlay int iconSize; // the transparency of the blended mimetype icon int iconAlpha; // Shared memory segment Id. The segment is allocated to a size // of extent x extent x 4 (32 bit image) on first need. int shmid; // And the data area uchar *shmaddr; // Root of thumbnail cache QString thumbRoot; void getOrCreateThumbnail(); bool statResultThumbnail(); void createThumbnail( const QString& ); void determineNextFile(); void emitPreview(const QImage &thumb); void startPreview(); void slotThumbData(KIO::Job *, const QByteArray &); Q_DECLARE_PUBLIC(PreviewJob) }; #ifndef KDE_NO_DEPRECATED PreviewJob::PreviewJob( const KFileItemList &items, int width, int height, int iconSize, int iconAlpha, bool scale, bool save, const QStringList *enabledPlugins ) : KIO::Job(*new PreviewJobPrivate(items, QSize(width, height ? height : width))) { Q_D(PreviewJob); d->enabledPlugins = enabledPlugins ? *enabledPlugins : availablePlugins(); d->iconSize = iconSize; d->iconAlpha = iconAlpha; d->bScale = scale; d->bSave = save && scale; // Return to event loop first, determineNextFile() might delete this; QTimer::singleShot(0, this, SLOT(startPreview())); } #endif PreviewJob::PreviewJob(const KFileItemList &items, const QSize &size, const QStringList *enabledPlugins) : KIO::Job(*new PreviewJobPrivate(items, size)) { Q_D(PreviewJob); if (enabledPlugins) { d->enabledPlugins = *enabledPlugins; } else { const KConfigGroup globalConfig(KSharedConfig::openConfig(), "PreviewSettings"); d->enabledPlugins = globalConfig.readEntry("Plugins", QStringList() << "directorythumbnail" << "imagethumbnail" << "jpegthumbnail"); } // Return to event loop first, determineNextFile() might delete this; QTimer::singleShot(0, this, SLOT(startPreview())); } PreviewJob::~PreviewJob() { #ifdef Q_OS_UNIX Q_D(PreviewJob); if (d->shmaddr) { shmdt((char*)d->shmaddr); shmctl(d->shmid, IPC_RMID, 0); } #endif } void PreviewJob::setOverlayIconSize(int size) { Q_D(PreviewJob); d->iconSize = size; } int PreviewJob::overlayIconSize() const { Q_D(const PreviewJob); return d->iconSize; } void PreviewJob::setOverlayIconAlpha(int alpha) { Q_D(PreviewJob); d->iconAlpha = qBound(0, alpha, 255); } int PreviewJob::overlayIconAlpha() const { Q_D(const PreviewJob); return d->iconAlpha; } void PreviewJob::setScaleType(ScaleType type) { Q_D(PreviewJob); switch (type) { case Unscaled: d->bScale = false; d->bSave = false; break; case Scaled: d->bScale = true; d->bSave = false; break; case ScaledAndCached: d->bScale = true; d->bSave = true; break; default: break; } } PreviewJob::ScaleType PreviewJob::scaleType() const { Q_D(const PreviewJob); if (d->bScale) { return d->bSave ? ScaledAndCached : Scaled; } return Unscaled; } void PreviewJobPrivate::startPreview() { Q_Q(PreviewJob); // Load the list of plugins to determine which mimetypes are supported const KService::List plugins = KServiceTypeTrader::self()->query("ThumbCreator"); QMap mimeMap; QHash > protocolMap; for (KService::List::ConstIterator it = plugins.constBegin(); it != plugins.constEnd(); ++it) { QStringList protocols = (*it)->property("X-KDE-Protocols").toStringList(); const QString p = (*it)->property("X-KDE-Protocol").toString(); if (!p.isEmpty()) { protocols.append(p); } foreach (const QString &protocol, protocols) { QStringList mtypes = (*it)->serviceTypes(); // Filter out non-mimetype servicetypes // TODO KDE5: use KService::mimeTypes() foreach (const QString &_mtype, mtypes) { if (!((*it)->hasMimeType(_mtype))) { mtypes.removeAll(_mtype); } } // Add supported mimetype for this protocol QStringList &_ms = m_remoteProtocolPlugins[protocol]; foreach (const QString &_m, mtypes) { protocolMap[protocol].insert(_m, *it); if (!_ms.contains(_m)) { _ms.append(_m); } } } if (enabledPlugins.contains((*it)->desktopEntryName())) { const QStringList mimeTypes = (*it)->serviceTypes(); for (QStringList::ConstIterator mt = mimeTypes.constBegin(); mt != mimeTypes.constEnd(); ++mt) mimeMap.insert(*mt, *it); } } // Look for images and store the items in our todo list :) bool bNeedCache = false; KFileItemList::const_iterator kit = initialItems.constBegin(); const KFileItemList::const_iterator kend = initialItems.constEnd(); for ( ; kit != kend; ++kit ) { PreviewItem item; item.item = *kit; const QString mimeType = item.item.mimetype(); KService::Ptr plugin(0); // look for protocol-specific thumbnail plugins first QHash >::const_iterator it = protocolMap.constFind(item.item.url().scheme()); if (it != protocolMap.constEnd()) { plugin = it.value().value(mimeType); } if (!plugin) { QMap::ConstIterator pluginIt = mimeMap.constFind(mimeType); if (pluginIt == mimeMap.constEnd()) { QString groupMimeType = mimeType; groupMimeType.replace(QRegExp("/.*"), "/*"); pluginIt = mimeMap.constFind(groupMimeType); if (pluginIt == mimeMap.constEnd()) { QMimeDatabase db; // check mime type inheritance, resolve aliases const QMimeType mimeInfo = db.mimeTypeForName(mimeType); if (mimeInfo.isValid()) { const QStringList parentMimeTypes = mimeInfo.allAncestors(); Q_FOREACH(const QString& parentMimeType, parentMimeTypes) { pluginIt = mimeMap.constFind(parentMimeType); if (pluginIt != mimeMap.constEnd()) break; } } } } if (pluginIt != mimeMap.constEnd()) { plugin = *pluginIt; } } if (plugin) { item.plugin = plugin; items.append(item); if (!bNeedCache && bSave && plugin->property("CacheThumbnail").toBool()) { const QUrl url = (*kit).url(); if (!url.isLocalFile() || !QString(QUrlPathInfo(url).directory(QUrlPathInfo::AppendTrailingSlash)).startsWith(thumbRoot)) { bNeedCache = true; } } } else { emit q->failed( *kit ); } } KConfigGroup cg( KSharedConfig::openConfig(), "PreviewSettings" ); maximumLocalSize = cg.readEntry( "MaximumSize", 5*1024*1024LL /* 5MB */ ); maximumRemoteSize = cg.readEntry( "MaximumRemoteSize", 0 ); if (bNeedCache) { if (width <= 128 && height <= 128) cacheWidth = cacheHeight = 128; else cacheWidth = cacheHeight = 256; thumbPath = thumbRoot + (cacheWidth == 128 ? "normal/" : "large/"); if (!QDir(thumbPath).exists()) { if (QDir().mkpath(thumbPath)) { // Qt5 TODO: mkpath(dirPath, permissions) QFile f(thumbPath); f.setPermissions(QFile::ReadUser | QFile::WriteUser | QFile::ExeUser); // 0700 } } } else bSave = false; initialItems.clear(); determineNextFile(); } void PreviewJob::removeItem(const QUrl& url) { Q_D(PreviewJob); for (QLinkedList::Iterator it = d->items.begin(); it != d->items.end(); ++it) if ((*it).item.url() == url) { d->items.erase(it); break; } if (d->currentItem.item.url() == url) { KJob* job = subjobs().first(); job->kill(); removeSubjob( job ); d->determineNextFile(); } } void KIO::PreviewJob::setSequenceIndex(int index) { d_func()->sequenceIndex = index; } int KIO::PreviewJob::sequenceIndex() const { return d_func()->sequenceIndex; } void PreviewJob::setIgnoreMaximumSize(bool ignoreSize) { d_func()->ignoreMaximumSize = ignoreSize; } void PreviewJobPrivate::determineNextFile() { Q_Q(PreviewJob); if (!currentItem.item.isNull()) { if (!succeeded) emit q->failed( currentItem.item ); } // No more items ? if ( items.isEmpty() ) { q->emitResult(); return; } else { // First, stat the orig file state = PreviewJobPrivate::STATE_STATORIG; currentItem = items.first(); succeeded = false; items.removeFirst(); KIO::Job *job = KIO::stat( currentItem.item.url(), KIO::HideProgressInfo ); job->addMetaData( "no-auth-prompt", "true" ); q->addSubjob(job); } } void PreviewJob::slotResult( KJob *job ) { Q_D(PreviewJob); removeSubjob(job); Q_ASSERT ( !hasSubjobs() ); // We should have only one job at a time ... switch ( d->state ) { case PreviewJobPrivate::STATE_STATORIG: { if (job->error()) // that's no good news... { // Drop this one and move on to the next one d->determineNextFile(); return; } const KIO::UDSEntry entry = static_cast(job)->statResult(); d->tOrig = entry.numberValue( KIO::UDSEntry::UDS_MODIFICATION_TIME, 0 ); bool skipCurrentItem = false; const KIO::filesize_t size = (KIO::filesize_t)entry.numberValue( KIO::UDSEntry::UDS_SIZE, 0 ); - const KUrl itemUrl = d->currentItem.item.mostLocalUrl(); + const QUrl itemUrl = d->currentItem.item.mostLocalUrl(); if (itemUrl.isLocalFile() || KProtocolInfo::protocolClass(itemUrl.scheme()) == QLatin1String(":local")) { skipCurrentItem = !d->ignoreMaximumSize && size > d->maximumLocalSize && !d->currentItem.plugin->property("IgnoreMaximumSize").toBool(); } else { // For remote items the "IgnoreMaximumSize" plugin property is not respected skipCurrentItem = !d->ignoreMaximumSize && size > d->maximumRemoteSize; // Remote directories are not supported, don't try to do a file_copy on them if (!skipCurrentItem) { // TODO update item.mimeType from the UDS entry, in case it wasn't set initially // But we don't use the mimetype anymore, we just use isDir(). if (d->currentItem.item.isDir()) { skipCurrentItem = true; } } } if (skipCurrentItem) { d->determineNextFile(); return; } bool pluginHandlesSequences = d->currentItem.plugin->property("HandleSequences", QVariant::Bool).toBool(); if ( !d->currentItem.plugin->property( "CacheThumbnail" ).toBool() || (d->sequenceIndex && pluginHandlesSequences) ) { // This preview will not be cached, no need to look for a saved thumbnail // Just create it, and be done d->getOrCreateThumbnail(); return; } if ( d->statResultThumbnail() ) return; d->getOrCreateThumbnail(); return; } case PreviewJobPrivate::STATE_GETORIG: { if (job->error()) { d->determineNextFile(); return; } d->createThumbnail( static_cast(job)->destUrl().toLocalFile() ); return; } case PreviewJobPrivate::STATE_CREATETHUMB: { if (!d->tempName.isEmpty()) { QFile::remove(d->tempName); d->tempName.clear(); } d->determineNextFile(); return; } } } bool PreviewJobPrivate::statResultThumbnail() { if ( thumbPath.isEmpty() ) return false; - KUrl url = currentItem.item.mostLocalUrl(); + QUrl url = currentItem.item.mostLocalUrl(); // Don't include the password if any - url.setPass(QString()); - origName = url.url(); + url.setPassword(QString()); + origName = url.toString(); QCryptographicHash md5(QCryptographicHash::Md5); md5.addData(QFile::encodeName( origName ) ); thumbName = QFile::encodeName( md5.result().toHex() ) + ".png"; QImage thumb; if ( !thumb.load( thumbPath + thumbName ) ) return false; if ( thumb.text( "Thumb::URI", 0 ) != origName || thumb.text( "Thumb::MTime", 0 ).toInt() != tOrig ) return false; QString thumbnailerVersion = currentItem.plugin->property("ThumbnailerVersion", QVariant::String).toString(); if (!thumbnailerVersion.isEmpty() && thumb.text("Software", 0).startsWith("KDE Thumbnail Generator")) { //Check if the version matches //The software string should read "KDE Thumbnail Generator pluginName (vX)" QString softwareString = thumb.text("Software", 0).remove("KDE Thumbnail Generator").trimmed(); if (softwareString.isEmpty()) { // The thumbnail has been created with an older version, recreating return false; } int versionIndex = softwareString.lastIndexOf("(v"); if (versionIndex < 0) { return false; } QString cachedVersion = softwareString.remove(0, versionIndex+2); cachedVersion.chop(1); uint thumbnailerMajor = thumbnailerVersion.toInt(); uint cachedMajor = cachedVersion.toInt(); if (thumbnailerMajor > cachedMajor) { return false; } } // Found it, use it emitPreview( thumb ); succeeded = true; determineNextFile(); return true; } void PreviewJobPrivate::getOrCreateThumbnail() { Q_Q(PreviewJob); // We still need to load the orig file ! (This is getting tedious) :) const KFileItem& item = currentItem.item; const QString localPath = item.localPath(); if (!localPath.isEmpty()) { createThumbnail( localPath ); } else { const QUrl fileUrl = item.url(); // heuristics for remote URL support bool supportsProtocol = false; if (m_remoteProtocolPlugins.value(fileUrl.scheme()).contains(item.mimetype())) { // There's a plugin supporting this protocol and mimetype supportsProtocol = true; } else if (m_remoteProtocolPlugins.value("KIO").contains(item.mimetype())) { // Assume KIO understands any URL, ThumbCreator slaves who have // X-KDE-Protocols=KIO will get fed the remote URL directly. supportsProtocol = true; } if (supportsProtocol) { createThumbnail(fileUrl.toString()); return; } // No plugin support access to this remote content, copy the file // to the local machine, then create the thumbnail state = PreviewJobPrivate::STATE_GETORIG; QTemporaryFile localFile; localFile.setAutoRemove(false); localFile.open(); tempName = localFile.fileName(); const QUrl currentURL = item.mostLocalUrl(); KIO::Job * job = KIO::file_copy( currentURL, QUrl::fromLocalFile(tempName), -1, KIO::Overwrite | KIO::HideProgressInfo /* No GUI */ ); job->addMetaData("thumbnail","1"); q->addSubjob(job); } } void PreviewJobPrivate::createThumbnail( const QString &pixPath ) { Q_Q(PreviewJob); state = PreviewJobPrivate::STATE_CREATETHUMB; QUrl thumbURL; thumbURL.setScheme("thumbnail"); thumbURL.setPath(pixPath); KIO::TransferJob *job = KIO::get(thumbURL, NoReload, HideProgressInfo); q->addSubjob(job); q->connect(job, SIGNAL(data(KIO::Job*,QByteArray)), SLOT(slotThumbData(KIO::Job*,QByteArray))); bool save = bSave && currentItem.plugin->property("CacheThumbnail").toBool() && !sequenceIndex; job->addMetaData("mimeType", currentItem.item.mimetype()); job->addMetaData("width", QString().setNum(save ? cacheWidth : width)); job->addMetaData("height", QString().setNum(save ? cacheHeight : height)); job->addMetaData("iconSize", QString().setNum(save ? 64 : iconSize)); job->addMetaData("iconAlpha", QString().setNum(iconAlpha)); job->addMetaData("plugin", currentItem.plugin->library()); if(sequenceIndex) job->addMetaData("sequence-index", QString().setNum(sequenceIndex)); #ifdef Q_OS_UNIX if (shmid == -1) { if (shmaddr) { shmdt((char*)shmaddr); shmctl(shmid, IPC_RMID, 0); } shmid = shmget(IPC_PRIVATE, cacheWidth * cacheHeight * 4, IPC_CREAT|0600); if (shmid != -1) { shmaddr = (uchar *)(shmat(shmid, 0, SHM_RDONLY)); if (shmaddr == (uchar *)-1) { shmctl(shmid, IPC_RMID, 0); shmaddr = 0; shmid = -1; } } else shmaddr = 0; } if (shmid != -1) job->addMetaData("shmid", QString().setNum(shmid)); #endif } void PreviewJobPrivate::slotThumbData(KIO::Job *, const QByteArray &data) { bool save = bSave && currentItem.plugin->property("CacheThumbnail").toBool() && (!currentItem.item.url().isLocalFile() || !QString(QUrlPathInfo(currentItem.item.url()).directory(QUrlPathInfo::AppendTrailingSlash)).startsWith(thumbRoot)) && !sequenceIndex; QImage thumb; #ifdef Q_OS_UNIX if (shmaddr) { // Keep this in sync with kdebase/kioslave/thumbnail.cpp QDataStream str(data); int width, height; quint8 iFormat; str >> width >> height >> iFormat; QImage::Format format = static_cast( iFormat ); thumb = QImage(shmaddr, width, height, format ).copy(); } else #endif thumb.loadFromData(data); if (thumb.isNull()) { QDataStream s(data); s >> thumb; } QString tempFileName; bool savedCorrectly = false; if (save) { thumb.setText("Thumb::URI", origName); thumb.setText("Thumb::MTime", QString::number(tOrig)); thumb.setText("Thumb::Size", number(currentItem.item.size())); thumb.setText("Thumb::Mimetype", currentItem.item.mimetype()); QString thumbnailerVersion = currentItem.plugin->property("ThumbnailerVersion", QVariant::String).toString(); QString signature = QString("KDE Thumbnail Generator "+currentItem.plugin->name()); if (!thumbnailerVersion.isEmpty()) { signature.append(" (v"+thumbnailerVersion+')'); } thumb.setText("Software", signature); QTemporaryFile temp(thumbPath + "kde-tmp-XXXXXX.png"); temp.setAutoRemove(false); if (temp.open()) //Only try to write out the thumbnail if we { //actually created the temp file. tempFileName = temp.fileName(); savedCorrectly = thumb.save(tempFileName, "PNG"); } } if(savedCorrectly) { Q_ASSERT(!tempFileName.isEmpty()); KDE::rename(tempFileName, thumbPath + thumbName); } emitPreview( thumb ); succeeded = true; } void PreviewJobPrivate::emitPreview(const QImage &thumb) { Q_Q(PreviewJob); QPixmap pix; if (thumb.width() > width || thumb.height() > height) pix = QPixmap::fromImage( thumb.scaled(QSize(width, height), Qt::KeepAspectRatio, Qt::SmoothTransformation) ); else pix = QPixmap::fromImage( thumb ); emit q->gotPreview(currentItem.item, pix); } QStringList PreviewJob::availablePlugins() { QStringList result; const KService::List plugins = KServiceTypeTrader::self()->query("ThumbCreator"); for (KService::List::ConstIterator it = plugins.begin(); it != plugins.end(); ++it) if (!result.contains((*it)->desktopEntryName())) result.append((*it)->desktopEntryName()); return result; } QStringList PreviewJob::supportedMimeTypes() { QStringList result; const KService::List plugins = KServiceTypeTrader::self()->query("ThumbCreator"); for (KService::List::ConstIterator it = plugins.begin(); it != plugins.end(); ++it) result += (*it)->serviceTypes(); return result; } #ifndef KDE_NO_DEPRECATED PreviewJob *KIO::filePreview( const KFileItemList &items, int width, int height, int iconSize, int iconAlpha, bool scale, bool save, const QStringList *enabledPlugins ) { return new PreviewJob(items, width, height, iconSize, iconAlpha, scale, save, enabledPlugins); } PreviewJob *KIO::filePreview( const QList &items, int width, int height, int iconSize, int iconAlpha, bool scale, bool save, const QStringList *enabledPlugins ) { KFileItemList fileItems; for (QList::const_iterator it = items.begin(); it != items.end(); ++it) { Q_ASSERT( (*it).isValid() ); // please call us with valid urls only fileItems.append(KFileItem(KFileItem::Unknown, KFileItem::Unknown, *it, true)); } return new PreviewJob(fileItems, width, height, iconSize, iconAlpha, scale, save, enabledPlugins); } #endif PreviewJob *KIO::filePreview(const KFileItemList &items, const QSize &size, const QStringList *enabledPlugins) { return new PreviewJob(items, size, enabledPlugins); } #ifndef KDE_NO_DEPRECATED KIO::filesize_t PreviewJob::maximumFileSize() { KConfigGroup cg( KSharedConfig::openConfig(), "PreviewSettings" ); return cg.readEntry( "MaximumSize", 5*1024*1024LL /* 5MB */ ); } #endif #include "moc_previewjob.cpp"