File indexing completed on 2024-05-05 16:13:06

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