File indexing completed on 2024-09-15 03:38:26
0001 /* 0002 This file is part of the KDE libraries 0003 SPDX-FileCopyrightText: 2000 Stephan Kulow <coolo@kde.org> 0004 SPDX-FileCopyrightText: 2000-2009 David Faure <faure@kde.org> 0005 SPDX-FileCopyrightText: 2000 Waldo Bastian <bastian@kde.org> 0006 0007 SPDX-License-Identifier: LGPL-2.0-or-later 0008 */ 0009 0010 #include "deletejob.h" 0011 0012 #include "../utils_p.h" 0013 #include "job.h" // buildErrorString 0014 #include "kcoredirlister.h" 0015 #include "kprotocolmanager.h" 0016 #include "listjob.h" 0017 #include "statjob.h" 0018 #include <KDirWatch> 0019 #include <kdirnotify.h> 0020 0021 #include <KLocalizedString> 0022 #include <kio/jobuidelegatefactory.h> 0023 0024 #include <QDir> 0025 #include <QFile> 0026 #include <QFileInfo> 0027 #include <QMetaObject> 0028 #include <QPointer> 0029 #include <QThread> 0030 #include <QTimer> 0031 0032 #include "job_p.h" 0033 0034 extern bool kio_resolve_local_urls; // from copyjob.cpp, abused here to save a symbol. 0035 0036 static bool isHttpProtocol(const QString &protocol) 0037 { 0038 return (protocol.startsWith(QLatin1String("webdav"), Qt::CaseInsensitive) || protocol.startsWith(QLatin1String("http"), Qt::CaseInsensitive)); 0039 } 0040 0041 namespace KIO 0042 { 0043 enum DeleteJobState { 0044 DELETEJOB_STATE_STATING, 0045 DELETEJOB_STATE_DELETING_FILES, 0046 DELETEJOB_STATE_DELETING_DIRS, 0047 }; 0048 0049 class DeleteJobIOWorker : public QObject 0050 { 0051 Q_OBJECT 0052 0053 Q_SIGNALS: 0054 void rmfileResult(bool succeeded, bool isLink); 0055 void rmddirResult(bool succeeded); 0056 0057 public Q_SLOTS: 0058 0059 /** 0060 * Deletes the file @p url points to 0061 * The file must be a LocalFile 0062 */ 0063 void rmfile(const QUrl &url, bool isLink) 0064 { 0065 Q_EMIT rmfileResult(QFile::remove(url.toLocalFile()), isLink); 0066 } 0067 0068 /** 0069 * Deletes the directory @p url points to 0070 * The directory must be a LocalFile 0071 */ 0072 void rmdir(const QUrl &url) 0073 { 0074 Q_EMIT rmddirResult(QDir().rmdir(url.toLocalFile())); 0075 } 0076 }; 0077 0078 class DeleteJobPrivate : public KIO::JobPrivate 0079 { 0080 public: 0081 explicit DeleteJobPrivate(const QList<QUrl> &src) 0082 : state(DELETEJOB_STATE_STATING) 0083 , m_processedFiles(0) 0084 , m_processedDirs(0) 0085 , m_totalFilesDirs(0) 0086 , m_srcList(src) 0087 , m_currentStat(m_srcList.begin()) 0088 , m_reportTimer(nullptr) 0089 { 0090 } 0091 DeleteJobState state; 0092 int m_processedFiles; 0093 int m_processedDirs; 0094 int m_totalFilesDirs; 0095 QUrl m_currentURL; 0096 QList<QUrl> files; 0097 QList<QUrl> symlinks; 0098 QList<QUrl> dirs; 0099 QList<QUrl> m_srcList; 0100 QList<QUrl>::iterator m_currentStat; 0101 QSet<QString> m_parentDirs; 0102 QTimer *m_reportTimer; 0103 DeleteJobIOWorker *m_ioworker = nullptr; 0104 QThread *m_thread = nullptr; 0105 0106 void statNextSrc(); 0107 DeleteJobIOWorker *worker(); 0108 void currentSourceStated(bool isDir, bool isLink); 0109 void finishedStatPhase(); 0110 void deleteNextFile(); 0111 void deleteNextDir(); 0112 void restoreDirWatch() const; 0113 void slotReport(); 0114 void slotStart(); 0115 void slotEntries(KIO::Job *, const KIO::UDSEntryList &list); 0116 0117 /// Callback of worker rmfile 0118 void rmFileResult(bool result, bool isLink); 0119 /// Callback of worker rmdir 0120 void rmdirResult(bool result); 0121 void deleteFileUsingJob(const QUrl &url, bool isLink); 0122 void deleteDirUsingJob(const QUrl &url); 0123 0124 ~DeleteJobPrivate() override; 0125 0126 Q_DECLARE_PUBLIC(DeleteJob) 0127 0128 static inline DeleteJob *newJob(const QList<QUrl> &src, JobFlags flags) 0129 { 0130 DeleteJob *job = new DeleteJob(*new DeleteJobPrivate(src)); 0131 job->setUiDelegate(KIO::createDefaultJobUiDelegate()); 0132 if (!(flags & HideProgressInfo)) { 0133 KIO::getJobTracker()->registerJob(job); 0134 } 0135 if (!(flags & NoPrivilegeExecution)) { 0136 job->d_func()->m_privilegeExecutionEnabled = true; 0137 job->d_func()->m_operationType = Delete; 0138 } 0139 return job; 0140 } 0141 }; 0142 0143 } // namespace KIO 0144 0145 using namespace KIO; 0146 0147 DeleteJob::DeleteJob(DeleteJobPrivate &dd) 0148 : Job(dd) 0149 { 0150 Q_D(DeleteJob); 0151 0152 d->m_reportTimer = new QTimer(this); 0153 connect(d->m_reportTimer, &QTimer::timeout, this, [d]() { 0154 d->slotReport(); 0155 }); 0156 // this will update the report dialog with 5 Hz, I think this is fast enough, aleXXX 0157 d->m_reportTimer->start(200); 0158 0159 QTimer::singleShot(0, this, [d]() { 0160 d->slotStart(); 0161 }); 0162 } 0163 0164 DeleteJob::~DeleteJob() 0165 { 0166 } 0167 0168 DeleteJobPrivate::~DeleteJobPrivate() 0169 { 0170 if (m_thread) { 0171 m_thread->quit(); 0172 m_thread->wait(); 0173 delete m_thread; 0174 } 0175 } 0176 0177 QList<QUrl> DeleteJob::urls() const 0178 { 0179 return d_func()->m_srcList; 0180 } 0181 0182 void DeleteJobPrivate::slotStart() 0183 { 0184 statNextSrc(); 0185 } 0186 0187 DeleteJobIOWorker *DeleteJobPrivate::worker() 0188 { 0189 Q_Q(DeleteJob); 0190 0191 if (!m_ioworker) { 0192 m_thread = new QThread(); 0193 0194 m_ioworker = new DeleteJobIOWorker; 0195 m_ioworker->moveToThread(m_thread); 0196 QObject::connect(m_thread, &QThread::finished, m_ioworker, &QObject::deleteLater); 0197 QObject::connect(m_ioworker, &DeleteJobIOWorker::rmfileResult, q, [=](bool result, bool isLink) { 0198 this->rmFileResult(result, isLink); 0199 }); 0200 QObject::connect(m_ioworker, &DeleteJobIOWorker::rmddirResult, q, [=](bool result) { 0201 this->rmdirResult(result); 0202 }); 0203 m_thread->start(); 0204 } 0205 0206 return m_ioworker; 0207 } 0208 0209 void DeleteJobPrivate::slotReport() 0210 { 0211 Q_Q(DeleteJob); 0212 Q_EMIT q->deleting(q, m_currentURL); 0213 0214 // TODO: maybe we could skip everything else when (flags & HideProgressInfo) ? 0215 JobPrivate::emitDeleting(q, m_currentURL); 0216 0217 switch (state) { 0218 case DELETEJOB_STATE_STATING: 0219 q->setTotalAmount(KJob::Files, files.count()); 0220 q->setTotalAmount(KJob::Directories, dirs.count()); 0221 break; 0222 case DELETEJOB_STATE_DELETING_DIRS: 0223 q->setProcessedAmount(KJob::Directories, m_processedDirs); 0224 q->emitPercent(m_processedFiles + m_processedDirs, m_totalFilesDirs); 0225 break; 0226 case DELETEJOB_STATE_DELETING_FILES: 0227 q->setProcessedAmount(KJob::Files, m_processedFiles); 0228 q->emitPercent(m_processedFiles, m_totalFilesDirs); 0229 break; 0230 } 0231 } 0232 0233 void DeleteJobPrivate::slotEntries(KIO::Job *job, const UDSEntryList &list) 0234 { 0235 UDSEntryList::ConstIterator it = list.begin(); 0236 const UDSEntryList::ConstIterator end = list.end(); 0237 for (; it != end; ++it) { 0238 const UDSEntry &entry = *it; 0239 const QString displayName = entry.stringValue(KIO::UDSEntry::UDS_NAME); 0240 0241 Q_ASSERT(!displayName.isEmpty()); 0242 if (displayName != QLatin1String("..") && displayName != QLatin1String(".")) { 0243 QUrl url; 0244 const QString urlStr = entry.stringValue(KIO::UDSEntry::UDS_URL); 0245 if (!urlStr.isEmpty()) { 0246 url = QUrl(urlStr); 0247 } else { 0248 url = static_cast<SimpleJob *>(job)->url(); // assumed to be a dir 0249 url.setPath(Utils::concatPaths(url.path(), displayName)); 0250 } 0251 0252 // qDebug() << displayName << "(" << url << ")"; 0253 if (entry.isLink()) { 0254 symlinks.append(url); 0255 } else if (entry.isDir()) { 0256 dirs.append(url); 0257 } else { 0258 files.append(url); 0259 } 0260 } 0261 } 0262 } 0263 0264 void DeleteJobPrivate::statNextSrc() 0265 { 0266 Q_Q(DeleteJob); 0267 // qDebug(); 0268 if (m_currentStat != m_srcList.end()) { 0269 m_currentURL = (*m_currentStat); 0270 0271 // if the file system doesn't support deleting, we do not even stat 0272 if (!KProtocolManager::supportsDeleting(m_currentURL)) { 0273 QPointer<DeleteJob> that = q; 0274 ++m_currentStat; 0275 Q_EMIT q->warning(q, buildErrorString(ERR_CANNOT_DELETE, m_currentURL.toDisplayString())); 0276 if (that) { 0277 statNextSrc(); 0278 } 0279 return; 0280 } 0281 // Stat it 0282 state = DELETEJOB_STATE_STATING; 0283 0284 // Fast path for KFileItems in directory views 0285 while (m_currentStat != m_srcList.end()) { 0286 m_currentURL = (*m_currentStat); 0287 const KFileItem cachedItem = KCoreDirLister::cachedItemForUrl(m_currentURL); 0288 if (cachedItem.isNull()) { 0289 break; 0290 } 0291 // qDebug() << "Found cached info about" << m_currentURL << "isDir=" << cachedItem.isDir() << "isLink=" << cachedItem.isLink(); 0292 currentSourceStated(cachedItem.isDir(), cachedItem.isLink()); 0293 ++m_currentStat; 0294 } 0295 0296 // Hook for unit test to disable the fast path. 0297 if (!kio_resolve_local_urls) { 0298 // Fast path for local files 0299 // (using a loop, instead of a huge recursion) 0300 while (m_currentStat != m_srcList.end() && (*m_currentStat).isLocalFile()) { 0301 m_currentURL = (*m_currentStat); 0302 QFileInfo fileInfo(m_currentURL.toLocalFile()); 0303 currentSourceStated(fileInfo.isDir(), fileInfo.isSymLink()); 0304 ++m_currentStat; 0305 } 0306 } 0307 if (m_currentStat == m_srcList.end()) { 0308 // Done, jump to the last else of this method 0309 statNextSrc(); 0310 } else { 0311 KIO::SimpleJob *job = KIO::stat(m_currentURL, StatJob::SourceSide, KIO::StatBasic, KIO::HideProgressInfo); 0312 // qDebug() << "stat'ing" << m_currentURL; 0313 q->addSubjob(job); 0314 } 0315 } else { 0316 if (!q->hasSubjobs()) { // don't go there yet if we're still listing some subdirs 0317 finishedStatPhase(); 0318 } 0319 } 0320 } 0321 0322 void DeleteJobPrivate::finishedStatPhase() 0323 { 0324 m_totalFilesDirs = files.count() + symlinks.count() + dirs.count(); 0325 slotReport(); 0326 // Now we know which dirs hold the files we're going to delete. 0327 // To speed things up and prevent double-notification, we disable KDirWatch 0328 // on those dirs temporarily (using KDirWatch::self, that's the instance 0329 // used by e.g. kdirlister). 0330 for (const QString &dir : std::as_const(m_parentDirs)) { 0331 KDirWatch::self()->stopDirScan(dir); 0332 } 0333 state = DELETEJOB_STATE_DELETING_FILES; 0334 deleteNextFile(); 0335 } 0336 0337 void DeleteJobPrivate::rmFileResult(bool result, bool isLink) 0338 { 0339 if (result) { 0340 m_processedFiles++; 0341 0342 if (isLink) { 0343 symlinks.removeFirst(); 0344 } else { 0345 files.removeFirst(); 0346 } 0347 0348 deleteNextFile(); 0349 } else { 0350 // fallback if QFile::remove() failed (we'll use the job's error handling in that case) 0351 deleteFileUsingJob(m_currentURL, isLink); 0352 } 0353 } 0354 0355 void DeleteJobPrivate::deleteFileUsingJob(const QUrl &url, bool isLink) 0356 { 0357 Q_Q(DeleteJob); 0358 0359 SimpleJob *job; 0360 if (isHttpProtocol(url.scheme())) { 0361 job = KIO::http_delete(url, KIO::HideProgressInfo); 0362 } else { 0363 job = KIO::file_delete(url, KIO::HideProgressInfo); 0364 job->setParentJob(q); 0365 } 0366 0367 if (isLink) { 0368 symlinks.removeFirst(); 0369 } else { 0370 files.removeFirst(); 0371 } 0372 0373 q->addSubjob(job); 0374 } 0375 0376 void DeleteJobPrivate::deleteNextFile() 0377 { 0378 // qDebug(); 0379 0380 // if there is something else to delete 0381 // the loop is run using callbacks slotResult and rmFileResult 0382 if (!files.isEmpty() || !symlinks.isEmpty()) { 0383 // Take first file to delete out of list 0384 QList<QUrl>::iterator it = files.begin(); 0385 const bool isLink = (it == files.end()); // No more files 0386 if (isLink) { 0387 it = symlinks.begin(); // Pick up a symlink to delete 0388 } 0389 m_currentURL = (*it); 0390 0391 // If local file, try do it directly 0392 if (m_currentURL.isLocalFile()) { 0393 // separate thread will do the work 0394 DeleteJobIOWorker *w = worker(); 0395 auto rmfileFunc = [this, w, isLink]() { 0396 w->rmfile(m_currentURL, isLink); 0397 }; 0398 QMetaObject::invokeMethod(w, rmfileFunc, Qt::QueuedConnection); 0399 } else { 0400 // if remote, use a job 0401 deleteFileUsingJob(m_currentURL, isLink); 0402 } 0403 return; 0404 } 0405 0406 state = DELETEJOB_STATE_DELETING_DIRS; 0407 deleteNextDir(); 0408 } 0409 0410 void DeleteJobPrivate::rmdirResult(bool result) 0411 { 0412 if (result) { 0413 m_processedDirs++; 0414 dirs.removeLast(); 0415 deleteNextDir(); 0416 } else { 0417 // fallback 0418 deleteDirUsingJob(m_currentURL); 0419 } 0420 } 0421 0422 void DeleteJobPrivate::deleteDirUsingJob(const QUrl &url) 0423 { 0424 Q_Q(DeleteJob); 0425 0426 // Call rmdir - works for KIO workers with canDeleteRecursive too, 0427 // CMD_DEL will trigger the recursive deletion in the worker. 0428 SimpleJob *job = KIO::rmdir(url); 0429 job->setParentJob(q); 0430 job->addMetaData(QStringLiteral("recurse"), QStringLiteral("true")); 0431 dirs.removeLast(); 0432 q->addSubjob(job); 0433 } 0434 0435 void DeleteJobPrivate::deleteNextDir() 0436 { 0437 Q_Q(DeleteJob); 0438 0439 if (!dirs.isEmpty()) { // some dirs to delete ? 0440 0441 // the loop is run using callbacks slotResult and rmdirResult 0442 // Take first dir to delete out of list - last ones first ! 0443 QList<QUrl>::iterator it = --dirs.end(); 0444 m_currentURL = (*it); 0445 // If local dir, try to rmdir it directly 0446 if (m_currentURL.isLocalFile()) { 0447 // delete it on separate worker thread 0448 DeleteJobIOWorker *w = worker(); 0449 auto rmdirFunc = [this, w]() { 0450 w->rmdir(m_currentURL); 0451 }; 0452 QMetaObject::invokeMethod(w, rmdirFunc, Qt::QueuedConnection); 0453 } else { 0454 deleteDirUsingJob(m_currentURL); 0455 } 0456 return; 0457 } 0458 0459 // Re-enable watching on the dirs that held the deleted files 0460 restoreDirWatch(); 0461 0462 // Finished - tell the world 0463 if (!m_srcList.isEmpty()) { 0464 // qDebug() << "KDirNotify'ing FilesRemoved" << m_srcList; 0465 #ifndef KIO_ANDROID_STUB 0466 org::kde::KDirNotify::emitFilesRemoved(m_srcList); 0467 #endif 0468 } 0469 if (m_reportTimer != nullptr) { 0470 m_reportTimer->stop(); 0471 } 0472 // display final numbers 0473 q->setProcessedAmount(KJob::Directories, m_processedDirs); 0474 q->setProcessedAmount(KJob::Files, m_processedFiles); 0475 q->emitPercent(m_processedFiles + m_processedDirs, m_totalFilesDirs); 0476 0477 q->emitResult(); 0478 } 0479 0480 void DeleteJobPrivate::restoreDirWatch() const 0481 { 0482 const auto itEnd = m_parentDirs.constEnd(); 0483 for (auto it = m_parentDirs.constBegin(); it != itEnd; ++it) { 0484 KDirWatch::self()->restartDirScan(*it); 0485 } 0486 } 0487 0488 void DeleteJobPrivate::currentSourceStated(bool isDir, bool isLink) 0489 { 0490 Q_Q(DeleteJob); 0491 const QUrl url = (*m_currentStat); 0492 if (isDir && !isLink) { 0493 // Add toplevel dir in list of dirs 0494 dirs.append(url); 0495 if (url.isLocalFile()) { 0496 // We are about to delete this dir, no need to watch it 0497 // Maybe we should ask kdirwatch to remove all watches recursively? 0498 // But then there would be no feedback (things disappearing progressively) during huge deletions 0499 KDirWatch::self()->stopDirScan(url.adjusted(QUrl::StripTrailingSlash).toLocalFile()); 0500 } 0501 if (!KProtocolManager::canDeleteRecursive(url)) { 0502 // qDebug() << url << "is a directory, let's list it"; 0503 ListJob *newjob = KIO::listRecursive(url, KIO::HideProgressInfo); 0504 newjob->addMetaData(QStringLiteral("details"), QString::number(KIO::StatBasic)); 0505 newjob->setUnrestricted(true); // No KIOSK restrictions 0506 QObject::connect(newjob, &KIO::ListJob::entries, q, [this](KIO::Job *job, const KIO::UDSEntryList &list) { 0507 slotEntries(job, list); 0508 }); 0509 q->addSubjob(newjob); 0510 // Note that this listing job will happen in parallel with other stat jobs. 0511 } 0512 } else { 0513 if (isLink) { 0514 // qDebug() << "Target is a symlink"; 0515 symlinks.append(url); 0516 } else { 0517 // qDebug() << "Target is a file"; 0518 files.append(url); 0519 } 0520 } 0521 if (url.isLocalFile()) { 0522 const QString parentDir = url.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).path(); 0523 m_parentDirs.insert(parentDir); 0524 } 0525 } 0526 0527 void DeleteJob::slotResult(KJob *job) 0528 { 0529 Q_D(DeleteJob); 0530 switch (d->state) { 0531 case DELETEJOB_STATE_STATING: 0532 removeSubjob(job); 0533 0534 // Was this a stat job or a list job? We do both in parallel. 0535 if (StatJob *statJob = qobject_cast<StatJob *>(job)) { 0536 // Was there an error while stating ? 0537 if (job->error()) { 0538 // Probably : doesn't exist 0539 Job::slotResult(job); // will set the error and emit result(this) 0540 d->restoreDirWatch(); 0541 return; 0542 } 0543 0544 const UDSEntry &entry = statJob->statResult(); 0545 // Is it a file or a dir ? 0546 const bool isLink = entry.isLink(); 0547 const bool isDir = entry.isDir(); 0548 d->currentSourceStated(isDir, isLink); 0549 0550 ++d->m_currentStat; 0551 d->statNextSrc(); 0552 } else { 0553 if (job->error()) { 0554 // Try deleting nonetheless, it may be empty (and non-listable) 0555 } 0556 if (!hasSubjobs()) { 0557 d->finishedStatPhase(); 0558 } 0559 } 0560 break; 0561 case DELETEJOB_STATE_DELETING_FILES: 0562 // Propagate the subjob's metadata (a SimpleJob) to the real DeleteJob 0563 // FIXME: setMetaData() in the KIO API only allows access to outgoing metadata, 0564 // but we need to alter the incoming one 0565 d->m_incomingMetaData = dynamic_cast<KIO::Job *>(job)->metaData(); 0566 0567 if (job->error()) { 0568 Job::slotResult(job); // will set the error and emit result(this) 0569 d->restoreDirWatch(); 0570 return; 0571 } 0572 removeSubjob(job); 0573 Q_ASSERT(!hasSubjobs()); 0574 d->m_processedFiles++; 0575 0576 d->deleteNextFile(); 0577 break; 0578 case DELETEJOB_STATE_DELETING_DIRS: 0579 if (job->error()) { 0580 Job::slotResult(job); // will set the error and emit result(this) 0581 d->restoreDirWatch(); 0582 return; 0583 } 0584 removeSubjob(job); 0585 Q_ASSERT(!hasSubjobs()); 0586 d->m_processedDirs++; 0587 // emit processedAmount( this, KJob::Directories, d->m_processedDirs ); 0588 // emitPercent( d->m_processedFiles + d->m_processedDirs, d->m_totalFilesDirs ); 0589 0590 d->deleteNextDir(); 0591 break; 0592 default: 0593 Q_ASSERT(0); 0594 } 0595 } 0596 0597 DeleteJob *KIO::del(const QUrl &src, JobFlags flags) 0598 { 0599 QList<QUrl> srcList; 0600 srcList.append(src); 0601 DeleteJob *job = DeleteJobPrivate::newJob(srcList, flags); 0602 if (job->uiDelegateExtension()) { 0603 job->uiDelegateExtension()->createClipboardUpdater(job, JobUiDelegateExtension::RemoveContent); 0604 } 0605 return job; 0606 } 0607 0608 DeleteJob *KIO::del(const QList<QUrl> &src, JobFlags flags) 0609 { 0610 DeleteJob *job = DeleteJobPrivate::newJob(src, flags); 0611 if (job->uiDelegateExtension()) { 0612 job->uiDelegateExtension()->createClipboardUpdater(job, JobUiDelegateExtension::RemoveContent); 0613 } 0614 return job; 0615 } 0616 0617 #include "deletejob.moc" 0618 #include "moc_deletejob.cpp"