File indexing completed on 2024-05-05 03:55:32

0001 /*
0002     This file is part of the KDE libraries
0003     SPDX-FileCopyrightText: 2000 Stephan Kulow <coolo@kde.org>
0004     SPDX-FileCopyrightText: 2000-2006 David Faure <faure@kde.org>
0005     SPDX-FileCopyrightText: 2000 Waldo Bastian <bastian@kde.org>
0006     SPDX-FileCopyrightText: 2021 Ahmad Samir <a.samirh78@gmail.com>
0007 
0008     SPDX-License-Identifier: LGPL-2.0-or-later
0009 */
0010 
0011 #include "copyjob.h"
0012 #include "../utils_p.h"
0013 #include "deletejob.h"
0014 #include "filecopyjob.h"
0015 #include "global.h"
0016 #include "job.h" // buildErrorString
0017 #include "kcoredirlister.h"
0018 #include "kfileitem.h"
0019 #include "kiocoredebug.h"
0020 #include "kioglobal_p.h"
0021 #include "listjob.h"
0022 #include "mkdirjob.h"
0023 #include "statjob.h"
0024 #include <cerrno>
0025 
0026 #include <KConfigGroup>
0027 #include <KDesktopFile>
0028 #include <KLocalizedString>
0029 
0030 #include "kprotocolmanager.h"
0031 #include "worker_p.h"
0032 #include <KDirWatch>
0033 
0034 #include "askuseractioninterface.h"
0035 #include <jobuidelegateextension.h>
0036 #include <kio/jobuidelegatefactory.h>
0037 
0038 #include <kdirnotify.h>
0039 
0040 #ifdef Q_OS_UNIX
0041 #include <utime.h>
0042 #endif
0043 
0044 #include <QDateTime>
0045 #include <QFile>
0046 #include <QFileInfo>
0047 #include <QPointer>
0048 #include <QTemporaryFile>
0049 #include <QTimeZone>
0050 #include <QTimer>
0051 
0052 #include <sys/stat.h> // mode_t
0053 
0054 #include "job_p.h"
0055 #include <KFileSystemType>
0056 #include <KFileUtils>
0057 #include <KIO/FileSystemFreeSpaceJob>
0058 
0059 #include <list>
0060 #include <set>
0061 
0062 #include <QLoggingCategory>
0063 Q_DECLARE_LOGGING_CATEGORY(KIO_COPYJOB_DEBUG)
0064 Q_LOGGING_CATEGORY(KIO_COPYJOB_DEBUG, "kf.kio.core.copyjob", QtWarningMsg)
0065 
0066 using namespace KIO;
0067 
0068 // this will update the report dialog with 5 Hz, I think this is fast enough, aleXXX
0069 static constexpr int s_reportTimeout = 200;
0070 
0071 #if !defined(NAME_MAX)
0072 #if defined(_MAX_FNAME)
0073 static constexpr int NAME_MAX = _MAX_FNAME; // For Windows
0074 #else
0075 static constexpr NAME_MAX = 0;
0076 #endif
0077 #endif
0078 
0079 enum DestinationState {
0080     DEST_NOT_STATED,
0081     DEST_IS_DIR,
0082     DEST_IS_FILE,
0083     DEST_DOESNT_EXIST,
0084 };
0085 
0086 /**
0087  * States:
0088  *     STATE_INITIAL the constructor was called
0089  *     STATE_STATING for the dest
0090  *     statCurrentSrc then does, for each src url:
0091  *      STATE_RENAMING if direct rename looks possible
0092  *         (on already exists, and user chooses rename, TODO: go to STATE_RENAMING again)
0093  *      STATE_STATING
0094  *         and then, if dir -> STATE_LISTING (filling 'd->dirs' and 'd->files')
0095  *     STATE_CREATING_DIRS (createNextDir, iterating over 'd->dirs')
0096  *          if conflict: STATE_CONFLICT_CREATING_DIRS
0097  *     STATE_COPYING_FILES (copyNextFile, iterating over 'd->files')
0098  *          if conflict: STATE_CONFLICT_COPYING_FILES
0099  *     STATE_DELETING_DIRS (deleteNextDir) (if moving)
0100  *     STATE_SETTING_DIR_ATTRIBUTES (setNextDirAttribute, iterating over d->m_directoriesCopied)
0101  *     done.
0102  */
0103 enum CopyJobState {
0104     STATE_INITIAL,
0105     STATE_STATING,
0106     STATE_RENAMING,
0107     STATE_LISTING,
0108     STATE_CREATING_DIRS,
0109     STATE_CONFLICT_CREATING_DIRS,
0110     STATE_COPYING_FILES,
0111     STATE_CONFLICT_COPYING_FILES,
0112     STATE_DELETING_DIRS,
0113     STATE_SETTING_DIR_ATTRIBUTES,
0114 };
0115 
0116 static QUrl addPathToUrl(const QUrl &url, const QString &relPath)
0117 {
0118     QUrl u(url);
0119     u.setPath(Utils::concatPaths(url.path(), relPath));
0120     return u;
0121 }
0122 
0123 static bool compareUrls(const QUrl &srcUrl, const QUrl &destUrl)
0124 {
0125     /* clang-format off */
0126     return srcUrl.scheme() == destUrl.scheme()
0127         && srcUrl.host() == destUrl.host()
0128         && srcUrl.port() == destUrl.port()
0129         && srcUrl.userName() == destUrl.userName()
0130         && srcUrl.password() == destUrl.password();
0131     /* clang-format on */
0132 }
0133 
0134 // https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file#naming-conventions
0135 static const char s_msdosInvalidChars[] = R"(<>:"/\|?*)";
0136 
0137 static bool hasInvalidChars(const QString &dest)
0138 {
0139     return std::any_of(std::begin(s_msdosInvalidChars), std::end(s_msdosInvalidChars), [=](const char c) {
0140         return dest.contains(QLatin1Char(c));
0141     });
0142 }
0143 
0144 static void cleanMsdosDestName(QString &name)
0145 {
0146     for (const char c : s_msdosInvalidChars) {
0147         name.replace(QLatin1Char(c), QLatin1String("_"));
0148     }
0149 }
0150 
0151 static bool isFatFs(KFileSystemType::Type fsType)
0152 {
0153     return fsType == KFileSystemType::Fat || fsType == KFileSystemType::Exfat;
0154 }
0155 
0156 static bool isFatOrNtfs(KFileSystemType::Type fsType)
0157 {
0158     return fsType == KFileSystemType::Ntfs || isFatFs(fsType);
0159 }
0160 
0161 static QString symlinkSupportMsg(const QString &path, const QString &fsName)
0162 {
0163     const QString msg = i18nc(
0164         "The first arg is the path to the symlink that couldn't be created, the second"
0165         "arg is the filesystem type (e.g. vfat, exfat)",
0166         "Could not create symlink \"%1\".\n"
0167         "The destination filesystem (%2) doesn't support symlinks.",
0168         path,
0169         fsName);
0170     return msg;
0171 }
0172 
0173 static QString invalidCharsSupportMsg(const QString &path, const QString &fsName, bool isDir = false)
0174 {
0175     QString msg;
0176     if (isDir) {
0177         msg = i18n(
0178             "Could not create \"%1\".\n"
0179             "The destination filesystem (%2) disallows the following characters in folder names: %3\n"
0180             "Selecting Replace will replace any invalid characters (in the destination folder name) with an underscore \"_\".",
0181             path,
0182             fsName,
0183             QLatin1String(s_msdosInvalidChars));
0184     } else {
0185         msg = i18n(
0186             "Could not create \"%1\".\n"
0187             "The destination filesystem (%2) disallows the following characters in file names: %3\n"
0188             "Selecting Replace will replace any invalid characters (in the destination file name) with an underscore \"_\".",
0189             path,
0190             fsName,
0191             QLatin1String(s_msdosInvalidChars));
0192     }
0193 
0194     return msg;
0195 }
0196 
0197 /** @internal */
0198 struct CopyInfo {
0199     QUrl uSource;
0200     QUrl uDest;
0201     QString linkDest; // for symlinks only
0202     int permissions;
0203     QDateTime ctime;
0204     QDateTime mtime;
0205     KIO::filesize_t size; // 0 for dirs
0206 };
0207 
0208 /** @internal */
0209 class KIO::CopyJobPrivate : public KIO::JobPrivate
0210 {
0211 public:
0212     CopyJobPrivate(const QList<QUrl> &src, const QUrl &dest, CopyJob::CopyMode mode, bool asMethod)
0213         : m_globalDest(dest)
0214         , m_globalDestinationState(DEST_NOT_STATED)
0215         , m_defaultPermissions(false)
0216         , m_bURLDirty(false)
0217         , m_mode(mode)
0218         , m_asMethod(asMethod)
0219         , destinationState(DEST_NOT_STATED)
0220         , state(STATE_INITIAL)
0221         , m_freeSpace(-1)
0222         , m_totalSize(0)
0223         , m_processedSize(0)
0224         , m_fileProcessedSize(0)
0225         , m_filesHandledByDirectRename(0)
0226         , m_processedFiles(0)
0227         , m_processedDirs(0)
0228         , m_srcList(src)
0229         , m_currentStatSrc(m_srcList.constBegin())
0230         , m_bCurrentOperationIsLink(false)
0231         , m_bSingleFileCopy(false)
0232         , m_bOnlyRenames(mode == CopyJob::Move)
0233         , m_dest(dest)
0234         , m_bAutoRenameFiles(false)
0235         , m_bAutoRenameDirs(false)
0236         , m_bAutoSkipFiles(false)
0237         , m_bAutoSkipDirs(false)
0238         , m_bOverwriteAllFiles(false)
0239         , m_bOverwriteAllDirs(false)
0240         , m_bOverwriteWhenOlder(false)
0241         , m_conflictError(0)
0242         , m_reportTimer(nullptr)
0243     {
0244     }
0245 
0246     // This is the dest URL that was initially given to CopyJob
0247     // It is copied into m_dest, which can be changed for a given src URL
0248     // (when using the RENAME dialog in slotResult),
0249     // and which will be reset for the next src URL.
0250     QUrl m_globalDest;
0251     // The state info about that global dest
0252     DestinationState m_globalDestinationState;
0253     // See setDefaultPermissions
0254     bool m_defaultPermissions;
0255     // Whether URLs changed (and need to be emitted by the next slotReport call)
0256     bool m_bURLDirty;
0257     // Used after copying all the files into the dirs, to set mtime (TODO: and permissions?)
0258     // after the copy is done
0259     std::list<CopyInfo> m_directoriesCopied;
0260     std::list<CopyInfo>::const_iterator m_directoriesCopiedIterator;
0261 
0262     CopyJob::CopyMode m_mode;
0263     bool m_asMethod; // See copyAs() method
0264     DestinationState destinationState;
0265     CopyJobState state;
0266 
0267     KIO::filesize_t m_freeSpace;
0268 
0269     KIO::filesize_t m_totalSize;
0270     KIO::filesize_t m_processedSize;
0271     KIO::filesize_t m_fileProcessedSize;
0272     int m_filesHandledByDirectRename;
0273     int m_processedFiles;
0274     int m_processedDirs;
0275     QList<CopyInfo> files;
0276     QList<CopyInfo> dirs;
0277     // List of dirs that will be copied then deleted when CopyMode is Move
0278     QList<QUrl> dirsToRemove;
0279     QList<QUrl> m_srcList;
0280     QList<QUrl> m_successSrcList; // Entries in m_srcList that have successfully been moved
0281     QList<QUrl>::const_iterator m_currentStatSrc;
0282     bool m_bCurrentSrcIsDir;
0283     bool m_bCurrentOperationIsLink;
0284     bool m_bSingleFileCopy;
0285     bool m_bOnlyRenames;
0286     QUrl m_dest;
0287     QUrl m_currentDest; // set during listing, used by slotEntries
0288     //
0289     QStringList m_skipList;
0290     QSet<QString> m_overwriteList;
0291     bool m_bAutoRenameFiles;
0292     bool m_bAutoRenameDirs;
0293     bool m_bAutoSkipFiles;
0294     bool m_bAutoSkipDirs;
0295     bool m_bOverwriteAllFiles;
0296     bool m_bOverwriteAllDirs;
0297     bool m_bOverwriteWhenOlder;
0298 
0299     bool m_autoSkipDirsWithInvalidChars = false;
0300     bool m_autoSkipFilesWithInvalidChars = false;
0301     bool m_autoReplaceInvalidChars = false;
0302 
0303     bool m_autoSkipFatSymlinks = false;
0304 
0305     enum SkipType {
0306         // No skip dialog is involved
0307         NoSkipType = 0,
0308         // SkipDialog is asking about invalid chars in destination file/dir names
0309         SkipInvalidChars,
0310         // SkipDialog is asking about how to handle symlinks why copying to a
0311         // filesystem that doesn't support symlinks
0312         SkipFatSymlinks,
0313     };
0314 
0315     int m_conflictError;
0316 
0317     QTimer *m_reportTimer;
0318 
0319     // The current src url being stat'ed or copied
0320     // During the stat phase, this is initially equal to *m_currentStatSrc but it can be resolved to a local file equivalent (#188903).
0321     QUrl m_currentSrcURL;
0322     QUrl m_currentDestURL;
0323 
0324     std::set<QString> m_parentDirs;
0325     bool m_ignoreSourcePermissions = false;
0326 
0327     void statCurrentSrc();
0328     void statNextSrc();
0329 
0330     // Those aren't slots but submethods for slotResult.
0331     void slotResultStating(KJob *job);
0332     void startListing(const QUrl &src);
0333 
0334     void slotResultCreatingDirs(KJob *job);
0335     void slotResultConflictCreatingDirs(KJob *job);
0336     void createNextDir();
0337     void processCreateNextDir(const QList<CopyInfo>::Iterator &it, int result);
0338 
0339     void slotResultCopyingFiles(KJob *job);
0340     void slotResultErrorCopyingFiles(KJob *job);
0341     void processFileRenameDialogResult(const QList<CopyInfo>::Iterator &it, RenameDialog_Result result, const QUrl &newUrl, const QDateTime &destmtime);
0342 
0343     //     KIO::Job* linkNextFile( const QUrl& uSource, const QUrl& uDest, bool overwrite );
0344     KIO::Job *linkNextFile(const QUrl &uSource, const QUrl &uDest, JobFlags flags);
0345     // MsDos filesystems don't allow certain characters in filenames, and VFAT and ExFAT
0346     // don't support symlinks, this method detects those conditions and tries to handle it
0347     bool handleMsdosFsQuirks(QList<CopyInfo>::Iterator it, KFileSystemType::Type fsType);
0348     void copyNextFile();
0349     void processCopyNextFile(const QList<CopyInfo>::Iterator &it, int result, SkipType skipType);
0350 
0351     void slotResultDeletingDirs(KJob *job);
0352     void deleteNextDir();
0353     void sourceStated(const UDSEntry &entry, const QUrl &sourceUrl);
0354     // Removes a dir from the "dirsToRemove" list
0355     void skip(const QUrl &sourceURL, bool isDir);
0356 
0357     void slotResultRenaming(KJob *job);
0358     void directRenamingFailed(const QUrl &dest);
0359     void processDirectRenamingConflictResult(RenameDialog_Result result,
0360                                              bool srcIsDir,
0361                                              bool destIsDir,
0362                                              const QDateTime &mtimeSrc,
0363                                              const QDateTime &mtimeDest,
0364                                              const QUrl &dest,
0365                                              const QUrl &newUrl);
0366 
0367     void slotResultSettingDirAttributes(KJob *job);
0368     void setNextDirAttribute();
0369 
0370     void startRenameJob(const QUrl &workerUrl);
0371     bool shouldOverwriteDir(const QString &path) const;
0372     bool shouldOverwriteFile(const QString &path) const;
0373     bool shouldSkip(const QString &path) const;
0374     void skipSrc(bool isDir);
0375     void renameDirectory(const QList<CopyInfo>::iterator &it, const QUrl &newUrl);
0376     QUrl finalDestUrl(const QUrl &src, const QUrl &dest) const;
0377 
0378     void slotStart();
0379     void slotEntries(KIO::Job *, const KIO::UDSEntryList &list);
0380     void slotSubError(KIO::ListJob *job, KIO::ListJob *subJob);
0381     void addCopyInfoFromUDSEntry(const UDSEntry &entry, const QUrl &srcUrl, bool srcIsDir, const QUrl &currentDest);
0382     /**
0383      * Forward signal from subjob
0384      */
0385     void slotProcessedSize(KJob *, qulonglong data_size);
0386     /**
0387      * Forward signal from subjob
0388      * @param size the total size
0389      */
0390     void slotTotalSize(KJob *, qulonglong size);
0391 
0392     void slotReport();
0393 
0394     Q_DECLARE_PUBLIC(CopyJob)
0395 
0396     static inline CopyJob *newJob(const QList<QUrl> &src, const QUrl &dest, CopyJob::CopyMode mode, bool asMethod, JobFlags flags)
0397     {
0398         CopyJob *job = new CopyJob(*new CopyJobPrivate(src, dest, mode, asMethod));
0399         job->setUiDelegate(KIO::createDefaultJobUiDelegate());
0400         if (!(flags & HideProgressInfo)) {
0401             KIO::getJobTracker()->registerJob(job);
0402         }
0403         if (flags & KIO::Overwrite) {
0404             job->d_func()->m_bOverwriteAllDirs = true;
0405             job->d_func()->m_bOverwriteAllFiles = true;
0406         }
0407         if (!(flags & KIO::NoPrivilegeExecution)) {
0408             job->d_func()->m_privilegeExecutionEnabled = true;
0409             FileOperationType copyType;
0410             switch (mode) {
0411             case CopyJob::Copy:
0412                 copyType = Copy;
0413                 break;
0414             case CopyJob::Move:
0415                 copyType = Move;
0416                 break;
0417             case CopyJob::Link:
0418                 copyType = Symlink;
0419                 break;
0420             default:
0421                 Q_UNREACHABLE();
0422             }
0423             job->d_func()->m_operationType = copyType;
0424         }
0425         return job;
0426     }
0427 };
0428 
0429 CopyJob::CopyJob(CopyJobPrivate &dd)
0430     : Job(dd)
0431 {
0432     Q_D(CopyJob);
0433     setProperty("destUrl", d_func()->m_dest.toString());
0434     QTimer::singleShot(0, this, [d]() {
0435         d->slotStart();
0436     });
0437     qRegisterMetaType<KIO::UDSEntry>();
0438 }
0439 
0440 CopyJob::~CopyJob()
0441 {
0442 }
0443 
0444 QList<QUrl> CopyJob::srcUrls() const
0445 {
0446     return d_func()->m_srcList;
0447 }
0448 
0449 QUrl CopyJob::destUrl() const
0450 {
0451     return d_func()->m_dest;
0452 }
0453 
0454 void CopyJobPrivate::slotStart()
0455 {
0456     Q_Q(CopyJob);
0457     if (q->isSuspended()) {
0458         return;
0459     }
0460 
0461     if (m_mode == CopyJob::CopyMode::Move) {
0462         for (const QUrl &url : std::as_const(m_srcList)) {
0463             if (m_dest.scheme() == url.scheme() && m_dest.host() == url.host()) {
0464                 const QString srcPath = Utils::slashAppended(url.path());
0465                 if (m_dest.path().startsWith(srcPath)) {
0466                     q->setError(KIO::ERR_CANNOT_MOVE_INTO_ITSELF);
0467                     q->emitResult();
0468                     return;
0469                 }
0470             }
0471         }
0472     }
0473 
0474     if (m_mode == CopyJob::CopyMode::Link && m_globalDest.isLocalFile()) {
0475         const QString destPath = m_globalDest.toLocalFile();
0476         const auto destFs = KFileSystemType::fileSystemType(destPath);
0477         if (isFatFs(destFs)) {
0478             q->setError(ERR_SYMLINKS_NOT_SUPPORTED);
0479             const QString errText = destPath + QLatin1String(" [") + KFileSystemType::fileSystemName(destFs) + QLatin1Char(']');
0480             q->setErrorText(errText);
0481             q->emitResult();
0482             return;
0483         }
0484     }
0485 
0486     /**
0487        We call the functions directly instead of using signals.
0488        Calling a function via a signal takes approx. 65 times the time
0489        compared to calling it directly (at least on my machine). aleXXX
0490     */
0491     m_reportTimer = new QTimer(q);
0492 
0493     q->connect(m_reportTimer, &QTimer::timeout, q, [this]() {
0494         slotReport();
0495     });
0496     m_reportTimer->start(s_reportTimeout);
0497 
0498     // Stat the dest
0499     state = STATE_STATING;
0500     const QUrl dest = m_asMethod ? m_dest.adjusted(QUrl::RemoveFilename) : m_dest;
0501     // We need isDir() and UDS_LOCAL_PATH (for workers who set it). Let's assume the latter is part of StatBasic too.
0502     KIO::Job *job = KIO::stat(dest, StatJob::DestinationSide, KIO::StatBasic | KIO::StatResolveSymlink, KIO::HideProgressInfo);
0503     qCDebug(KIO_COPYJOB_DEBUG) << "CopyJob: stating the dest" << dest;
0504     q->addSubjob(job);
0505 }
0506 
0507 // For unit test purposes
0508 KIOCORE_EXPORT bool kio_resolve_local_urls = true;
0509 
0510 void CopyJobPrivate::slotResultStating(KJob *job)
0511 {
0512     Q_Q(CopyJob);
0513     qCDebug(KIO_COPYJOB_DEBUG);
0514     // Was there an error while stating the src ?
0515     if (job->error() && destinationState != DEST_NOT_STATED) {
0516         const QUrl srcurl = static_cast<SimpleJob *>(job)->url();
0517         if (!srcurl.isLocalFile()) {
0518             // Probably : src doesn't exist. Well, over some protocols (e.g. FTP)
0519             // this info isn't really reliable (thanks to MS FTP servers).
0520             // We'll assume a file, and try to download anyway.
0521             qCDebug(KIO_COPYJOB_DEBUG) << "Error while stating source. Activating hack";
0522             q->removeSubjob(job);
0523             Q_ASSERT(!q->hasSubjobs()); // We should have only one job at a time ...
0524             struct CopyInfo info;
0525             info.permissions = (mode_t)-1;
0526             info.size = KIO::invalidFilesize;
0527             info.uSource = srcurl;
0528             info.uDest = m_dest;
0529             // Append filename or dirname to destination URL, if allowed
0530             if (destinationState == DEST_IS_DIR && !m_asMethod) {
0531                 const QString fileName = srcurl.scheme() == QLatin1String("data") ? QStringLiteral("data") : srcurl.fileName(); // #379093
0532                 info.uDest = addPathToUrl(info.uDest, fileName);
0533             }
0534 
0535             files.append(info);
0536             statNextSrc();
0537             return;
0538         }
0539         // Local file. If stat fails, the file definitely doesn't exist.
0540         // yes, q->Job::, because we don't want to call our override
0541         q->Job::slotResult(job); // will set the error and emit result(this)
0542         return;
0543     }
0544 
0545     // Keep copy of the stat result
0546     auto statJob = static_cast<StatJob *>(job);
0547     const UDSEntry entry = statJob->statResult();
0548 
0549     if (destinationState == DEST_NOT_STATED) {
0550         const bool isGlobalDest = m_dest == m_globalDest;
0551 
0552         // we were stating the dest
0553         if (job->error()) {
0554             destinationState = DEST_DOESNT_EXIST;
0555             qCDebug(KIO_COPYJOB_DEBUG) << "dest does not exist";
0556         } else {
0557             const bool isDir = entry.isDir();
0558 
0559             // Check for writability, before spending time stat'ing everything (#141564).
0560             // This assumes all KIO workers set permissions correctly...
0561             const int permissions = entry.numberValue(KIO::UDSEntry::UDS_ACCESS, -1);
0562             const bool isWritable = (permissions != -1) && (permissions & S_IWUSR);
0563             if (!m_privilegeExecutionEnabled && !isWritable) {
0564                 const QUrl dest = m_asMethod ? m_dest.adjusted(QUrl::RemoveFilename) : m_dest;
0565                 q->setError(ERR_WRITE_ACCESS_DENIED);
0566                 q->setErrorText(dest.toDisplayString(QUrl::PreferLocalFile));
0567                 q->emitResult();
0568                 return;
0569             }
0570 
0571             // Treat symlinks to dirs as dirs here, so no test on isLink
0572             destinationState = isDir ? DEST_IS_DIR : DEST_IS_FILE;
0573             qCDebug(KIO_COPYJOB_DEBUG) << "dest is dir:" << isDir;
0574 
0575             if (isGlobalDest) {
0576                 m_globalDestinationState = destinationState;
0577             }
0578 
0579             const QString sLocalPath = entry.stringValue(KIO::UDSEntry::UDS_LOCAL_PATH);
0580             if (!sLocalPath.isEmpty() && kio_resolve_local_urls && statJob->url().scheme() != QStringLiteral("trash")) {
0581                 const QString fileName = m_dest.fileName();
0582                 m_dest = QUrl::fromLocalFile(sLocalPath);
0583                 if (m_asMethod) {
0584                     m_dest = addPathToUrl(m_dest, fileName);
0585                 }
0586                 qCDebug(KIO_COPYJOB_DEBUG) << "Setting m_dest to the local path:" << sLocalPath;
0587                 if (isGlobalDest) {
0588                     m_globalDest = m_dest;
0589                 }
0590             }
0591         }
0592 
0593         q->removeSubjob(job);
0594         Q_ASSERT(!q->hasSubjobs());
0595 
0596         // In copy-as mode, we want to check the directory to which we're
0597         // copying. The target file or directory does not exist yet, which
0598         // might confuse FileSystemFreeSpaceJob.
0599         const QUrl existingDest = m_asMethod ? m_dest.adjusted(QUrl::RemoveFilename) : m_dest;
0600         KIO::FileSystemFreeSpaceJob *spaceJob = KIO::fileSystemFreeSpace(existingDest);
0601         q->connect(spaceJob, &KJob::result, q, [this, existingDest, spaceJob]() {
0602             if (!spaceJob->error()) {
0603                 m_freeSpace = spaceJob->availableSize();
0604             } else {
0605                 qCDebug(KIO_COPYJOB_DEBUG) << "Couldn't determine free space information for" << existingDest;
0606             }
0607             // After knowing what the dest is, we can start stat'ing the first src.
0608             statCurrentSrc();
0609         });
0610         return;
0611     } else {
0612         sourceStated(entry, static_cast<SimpleJob *>(job)->url());
0613         q->removeSubjob(job);
0614     }
0615 }
0616 
0617 void CopyJobPrivate::sourceStated(const UDSEntry &entry, const QUrl &sourceUrl)
0618 {
0619     const QString sLocalPath = sourceUrl.scheme() != QStringLiteral("trash") ? entry.stringValue(KIO::UDSEntry::UDS_LOCAL_PATH) : QString();
0620     const bool isDir = entry.isDir();
0621 
0622     // We were stating the current source URL
0623     // Is it a file or a dir ?
0624 
0625     // There 6 cases, and all end up calling addCopyInfoFromUDSEntry first :
0626     // 1 - src is a dir, destination is a directory,
0627     // slotEntries will append the source-dir-name to the destination
0628     // 2 - src is a dir, destination is a file -- will offer to overwrite, later on.
0629     // 3 - src is a dir, destination doesn't exist, then it's the destination dirname,
0630     // so slotEntries will use it as destination.
0631 
0632     // 4 - src is a file, destination is a directory,
0633     // slotEntries will append the filename to the destination.
0634     // 5 - src is a file, destination is a file, m_dest is the exact destination name
0635     // 6 - src is a file, destination doesn't exist, m_dest is the exact destination name
0636 
0637     QUrl srcurl;
0638     if (!sLocalPath.isEmpty() && destinationState != DEST_DOESNT_EXIST) {
0639         qCDebug(KIO_COPYJOB_DEBUG) << "Using sLocalPath. destinationState=" << destinationState;
0640         // Prefer the local path -- but only if we were able to stat() the dest.
0641         // Otherwise, renaming a desktop:/ url would copy from src=file to dest=desktop (#218719)
0642         srcurl = QUrl::fromLocalFile(sLocalPath);
0643     } else {
0644         srcurl = sourceUrl;
0645     }
0646     addCopyInfoFromUDSEntry(entry, srcurl, false, m_dest);
0647 
0648     m_currentDest = m_dest;
0649     m_bCurrentSrcIsDir = false;
0650 
0651     if (isDir //
0652         && !entry.isLink() // treat symlinks as files (no recursion)
0653         && m_mode != CopyJob::Link) { // No recursion in Link mode either.
0654         qCDebug(KIO_COPYJOB_DEBUG) << "Source is a directory";
0655 
0656         if (srcurl.isLocalFile()) {
0657             const QString parentDir = srcurl.adjusted(QUrl::StripTrailingSlash).toLocalFile();
0658             m_parentDirs.insert(parentDir);
0659         }
0660 
0661         m_bCurrentSrcIsDir = true; // used by slotEntries
0662         if (destinationState == DEST_IS_DIR) { // (case 1)
0663             if (!m_asMethod) {
0664                 // Use <desturl>/<directory_copied> as destination, from now on
0665                 QString directory = srcurl.fileName();
0666                 const QString sName = entry.stringValue(KIO::UDSEntry::UDS_NAME);
0667                 KProtocolInfo::FileNameUsedForCopying fnu = KProtocolManager::fileNameUsedForCopying(srcurl);
0668                 if (fnu == KProtocolInfo::Name) {
0669                     if (!sName.isEmpty()) {
0670                         directory = sName;
0671                     }
0672                 } else if (fnu == KProtocolInfo::DisplayName) {
0673                     const QString dispName = entry.stringValue(KIO::UDSEntry::UDS_DISPLAY_NAME);
0674                     if (!dispName.isEmpty()) {
0675                         directory = dispName;
0676                     } else if (!sName.isEmpty()) {
0677                         directory = sName;
0678                     }
0679                 }
0680                 m_currentDest = addPathToUrl(m_currentDest, directory);
0681             }
0682         } else { // (case 3)
0683             // otherwise dest is new name for toplevel dir
0684             // so the destination exists, in fact, from now on.
0685             // (This even works with other src urls in the list, since the
0686             //  dir has effectively been created)
0687             destinationState = DEST_IS_DIR;
0688             if (m_dest == m_globalDest) {
0689                 m_globalDestinationState = destinationState;
0690             }
0691         }
0692 
0693         startListing(srcurl);
0694     } else {
0695         qCDebug(KIO_COPYJOB_DEBUG) << "Source is a file (or a symlink), or we are linking -> no recursive listing";
0696 
0697         if (srcurl.isLocalFile()) {
0698             const QString parentDir = srcurl.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).path();
0699             m_parentDirs.insert(parentDir);
0700         }
0701 
0702         statNextSrc();
0703     }
0704 }
0705 
0706 bool CopyJob::doSuspend()
0707 {
0708     Q_D(CopyJob);
0709     d->slotReport();
0710     return Job::doSuspend();
0711 }
0712 
0713 bool CopyJob::doResume()
0714 {
0715     Q_D(CopyJob);
0716     switch (d->state) {
0717     case STATE_INITIAL:
0718         QTimer::singleShot(0, this, [d]() {
0719             d->slotStart();
0720         });
0721         break;
0722     default:
0723         // not implemented
0724         break;
0725     }
0726     return Job::doResume();
0727 }
0728 
0729 void CopyJobPrivate::slotReport()
0730 {
0731     Q_Q(CopyJob);
0732     if (q->isSuspended()) {
0733         return;
0734     }
0735 
0736     // If showProgressInfo was set, progressId() is > 0.
0737     switch (state) {
0738     case STATE_RENAMING:
0739         if (m_bURLDirty) {
0740             m_bURLDirty = false;
0741             Q_ASSERT(m_mode == CopyJob::Move);
0742             emitMoving(q, m_currentSrcURL, m_currentDestURL);
0743             Q_EMIT q->moving(q, m_currentSrcURL, m_currentDestURL);
0744         }
0745         // "N" files renamed shouldn't include skipped files
0746         q->setProcessedAmount(KJob::Files, m_processedFiles);
0747         // % value should include skipped files
0748         q->emitPercent(m_filesHandledByDirectRename, q->totalAmount(KJob::Files));
0749         break;
0750 
0751     case STATE_COPYING_FILES:
0752         q->setProcessedAmount(KJob::Files, m_processedFiles);
0753         q->setProcessedAmount(KJob::Bytes, m_processedSize + m_fileProcessedSize);
0754         if (m_bURLDirty) {
0755             // Only emit urls when they changed. This saves time, and fixes #66281
0756             m_bURLDirty = false;
0757             if (m_mode == CopyJob::Move) {
0758                 emitMoving(q, m_currentSrcURL, m_currentDestURL);
0759                 Q_EMIT q->moving(q, m_currentSrcURL, m_currentDestURL);
0760             } else if (m_mode == CopyJob::Link) {
0761                 emitCopying(q, m_currentSrcURL, m_currentDestURL); // we don't have a delegate->linking
0762                 Q_EMIT q->linking(q, m_currentSrcURL.path(), m_currentDestURL);
0763             } else {
0764                 emitCopying(q, m_currentSrcURL, m_currentDestURL);
0765                 Q_EMIT q->copying(q, m_currentSrcURL, m_currentDestURL);
0766             }
0767         }
0768         break;
0769 
0770     case STATE_CREATING_DIRS:
0771         q->setProcessedAmount(KJob::Directories, m_processedDirs);
0772         if (m_bURLDirty) {
0773             m_bURLDirty = false;
0774             Q_EMIT q->creatingDir(q, m_currentDestURL);
0775             emitCreatingDir(q, m_currentDestURL);
0776         }
0777         break;
0778 
0779     case STATE_STATING:
0780     case STATE_LISTING:
0781         if (m_bURLDirty) {
0782             m_bURLDirty = false;
0783             if (m_mode == CopyJob::Move) {
0784                 emitMoving(q, m_currentSrcURL, m_currentDestURL);
0785             } else {
0786                 emitCopying(q, m_currentSrcURL, m_currentDestURL);
0787             }
0788         }
0789         q->setProgressUnit(KJob::Bytes);
0790         q->setTotalAmount(KJob::Bytes, m_totalSize);
0791         q->setTotalAmount(KJob::Files, files.count() + m_filesHandledByDirectRename);
0792         q->setTotalAmount(KJob::Directories, dirs.count());
0793         break;
0794 
0795     default:
0796         break;
0797     }
0798 }
0799 
0800 void CopyJobPrivate::slotEntries(KIO::Job *job, const UDSEntryList &list)
0801 {
0802     // Q_Q(CopyJob);
0803     UDSEntryList::ConstIterator it = list.constBegin();
0804     UDSEntryList::ConstIterator end = list.constEnd();
0805     for (; it != end; ++it) {
0806         const UDSEntry &entry = *it;
0807         addCopyInfoFromUDSEntry(entry, static_cast<SimpleJob *>(job)->url(), m_bCurrentSrcIsDir, m_currentDest);
0808     }
0809 }
0810 
0811 void CopyJobPrivate::slotSubError(ListJob *job, ListJob *subJob)
0812 {
0813     const QUrl &url = subJob->url();
0814     qCWarning(KIO_CORE) << url << subJob->errorString();
0815 
0816     Q_Q(CopyJob);
0817 
0818     Q_EMIT q->warning(job, subJob->errorString());
0819     skip(url, true);
0820 }
0821 
0822 void CopyJobPrivate::addCopyInfoFromUDSEntry(const UDSEntry &entry, const QUrl &srcUrl, bool srcIsDir, const QUrl &currentDest)
0823 {
0824     struct CopyInfo info;
0825     info.permissions = entry.numberValue(KIO::UDSEntry::UDS_ACCESS, -1);
0826     const auto timeVal = entry.numberValue(KIO::UDSEntry::UDS_MODIFICATION_TIME, -1);
0827     if (timeVal != -1) {
0828         info.mtime = QDateTime::fromSecsSinceEpoch(timeVal, QTimeZone::UTC);
0829     }
0830     info.ctime = QDateTime::fromSecsSinceEpoch(entry.numberValue(KIO::UDSEntry::UDS_CREATION_TIME, -1), QTimeZone::UTC);
0831     info.size = static_cast<KIO::filesize_t>(entry.numberValue(KIO::UDSEntry::UDS_SIZE, -1));
0832     const bool isDir = entry.isDir();
0833 
0834     if (!isDir && info.size != KIO::invalidFilesize) {
0835         m_totalSize += info.size;
0836     }
0837 
0838     // recursive listing, displayName can be a/b/c/d
0839     const QString fileName = entry.stringValue(KIO::UDSEntry::UDS_NAME);
0840     const QString urlStr = entry.stringValue(KIO::UDSEntry::UDS_URL);
0841     QUrl url;
0842     if (!urlStr.isEmpty()) {
0843         url = QUrl(urlStr);
0844     }
0845     QString localPath = srcUrl.scheme() != QStringLiteral("trash") ? entry.stringValue(KIO::UDSEntry::UDS_LOCAL_PATH) : QString();
0846     info.linkDest = entry.stringValue(KIO::UDSEntry::UDS_LINK_DEST);
0847 
0848     if (fileName != QLatin1String("..") && fileName != QLatin1String(".")) {
0849         const bool hasCustomURL = !url.isEmpty() || !localPath.isEmpty();
0850         if (!hasCustomURL) {
0851             // Make URL from displayName
0852             url = srcUrl;
0853             if (srcIsDir) { // Only if src is a directory. Otherwise uSource is fine as is
0854                 qCDebug(KIO_COPYJOB_DEBUG) << "adding path" << fileName;
0855                 url = addPathToUrl(url, fileName);
0856             }
0857         }
0858         qCDebug(KIO_COPYJOB_DEBUG) << "fileName=" << fileName << "url=" << url;
0859         if (!localPath.isEmpty() && kio_resolve_local_urls && destinationState != DEST_DOESNT_EXIST) {
0860             url = QUrl::fromLocalFile(localPath);
0861         }
0862 
0863         info.uSource = url;
0864         info.uDest = currentDest;
0865         qCDebug(KIO_COPYJOB_DEBUG) << "uSource=" << info.uSource << "uDest(1)=" << info.uDest;
0866         // Append filename or dirname to destination URL, if allowed
0867         if (destinationState == DEST_IS_DIR &&
0868             // "copy/move as <foo>" means 'foo' is the dest for the base srcurl
0869             // (passed here during stating) but not its children (during listing)
0870             (!(m_asMethod && state == STATE_STATING))) {
0871             QString destFileName;
0872             KProtocolInfo::FileNameUsedForCopying fnu = KProtocolManager::fileNameUsedForCopying(url);
0873             if (hasCustomURL && fnu == KProtocolInfo::FromUrl) {
0874                 // destFileName = url.fileName(); // Doesn't work for recursive listing
0875                 // Count the number of prefixes used by the recursive listjob
0876                 int numberOfSlashes = fileName.count(QLatin1Char('/')); // don't make this a find()!
0877                 QString path = url.path();
0878                 int pos = 0;
0879                 for (int n = 0; n < numberOfSlashes + 1; ++n) {
0880                     pos = path.lastIndexOf(QLatin1Char('/'), pos - 1);
0881                     if (pos == -1) { // error
0882                         qCWarning(KIO_CORE) << "KIO worker bug: not enough slashes in UDS_URL" << path << "- looking for" << numberOfSlashes << "slashes";
0883                         break;
0884                     }
0885                 }
0886                 if (pos >= 0) {
0887                     destFileName = path.mid(pos + 1);
0888                 }
0889 
0890             } else if (fnu == KProtocolInfo::Name) { // destination filename taken from UDS_NAME
0891                 destFileName = fileName;
0892             } else { // from display name (with fallback to name)
0893                 const QString displayName = entry.stringValue(KIO::UDSEntry::UDS_DISPLAY_NAME);
0894                 destFileName = displayName.isEmpty() ? fileName : displayName;
0895             }
0896 
0897             // Here we _really_ have to add some filename to the dest.
0898             // Otherwise, we end up with e.g. dest=..../Desktop/ itself.
0899             // (This can happen when dropping a link to a webpage with no path)
0900             if (destFileName.isEmpty()) {
0901                 destFileName = KIO::encodeFileName(info.uSource.toDisplayString());
0902             }
0903 
0904             qCDebug(KIO_COPYJOB_DEBUG) << " adding destFileName=" << destFileName;
0905             info.uDest = addPathToUrl(info.uDest, destFileName);
0906         }
0907         qCDebug(KIO_COPYJOB_DEBUG) << " uDest(2)=" << info.uDest;
0908         qCDebug(KIO_COPYJOB_DEBUG) << " " << info.uSource << "->" << info.uDest;
0909         if (info.linkDest.isEmpty() && isDir && m_mode != CopyJob::Link) { // Dir
0910             dirs.append(info); // Directories
0911             if (m_mode == CopyJob::Move) {
0912                 dirsToRemove.append(info.uSource);
0913             }
0914         } else {
0915             files.append(info); // Files and any symlinks
0916         }
0917     }
0918 }
0919 
0920 // Adjust for kio_trash choosing its own dest url...
0921 QUrl CopyJobPrivate::finalDestUrl(const QUrl &src, const QUrl &dest) const
0922 {
0923     Q_Q(const CopyJob);
0924     if (dest.scheme() == QLatin1String("trash")) {
0925         const QMap<QString, QString> &metaData = q->metaData();
0926         QMap<QString, QString>::ConstIterator it = metaData.find(QLatin1String("trashURL-") + src.path());
0927         if (it != metaData.constEnd()) {
0928             qCDebug(KIO_COPYJOB_DEBUG) << "finalDestUrl=" << it.value();
0929             return QUrl(it.value());
0930         }
0931     }
0932     return dest;
0933 }
0934 
0935 void CopyJobPrivate::skipSrc(bool isDir)
0936 {
0937     m_dest = m_globalDest;
0938     destinationState = m_globalDestinationState;
0939     skip(*m_currentStatSrc, isDir);
0940     ++m_currentStatSrc;
0941     statCurrentSrc();
0942 }
0943 
0944 void CopyJobPrivate::statNextSrc()
0945 {
0946     /* Revert to the global destination, the one that applies to all source urls.
0947      * 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.
0948      * d->m_dest is /foo/b for b, but we have to revert to /d for item c and following.
0949      */
0950     m_dest = m_globalDest;
0951     qCDebug(KIO_COPYJOB_DEBUG) << "Setting m_dest to" << m_dest;
0952     destinationState = m_globalDestinationState;
0953     ++m_currentStatSrc;
0954     statCurrentSrc();
0955 }
0956 
0957 void CopyJobPrivate::statCurrentSrc()
0958 {
0959     Q_Q(CopyJob);
0960     if (m_currentStatSrc != m_srcList.constEnd()) {
0961         m_currentSrcURL = (*m_currentStatSrc);
0962         m_bURLDirty = true;
0963         m_ignoreSourcePermissions = !KProtocolManager::supportsListing(m_currentSrcURL) || m_currentSrcURL.scheme() == QLatin1String("trash");
0964 
0965         if (m_mode == CopyJob::Link) {
0966             // Skip the "stating the source" stage, we don't need it for linking
0967             m_currentDest = m_dest;
0968             struct CopyInfo info;
0969             info.permissions = -1;
0970             info.size = KIO::invalidFilesize;
0971             info.uSource = m_currentSrcURL;
0972             info.uDest = m_currentDest;
0973             // Append filename or dirname to destination URL, if allowed
0974             if (destinationState == DEST_IS_DIR && !m_asMethod) {
0975                 if (compareUrls(m_currentSrcURL, info.uDest)) {
0976                     // This is the case of creating a real symlink
0977                     info.uDest = addPathToUrl(info.uDest, m_currentSrcURL.fileName());
0978                 } else {
0979                     // Different protocols, we'll create a .desktop file
0980                     // We have to change the extension anyway, so while we're at it,
0981                     // name the file like the URL
0982                     QByteArray encodedFilename = QFile::encodeName(m_currentSrcURL.toDisplayString());
0983                     const int truncatePos = NAME_MAX - (info.uDest.toDisplayString().length() + 8); // length(.desktop) = 8
0984                     if (truncatePos > 0) {
0985                         encodedFilename.truncate(truncatePos);
0986                     }
0987                     const QString decodedFilename = QFile::decodeName(encodedFilename);
0988                     info.uDest = addPathToUrl(info.uDest, KIO::encodeFileName(decodedFilename) + QLatin1String(".desktop"));
0989                 }
0990             }
0991             files.append(info); // Files and any symlinks
0992             statNextSrc(); // we could use a loop instead of a recursive call :)
0993             return;
0994         }
0995 
0996         // Let's see if we can skip stat'ing, for the case where a directory view has the info already
0997         KIO::UDSEntry entry;
0998         const KFileItem cachedItem = KCoreDirLister::cachedItemForUrl(m_currentSrcURL);
0999         if (!cachedItem.isNull()) {
1000             entry = cachedItem.entry();
1001             if (destinationState != DEST_DOESNT_EXIST
1002                 && m_currentSrcURL.scheme() != QStringLiteral("trash")) { // only resolve src if we could resolve dest (#218719)
1003 
1004                 m_currentSrcURL = cachedItem.mostLocalUrl(); // #183585
1005             }
1006         }
1007 
1008         // Don't go renaming right away if we need a stat() to find out the destination filename
1009         const bool needStat =
1010             KProtocolManager::fileNameUsedForCopying(m_currentSrcURL) == KProtocolInfo::FromUrl || destinationState != DEST_IS_DIR || m_asMethod;
1011         if (m_mode == CopyJob::Move && needStat) {
1012             // If moving, before going for the full stat+[list+]copy+del thing, try to rename
1013             // The logic is pretty similar to FileCopyJobPrivate::slotStart()
1014             if (compareUrls(m_currentSrcURL, m_dest)) {
1015                 startRenameJob(m_currentSrcURL);
1016                 return;
1017             } else if (m_currentSrcURL.isLocalFile() && KProtocolManager::canRenameFromFile(m_dest)) {
1018                 startRenameJob(m_dest);
1019                 return;
1020             } else if (m_dest.isLocalFile() && KProtocolManager::canRenameToFile(m_currentSrcURL)) {
1021                 startRenameJob(m_currentSrcURL);
1022                 return;
1023             }
1024         }
1025 
1026         // if the source file system doesn't support deleting, we do not even stat
1027         if (m_mode == CopyJob::Move && !KProtocolManager::supportsDeleting(m_currentSrcURL)) {
1028             QPointer<CopyJob> that = q;
1029             Q_EMIT q->warning(q, buildErrorString(ERR_CANNOT_DELETE, m_currentSrcURL.toDisplayString()));
1030             if (that) {
1031                 statNextSrc(); // we could use a loop instead of a recursive call :)
1032             }
1033             return;
1034         }
1035 
1036         m_bOnlyRenames = false;
1037 
1038         // Testing for entry.count()>0 here is not good enough; KFileItem inserts
1039         // entries for UDS_USER and UDS_GROUP even on initially empty UDSEntries (#192185)
1040         if (entry.contains(KIO::UDSEntry::UDS_NAME)) {
1041             qCDebug(KIO_COPYJOB_DEBUG) << "fast path! found info about" << m_currentSrcURL << "in KCoreDirLister";
1042             // sourceStated(entry, m_currentSrcURL); // don't recurse, see #319747, use queued invokeMethod instead
1043             auto srcStatedFunc = [this, entry]() {
1044                 sourceStated(entry, m_currentSrcURL);
1045             };
1046             QMetaObject::invokeMethod(q, srcStatedFunc, Qt::QueuedConnection);
1047             return;
1048         }
1049 
1050         // Stat the next src url
1051         Job *job = KIO::stat(m_currentSrcURL, KIO::HideProgressInfo);
1052         qCDebug(KIO_COPYJOB_DEBUG) << "KIO::stat on" << m_currentSrcURL;
1053         state = STATE_STATING;
1054         q->addSubjob(job);
1055         m_currentDestURL = m_dest;
1056         m_bURLDirty = true;
1057     } else {
1058         // Finished the stat'ing phase
1059         // First make sure that the totals were correctly emitted
1060         m_bURLDirty = true;
1061         slotReport();
1062 
1063         qCDebug(KIO_COPYJOB_DEBUG) << "Stating finished. To copy:" << m_totalSize << ", available:" << m_freeSpace;
1064 
1065         if (m_totalSize > m_freeSpace && m_freeSpace != static_cast<KIO::filesize_t>(-1)) {
1066             q->setError(ERR_DISK_FULL);
1067             q->setErrorText(m_currentSrcURL.toDisplayString());
1068             q->emitResult();
1069             return;
1070         }
1071 
1072         // Check if we are copying a single file
1073         m_bSingleFileCopy = (files.count() == 1 && dirs.isEmpty());
1074         // Then start copying things
1075         state = STATE_CREATING_DIRS;
1076         createNextDir();
1077     }
1078 }
1079 
1080 void CopyJobPrivate::startRenameJob(const QUrl &workerUrl)
1081 {
1082     Q_Q(CopyJob);
1083 
1084     // Silence KDirWatch notifications, otherwise performance is horrible
1085     if (m_currentSrcURL.isLocalFile()) {
1086         const QString parentDir = m_currentSrcURL.adjusted(QUrl::RemoveFilename).path();
1087         const auto [it, isInserted] = m_parentDirs.insert(parentDir);
1088         if (isInserted) {
1089             KDirWatch::self()->stopDirScan(parentDir);
1090         }
1091     }
1092 
1093     QUrl dest = m_dest;
1094     // Append filename or dirname to destination URL, if allowed
1095     if (destinationState == DEST_IS_DIR && !m_asMethod) {
1096         dest = addPathToUrl(dest, m_currentSrcURL.fileName());
1097     }
1098     m_currentDestURL = dest;
1099     qCDebug(KIO_COPYJOB_DEBUG) << m_currentSrcURL << "->" << dest << "trying direct rename first";
1100     if (state != STATE_RENAMING) {
1101         q->setTotalAmount(KJob::Files, m_srcList.count());
1102     }
1103     state = STATE_RENAMING;
1104 
1105     struct CopyInfo info;
1106     info.permissions = -1;
1107     info.size = KIO::invalidFilesize;
1108     info.uSource = m_currentSrcURL;
1109     info.uDest = dest;
1110 
1111     KIO_ARGS << m_currentSrcURL << dest << (qint8) false /*no overwrite*/;
1112     SimpleJob *newJob = SimpleJobPrivate::newJobNoUi(workerUrl, CMD_RENAME, packedArgs);
1113     newJob->setParentJob(q);
1114     q->addSubjob(newJob);
1115     if (m_currentSrcURL.adjusted(QUrl::RemoveFilename) != dest.adjusted(QUrl::RemoveFilename)) { // For the user, moving isn't renaming. Only renaming is.
1116         m_bOnlyRenames = false;
1117     }
1118 }
1119 
1120 void CopyJobPrivate::startListing(const QUrl &src)
1121 {
1122     Q_Q(CopyJob);
1123     state = STATE_LISTING;
1124     m_bURLDirty = true;
1125     ListJob *newjob = listRecursive(src, KIO::HideProgressInfo);
1126     newjob->setUnrestricted(true);
1127     q->connect(newjob, &ListJob::entries, q, [this](KIO::Job *job, const KIO::UDSEntryList &list) {
1128         slotEntries(job, list);
1129     });
1130     q->connect(newjob, &ListJob::subError, q, [this](KIO::ListJob *job, KIO::ListJob *subJob) {
1131         slotSubError(job, subJob);
1132     });
1133     q->addSubjob(newjob);
1134 }
1135 
1136 void CopyJobPrivate::skip(const QUrl &sourceUrl, bool isDir)
1137 {
1138     QUrl dir(sourceUrl);
1139     if (!isDir) {
1140         // Skipping a file: make sure not to delete the parent dir (#208418)
1141         dir = dir.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash);
1142     }
1143     while (dirsToRemove.removeAll(dir) > 0) {
1144         // Do not rely on rmdir() on the parent directories aborting.
1145         // Exclude the parent dirs explicitly.
1146         dir = dir.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash);
1147     }
1148 }
1149 
1150 bool CopyJobPrivate::shouldOverwriteDir(const QString &path) const
1151 {
1152     if (m_bOverwriteAllDirs) {
1153         return true;
1154     }
1155     return m_overwriteList.contains(path);
1156 }
1157 
1158 bool CopyJobPrivate::shouldOverwriteFile(const QString &path) const
1159 {
1160     if (m_bOverwriteAllFiles) {
1161         return true;
1162     }
1163     return m_overwriteList.contains(path);
1164 }
1165 
1166 bool CopyJobPrivate::shouldSkip(const QString &path) const
1167 {
1168     for (const QString &skipPath : std::as_const(m_skipList)) {
1169         if (path.startsWith(skipPath)) {
1170             return true;
1171         }
1172     }
1173     return false;
1174 }
1175 
1176 void CopyJobPrivate::renameDirectory(const QList<CopyInfo>::iterator &it, const QUrl &newUrl)
1177 {
1178     Q_Q(CopyJob);
1179     Q_EMIT q->renamed(q, (*it).uDest, newUrl); // for e.g. KPropertiesDialog
1180 
1181     const QString oldPath = Utils::slashAppended((*it).uDest.path());
1182 
1183     // Change the current one and strip the trailing '/'
1184     (*it).uDest = newUrl.adjusted(QUrl::StripTrailingSlash);
1185 
1186     const QString newPath = Utils::slashAppended(newUrl.path()); // With trailing slash
1187 
1188     QList<CopyInfo>::Iterator renamedirit = it;
1189     ++renamedirit;
1190     // Change the name of subdirectories inside the directory
1191     for (; renamedirit != dirs.end(); ++renamedirit) {
1192         QString path = (*renamedirit).uDest.path();
1193         if (path.startsWith(oldPath)) {
1194             QString n = path;
1195             n.replace(0, oldPath.length(), newPath);
1196             /*qDebug() << "dirs list:" << (*renamedirit).uSource.path()
1197                          << "was going to be" << path
1198                          << ", changed into" << n;*/
1199             (*renamedirit).uDest.setPath(n, QUrl::DecodedMode);
1200         }
1201     }
1202     // Change filenames inside the directory
1203     QList<CopyInfo>::Iterator renamefileit = files.begin();
1204     for (; renamefileit != files.end(); ++renamefileit) {
1205         QString path = (*renamefileit).uDest.path(QUrl::FullyDecoded);
1206         if (path.startsWith(oldPath)) {
1207             QString n = path;
1208             n.replace(0, oldPath.length(), newPath);
1209             /*qDebug() << "files list:" << (*renamefileit).uSource.path()
1210                          << "was going to be" << path
1211                          << ", changed into" << n;*/
1212             (*renamefileit).uDest.setPath(n, QUrl::DecodedMode);
1213         }
1214     }
1215 }
1216 
1217 void CopyJobPrivate::slotResultCreatingDirs(KJob *job)
1218 {
1219     Q_Q(CopyJob);
1220     // The dir we are trying to create:
1221     QList<CopyInfo>::Iterator it = dirs.begin();
1222     // Was there an error creating a dir ?
1223     if (job->error()) {
1224         m_conflictError = job->error();
1225         if (m_conflictError == ERR_DIR_ALREADY_EXIST //
1226             || m_conflictError == ERR_FILE_ALREADY_EXIST) { // can't happen?
1227             QUrl oldURL = ((SimpleJob *)job)->url();
1228             // Should we skip automatically ?
1229             if (m_bAutoSkipDirs) {
1230                 // We don't want to copy files in this directory, so we put it on the skip list
1231                 const QString path = Utils::slashAppended(oldURL.path());
1232                 m_skipList.append(path);
1233                 skip(oldURL, true);
1234                 dirs.erase(it); // Move on to next dir
1235             } else {
1236                 // Did the user choose to overwrite already?
1237                 const QString destDir = (*it).uDest.path();
1238                 if (shouldOverwriteDir(destDir)) { // overwrite => just skip
1239                     Q_EMIT q->copyingDone(q, (*it).uSource, finalDestUrl((*it).uSource, (*it).uDest), (*it).mtime, true /* directory */, false /* renamed */);
1240                     dirs.erase(it); // Move on to next dir
1241                     ++m_processedDirs;
1242                 } else {
1243                     if (m_bAutoRenameDirs) {
1244                         const QUrl destDirectory = (*it).uDest.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash);
1245                         const QString newName = KFileUtils::suggestName(destDirectory, (*it).uDest.fileName());
1246                         QUrl newUrl(destDirectory);
1247                         newUrl.setPath(Utils::concatPaths(newUrl.path(), newName));
1248                         renameDirectory(it, newUrl);
1249                     } else {
1250                         if (!KIO::delegateExtension<AskUserActionInterface *>(q)) {
1251                             q->Job::slotResult(job); // will set the error and emit result(this)
1252                             return;
1253                         }
1254 
1255                         Q_ASSERT(((SimpleJob *)job)->url() == (*it).uDest);
1256                         q->removeSubjob(job);
1257                         Q_ASSERT(!q->hasSubjobs()); // We should have only one job at a time ...
1258 
1259                         // We need to stat the existing dir, to get its last-modification time
1260                         QUrl existingDest((*it).uDest);
1261                         SimpleJob *newJob = KIO::stat(existingDest, StatJob::DestinationSide, KIO::StatDefaultDetails, KIO::HideProgressInfo);
1262                         qCDebug(KIO_COPYJOB_DEBUG) << "KIO::stat for resolving conflict on" << existingDest;
1263                         state = STATE_CONFLICT_CREATING_DIRS;
1264                         q->addSubjob(newJob);
1265                         return; // Don't move to next dir yet !
1266                     }
1267                 }
1268             }
1269         } else {
1270             // Severe error, abort
1271             q->Job::slotResult(job); // will set the error and emit result(this)
1272             return;
1273         }
1274     } else { // no error : remove from list, to move on to next dir
1275         // this is required for the undo feature
1276         Q_EMIT q->copyingDone(q, (*it).uSource, finalDestUrl((*it).uSource, (*it).uDest), (*it).mtime, true, false);
1277         m_directoriesCopied.push_back(*it);
1278         dirs.erase(it);
1279         ++m_processedDirs;
1280     }
1281 
1282     q->removeSubjob(job);
1283     Q_ASSERT(!q->hasSubjobs()); // We should have only one job at a time ...
1284     createNextDir();
1285 }
1286 
1287 void CopyJobPrivate::slotResultConflictCreatingDirs(KJob *job)
1288 {
1289     Q_Q(CopyJob);
1290     // We come here after a conflict has been detected and we've stated the existing dir
1291 
1292     // The dir we were trying to create:
1293     QList<CopyInfo>::Iterator it = dirs.begin();
1294 
1295     const UDSEntry entry = ((KIO::StatJob *)job)->statResult();
1296 
1297     QDateTime destmtime;
1298     QDateTime destctime;
1299     const KIO::filesize_t destsize = entry.numberValue(KIO::UDSEntry::UDS_SIZE);
1300     const QString linkDest = entry.stringValue(KIO::UDSEntry::UDS_LINK_DEST);
1301 
1302     q->removeSubjob(job);
1303     Q_ASSERT(!q->hasSubjobs()); // We should have only one job at a time ...
1304 
1305     // Always multi and skip (since there are files after that)
1306     RenameDialog_Options options(RenameDialog_MultipleItems | RenameDialog_Skip | RenameDialog_DestIsDirectory);
1307     // Overwrite only if the existing thing is a dir (no chance with a file)
1308     if (m_conflictError == ERR_DIR_ALREADY_EXIST) {
1309         // We are in slotResultConflictCreatingDirs(), so the source is a dir
1310         options |= RenameDialog_SourceIsDirectory;
1311 
1312         if ((*it).uSource == (*it).uDest
1313             || ((*it).uSource.scheme() == (*it).uDest.scheme() && (*it).uSource.adjusted(QUrl::StripTrailingSlash).path() == linkDest)) {
1314             options |= RenameDialog_OverwriteItself;
1315         } else {
1316             options |= RenameDialog_Overwrite;
1317             destmtime = QDateTime::fromSecsSinceEpoch(entry.numberValue(KIO::UDSEntry::UDS_MODIFICATION_TIME, -1), QTimeZone::UTC);
1318             destctime = QDateTime::fromSecsSinceEpoch(entry.numberValue(KIO::UDSEntry::UDS_CREATION_TIME, -1), QTimeZone::UTC);
1319         }
1320     }
1321 
1322     if (m_reportTimer) {
1323         m_reportTimer->stop();
1324     }
1325 
1326     auto *askUserActionInterface = KIO::delegateExtension<KIO::AskUserActionInterface *>(q);
1327 
1328     auto renameSignal = &KIO::AskUserActionInterface::askUserRenameResult;
1329     QObject::connect(askUserActionInterface, renameSignal, q, [=](RenameDialog_Result result, const QUrl &newUrl, KJob *parentJob) {
1330         Q_ASSERT(parentJob == q);
1331         // Only receive askUserRenameResult once per rename dialog
1332         QObject::disconnect(askUserActionInterface, renameSignal, q, nullptr);
1333 
1334         if (m_reportTimer) {
1335             m_reportTimer->start(s_reportTimeout);
1336         }
1337 
1338         const QString existingDest = (*it).uDest.path();
1339 
1340         switch (result) {
1341         case Result_Cancel:
1342             q->setError(ERR_USER_CANCELED);
1343             q->emitResult();
1344             return;
1345         case Result_AutoRename:
1346             m_bAutoRenameDirs = true;
1347             // fall through
1348             Q_FALLTHROUGH();
1349         case Result_Rename:
1350             renameDirectory(it, newUrl);
1351             break;
1352         case Result_AutoSkip:
1353             m_bAutoSkipDirs = true;
1354             // fall through
1355             Q_FALLTHROUGH();
1356         case Result_Skip:
1357             m_skipList.append(Utils::slashAppended(existingDest));
1358             skip((*it).uSource, true);
1359             // Move on to next dir
1360             dirs.erase(it);
1361             ++m_processedDirs;
1362             break;
1363         case Result_Overwrite:
1364             m_overwriteList.insert(existingDest);
1365             Q_EMIT q->copyingDone(q, (*it).uSource, finalDestUrl((*it).uSource, (*it).uDest), (*it).mtime, true /* directory */, false /* renamed */);
1366             // Move on to next dir
1367             dirs.erase(it);
1368             ++m_processedDirs;
1369             break;
1370         case Result_OverwriteAll:
1371             m_bOverwriteAllDirs = true;
1372             Q_EMIT q->copyingDone(q, (*it).uSource, finalDestUrl((*it).uSource, (*it).uDest), (*it).mtime, true /* directory */, false /* renamed */);
1373             // Move on to next dir
1374             dirs.erase(it);
1375             ++m_processedDirs;
1376             break;
1377         default:
1378             Q_ASSERT(0);
1379         }
1380         state = STATE_CREATING_DIRS;
1381         createNextDir();
1382     });
1383 
1384     /* clang-format off */
1385     askUserActionInterface->askUserRename(q, i18n("Folder Already Exists"),
1386                                           (*it).uSource, (*it).uDest,
1387                                           options,
1388                                           (*it).size, destsize,
1389                                           (*it).ctime, destctime,
1390                                           (*it).mtime, destmtime);
1391     /* clang-format on */
1392 }
1393 
1394 void CopyJobPrivate::createNextDir()
1395 {
1396     Q_Q(CopyJob);
1397 
1398     // Take first dir to create out of list
1399     QList<CopyInfo>::Iterator it = dirs.begin();
1400     // Is this URL on the skip list or the overwrite list ?
1401     while (it != dirs.end()) {
1402         const QString dir = it->uDest.path();
1403         if (shouldSkip(dir)) {
1404             it = dirs.erase(it);
1405         } else {
1406             break;
1407         }
1408     }
1409 
1410     if (it != dirs.end()) { // any dir to create, finally ?
1411         if (it->uDest.isLocalFile()) {
1412             // uDest doesn't exist yet, check the filesystem of the parent dir
1413             const auto destFileSystem = KFileSystemType::fileSystemType(it->uDest.adjusted(QUrl::StripTrailingSlash | QUrl::RemoveFilename).toLocalFile());
1414             if (isFatOrNtfs(destFileSystem)) {
1415                 const QString dirName = it->uDest.adjusted(QUrl::StripTrailingSlash).fileName();
1416                 if (hasInvalidChars(dirName)) {
1417                     // We already asked the user?
1418                     if (m_autoReplaceInvalidChars) {
1419                         processCreateNextDir(it, KIO::Result_ReplaceInvalidChars);
1420                         return;
1421                     } else if (m_autoSkipDirsWithInvalidChars) {
1422                         processCreateNextDir(it, KIO::Result_Skip);
1423                         return;
1424                     }
1425 
1426                     const QString msg = invalidCharsSupportMsg(it->uDest.toDisplayString(QUrl::PreferLocalFile),
1427                                                                KFileSystemType::fileSystemName(destFileSystem),
1428                                                                true /* isDir */);
1429 
1430                     if (auto *askUserActionInterface = KIO::delegateExtension<KIO::AskUserActionInterface *>(q)) {
1431                         SkipDialog_Options options = KIO::SkipDialog_Replace_Invalid_Chars;
1432                         if (dirs.size() > 1) {
1433                             options |= SkipDialog_MultipleItems;
1434                         }
1435 
1436                         auto skipSignal = &KIO::AskUserActionInterface::askUserSkipResult;
1437                         QObject::connect(askUserActionInterface, skipSignal, q, [=](SkipDialog_Result result, KJob *parentJob) {
1438                             Q_ASSERT(parentJob == q);
1439 
1440                             // Only receive askUserSkipResult once per skip dialog
1441                             QObject::disconnect(askUserActionInterface, skipSignal, q, nullptr);
1442 
1443                             processCreateNextDir(it, result);
1444                         });
1445 
1446                         askUserActionInterface->askUserSkip(q, options, msg);
1447 
1448                         return;
1449                     } else { // No Job Ui delegate
1450                         qCWarning(KIO_COPYJOB_DEBUG) << msg;
1451                         q->emitResult();
1452                         return;
1453                     }
1454                 }
1455             }
1456         }
1457 
1458         processCreateNextDir(it, -1);
1459     } else { // we have finished creating dirs
1460         q->setProcessedAmount(KJob::Directories, m_processedDirs); // make sure final number appears
1461 
1462         if (m_mode == CopyJob::Move) {
1463             // Now we know which dirs hold the files we're going to delete.
1464             // To speed things up and prevent double-notification, we disable KDirWatch
1465             // on those dirs temporarily (using KDirWatch::self, that's the instance
1466             // used by e.g. kdirlister).
1467             for (const auto &dir : m_parentDirs) {
1468                 KDirWatch::self()->stopDirScan(dir);
1469             }
1470         }
1471 
1472         state = STATE_COPYING_FILES;
1473         ++m_processedFiles; // Ralf wants it to start at 1, not 0
1474         copyNextFile();
1475     }
1476 }
1477 
1478 void CopyJobPrivate::processCreateNextDir(const QList<CopyInfo>::Iterator &it, int result)
1479 {
1480     Q_Q(CopyJob);
1481 
1482     switch (result) {
1483     case Result_Cancel:
1484         q->setError(ERR_USER_CANCELED);
1485         q->emitResult();
1486         return;
1487     case KIO::Result_ReplaceAllInvalidChars:
1488         m_autoReplaceInvalidChars = true;
1489         Q_FALLTHROUGH();
1490     case KIO::Result_ReplaceInvalidChars: {
1491         it->uDest = it->uDest.adjusted(QUrl::StripTrailingSlash);
1492         QString dirName = it->uDest.fileName();
1493         const int len = dirName.size();
1494         cleanMsdosDestName(dirName);
1495         QString path = it->uDest.path();
1496         path.replace(path.size() - len, len, dirName);
1497         it->uDest.setPath(path);
1498         break;
1499     }
1500     case KIO::Result_AutoSkip:
1501         m_autoSkipDirsWithInvalidChars = true;
1502         Q_FALLTHROUGH();
1503     case KIO::Result_Skip:
1504         m_skipList.append(Utils::slashAppended(it->uDest.path()));
1505         skip(it->uSource, true);
1506         dirs.erase(it); // Move on to next dir
1507         ++m_processedDirs;
1508         createNextDir();
1509         return;
1510     default:
1511         break;
1512     }
1513 
1514     // Create the directory - with default permissions so that we can put files into it
1515     // TODO : change permissions once all is finished; but for stuff coming from CDROM it sucks...
1516     KIO::SimpleJob *newjob = KIO::mkdir(it->uDest, -1);
1517     newjob->setParentJob(q);
1518     if (shouldOverwriteFile(it->uDest.path())) { // if we are overwriting an existing file or symlink
1519         newjob->addMetaData(QStringLiteral("overwrite"), QStringLiteral("true"));
1520     }
1521 
1522     m_currentDestURL = it->uDest;
1523     m_bURLDirty = true;
1524 
1525     q->addSubjob(newjob);
1526 }
1527 
1528 void CopyJobPrivate::slotResultCopyingFiles(KJob *job)
1529 {
1530     Q_Q(CopyJob);
1531     // The file we were trying to copy:
1532     QList<CopyInfo>::Iterator it = files.begin();
1533     if (job->error()) {
1534         // Should we skip automatically ?
1535         if (m_bAutoSkipFiles) {
1536             skip((*it).uSource, false);
1537             m_fileProcessedSize = (*it).size;
1538             files.erase(it); // Move on to next file
1539         } else {
1540             m_conflictError = job->error(); // save for later
1541             // Existing dest ?
1542             if (m_conflictError == ERR_FILE_ALREADY_EXIST //
1543                 || m_conflictError == ERR_DIR_ALREADY_EXIST //
1544                 || m_conflictError == ERR_IDENTICAL_FILES) {
1545                 if (m_bAutoRenameFiles) {
1546                     QUrl destDirectory = (*it).uDest.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash);
1547                     const QString newName = KFileUtils::suggestName(destDirectory, (*it).uDest.fileName());
1548                     QUrl newDest(destDirectory);
1549                     newDest.setPath(Utils::concatPaths(newDest.path(), newName));
1550                     Q_EMIT q->renamed(q, (*it).uDest, newDest); // for e.g. kpropsdlg
1551                     (*it).uDest = newDest;
1552                 } else {
1553                     if (!KIO::delegateExtension<AskUserActionInterface *>(q)) {
1554                         q->Job::slotResult(job); // will set the error and emit result(this)
1555                         return;
1556                     }
1557 
1558                     q->removeSubjob(job);
1559                     Q_ASSERT(!q->hasSubjobs());
1560                     // We need to stat the existing file, to get its last-modification time
1561                     QUrl existingFile((*it).uDest);
1562                     SimpleJob *newJob =
1563                         KIO::stat(existingFile, StatJob::DestinationSide, KIO::StatDetail::StatBasic | KIO::StatDetail::StatTime, KIO::HideProgressInfo);
1564                     qCDebug(KIO_COPYJOB_DEBUG) << "KIO::stat for resolving conflict on" << existingFile;
1565                     state = STATE_CONFLICT_COPYING_FILES;
1566                     q->addSubjob(newJob);
1567                     return; // Don't move to next file yet !
1568                 }
1569             } else {
1570                 if (m_bCurrentOperationIsLink && qobject_cast<KIO::DeleteJob *>(job)) {
1571                     // Very special case, see a few lines below
1572                     // We are deleting the source of a symlink we successfully moved... ignore error
1573                     m_fileProcessedSize = (*it).size;
1574                     ++m_processedFiles;
1575                     files.erase(it);
1576                 } else {
1577                     if (!KIO::delegateExtension<AskUserActionInterface *>(q)) {
1578                         q->Job::slotResult(job); // will set the error and emit result(this)
1579                         return;
1580                     }
1581 
1582                     // Go directly to the conflict resolution, there is nothing to stat
1583                     slotResultErrorCopyingFiles(job);
1584                     return;
1585                 }
1586             }
1587         }
1588     } else { // no error
1589         // Special case for moving links. That operation needs two jobs, unlike others.
1590         if (m_bCurrentOperationIsLink //
1591             && m_mode == CopyJob::Move //
1592             && !qobject_cast<KIO::DeleteJob *>(job) // Deleting source not already done
1593         ) {
1594             q->removeSubjob(job);
1595             Q_ASSERT(!q->hasSubjobs());
1596             // The only problem with this trick is that the error handling for this del operation
1597             // is not going to be right... see 'Very special case' above.
1598             KIO::Job *newjob = KIO::del((*it).uSource, HideProgressInfo);
1599             newjob->setParentJob(q);
1600             q->addSubjob(newjob);
1601             return; // Don't move to next file yet !
1602         }
1603 
1604         const QUrl finalUrl = finalDestUrl((*it).uSource, (*it).uDest);
1605 
1606         if (m_bCurrentOperationIsLink) {
1607             QString target = (m_mode == CopyJob::Link ? (*it).uSource.path() : (*it).linkDest);
1608             // required for the undo feature
1609             Q_EMIT q->copyingLinkDone(q, (*it).uSource, target, finalUrl);
1610         } else {
1611             // required for the undo feature
1612             Q_EMIT q->copyingDone(q, (*it).uSource, finalUrl, (*it).mtime, false, false);
1613             if (m_mode == CopyJob::Move) {
1614 #ifndef KIO_ANDROID_STUB
1615                 org::kde::KDirNotify::emitFileMoved((*it).uSource, finalUrl);
1616 #endif
1617             }
1618             m_successSrcList.append((*it).uSource);
1619             if (m_freeSpace != KIO::invalidFilesize && (*it).size != KIO::invalidFilesize) {
1620                 m_freeSpace -= (*it).size;
1621             }
1622         }
1623         // remove from list, to move on to next file
1624         files.erase(it);
1625         ++m_processedFiles;
1626     }
1627 
1628     // clear processed size for last file and add it to overall processed size
1629     m_processedSize += m_fileProcessedSize;
1630     m_fileProcessedSize = 0;
1631 
1632     qCDebug(KIO_COPYJOB_DEBUG) << files.count() << "files remaining";
1633 
1634     // Merge metadata from subjob
1635     KIO::Job *kiojob = qobject_cast<KIO::Job *>(job);
1636     Q_ASSERT(kiojob);
1637     m_incomingMetaData += kiojob->metaData();
1638     q->removeSubjob(job);
1639     Q_ASSERT(!q->hasSubjobs()); // We should have only one job at a time ...
1640     copyNextFile();
1641 }
1642 
1643 void CopyJobPrivate::slotResultErrorCopyingFiles(KJob *job)
1644 {
1645     Q_Q(CopyJob);
1646     // We come here after a conflict has been detected and we've stated the existing file
1647     // The file we were trying to create:
1648     QList<CopyInfo>::Iterator it = files.begin();
1649 
1650     RenameDialog_Result res = Result_Cancel;
1651 
1652     if (m_reportTimer) {
1653         m_reportTimer->stop();
1654     }
1655 
1656     q->removeSubjob(job);
1657     Q_ASSERT(!q->hasSubjobs());
1658     auto *askUserActionInterface = KIO::delegateExtension<KIO::AskUserActionInterface *>(q);
1659 
1660     if (m_conflictError == ERR_FILE_ALREADY_EXIST //
1661         || m_conflictError == ERR_DIR_ALREADY_EXIST //
1662         || m_conflictError == ERR_IDENTICAL_FILES) {
1663         // Its modification time:
1664         const UDSEntry entry = static_cast<KIO::StatJob *>(job)->statResult();
1665 
1666         QDateTime destmtime;
1667         QDateTime destctime;
1668         const KIO::filesize_t destsize = entry.numberValue(KIO::UDSEntry::UDS_SIZE);
1669         const QString linkDest = entry.stringValue(KIO::UDSEntry::UDS_LINK_DEST);
1670 
1671         // Offer overwrite only if the existing thing is a file
1672         // If src==dest, use "overwrite-itself"
1673         RenameDialog_Options options;
1674         bool isDir = true;
1675 
1676         if (m_conflictError == ERR_DIR_ALREADY_EXIST) {
1677             options = RenameDialog_DestIsDirectory;
1678         } else {
1679             if ((*it).uSource == (*it).uDest
1680                 || ((*it).uSource.scheme() == (*it).uDest.scheme() && (*it).uSource.adjusted(QUrl::StripTrailingSlash).path() == linkDest)) {
1681                 options = RenameDialog_OverwriteItself;
1682             } else {
1683                 const qint64 destMTimeStamp = entry.numberValue(KIO::UDSEntry::UDS_MODIFICATION_TIME, -1);
1684                 if (m_bOverwriteWhenOlder && (*it).mtime.isValid() && destMTimeStamp != -1) {
1685                     if ((*it).mtime.currentSecsSinceEpoch() > destMTimeStamp) {
1686                         qCDebug(KIO_COPYJOB_DEBUG) << "dest is older, overwriting" << (*it).uDest;
1687                         res = Result_Overwrite;
1688                     } else {
1689                         qCDebug(KIO_COPYJOB_DEBUG) << "dest is newer, skipping" << (*it).uDest;
1690                         res = Result_Skip;
1691                     }
1692                 } else {
1693                     // These timestamps are used only when RenameDialog_Overwrite is set.
1694                     destmtime = QDateTime::fromSecsSinceEpoch(destMTimeStamp, QTimeZone::UTC);
1695                     destctime = QDateTime::fromSecsSinceEpoch(entry.numberValue(KIO::UDSEntry::UDS_CREATION_TIME, -1), QTimeZone::UTC);
1696 
1697                     options = RenameDialog_Overwrite;
1698                 }
1699             }
1700             isDir = false;
1701         }
1702 
1703         // if no preset value was set
1704         if (res == Result_Cancel) {
1705             if (!m_bSingleFileCopy) {
1706                 options = RenameDialog_Options(options | RenameDialog_MultipleItems | RenameDialog_Skip);
1707             }
1708 
1709             const QString title = !isDir ? i18n("File Already Exists") : i18n("Already Exists as Folder");
1710 
1711             auto renameSignal = &KIO::AskUserActionInterface::askUserRenameResult;
1712             QObject::connect(askUserActionInterface, renameSignal, q, [=](RenameDialog_Result result, const QUrl &newUrl, KJob *parentJob) {
1713                 Q_ASSERT(parentJob == q);
1714                 // Only receive askUserRenameResult once per rename dialog
1715                 QObject::disconnect(askUserActionInterface, renameSignal, q, nullptr);
1716                 processFileRenameDialogResult(it, result, newUrl, destmtime);
1717             });
1718 
1719             /* clang-format off */
1720             askUserActionInterface->askUserRename(q, title,
1721                                                   (*it).uSource, (*it).uDest,
1722                                                   options,
1723                                                   (*it).size, destsize,
1724                                                   (*it).ctime, destctime,
1725                                                   (*it).mtime, destmtime); /* clang-format on */
1726             return;
1727         }
1728     } else {
1729         if (job->error() == ERR_USER_CANCELED) {
1730             res = Result_Cancel;
1731         } else if (!askUserActionInterface) {
1732             q->Job::slotResult(job); // will set the error and emit result(this)
1733             return;
1734         } else {
1735             SkipDialog_Options options;
1736             if (files.count() > 1) {
1737                 options |= SkipDialog_MultipleItems;
1738             }
1739 
1740             auto skipSignal = &KIO::AskUserActionInterface::askUserSkipResult;
1741             QObject::connect(askUserActionInterface, skipSignal, q, [=](SkipDialog_Result result, KJob *parentJob) {
1742                 Q_ASSERT(parentJob == q);
1743                 // Only receive askUserSkipResult once per skip dialog
1744                 QObject::disconnect(askUserActionInterface, skipSignal, q, nullptr);
1745                 processFileRenameDialogResult(it, result, QUrl() /* no new url in skip */, QDateTime{});
1746             });
1747 
1748             askUserActionInterface->askUserSkip(q, options, job->errorString());
1749             return;
1750         }
1751     }
1752 
1753     processFileRenameDialogResult(it, res, QUrl{}, QDateTime{});
1754 }
1755 
1756 void CopyJobPrivate::processFileRenameDialogResult(const QList<CopyInfo>::Iterator &it,
1757                                                    RenameDialog_Result result,
1758                                                    const QUrl &newUrl,
1759                                                    const QDateTime &destmtime)
1760 {
1761     Q_Q(CopyJob);
1762 
1763     if (m_reportTimer) {
1764         m_reportTimer->start(s_reportTimeout);
1765     }
1766 
1767     if (result == Result_OverwriteWhenOlder) {
1768         m_bOverwriteWhenOlder = true;
1769         if ((*it).mtime > destmtime) {
1770             qCDebug(KIO_COPYJOB_DEBUG) << "dest is older, overwriting" << (*it).uDest;
1771             result = Result_Overwrite;
1772         } else {
1773             qCDebug(KIO_COPYJOB_DEBUG) << "dest is newer, skipping" << (*it).uDest;
1774             result = Result_Skip;
1775         }
1776     }
1777 
1778     switch (result) {
1779     case Result_Cancel:
1780         q->setError(ERR_USER_CANCELED);
1781         q->emitResult();
1782         return;
1783     case Result_AutoRename:
1784         m_bAutoRenameFiles = true;
1785         // fall through
1786         Q_FALLTHROUGH();
1787     case Result_Rename: {
1788         Q_EMIT q->renamed(q, (*it).uDest, newUrl); // for e.g. kpropsdlg
1789         (*it).uDest = newUrl;
1790         m_bURLDirty = true;
1791         break;
1792     }
1793     case Result_AutoSkip:
1794         m_bAutoSkipFiles = true;
1795         // fall through
1796         Q_FALLTHROUGH();
1797     case Result_Skip:
1798         // Move on to next file
1799         skip((*it).uSource, false);
1800         m_processedSize += (*it).size;
1801         files.erase(it);
1802         break;
1803     case Result_OverwriteAll:
1804         m_bOverwriteAllFiles = true;
1805         break;
1806     case Result_Overwrite:
1807         // Add to overwrite list, so that copyNextFile knows to overwrite
1808         m_overwriteList.insert((*it).uDest.path());
1809         break;
1810     case Result_Retry:
1811         // Do nothing, copy file again
1812         break;
1813     default:
1814         Q_ASSERT(0);
1815     }
1816     state = STATE_COPYING_FILES;
1817     copyNextFile();
1818 }
1819 
1820 KIO::Job *CopyJobPrivate::linkNextFile(const QUrl &uSource, const QUrl &uDest, JobFlags flags)
1821 {
1822     qCDebug(KIO_COPYJOB_DEBUG) << "Linking";
1823     if (compareUrls(uSource, uDest)) {
1824         // This is the case of creating a real symlink
1825         KIO::SimpleJob *newJob = KIO::symlink(uSource.path(), uDest, flags | HideProgressInfo /*no GUI*/);
1826         newJob->setParentJob(q_func());
1827         qCDebug(KIO_COPYJOB_DEBUG) << "Linking target=" << uSource.path() << "link=" << uDest;
1828         // emit linking( this, uSource.path(), uDest );
1829         m_bCurrentOperationIsLink = true;
1830         m_currentSrcURL = uSource;
1831         m_currentDestURL = uDest;
1832         m_bURLDirty = true;
1833         // Observer::self()->slotCopying( this, uSource, uDest ); // should be slotLinking perhaps
1834         return newJob;
1835     } else {
1836         Q_Q(CopyJob);
1837         qCDebug(KIO_COPYJOB_DEBUG) << "Linking URL=" << uSource << "link=" << uDest;
1838         if (uDest.isLocalFile()) {
1839             // if the source is a devices url, handle it a littlebit special
1840 
1841             QString path = uDest.toLocalFile();
1842             qCDebug(KIO_COPYJOB_DEBUG) << "path=" << path;
1843             QFile f(path);
1844             if (f.open(QIODevice::ReadWrite)) {
1845                 f.close();
1846                 KDesktopFile desktopFile(path);
1847                 KConfigGroup config = desktopFile.desktopGroup();
1848                 QUrl url = uSource;
1849                 url.setPassword(QString());
1850                 config.writePathEntry("URL", url.toString());
1851                 config.writeEntry("Name", url.toString());
1852                 config.writeEntry("Type", QStringLiteral("Link"));
1853                 QString protocol = uSource.scheme();
1854                 if (protocol == QLatin1String("ftp")) {
1855                     config.writeEntry("Icon", QStringLiteral("folder-remote"));
1856                 } else if (protocol == QLatin1String("http") || protocol == QLatin1String("https")) {
1857                     config.writeEntry("Icon", QStringLiteral("text-html"));
1858                 } else if (protocol == QLatin1String("info")) {
1859                     config.writeEntry("Icon", QStringLiteral("text-x-texinfo"));
1860                 } else if (protocol == QLatin1String("mailto")) { // sven:
1861                     config.writeEntry("Icon", QStringLiteral("internet-mail")); // added mailto: support
1862                 } else if (protocol == QLatin1String("trash") && url.path().length() <= 1) { // trash:/ link
1863                     config.writeEntry("Name", i18n("Trash"));
1864                     config.writeEntry("Icon", QStringLiteral("user-trash-full"));
1865                     config.writeEntry("EmptyIcon", QStringLiteral("user-trash"));
1866                 } else {
1867                     config.writeEntry("Icon", QStringLiteral("unknown"));
1868                 }
1869                 config.sync();
1870                 files.erase(files.begin()); // done with this one, move on
1871                 ++m_processedFiles;
1872                 copyNextFile();
1873                 return nullptr;
1874             } else {
1875                 qCDebug(KIO_COPYJOB_DEBUG) << "ERR_CANNOT_OPEN_FOR_WRITING";
1876                 q->setError(ERR_CANNOT_OPEN_FOR_WRITING);
1877                 q->setErrorText(uDest.toLocalFile());
1878                 q->emitResult();
1879                 return nullptr;
1880             }
1881         } else {
1882             // Todo: not show "link" on remote dirs if the src urls are not from the same protocol+host+...
1883             q->setError(ERR_CANNOT_SYMLINK);
1884             q->setErrorText(uDest.toDisplayString());
1885             q->emitResult();
1886             return nullptr;
1887         }
1888     }
1889 }
1890 
1891 bool CopyJobPrivate::handleMsdosFsQuirks(QList<CopyInfo>::Iterator it, KFileSystemType::Type fsType)
1892 {
1893     Q_Q(CopyJob);
1894 
1895     QString msg;
1896     SkipDialog_Options options;
1897     SkipType skipType = NoSkipType;
1898 
1899     if (isFatFs(fsType) && !it->linkDest.isEmpty()) { // Copying a symlink
1900         skipType = SkipFatSymlinks;
1901         if (m_autoSkipFatSymlinks) { // Have we already asked the user?
1902             processCopyNextFile(it, KIO::Result_Skip, skipType);
1903             return true;
1904         }
1905         options = KIO::SkipDialog_Hide_Retry;
1906         msg = symlinkSupportMsg(it->uDest.toLocalFile(), KFileSystemType::fileSystemName(fsType));
1907     } else if (hasInvalidChars(it->uDest.fileName())) {
1908         skipType = SkipInvalidChars;
1909         if (m_autoReplaceInvalidChars) { // Have we already asked the user?
1910             processCopyNextFile(it, KIO::Result_ReplaceInvalidChars, skipType);
1911             return true;
1912         } else if (m_autoSkipFilesWithInvalidChars) { // Have we already asked the user?
1913             processCopyNextFile(it, KIO::Result_Skip, skipType);
1914             return true;
1915         }
1916 
1917         options = KIO::SkipDialog_Replace_Invalid_Chars;
1918         msg = invalidCharsSupportMsg(it->uDest.toDisplayString(QUrl::PreferLocalFile), KFileSystemType::fileSystemName(fsType));
1919     }
1920 
1921     if (!msg.isEmpty()) {
1922         if (auto *askUserActionInterface = KIO::delegateExtension<KIO::AskUserActionInterface *>(q)) {
1923             if (files.size() > 1) {
1924                 options |= SkipDialog_MultipleItems;
1925             }
1926 
1927             auto skipSignal = &KIO::AskUserActionInterface::askUserSkipResult;
1928             QObject::connect(askUserActionInterface, skipSignal, q, [=](SkipDialog_Result result, KJob *parentJob) {
1929                 Q_ASSERT(parentJob == q);
1930                 // Only receive askUserSkipResult once per skip dialog
1931                 QObject::disconnect(askUserActionInterface, skipSignal, q, nullptr);
1932 
1933                 processCopyNextFile(it, result, skipType);
1934             });
1935 
1936             askUserActionInterface->askUserSkip(q, options, msg);
1937 
1938             return true;
1939         } else { // No Job Ui delegate
1940             qCWarning(KIO_COPYJOB_DEBUG) << msg;
1941             q->emitResult();
1942             return true;
1943         }
1944     }
1945 
1946     return false; // Not handled, move on
1947 }
1948 
1949 void CopyJobPrivate::copyNextFile()
1950 {
1951     Q_Q(CopyJob);
1952     bool bCopyFile = false;
1953     qCDebug(KIO_COPYJOB_DEBUG);
1954 
1955     bool isDestLocal = m_globalDest.isLocalFile();
1956 
1957     // Take the first file in the list
1958     QList<CopyInfo>::Iterator it = files.begin();
1959     // Is this URL on the skip list ?
1960     while (it != files.end() && !bCopyFile) {
1961         const QString destFile = (*it).uDest.path();
1962         bCopyFile = !shouldSkip(destFile);
1963         if (!bCopyFile) {
1964             it = files.erase(it);
1965         }
1966 
1967         if (it != files.end() && isDestLocal && (*it).size > 0xFFFFFFFF) { // 4GB-1
1968             const auto destFileSystem = KFileSystemType::fileSystemType(m_globalDest.toLocalFile());
1969             if (destFileSystem == KFileSystemType::Fat) {
1970                 q->setError(ERR_FILE_TOO_LARGE_FOR_FAT32);
1971                 q->setErrorText((*it).uDest.toDisplayString());
1972                 q->emitResult();
1973                 return;
1974             }
1975         }
1976     }
1977 
1978     if (bCopyFile) { // any file to create, finally ?
1979         if (isDestLocal) {
1980             const auto destFileSystem = KFileSystemType::fileSystemType(m_globalDest.toLocalFile());
1981             if (isFatOrNtfs(destFileSystem)) {
1982                 if (handleMsdosFsQuirks(it, destFileSystem)) {
1983                     return;
1984                 }
1985             }
1986         }
1987 
1988         processCopyNextFile(it, -1, NoSkipType);
1989     } else {
1990         // We're done
1991         qCDebug(KIO_COPYJOB_DEBUG) << "copyNextFile finished";
1992         --m_processedFiles; // undo the "start at 1" hack
1993         slotReport(); // display final numbers, important if progress dialog stays up
1994 
1995         deleteNextDir();
1996     }
1997 }
1998 
1999 void CopyJobPrivate::processCopyNextFile(const QList<CopyInfo>::Iterator &it, int result, SkipType skipType)
2000 {
2001     Q_Q(CopyJob);
2002 
2003     switch (result) {
2004     case Result_Cancel:
2005         q->setError(ERR_USER_CANCELED);
2006         q->emitResult();
2007         return;
2008     case KIO::Result_ReplaceAllInvalidChars:
2009         m_autoReplaceInvalidChars = true;
2010         Q_FALLTHROUGH();
2011     case KIO::Result_ReplaceInvalidChars: {
2012         QString fileName = it->uDest.fileName();
2013         const int len = fileName.size();
2014         cleanMsdosDestName(fileName);
2015         QString path = it->uDest.path();
2016         path.replace(path.size() - len, len, fileName);
2017         it->uDest.setPath(path);
2018         break;
2019     }
2020     case KIO::Result_AutoSkip:
2021         if (skipType == SkipInvalidChars) {
2022             m_autoSkipFilesWithInvalidChars = true;
2023         } else if (skipType == SkipFatSymlinks) {
2024             m_autoSkipFatSymlinks = true;
2025         }
2026         Q_FALLTHROUGH();
2027     case KIO::Result_Skip:
2028         // Move on the next file
2029         files.erase(it);
2030         copyNextFile();
2031         return;
2032     default:
2033         break;
2034     }
2035 
2036     qCDebug(KIO_COPYJOB_DEBUG) << "preparing to copy" << (*it).uSource << (*it).size << m_freeSpace;
2037     if (m_freeSpace != KIO::invalidFilesize && (*it).size != KIO::invalidFilesize) {
2038         if (m_freeSpace < (*it).size) {
2039             q->setError(ERR_DISK_FULL);
2040             q->emitResult();
2041             return;
2042         }
2043     }
2044 
2045     const QUrl &uSource = (*it).uSource;
2046     const QUrl &uDest = (*it).uDest;
2047     // Do we set overwrite ?
2048     bool bOverwrite;
2049     const QString destFile = uDest.path();
2050     qCDebug(KIO_COPYJOB_DEBUG) << "copying" << destFile;
2051     if (uDest == uSource) {
2052         bOverwrite = false;
2053     } else {
2054         bOverwrite = shouldOverwriteFile(destFile);
2055     }
2056 
2057     // If source isn't local and target is local, we ignore the original permissions
2058     // Otherwise, files downloaded from HTTP end up with -r--r--r--
2059     int permissions = (*it).permissions;
2060     if (m_defaultPermissions || (m_ignoreSourcePermissions && uDest.isLocalFile())) {
2061         permissions = -1;
2062     }
2063     const JobFlags flags = bOverwrite ? Overwrite : DefaultFlags;
2064 
2065     m_bCurrentOperationIsLink = false;
2066     KIO::Job *newjob = nullptr;
2067     if (m_mode == CopyJob::Link) {
2068         // User requested that a symlink be made
2069         newjob = linkNextFile(uSource, uDest, flags);
2070         if (!newjob) {
2071             return;
2072         }
2073     } else if (!(*it).linkDest.isEmpty() && compareUrls(uSource, uDest))
2074     // Copying a symlink - only on the same protocol/host/etc. (#5601, downloading an FTP file through its link),
2075     {
2076         KIO::SimpleJob *newJob = KIO::symlink((*it).linkDest, uDest, flags | HideProgressInfo /*no GUI*/);
2077         newJob->setParentJob(q);
2078         newjob = newJob;
2079         qCDebug(KIO_COPYJOB_DEBUG) << "Linking target=" << (*it).linkDest << "link=" << uDest;
2080         m_currentSrcURL = QUrl::fromUserInput((*it).linkDest);
2081         m_currentDestURL = uDest;
2082         m_bURLDirty = true;
2083         // emit linking( this, (*it).linkDest, uDest );
2084         // Observer::self()->slotCopying( this, m_currentSrcURL, uDest ); // should be slotLinking perhaps
2085         m_bCurrentOperationIsLink = true;
2086         // NOTE: if we are moving stuff, the deletion of the source will be done in slotResultCopyingFiles
2087     } else if (m_mode == CopyJob::Move) { // Moving a file
2088         KIO::FileCopyJob *moveJob = KIO::file_move(uSource, uDest, permissions, flags | HideProgressInfo /*no GUI*/);
2089         moveJob->setParentJob(q);
2090         moveJob->setSourceSize((*it).size);
2091         moveJob->setModificationTime((*it).mtime); // #55804
2092         newjob = moveJob;
2093         qCDebug(KIO_COPYJOB_DEBUG) << "Moving" << uSource << "to" << uDest;
2094         // emit moving( this, uSource, uDest );
2095         m_currentSrcURL = uSource;
2096         m_currentDestURL = uDest;
2097         m_bURLDirty = true;
2098         // Observer::self()->slotMoving( this, uSource, uDest );
2099     } else { // Copying a file
2100         KIO::FileCopyJob *copyJob = KIO::file_copy(uSource, uDest, permissions, flags | HideProgressInfo /*no GUI*/);
2101         copyJob->setParentJob(q); // in case of rename dialog
2102         copyJob->setSourceSize((*it).size);
2103         copyJob->setModificationTime((*it).mtime);
2104         newjob = copyJob;
2105         qCDebug(KIO_COPYJOB_DEBUG) << "Copying" << uSource << "to" << uDest;
2106         m_currentSrcURL = uSource;
2107         m_currentDestURL = uDest;
2108         m_bURLDirty = true;
2109     }
2110     q->addSubjob(newjob);
2111     q->connect(newjob, &Job::processedSize, q, [this](KJob *job, qulonglong processedSize) {
2112         slotProcessedSize(job, processedSize);
2113     });
2114     q->connect(newjob, &Job::totalSize, q, [this](KJob *job, qulonglong totalSize) {
2115         slotTotalSize(job, totalSize);
2116     });
2117 }
2118 
2119 void CopyJobPrivate::deleteNextDir()
2120 {
2121     Q_Q(CopyJob);
2122     if (m_mode == CopyJob::Move && !dirsToRemove.isEmpty()) { // some dirs to delete ?
2123         state = STATE_DELETING_DIRS;
2124         m_bURLDirty = true;
2125         // Take first dir to delete out of list - last ones first !
2126         QList<QUrl>::Iterator it = --dirsToRemove.end();
2127         SimpleJob *job = KIO::rmdir(*it);
2128         job->setParentJob(q);
2129         dirsToRemove.erase(it);
2130         q->addSubjob(job);
2131     } else {
2132         // This step is done, move on
2133         state = STATE_SETTING_DIR_ATTRIBUTES;
2134         m_directoriesCopiedIterator = m_directoriesCopied.cbegin();
2135         setNextDirAttribute();
2136     }
2137 }
2138 
2139 void CopyJobPrivate::setNextDirAttribute()
2140 {
2141     Q_Q(CopyJob);
2142     while (m_directoriesCopiedIterator != m_directoriesCopied.cend() && !(*m_directoriesCopiedIterator).mtime.isValid()) {
2143         ++m_directoriesCopiedIterator;
2144     }
2145     if (m_directoriesCopiedIterator != m_directoriesCopied.cend()) {
2146         const QUrl url = (*m_directoriesCopiedIterator).uDest;
2147         const QDateTime dt = (*m_directoriesCopiedIterator).mtime;
2148         ++m_directoriesCopiedIterator;
2149 
2150         KIO::SimpleJob *job = KIO::setModificationTime(url, dt);
2151         job->setParentJob(q);
2152         q->addSubjob(job);
2153     } else {
2154         if (m_reportTimer) {
2155             m_reportTimer->stop();
2156         }
2157 
2158         q->emitResult();
2159     }
2160 }
2161 
2162 void CopyJob::emitResult()
2163 {
2164     Q_D(CopyJob);
2165     // Before we go, tell the world about the changes that were made.
2166     // Even if some error made us abort midway, we might still have done
2167     // part of the job so we better update the views! (#118583)
2168     if (!d->m_bOnlyRenames) {
2169         // If only renaming happened, KDirNotify::FileRenamed was emitted by the rename jobs
2170         QUrl url(d->m_globalDest);
2171         if (d->m_globalDestinationState != DEST_IS_DIR || d->m_asMethod) {
2172             url = url.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash);
2173         }
2174         qCDebug(KIO_COPYJOB_DEBUG) << "KDirNotify'ing FilesAdded" << url;
2175 #ifndef KIO_ANDROID_STUB
2176         org::kde::KDirNotify::emitFilesAdded(url);
2177 #endif
2178 
2179         if (d->m_mode == CopyJob::Move && !d->m_successSrcList.isEmpty()) {
2180             qCDebug(KIO_COPYJOB_DEBUG) << "KDirNotify'ing FilesRemoved" << d->m_successSrcList;
2181 #ifndef KIO_ANDROID_STUB
2182             org::kde::KDirNotify::emitFilesRemoved(d->m_successSrcList);
2183 #endif
2184         }
2185     }
2186 
2187     // Re-enable watching on the dirs that held the deleted/moved files
2188     if (d->m_mode == CopyJob::Move) {
2189         for (const auto &dir : d->m_parentDirs) {
2190             KDirWatch::self()->restartDirScan(dir);
2191         }
2192     }
2193     Job::emitResult();
2194 }
2195 
2196 void CopyJobPrivate::slotProcessedSize(KJob *, qulonglong data_size)
2197 {
2198     Q_Q(CopyJob);
2199     qCDebug(KIO_COPYJOB_DEBUG) << data_size;
2200     m_fileProcessedSize = data_size;
2201 
2202     if (m_processedSize + m_fileProcessedSize > m_totalSize) {
2203         // Example: download any attachment from bugs.kde.org
2204         m_totalSize = m_processedSize + m_fileProcessedSize;
2205         qCDebug(KIO_COPYJOB_DEBUG) << "Adjusting m_totalSize to" << m_totalSize;
2206         q->setTotalAmount(KJob::Bytes, m_totalSize); // safety
2207     }
2208     qCDebug(KIO_COPYJOB_DEBUG) << "emit processedSize" << (unsigned long)(m_processedSize + m_fileProcessedSize);
2209 }
2210 
2211 void CopyJobPrivate::slotTotalSize(KJob *, qulonglong size)
2212 {
2213     Q_Q(CopyJob);
2214     qCDebug(KIO_COPYJOB_DEBUG) << size;
2215     // Special case for copying a single file
2216     // This is because some protocols don't implement stat properly
2217     // (e.g. HTTP), and don't give us a size in some cases (redirection)
2218     // so we'd rather rely on the size given for the transfer
2219     if (m_bSingleFileCopy && size != m_totalSize) {
2220         qCDebug(KIO_COPYJOB_DEBUG) << "slotTotalSize: updating totalsize to" << size;
2221         m_totalSize = size;
2222         q->setTotalAmount(KJob::Bytes, size);
2223     }
2224 }
2225 
2226 void CopyJobPrivate::slotResultDeletingDirs(KJob *job)
2227 {
2228     Q_Q(CopyJob);
2229     if (job->error()) {
2230         // Couldn't remove directory. Well, perhaps it's not empty
2231         // because the user pressed Skip for a given file in it.
2232         // Let's not display "Could not remove dir ..." for each of those dir !
2233     } else {
2234         m_successSrcList.append(static_cast<KIO::SimpleJob *>(job)->url());
2235     }
2236     q->removeSubjob(job);
2237     Q_ASSERT(!q->hasSubjobs());
2238     deleteNextDir();
2239 }
2240 
2241 void CopyJobPrivate::slotResultSettingDirAttributes(KJob *job)
2242 {
2243     Q_Q(CopyJob);
2244     if (job->error()) {
2245         // Couldn't set directory attributes. Ignore the error, it can happen
2246         // with inferior file systems like VFAT.
2247         // Let's not display warnings for each dir like "cp -a" does.
2248     }
2249     q->removeSubjob(job);
2250     Q_ASSERT(!q->hasSubjobs());
2251     setNextDirAttribute();
2252 }
2253 
2254 void CopyJobPrivate::directRenamingFailed(const QUrl &dest)
2255 {
2256     Q_Q(CopyJob);
2257 
2258     qCDebug(KIO_COPYJOB_DEBUG) << "Couldn't rename" << m_currentSrcURL << "to" << dest << ", reverting to normal way, starting with stat";
2259     qCDebug(KIO_COPYJOB_DEBUG) << "KIO::stat on" << m_currentSrcURL;
2260 
2261     KIO::Job *job = KIO::stat(m_currentSrcURL, KIO::HideProgressInfo);
2262     state = STATE_STATING;
2263     q->addSubjob(job);
2264     m_bOnlyRenames = false;
2265 }
2266 
2267 // We were trying to do a direct renaming, before even stat'ing
2268 void CopyJobPrivate::slotResultRenaming(KJob *job)
2269 {
2270     Q_Q(CopyJob);
2271     int err = job->error();
2272     const QString errText = job->errorText();
2273     // Merge metadata from subjob
2274     KIO::Job *kiojob = qobject_cast<KIO::Job *>(job);
2275     Q_ASSERT(kiojob);
2276     m_incomingMetaData += kiojob->metaData();
2277     q->removeSubjob(job);
2278     Q_ASSERT(!q->hasSubjobs());
2279     // Determine dest again
2280     QUrl dest = m_dest;
2281     if (destinationState == DEST_IS_DIR && !m_asMethod) {
2282         dest = addPathToUrl(dest, m_currentSrcURL.fileName());
2283     }
2284     auto *askUserActionInterface = KIO::delegateExtension<KIO::AskUserActionInterface *>(q);
2285 
2286     if (err) {
2287         // This code is similar to CopyJobPrivate::slotResultErrorCopyingFiles
2288         // but here it's about the base src url being moved/renamed
2289         // (m_currentSrcURL) and its dest (m_dest), not about a single file.
2290         // It also means we already stated the dest, here.
2291         // On the other hand we haven't stated the src yet (we skipped doing it
2292         // to save time, since it's not necessary to rename directly!)...
2293 
2294         // Existing dest?
2295         if (err == ERR_DIR_ALREADY_EXIST || err == ERR_FILE_ALREADY_EXIST || err == ERR_IDENTICAL_FILES) {
2296             // Should we skip automatically ?
2297             bool isDir = (err == ERR_DIR_ALREADY_EXIST); // ## technically, isDir means "source is dir", not "dest is dir" #######
2298             if ((isDir && m_bAutoSkipDirs) || (!isDir && m_bAutoSkipFiles)) {
2299                 // Move on to next source url
2300                 ++m_filesHandledByDirectRename;
2301                 skipSrc(isDir);
2302                 return;
2303             } else if ((isDir && m_bOverwriteAllDirs) || (!isDir && m_bOverwriteAllFiles)) {
2304                 ; // nothing to do, stat+copy+del will overwrite
2305             } else if ((isDir && m_bAutoRenameDirs) || (!isDir && m_bAutoRenameFiles)) {
2306                 QUrl destDirectory = m_currentDestURL.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash); // m_currendDestURL includes filename
2307                 const QString newName = KFileUtils::suggestName(destDirectory, m_currentDestURL.fileName());
2308 
2309                 m_dest = destDirectory;
2310                 m_dest.setPath(Utils::concatPaths(m_dest.path(), newName));
2311                 Q_EMIT q->renamed(q, dest, m_dest);
2312                 KIO::Job *job = KIO::stat(m_dest, StatJob::DestinationSide, KIO::StatDefaultDetails, KIO::HideProgressInfo);
2313                 state = STATE_STATING;
2314                 destinationState = DEST_NOT_STATED;
2315                 q->addSubjob(job);
2316                 return;
2317             } else if (askUserActionInterface) {
2318                 // we lack mtime info for both the src (not stated)
2319                 // and the dest (stated but this info wasn't stored)
2320                 // Let's do it for local files, at least
2321                 KIO::filesize_t sizeSrc = KIO::invalidFilesize;
2322                 KIO::filesize_t sizeDest = KIO::invalidFilesize;
2323                 QDateTime ctimeSrc;
2324                 QDateTime ctimeDest;
2325                 QDateTime mtimeSrc;
2326                 QDateTime mtimeDest;
2327 
2328                 bool destIsDir = err == ERR_DIR_ALREADY_EXIST;
2329 
2330                 // ## TODO we need to stat the source using KIO::stat
2331                 // so that this code is properly network-transparent.
2332 
2333                 if (m_currentSrcURL.isLocalFile()) {
2334                     QFileInfo info(m_currentSrcURL.toLocalFile());
2335                     if (info.exists()) {
2336                         sizeSrc = info.size();
2337                         ctimeSrc = info.birthTime();
2338                         mtimeSrc = info.lastModified();
2339                         isDir = info.isDir();
2340                     }
2341                 }
2342                 if (dest.isLocalFile()) {
2343                     QFileInfo destInfo(dest.toLocalFile());
2344                     if (destInfo.exists()) {
2345                         sizeDest = destInfo.size();
2346                         ctimeDest = destInfo.birthTime();
2347                         mtimeDest = destInfo.lastModified();
2348                         destIsDir = destInfo.isDir();
2349                     }
2350                 }
2351 
2352                 // If src==dest, use "overwrite-itself"
2353                 RenameDialog_Options options = (m_currentSrcURL == dest) ? RenameDialog_OverwriteItself : RenameDialog_Overwrite;
2354                 if (!isDir && destIsDir) {
2355                     // We can't overwrite a dir with a file.
2356                     options = RenameDialog_Options();
2357                 }
2358 
2359                 if (m_srcList.count() > 1) {
2360                     options |= RenameDialog_Options(RenameDialog_MultipleItems | RenameDialog_Skip);
2361                 }
2362 
2363                 if (destIsDir) {
2364                     options |= RenameDialog_DestIsDirectory;
2365                 }
2366 
2367                 if (m_reportTimer) {
2368                     m_reportTimer->stop();
2369                 }
2370 
2371                 RenameDialog_Result r;
2372                 if (m_bOverwriteWhenOlder && mtimeSrc.isValid() && mtimeDest.isValid()) {
2373                     if (mtimeSrc > mtimeDest) {
2374                         qCDebug(KIO_COPYJOB_DEBUG) << "dest is older, overwriting" << dest;
2375                         r = Result_Overwrite;
2376                     } else {
2377                         qCDebug(KIO_COPYJOB_DEBUG) << "dest is newer, skipping" << dest;
2378                         r = Result_Skip;
2379                     }
2380 
2381                     processDirectRenamingConflictResult(r, isDir, destIsDir, mtimeSrc, mtimeDest, dest, QUrl{});
2382                     return;
2383                 } else {
2384                     auto renameSignal = &KIO::AskUserActionInterface::askUserRenameResult;
2385                     QObject::connect(askUserActionInterface, renameSignal, q, [=](RenameDialog_Result result, const QUrl &newUrl, KJob *parentJob) {
2386                         Q_ASSERT(parentJob == q);
2387                         // Only receive askUserRenameResult once per rename dialog
2388                         QObject::disconnect(askUserActionInterface, renameSignal, q, nullptr);
2389 
2390                         processDirectRenamingConflictResult(result, isDir, destIsDir, mtimeSrc, mtimeDest, dest, newUrl);
2391                     });
2392 
2393                     const QString title = err != ERR_DIR_ALREADY_EXIST ? i18n("File Already Exists") : i18n("Already Exists as Folder");
2394 
2395                     /* clang-format off */
2396                     askUserActionInterface->askUserRename(q, title,
2397                                                           m_currentSrcURL, dest,
2398                                                           options,
2399                                                           sizeSrc, sizeDest,
2400                                                           ctimeSrc, ctimeDest,
2401                                                           mtimeSrc, mtimeDest);
2402                     /* clang-format on */
2403 
2404                     return;
2405                 }
2406             } else if (err != KIO::ERR_UNSUPPORTED_ACTION) {
2407                 // Dest already exists, and job is not interactive -> abort with error
2408                 q->setError(err);
2409                 q->setErrorText(errText);
2410                 q->emitResult();
2411                 return;
2412             }
2413         } else if (err != KIO::ERR_UNSUPPORTED_ACTION) {
2414             qCDebug(KIO_COPYJOB_DEBUG) << "Couldn't rename" << m_currentSrcURL << "to" << dest << ", aborting";
2415             q->setError(err);
2416             q->setErrorText(errText);
2417             q->emitResult();
2418             return;
2419         }
2420 
2421         directRenamingFailed(dest);
2422         return;
2423     }
2424 
2425     // No error
2426     qCDebug(KIO_COPYJOB_DEBUG) << "Renaming succeeded, move on";
2427     ++m_processedFiles;
2428     ++m_filesHandledByDirectRename;
2429     // Emit copyingDone for FileUndoManager to remember what we did.
2430     // Use resolved URL m_currentSrcURL since that's what we just used for renaming. See bug 391606 and kio_desktop's testTrashAndUndo().
2431     const bool srcIsDir = false; // # TODO: we just don't know, since we never stat'ed it
2432     Q_EMIT q->copyingDone(q, m_currentSrcURL, finalDestUrl(m_currentSrcURL, dest), QDateTime() /*mtime unknown, and not needed*/, srcIsDir, true);
2433     m_successSrcList.append(*m_currentStatSrc);
2434     statNextSrc();
2435 }
2436 
2437 void CopyJobPrivate::processDirectRenamingConflictResult(RenameDialog_Result result,
2438                                                          bool srcIsDir,
2439                                                          bool destIsDir,
2440                                                          const QDateTime &mtimeSrc,
2441                                                          const QDateTime &mtimeDest,
2442                                                          const QUrl &dest,
2443                                                          const QUrl &newUrl)
2444 {
2445     Q_Q(CopyJob);
2446 
2447     if (m_reportTimer) {
2448         m_reportTimer->start(s_reportTimeout);
2449     }
2450 
2451     if (result == Result_OverwriteWhenOlder) {
2452         m_bOverwriteWhenOlder = true;
2453         if (mtimeSrc > mtimeDest) {
2454             qCDebug(KIO_COPYJOB_DEBUG) << "dest is older, overwriting" << dest;
2455             result = Result_Overwrite;
2456         } else {
2457             qCDebug(KIO_COPYJOB_DEBUG) << "dest is newer, skipping" << dest;
2458             result = Result_Skip;
2459         }
2460     }
2461 
2462     switch (result) {
2463     case Result_Cancel: {
2464         q->setError(ERR_USER_CANCELED);
2465         q->emitResult();
2466         return;
2467     }
2468     case Result_AutoRename:
2469         if (srcIsDir) {
2470             m_bAutoRenameDirs = true;
2471         } else {
2472             m_bAutoRenameFiles = true;
2473         }
2474         // fall through
2475         Q_FALLTHROUGH();
2476     case Result_Rename: {
2477         // Set m_dest to the chosen destination
2478         // This is only for this src url; the next one will revert to m_globalDest
2479         m_dest = newUrl;
2480         Q_EMIT q->renamed(q, dest, m_dest); // For e.g. KPropertiesDialog
2481         KIO::Job *job = KIO::stat(m_dest, StatJob::DestinationSide, KIO::StatDefaultDetails, KIO::HideProgressInfo);
2482         state = STATE_STATING;
2483         destinationState = DEST_NOT_STATED;
2484         q->addSubjob(job);
2485         return;
2486     }
2487     case Result_AutoSkip:
2488         if (srcIsDir) {
2489             m_bAutoSkipDirs = true;
2490         } else {
2491             m_bAutoSkipFiles = true;
2492         }
2493         // fall through
2494         Q_FALLTHROUGH();
2495     case Result_Skip:
2496         // Move on to next url
2497         ++m_filesHandledByDirectRename;
2498         skipSrc(srcIsDir);
2499         return;
2500     case Result_OverwriteAll:
2501         if (destIsDir) {
2502             m_bOverwriteAllDirs = true;
2503         } else {
2504             m_bOverwriteAllFiles = true;
2505         }
2506         break;
2507     case Result_Overwrite:
2508         // Add to overwrite list
2509         // Note that we add dest, not m_dest.
2510         // This ensures that when moving several urls into a dir (m_dest),
2511         // we only overwrite for the current one, not for all.
2512         // When renaming a single file (m_asMethod), it makes no difference.
2513         qCDebug(KIO_COPYJOB_DEBUG) << "adding to overwrite list: " << dest.path();
2514         m_overwriteList.insert(dest.path());
2515         break;
2516     default:
2517         // Q_ASSERT( 0 );
2518         break;
2519     }
2520 
2521     directRenamingFailed(dest);
2522 }
2523 
2524 void CopyJob::slotResult(KJob *job)
2525 {
2526     Q_D(CopyJob);
2527     qCDebug(KIO_COPYJOB_DEBUG) << "d->state=" << (int)d->state;
2528     // In each case, what we have to do is :
2529     // 1 - check for errors and treat them
2530     // 2 - removeSubjob(job);
2531     // 3 - decide what to do next
2532 
2533     switch (d->state) {
2534     case STATE_STATING: // We were trying to stat a src url or the dest
2535         d->slotResultStating(job);
2536         break;
2537     case STATE_RENAMING: { // We were trying to do a direct renaming, before even stat'ing
2538         d->slotResultRenaming(job);
2539         break;
2540     }
2541     case STATE_LISTING: // recursive listing finished
2542         qCDebug(KIO_COPYJOB_DEBUG) << "totalSize:" << (unsigned int)d->m_totalSize << "files:" << d->files.count() << "d->dirs:" << d->dirs.count();
2543         // Was there an error ?
2544         if (job->error()) {
2545             Job::slotResult(job); // will set the error and emit result(this)
2546             return;
2547         }
2548 
2549         removeSubjob(job);
2550         Q_ASSERT(!hasSubjobs());
2551 
2552         d->statNextSrc();
2553         break;
2554     case STATE_CREATING_DIRS:
2555         d->slotResultCreatingDirs(job);
2556         break;
2557     case STATE_CONFLICT_CREATING_DIRS:
2558         d->slotResultConflictCreatingDirs(job);
2559         break;
2560     case STATE_COPYING_FILES:
2561         d->slotResultCopyingFiles(job);
2562         break;
2563     case STATE_CONFLICT_COPYING_FILES:
2564         d->slotResultErrorCopyingFiles(job);
2565         break;
2566     case STATE_DELETING_DIRS:
2567         d->slotResultDeletingDirs(job);
2568         break;
2569     case STATE_SETTING_DIR_ATTRIBUTES:
2570         d->slotResultSettingDirAttributes(job);
2571         break;
2572     default:
2573         Q_ASSERT(0);
2574     }
2575 }
2576 
2577 void KIO::CopyJob::setDefaultPermissions(bool b)
2578 {
2579     d_func()->m_defaultPermissions = b;
2580 }
2581 
2582 KIO::CopyJob::CopyMode KIO::CopyJob::operationMode() const
2583 {
2584     return d_func()->m_mode;
2585 }
2586 
2587 void KIO::CopyJob::setAutoSkip(bool autoSkip)
2588 {
2589     d_func()->m_bAutoSkipFiles = autoSkip;
2590     d_func()->m_bAutoSkipDirs = autoSkip;
2591 }
2592 
2593 void KIO::CopyJob::setAutoRename(bool autoRename)
2594 {
2595     d_func()->m_bAutoRenameFiles = autoRename;
2596     d_func()->m_bAutoRenameDirs = autoRename;
2597 }
2598 
2599 void KIO::CopyJob::setWriteIntoExistingDirectories(bool overwriteAll) // #65926
2600 {
2601     d_func()->m_bOverwriteAllDirs = overwriteAll;
2602 }
2603 
2604 CopyJob *KIO::copy(const QUrl &src, const QUrl &dest, JobFlags flags)
2605 {
2606     qCDebug(KIO_COPYJOB_DEBUG) << "src=" << src << "dest=" << dest;
2607     QList<QUrl> srcList;
2608     srcList.append(src);
2609     return CopyJobPrivate::newJob(srcList, dest, CopyJob::Copy, false, flags);
2610 }
2611 
2612 CopyJob *KIO::copyAs(const QUrl &src, const QUrl &dest, JobFlags flags)
2613 {
2614     qCDebug(KIO_COPYJOB_DEBUG) << "src=" << src << "dest=" << dest;
2615     QList<QUrl> srcList;
2616     srcList.append(src);
2617     return CopyJobPrivate::newJob(srcList, dest, CopyJob::Copy, true, flags);
2618 }
2619 
2620 CopyJob *KIO::copy(const QList<QUrl> &src, const QUrl &dest, JobFlags flags)
2621 {
2622     qCDebug(KIO_COPYJOB_DEBUG) << src << dest;
2623     return CopyJobPrivate::newJob(src, dest, CopyJob::Copy, false, flags);
2624 }
2625 
2626 CopyJob *KIO::move(const QUrl &src, const QUrl &dest, JobFlags flags)
2627 {
2628     qCDebug(KIO_COPYJOB_DEBUG) << src << dest;
2629     QList<QUrl> srcList;
2630     srcList.append(src);
2631     CopyJob *job = CopyJobPrivate::newJob(srcList, dest, CopyJob::Move, false, flags);
2632     if (job->uiDelegateExtension()) {
2633         job->uiDelegateExtension()->createClipboardUpdater(job, JobUiDelegateExtension::UpdateContent);
2634     }
2635     return job;
2636 }
2637 
2638 CopyJob *KIO::moveAs(const QUrl &src, const QUrl &dest, JobFlags flags)
2639 {
2640     qCDebug(KIO_COPYJOB_DEBUG) << src << dest;
2641     QList<QUrl> srcList;
2642     srcList.append(src);
2643     CopyJob *job = CopyJobPrivate::newJob(srcList, dest, CopyJob::Move, true, flags);
2644     if (job->uiDelegateExtension()) {
2645         job->uiDelegateExtension()->createClipboardUpdater(job, JobUiDelegateExtension::UpdateContent);
2646     }
2647     return job;
2648 }
2649 
2650 CopyJob *KIO::move(const QList<QUrl> &src, const QUrl &dest, JobFlags flags)
2651 {
2652     qCDebug(KIO_COPYJOB_DEBUG) << src << dest;
2653     CopyJob *job = CopyJobPrivate::newJob(src, dest, CopyJob::Move, false, flags);
2654     if (job->uiDelegateExtension()) {
2655         job->uiDelegateExtension()->createClipboardUpdater(job, JobUiDelegateExtension::UpdateContent);
2656     }
2657     return job;
2658 }
2659 
2660 CopyJob *KIO::link(const QUrl &src, const QUrl &destDir, JobFlags flags)
2661 {
2662     QList<QUrl> srcList;
2663     srcList.append(src);
2664     return CopyJobPrivate::newJob(srcList, destDir, CopyJob::Link, false, flags);
2665 }
2666 
2667 CopyJob *KIO::link(const QList<QUrl> &srcList, const QUrl &destDir, JobFlags flags)
2668 {
2669     return CopyJobPrivate::newJob(srcList, destDir, CopyJob::Link, false, flags);
2670 }
2671 
2672 CopyJob *KIO::linkAs(const QUrl &src, const QUrl &destDir, JobFlags flags)
2673 {
2674     QList<QUrl> srcList;
2675     srcList.append(src);
2676     return CopyJobPrivate::newJob(srcList, destDir, CopyJob::Link, true, flags);
2677 }
2678 
2679 CopyJob *KIO::trash(const QUrl &src, JobFlags flags)
2680 {
2681     QList<QUrl> srcList;
2682     srcList.append(src);
2683     return CopyJobPrivate::newJob(srcList, QUrl(QStringLiteral("trash:/")), CopyJob::Move, false, flags);
2684 }
2685 
2686 CopyJob *KIO::trash(const QList<QUrl> &srcList, JobFlags flags)
2687 {
2688     return CopyJobPrivate::newJob(srcList, QUrl(QStringLiteral("trash:/")), CopyJob::Move, false, flags);
2689 }
2690 
2691 #include "moc_copyjob.cpp"