File indexing completed on 2024-04-21 03:55:01

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"