1 /*
2     This file is part of the KDE libraries
3     SPDX-FileCopyrightText: 2000 Stephan Kulow <coolo@kde.org>
4     SPDX-FileCopyrightText: 2000-2006 David Faure <faure@kde.org>
5     SPDX-FileCopyrightText: 2000 Waldo Bastian <bastian@kde.org>
6     SPDX-FileCopyrightText: 2021 Ahmad Samir <a.samirh78@gmail.com>
7 
8     SPDX-License-Identifier: LGPL-2.0-or-later
9 */
10 
11 #include "copyjob.h"
12 #include "../pathhelpers_p.h"
13 #include "deletejob.h"
14 #include "filecopyjob.h"
15 #include "global.h"
16 #include "job.h" // buildErrorString
17 #include "kcoredirlister.h"
18 #include "kfileitem.h"
19 #include "kiocoredebug.h"
20 #include "kioglobal_p.h"
21 #include "listjob.h"
22 #include "mkdirjob.h"
23 #include "statjob.h"
24 #include <cerrno>
25 
26 #include <KConfigGroup>
27 #include <KDesktopFile>
28 #include <KLocalizedString>
29 
30 #include "kprotocolmanager.h"
31 #include "scheduler.h"
32 #include "slave.h"
33 #include <KDirWatch>
34 
35 #include "askuseractioninterface.h"
36 #include <jobuidelegateextension.h>
37 #include <kio/jobuidelegatefactory.h>
38 
39 #include <kdirnotify.h>
40 
41 #ifdef Q_OS_UNIX
42 #include <utime.h>
43 #endif
44 
45 #include <QFile>
46 #include <QFileInfo>
47 #include <QPointer>
48 #include <QTemporaryFile>
49 #include <QTimer>
50 
51 #include <sys/stat.h> // mode_t
52 
53 #include "job_p.h"
54 #include <KFileSystemType>
55 #include <KFileUtils>
56 #include <KIO/FileSystemFreeSpaceJob>
57 
58 #include <list>
59 #include <set>
60 
61 #include <QLoggingCategory>
62 Q_DECLARE_LOGGING_CATEGORY(KIO_COPYJOB_DEBUG)
63 Q_LOGGING_CATEGORY(KIO_COPYJOB_DEBUG, "kf.kio.core.copyjob", QtWarningMsg)
64 
65 using namespace KIO;
66 
67 // this will update the report dialog with 5 Hz, I think this is fast enough, aleXXX
68 static constexpr int s_reportTimeout = 200;
69 
70 #if !defined(NAME_MAX)
71 #if defined(_MAX_FNAME)
72 static constexpr int NAME_MAX = _MAX_FNAME; // For Windows
73 #else
74 static constexpr NAME_MAX = 0;
75 #endif
76 #endif
77 
78 enum DestinationState {
79     DEST_NOT_STATED,
80     DEST_IS_DIR,
81     DEST_IS_FILE,
82     DEST_DOESNT_EXIST,
83 };
84 
85 /**
86  * States:
87  *     STATE_INITIAL the constructor was called
88  *     STATE_STATING for the dest
89  *     statCurrentSrc then does, for each src url:
90  *      STATE_RENAMING if direct rename looks possible
91  *         (on already exists, and user chooses rename, TODO: go to STATE_RENAMING again)
92  *      STATE_STATING
93  *         and then, if dir -> STATE_LISTING (filling 'd->dirs' and 'd->files')
94  *     STATE_CREATING_DIRS (createNextDir, iterating over 'd->dirs')
95  *          if conflict: STATE_CONFLICT_CREATING_DIRS
96  *     STATE_COPYING_FILES (copyNextFile, iterating over 'd->files')
97  *          if conflict: STATE_CONFLICT_COPYING_FILES
98  *     STATE_DELETING_DIRS (deleteNextDir) (if moving)
99  *     STATE_SETTING_DIR_ATTRIBUTES (setNextDirAttribute, iterating over d->m_directoriesCopied)
100  *     done.
101  */
102 enum CopyJobState {
103     STATE_INITIAL,
104     STATE_STATING,
105     STATE_RENAMING,
106     STATE_LISTING,
107     STATE_CREATING_DIRS,
108     STATE_CONFLICT_CREATING_DIRS,
109     STATE_COPYING_FILES,
110     STATE_CONFLICT_COPYING_FILES,
111     STATE_DELETING_DIRS,
112     STATE_SETTING_DIR_ATTRIBUTES,
113 };
114 
addPathToUrl(const QUrl & url,const QString & relPath)115 static QUrl addPathToUrl(const QUrl &url, const QString &relPath)
116 {
117     QUrl u(url);
118     u.setPath(concatPaths(url.path(), relPath));
119     return u;
120 }
121 
compareUrls(const QUrl & srcUrl,const QUrl & destUrl)122 static bool compareUrls(const QUrl &srcUrl, const QUrl &destUrl)
123 {
124     /* clang-format off */
125     return srcUrl.scheme() == destUrl.scheme()
126         && srcUrl.host() == destUrl.host()
127         && srcUrl.port() == destUrl.port()
128         && srcUrl.userName() == destUrl.userName()
129         && srcUrl.password() == destUrl.password();
130     /* clang-format on */
131 }
132 
133 // https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file#naming-conventions
134 static const char s_msdosInvalidChars[] = R"(<>:"/\|?*)";
135 
hasInvalidChars(const QString & dest)136 static bool hasInvalidChars(const QString &dest)
137 {
138     return std::any_of(std::begin(s_msdosInvalidChars), std::end(s_msdosInvalidChars), [=](const char c) {
139         return dest.contains(QLatin1Char(c));
140     });
141 }
142 
143 static void cleanMsdosDestName(QString &name)
144 {
145     for (const char c : s_msdosInvalidChars) {
146         name.replace(QLatin1Char(c), QLatin1String("_"));
147     }
148 }
149 
150 static bool isFatFs(KFileSystemType::Type fsType)
151 {
152     return fsType == KFileSystemType::Fat || fsType == KFileSystemType::Exfat;
153 }
154 
155 static bool isFatOrNtfs(KFileSystemType::Type fsType)
156 {
157     return fsType == KFileSystemType::Ntfs || isFatFs(fsType);
158 }
159 
160 static QString symlinkSupportMsg(const QString &path, const QString &fsName)
161 {
162     const QString msg = i18nc(
163         "The first arg is the path to the symlink that couldn't be created, the second"
164         "arg is the filesystem type (e.g. vfat, exfat)",
165         "Could not create symlink \"%1\".\n"
166         "The destination filesystem (%2) doesn't support symlinks.",
167         path,
168         fsName);
169     return msg;
170 }
171 
172 static QString invalidCharsSupportMsg(const QString &path, const QString &fsName, bool isDir = false)
173 {
174     QString msg;
175     if (isDir) {
176         msg = i18n(
177             "Could not create \"%1\".\n"
178             "The destination filesystem (%2) disallows the following characters in folder names: %3\n"
179             "Selecting Replace will replace any invalid characters (in the destination folder name) with an underscore \"_\".",
180             path,
181             fsName,
182             QLatin1String(s_msdosInvalidChars));
183     } else {
184         msg = i18n(
185             "Could not create \"%1\".\n"
186             "The destination filesystem (%2) disallows the following characters in file names: %3\n"
187             "Selecting Replace will replace any invalid characters (in the destination file name) with an underscore \"_\".",
188             path,
189             fsName,
190             QLatin1String(s_msdosInvalidChars));
191     }
192 
193     return msg;
194 }
195 
196 /** @internal */
197 class KIO::CopyJobPrivate : public KIO::JobPrivate
198 {
199 public:
CopyJobPrivate(const QList<QUrl> & src,const QUrl & dest,CopyJob::CopyMode mode,bool asMethod)200     CopyJobPrivate(const QList<QUrl> &src, const QUrl &dest, CopyJob::CopyMode mode, bool asMethod)
201         : m_globalDest(dest)
202         , m_globalDestinationState(DEST_NOT_STATED)
203         , m_defaultPermissions(false)
204         , m_bURLDirty(false)
205         , m_mode(mode)
206         , m_asMethod(asMethod)
207         , destinationState(DEST_NOT_STATED)
208         , state(STATE_INITIAL)
209         , m_freeSpace(-1)
210         , m_totalSize(0)
211         , m_processedSize(0)
212         , m_fileProcessedSize(0)
213         , m_filesHandledByDirectRename(0)
214         , m_processedFiles(0)
215         , m_processedDirs(0)
216         , m_srcList(src)
217         , m_currentStatSrc(m_srcList.constBegin())
218         , m_bCurrentOperationIsLink(false)
219         , m_bSingleFileCopy(false)
220         , m_bOnlyRenames(mode == CopyJob::Move)
221         , m_dest(dest)
222         , m_bAutoRenameFiles(false)
223         , m_bAutoRenameDirs(false)
224         , m_bAutoSkipFiles(false)
225         , m_bAutoSkipDirs(false)
226         , m_bOverwriteAllFiles(false)
227         , m_bOverwriteAllDirs(false)
228         , m_bOverwriteWhenOlder(false)
229         , m_conflictError(0)
230         , m_reportTimer(nullptr)
231     {
232     }
233 
234     // This is the dest URL that was initially given to CopyJob
235     // It is copied into m_dest, which can be changed for a given src URL
236     // (when using the RENAME dialog in slotResult),
237     // and which will be reset for the next src URL.
238     QUrl m_globalDest;
239     // The state info about that global dest
240     DestinationState m_globalDestinationState;
241     // See setDefaultPermissions
242     bool m_defaultPermissions;
243     // Whether URLs changed (and need to be emitted by the next slotReport call)
244     bool m_bURLDirty;
245     // Used after copying all the files into the dirs, to set mtime (TODO: and permissions?)
246     // after the copy is done
247     std::list<CopyInfo> m_directoriesCopied;
248     std::list<CopyInfo>::const_iterator m_directoriesCopiedIterator;
249 
250     CopyJob::CopyMode m_mode;
251     bool m_asMethod; // See copyAs() method
252     DestinationState destinationState;
253     CopyJobState state;
254 
255     KIO::filesize_t m_freeSpace;
256 
257     KIO::filesize_t m_totalSize;
258     KIO::filesize_t m_processedSize;
259     KIO::filesize_t m_fileProcessedSize;
260     int m_filesHandledByDirectRename;
261     int m_processedFiles;
262     int m_processedDirs;
263     QList<CopyInfo> files;
264     QList<CopyInfo> dirs;
265     // List of dirs that will be copied then deleted when CopyMode is Move
266     QList<QUrl> dirsToRemove;
267     QList<QUrl> m_srcList;
268     QList<QUrl> m_successSrcList; // Entries in m_srcList that have successfully been moved
269     QList<QUrl>::const_iterator m_currentStatSrc;
270     bool m_bCurrentSrcIsDir;
271     bool m_bCurrentOperationIsLink;
272     bool m_bSingleFileCopy;
273     bool m_bOnlyRenames;
274     QUrl m_dest;
275     QUrl m_currentDest; // set during listing, used by slotEntries
276     //
277     QStringList m_skipList;
278     QSet<QString> m_overwriteList;
279     bool m_bAutoRenameFiles;
280     bool m_bAutoRenameDirs;
281     bool m_bAutoSkipFiles;
282     bool m_bAutoSkipDirs;
283     bool m_bOverwriteAllFiles;
284     bool m_bOverwriteAllDirs;
285     bool m_bOverwriteWhenOlder;
286 
287     bool m_autoSkipDirsWithInvalidChars = false;
288     bool m_autoSkipFilesWithInvalidChars = false;
289     bool m_autoReplaceInvalidChars = false;
290 
291     bool m_autoSkipFatSymlinks = false;
292 
293     enum SkipType {
294         // No skip dialog is involved
295         NoSkipType = 0,
296         // SkipDialog is asking about invalid chars in destination file/dir names
297         SkipInvalidChars,
298         // SkipDialog is asking about how to handle symlinks why copying to a
299         // filesystem that doesn't support symlinks
300         SkipFatSymlinks,
301     };
302 
303     int m_conflictError;
304 
305     QTimer *m_reportTimer;
306 
307     // The current src url being stat'ed or copied
308     // During the stat phase, this is initially equal to *m_currentStatSrc but it can be resolved to a local file equivalent (#188903).
309     QUrl m_currentSrcURL;
310     QUrl m_currentDestURL;
311 
312     std::set<QString> m_parentDirs;
313 
314     void statCurrentSrc();
315     void statNextSrc();
316 
317     // Those aren't slots but submethods for slotResult.
318     void slotResultStating(KJob *job);
319     void startListing(const QUrl &src);
320 
321     void slotResultCreatingDirs(KJob *job);
322     void slotResultConflictCreatingDirs(KJob *job);
323     void createNextDir();
324     void processCreateNextDir(const QList<CopyInfo>::Iterator &it, int result);
325 
326     void slotResultCopyingFiles(KJob *job);
327     void slotResultErrorCopyingFiles(KJob *job);
328     void processFileRenameDialogResult(const QList<CopyInfo>::Iterator &it, RenameDialog_Result result, const QUrl &newUrl, const QDateTime &destmtime);
329 
330     //     KIO::Job* linkNextFile( const QUrl& uSource, const QUrl& uDest, bool overwrite );
331     KIO::Job *linkNextFile(const QUrl &uSource, const QUrl &uDest, JobFlags flags);
332     // MsDos filesystems don't allow certain characters in filenames, and VFAT and ExFAT
333     // don't support symlinks, this method detects those conditions and tries to handle it
334     bool handleMsdosFsQuirks(QList<CopyInfo>::Iterator it, KFileSystemType::Type fsType);
335     void copyNextFile();
336     void processCopyNextFile(const QList<CopyInfo>::Iterator &it, int result, SkipType skipType);
337 
338     void slotResultDeletingDirs(KJob *job);
339     void deleteNextDir();
340     void sourceStated(const UDSEntry &entry, const QUrl &sourceUrl);
341     // Removes a dir from the "dirsToRemove" list
342     void skip(const QUrl &sourceURL, bool isDir);
343 
344     void slotResultRenaming(KJob *job);
345     void directRenamingFailed(const QUrl &dest);
346     void processDirectRenamingConflictResult(RenameDialog_Result result,
347                                              bool srcIsDir,
348                                              bool destIsDir,
349                                              const QDateTime &mtimeSrc,
350                                              const QDateTime &mtimeDest,
351                                              const QUrl &dest,
352                                              const QUrl &newUrl);
353 
354     void slotResultSettingDirAttributes(KJob *job);
355     void setNextDirAttribute();
356 
357     void startRenameJob(const QUrl &slave_url);
358     bool shouldOverwriteDir(const QString &path) const;
359     bool shouldOverwriteFile(const QString &path) const;
360     bool shouldSkip(const QString &path) const;
361     void skipSrc(bool isDir);
362     void renameDirectory(const QList<CopyInfo>::iterator &it, const QUrl &newUrl);
363     QUrl finalDestUrl(const QUrl &src, const QUrl &dest) const;
364 
365     void slotStart();
366     void slotEntries(KIO::Job *, const KIO::UDSEntryList &list);
367     void slotSubError(KIO::ListJob *job, KIO::ListJob *subJob);
368     void addCopyInfoFromUDSEntry(const UDSEntry &entry, const QUrl &srcUrl, bool srcIsDir, const QUrl &currentDest);
369     /**
370      * Forward signal from subjob
371      */
372     void slotProcessedSize(KJob *, qulonglong data_size);
373     /**
374      * Forward signal from subjob
375      * @param size the total size
376      */
377     void slotTotalSize(KJob *, qulonglong size);
378 
379     void slotReport();
380 
Q_DECLARE_PUBLIC(CopyJob)381     Q_DECLARE_PUBLIC(CopyJob)
382 
383     static inline CopyJob *newJob(const QList<QUrl> &src, const QUrl &dest, CopyJob::CopyMode mode, bool asMethod, JobFlags flags)
384     {
385         CopyJob *job = new CopyJob(*new CopyJobPrivate(src, dest, mode, asMethod));
386         job->setUiDelegate(KIO::createDefaultJobUiDelegate());
387         if (!(flags & HideProgressInfo)) {
388             KIO::getJobTracker()->registerJob(job);
389         }
390         if (flags & KIO::Overwrite) {
391             job->d_func()->m_bOverwriteAllDirs = true;
392             job->d_func()->m_bOverwriteAllFiles = true;
393         }
394         if (!(flags & KIO::NoPrivilegeExecution)) {
395             job->d_func()->m_privilegeExecutionEnabled = true;
396             FileOperationType copyType;
397             switch (mode) {
398             case CopyJob::Copy:
399                 copyType = Copy;
400                 break;
401             case CopyJob::Move:
402                 copyType = Move;
403                 break;
404             case CopyJob::Link:
405                 copyType = Symlink;
406                 break;
407             default:
408                 Q_UNREACHABLE();
409             }
410             job->d_func()->m_operationType = copyType;
411         }
412         return job;
413     }
414 };
415 
CopyJob(CopyJobPrivate & dd)416 CopyJob::CopyJob(CopyJobPrivate &dd)
417     : Job(dd)
418 {
419     Q_D(CopyJob);
420     setProperty("destUrl", d_func()->m_dest.toString());
421     QTimer::singleShot(0, this, [d]() {
422         d->slotStart();
423     });
424     qRegisterMetaType<KIO::UDSEntry>();
425 }
426 
~CopyJob()427 CopyJob::~CopyJob()
428 {
429 }
430 
srcUrls() const431 QList<QUrl> CopyJob::srcUrls() const
432 {
433     return d_func()->m_srcList;
434 }
435 
destUrl() const436 QUrl CopyJob::destUrl() const
437 {
438     return d_func()->m_dest;
439 }
440 
slotStart()441 void CopyJobPrivate::slotStart()
442 {
443     Q_Q(CopyJob);
444     if (q->isSuspended()) {
445         return;
446     }
447 
448     if (m_mode == CopyJob::CopyMode::Move) {
449         for (const QUrl &url : std::as_const(m_srcList)) {
450             if (m_dest.scheme() == url.scheme() && m_dest.host() == url.host()) {
451                 QString srcPath = url.path();
452                 if (!srcPath.endsWith(QLatin1Char('/'))) {
453                     srcPath += QLatin1Char('/');
454                 }
455                 if (m_dest.path().startsWith(srcPath)) {
456                     q->setError(KIO::ERR_CANNOT_MOVE_INTO_ITSELF);
457                     q->emitResult();
458                     return;
459                 }
460             }
461         }
462     }
463 
464     if (m_mode == CopyJob::CopyMode::Link && m_globalDest.isLocalFile()) {
465         const QString destPath = m_globalDest.toLocalFile();
466         const auto destFs = KFileSystemType::fileSystemType(destPath);
467         if (isFatFs(destFs)) {
468             q->setError(ERR_SYMLINKS_NOT_SUPPORTED);
469             const QString errText = destPath + QLatin1String(" [") + KFileSystemType::fileSystemName(destFs) + QLatin1Char(']');
470             q->setErrorText(errText);
471             q->emitResult();
472             return;
473         }
474     }
475 
476     /**
477        We call the functions directly instead of using signals.
478        Calling a function via a signal takes approx. 65 times the time
479        compared to calling it directly (at least on my machine). aleXXX
480     */
481     m_reportTimer = new QTimer(q);
482 
483     q->connect(m_reportTimer, &QTimer::timeout, q, [this]() {
484         slotReport();
485     });
486     m_reportTimer->start(s_reportTimeout);
487 
488     // Stat the dest
489     state = STATE_STATING;
490     const QUrl dest = m_asMethod ? m_dest.adjusted(QUrl::RemoveFilename) : m_dest;
491     // We need isDir() and UDS_LOCAL_PATH (for slaves who set it). Let's assume the latter is part of StatBasic too.
492     KIO::Job *job = KIO::statDetails(dest, StatJob::DestinationSide, KIO::StatBasic | KIO::StatResolveSymlink, KIO::HideProgressInfo);
493     qCDebug(KIO_COPYJOB_DEBUG) << "CopyJob: stating the dest" << dest;
494     q->addSubjob(job);
495 }
496 
497 // For unit test purposes
498 KIOCORE_EXPORT bool kio_resolve_local_urls = true;
499 
slotResultStating(KJob * job)500 void CopyJobPrivate::slotResultStating(KJob *job)
501 {
502     Q_Q(CopyJob);
503     qCDebug(KIO_COPYJOB_DEBUG);
504     // Was there an error while stating the src ?
505     if (job->error() && destinationState != DEST_NOT_STATED) {
506         const QUrl srcurl = static_cast<SimpleJob *>(job)->url();
507         if (!srcurl.isLocalFile()) {
508             // Probably : src doesn't exist. Well, over some protocols (e.g. FTP)
509             // this info isn't really reliable (thanks to MS FTP servers).
510             // We'll assume a file, and try to download anyway.
511             qCDebug(KIO_COPYJOB_DEBUG) << "Error while stating source. Activating hack";
512             q->removeSubjob(job);
513             Q_ASSERT(!q->hasSubjobs()); // We should have only one job at a time ...
514             struct CopyInfo info;
515             info.permissions = (mode_t)-1;
516             info.size = KIO::invalidFilesize;
517             info.uSource = srcurl;
518             info.uDest = m_dest;
519             // Append filename or dirname to destination URL, if allowed
520             if (destinationState == DEST_IS_DIR && !m_asMethod) {
521                 const QString fileName = srcurl.scheme() == QLatin1String("data") ? QStringLiteral("data") : srcurl.fileName(); // #379093
522                 info.uDest = addPathToUrl(info.uDest, fileName);
523             }
524 
525             files.append(info);
526             statNextSrc();
527             return;
528         }
529         // Local file. If stat fails, the file definitely doesn't exist.
530         // yes, q->Job::, because we don't want to call our override
531         q->Job::slotResult(job); // will set the error and emit result(this)
532         return;
533     }
534 
535     // Keep copy of the stat result
536     const UDSEntry entry = static_cast<StatJob *>(job)->statResult();
537 
538     if (destinationState == DEST_NOT_STATED) {
539         const bool isGlobalDest = m_dest == m_globalDest;
540 
541         // we were stating the dest
542         if (job->error()) {
543             destinationState = DEST_DOESNT_EXIST;
544             qCDebug(KIO_COPYJOB_DEBUG) << "dest does not exist";
545         } else {
546             const bool isDir = entry.isDir();
547 
548             // Check for writability, before spending time stat'ing everything (#141564).
549             // This assumes all kioslaves set permissions correctly...
550             const int permissions = entry.numberValue(KIO::UDSEntry::UDS_ACCESS, -1);
551             const bool isWritable = (permissions != -1) && (permissions & S_IWUSR);
552             if (!m_privilegeExecutionEnabled && !isWritable) {
553                 const QUrl dest = m_asMethod ? m_dest.adjusted(QUrl::RemoveFilename) : m_dest;
554                 q->setError(ERR_WRITE_ACCESS_DENIED);
555                 q->setErrorText(dest.toDisplayString(QUrl::PreferLocalFile));
556                 q->emitResult();
557                 return;
558             }
559 
560             // Treat symlinks to dirs as dirs here, so no test on isLink
561             destinationState = isDir ? DEST_IS_DIR : DEST_IS_FILE;
562             qCDebug(KIO_COPYJOB_DEBUG) << "dest is dir:" << isDir;
563 
564             if (isGlobalDest) {
565                 m_globalDestinationState = destinationState;
566             }
567 
568             const QString sLocalPath = entry.stringValue(KIO::UDSEntry::UDS_LOCAL_PATH);
569             if (!sLocalPath.isEmpty() && kio_resolve_local_urls) {
570                 const QString fileName = m_dest.fileName();
571                 m_dest = QUrl::fromLocalFile(sLocalPath);
572                 if (m_asMethod) {
573                     m_dest = addPathToUrl(m_dest, fileName);
574                 }
575                 qCDebug(KIO_COPYJOB_DEBUG) << "Setting m_dest to the local path:" << sLocalPath;
576                 if (isGlobalDest) {
577                     m_globalDest = m_dest;
578                 }
579             }
580         }
581 
582         q->removeSubjob(job);
583         Q_ASSERT(!q->hasSubjobs());
584 
585         // In copy-as mode, we want to check the directory to which we're
586         // copying. The target file or directory does not exist yet, which
587         // might confuse FileSystemFreeSpaceJob.
588         const QUrl existingDest = m_asMethod ? m_dest.adjusted(QUrl::RemoveFilename) : m_dest;
589         KIO::FileSystemFreeSpaceJob *spaceJob = KIO::fileSystemFreeSpace(existingDest);
590         q->connect(spaceJob,
591                    &KIO::FileSystemFreeSpaceJob::result,
592                    q,
593                    [this, existingDest](KIO::Job *spaceJob, KIO::filesize_t /*size*/, KIO::filesize_t available) {
594                        if (!spaceJob->error()) {
595                            m_freeSpace = available;
596                        } else {
597                            qCDebug(KIO_COPYJOB_DEBUG) << "Couldn't determine free space information for" << existingDest;
598                        }
599                        // After knowing what the dest is, we can start stat'ing the first src.
600                        statCurrentSrc();
601                    });
602         return;
603     } else {
604         sourceStated(entry, static_cast<SimpleJob *>(job)->url());
605         q->removeSubjob(job);
606     }
607 }
608 
sourceStated(const UDSEntry & entry,const QUrl & sourceUrl)609 void CopyJobPrivate::sourceStated(const UDSEntry &entry, const QUrl &sourceUrl)
610 {
611     const QString sLocalPath = entry.stringValue(KIO::UDSEntry::UDS_LOCAL_PATH);
612     const bool isDir = entry.isDir();
613 
614     // We were stating the current source URL
615     // Is it a file or a dir ?
616 
617     // There 6 cases, and all end up calling addCopyInfoFromUDSEntry first :
618     // 1 - src is a dir, destination is a directory,
619     // slotEntries will append the source-dir-name to the destination
620     // 2 - src is a dir, destination is a file -- will offer to overwrite, later on.
621     // 3 - src is a dir, destination doesn't exist, then it's the destination dirname,
622     // so slotEntries will use it as destination.
623 
624     // 4 - src is a file, destination is a directory,
625     // slotEntries will append the filename to the destination.
626     // 5 - src is a file, destination is a file, m_dest is the exact destination name
627     // 6 - src is a file, destination doesn't exist, m_dest is the exact destination name
628 
629     QUrl srcurl;
630     if (!sLocalPath.isEmpty() && destinationState != DEST_DOESNT_EXIST) {
631         qCDebug(KIO_COPYJOB_DEBUG) << "Using sLocalPath. destinationState=" << destinationState;
632         // Prefer the local path -- but only if we were able to stat() the dest.
633         // Otherwise, renaming a desktop:/ url would copy from src=file to dest=desktop (#218719)
634         srcurl = QUrl::fromLocalFile(sLocalPath);
635     } else {
636         srcurl = sourceUrl;
637     }
638     addCopyInfoFromUDSEntry(entry, srcurl, false, m_dest);
639 
640     m_currentDest = m_dest;
641     m_bCurrentSrcIsDir = false;
642 
643     if (isDir //
644         && !entry.isLink() // treat symlinks as files (no recursion)
645         && m_mode != CopyJob::Link) { // No recursion in Link mode either.
646         qCDebug(KIO_COPYJOB_DEBUG) << "Source is a directory";
647 
648         if (srcurl.isLocalFile()) {
649             const QString parentDir = srcurl.adjusted(QUrl::StripTrailingSlash).toLocalFile();
650             m_parentDirs.insert(parentDir);
651         }
652 
653         m_bCurrentSrcIsDir = true; // used by slotEntries
654         if (destinationState == DEST_IS_DIR) { // (case 1)
655             if (!m_asMethod) {
656                 // Use <desturl>/<directory_copied> as destination, from now on
657                 QString directory = srcurl.fileName();
658                 const QString sName = entry.stringValue(KIO::UDSEntry::UDS_NAME);
659                 KProtocolInfo::FileNameUsedForCopying fnu = KProtocolManager::fileNameUsedForCopying(srcurl);
660                 if (fnu == KProtocolInfo::Name) {
661                     if (!sName.isEmpty()) {
662                         directory = sName;
663                     }
664                 } else if (fnu == KProtocolInfo::DisplayName) {
665                     const QString dispName = entry.stringValue(KIO::UDSEntry::UDS_DISPLAY_NAME);
666                     if (!dispName.isEmpty()) {
667                         directory = dispName;
668                     } else if (!sName.isEmpty()) {
669                         directory = sName;
670                     }
671                 }
672                 m_currentDest = addPathToUrl(m_currentDest, directory);
673             }
674         } else { // (case 3)
675             // otherwise dest is new name for toplevel dir
676             // so the destination exists, in fact, from now on.
677             // (This even works with other src urls in the list, since the
678             //  dir has effectively been created)
679             destinationState = DEST_IS_DIR;
680             if (m_dest == m_globalDest) {
681                 m_globalDestinationState = destinationState;
682             }
683         }
684 
685         startListing(srcurl);
686     } else {
687         qCDebug(KIO_COPYJOB_DEBUG) << "Source is a file (or a symlink), or we are linking -> no recursive listing";
688 
689         if (srcurl.isLocalFile()) {
690             const QString parentDir = srcurl.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).path();
691             m_parentDirs.insert(parentDir);
692         }
693 
694         statNextSrc();
695     }
696 }
697 
doSuspend()698 bool CopyJob::doSuspend()
699 {
700     Q_D(CopyJob);
701     d->slotReport();
702     return Job::doSuspend();
703 }
704 
doResume()705 bool CopyJob::doResume()
706 {
707     Q_D(CopyJob);
708     switch (d->state) {
709     case STATE_INITIAL:
710         QTimer::singleShot(0, this, [d]() {
711             d->slotStart();
712         });
713         break;
714     default:
715         // not implemented
716         break;
717     }
718     return Job::doResume();
719 }
720 
slotReport()721 void CopyJobPrivate::slotReport()
722 {
723     Q_Q(CopyJob);
724     if (q->isSuspended()) {
725         return;
726     }
727 
728     // If showProgressInfo was set, progressId() is > 0.
729     switch (state) {
730     case STATE_RENAMING:
731         if (m_bURLDirty) {
732             m_bURLDirty = false;
733             Q_ASSERT(m_mode == CopyJob::Move);
734             emitMoving(q, m_currentSrcURL, m_currentDestURL);
735             Q_EMIT q->moving(q, m_currentSrcURL, m_currentDestURL);
736         }
737         // "N" files renamed shouldn't include skipped files
738         q->setProcessedAmount(KJob::Files, m_processedFiles);
739         // % value should include skipped files
740         q->emitPercent(m_filesHandledByDirectRename, q->totalAmount(KJob::Files));
741         break;
742 
743     case STATE_COPYING_FILES:
744         q->setProcessedAmount(KJob::Files, m_processedFiles);
745         q->setProcessedAmount(KJob::Bytes, m_processedSize + m_fileProcessedSize);
746         if (m_bURLDirty) {
747             // Only emit urls when they changed. This saves time, and fixes #66281
748             m_bURLDirty = false;
749             if (m_mode == CopyJob::Move) {
750                 emitMoving(q, m_currentSrcURL, m_currentDestURL);
751                 Q_EMIT q->moving(q, m_currentSrcURL, m_currentDestURL);
752             } else if (m_mode == CopyJob::Link) {
753                 emitCopying(q, m_currentSrcURL, m_currentDestURL); // we don't have a delegate->linking
754                 Q_EMIT q->linking(q, m_currentSrcURL.path(), m_currentDestURL);
755             } else {
756                 emitCopying(q, m_currentSrcURL, m_currentDestURL);
757                 Q_EMIT q->copying(q, m_currentSrcURL, m_currentDestURL);
758             }
759         }
760         break;
761 
762     case STATE_CREATING_DIRS:
763         q->setProcessedAmount(KJob::Directories, m_processedDirs);
764         if (m_bURLDirty) {
765             m_bURLDirty = false;
766             Q_EMIT q->creatingDir(q, m_currentDestURL);
767             emitCreatingDir(q, m_currentDestURL);
768         }
769         break;
770 
771     case STATE_STATING:
772     case STATE_LISTING:
773         if (m_bURLDirty) {
774             m_bURLDirty = false;
775             if (m_mode == CopyJob::Move) {
776                 emitMoving(q, m_currentSrcURL, m_currentDestURL);
777             } else {
778                 emitCopying(q, m_currentSrcURL, m_currentDestURL);
779             }
780         }
781         q->setProgressUnit(KJob::Bytes);
782         q->setTotalAmount(KJob::Bytes, m_totalSize);
783         q->setTotalAmount(KJob::Files, files.count() + m_filesHandledByDirectRename);
784         q->setTotalAmount(KJob::Directories, dirs.count());
785         break;
786 
787     default:
788         break;
789     }
790 }
791 
slotEntries(KIO::Job * job,const UDSEntryList & list)792 void CopyJobPrivate::slotEntries(KIO::Job *job, const UDSEntryList &list)
793 {
794     // Q_Q(CopyJob);
795     UDSEntryList::ConstIterator it = list.constBegin();
796     UDSEntryList::ConstIterator end = list.constEnd();
797     for (; it != end; ++it) {
798         const UDSEntry &entry = *it;
799         addCopyInfoFromUDSEntry(entry, static_cast<SimpleJob *>(job)->url(), m_bCurrentSrcIsDir, m_currentDest);
800     }
801 }
802 
slotSubError(ListJob * job,ListJob * subJob)803 void CopyJobPrivate::slotSubError(ListJob *job, ListJob *subJob)
804 {
805     const QUrl &url = subJob->url();
806     qCWarning(KIO_CORE) << url << subJob->errorString();
807 
808     Q_Q(CopyJob);
809 
810     Q_EMIT q->warning(job, subJob->errorString(), QString());
811     skip(url, true);
812 }
813 
addCopyInfoFromUDSEntry(const UDSEntry & entry,const QUrl & srcUrl,bool srcIsDir,const QUrl & currentDest)814 void CopyJobPrivate::addCopyInfoFromUDSEntry(const UDSEntry &entry, const QUrl &srcUrl, bool srcIsDir, const QUrl &currentDest)
815 {
816     struct CopyInfo info;
817     info.permissions = entry.numberValue(KIO::UDSEntry::UDS_ACCESS, -1);
818     const auto timeVal = entry.numberValue(KIO::UDSEntry::UDS_MODIFICATION_TIME, -1);
819     if (timeVal != -1) {
820         info.mtime = QDateTime::fromMSecsSinceEpoch(1000 * timeVal, Qt::UTC);
821     }
822     info.ctime = QDateTime::fromMSecsSinceEpoch(1000 * entry.numberValue(KIO::UDSEntry::UDS_CREATION_TIME, -1), Qt::UTC);
823     info.size = static_cast<KIO::filesize_t>(entry.numberValue(KIO::UDSEntry::UDS_SIZE, -1));
824     const bool isDir = entry.isDir();
825 
826     if (!isDir && info.size != KIO::invalidFilesize) {
827         m_totalSize += info.size;
828     }
829 
830     // recursive listing, displayName can be a/b/c/d
831     const QString fileName = entry.stringValue(KIO::UDSEntry::UDS_NAME);
832     const QString urlStr = entry.stringValue(KIO::UDSEntry::UDS_URL);
833     QUrl url;
834     if (!urlStr.isEmpty()) {
835         url = QUrl(urlStr);
836     }
837     QString localPath = entry.stringValue(KIO::UDSEntry::UDS_LOCAL_PATH);
838     info.linkDest = entry.stringValue(KIO::UDSEntry::UDS_LINK_DEST);
839 
840     if (fileName != QLatin1String("..") && fileName != QLatin1String(".")) {
841         const bool hasCustomURL = !url.isEmpty() || !localPath.isEmpty();
842         if (!hasCustomURL) {
843             // Make URL from displayName
844             url = srcUrl;
845             if (srcIsDir) { // Only if src is a directory. Otherwise uSource is fine as is
846                 qCDebug(KIO_COPYJOB_DEBUG) << "adding path" << fileName;
847                 url = addPathToUrl(url, fileName);
848             }
849         }
850         qCDebug(KIO_COPYJOB_DEBUG) << "fileName=" << fileName << "url=" << url;
851         if (!localPath.isEmpty() && kio_resolve_local_urls && destinationState != DEST_DOESNT_EXIST) {
852             url = QUrl::fromLocalFile(localPath);
853         }
854 
855         info.uSource = url;
856         info.uDest = currentDest;
857         qCDebug(KIO_COPYJOB_DEBUG) << "uSource=" << info.uSource << "uDest(1)=" << info.uDest;
858         // Append filename or dirname to destination URL, if allowed
859         if (destinationState == DEST_IS_DIR &&
860             // "copy/move as <foo>" means 'foo' is the dest for the base srcurl
861             // (passed here during stating) but not its children (during listing)
862             (!(m_asMethod && state == STATE_STATING))) {
863             QString destFileName;
864             KProtocolInfo::FileNameUsedForCopying fnu = KProtocolManager::fileNameUsedForCopying(url);
865             if (hasCustomURL && fnu == KProtocolInfo::FromUrl) {
866                 // destFileName = url.fileName(); // Doesn't work for recursive listing
867                 // Count the number of prefixes used by the recursive listjob
868                 int numberOfSlashes = fileName.count(QLatin1Char('/')); // don't make this a find()!
869                 QString path = url.path();
870                 int pos = 0;
871                 for (int n = 0; n < numberOfSlashes + 1; ++n) {
872                     pos = path.lastIndexOf(QLatin1Char('/'), pos - 1);
873                     if (pos == -1) { // error
874                         qCWarning(KIO_CORE) << "kioslave bug: not enough slashes in UDS_URL" << path << "- looking for" << numberOfSlashes << "slashes";
875                         break;
876                     }
877                 }
878                 if (pos >= 0) {
879                     destFileName = path.mid(pos + 1);
880                 }
881 
882             } else if (fnu == KProtocolInfo::Name) { // destination filename taken from UDS_NAME
883                 destFileName = fileName;
884             } else { // from display name (with fallback to name)
885                 const QString displayName = entry.stringValue(KIO::UDSEntry::UDS_DISPLAY_NAME);
886                 destFileName = displayName.isEmpty() ? fileName : displayName;
887             }
888 
889             // Here we _really_ have to add some filename to the dest.
890             // Otherwise, we end up with e.g. dest=..../Desktop/ itself.
891             // (This can happen when dropping a link to a webpage with no path)
892             if (destFileName.isEmpty()) {
893                 destFileName = KIO::encodeFileName(info.uSource.toDisplayString());
894             }
895 
896             qCDebug(KIO_COPYJOB_DEBUG) << " adding destFileName=" << destFileName;
897             info.uDest = addPathToUrl(info.uDest, destFileName);
898         }
899         qCDebug(KIO_COPYJOB_DEBUG) << " uDest(2)=" << info.uDest;
900         qCDebug(KIO_COPYJOB_DEBUG) << " " << info.uSource << "->" << info.uDest;
901         if (info.linkDest.isEmpty() && isDir && m_mode != CopyJob::Link) { // Dir
902             dirs.append(info); // Directories
903             if (m_mode == CopyJob::Move) {
904                 dirsToRemove.append(info.uSource);
905             }
906         } else {
907             files.append(info); // Files and any symlinks
908         }
909     }
910 }
911 
912 // Adjust for kio_trash choosing its own dest url...
finalDestUrl(const QUrl & src,const QUrl & dest) const913 QUrl CopyJobPrivate::finalDestUrl(const QUrl &src, const QUrl &dest) const
914 {
915     Q_Q(const CopyJob);
916     if (dest.scheme() == QLatin1String("trash")) {
917         const QMap<QString, QString> &metaData = q->metaData();
918         QMap<QString, QString>::ConstIterator it = metaData.find(QLatin1String("trashURL-") + src.path());
919         if (it != metaData.constEnd()) {
920             qCDebug(KIO_COPYJOB_DEBUG) << "finalDestUrl=" << it.value();
921             return QUrl(it.value());
922         }
923     }
924     return dest;
925 }
926 
skipSrc(bool isDir)927 void CopyJobPrivate::skipSrc(bool isDir)
928 {
929     m_dest = m_globalDest;
930     destinationState = m_globalDestinationState;
931     skip(*m_currentStatSrc, isDir);
932     ++m_currentStatSrc;
933     statCurrentSrc();
934 }
935 
statNextSrc()936 void CopyJobPrivate::statNextSrc()
937 {
938     /* Revert to the global destination, the one that applies to all source urls.
939      * 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.
940      * d->m_dest is /foo/b for b, but we have to revert to /d for item c and following.
941      */
942     m_dest = m_globalDest;
943     qCDebug(KIO_COPYJOB_DEBUG) << "Setting m_dest to" << m_dest;
944     destinationState = m_globalDestinationState;
945     ++m_currentStatSrc;
946     statCurrentSrc();
947 }
948 
statCurrentSrc()949 void CopyJobPrivate::statCurrentSrc()
950 {
951     Q_Q(CopyJob);
952     if (m_currentStatSrc != m_srcList.constEnd()) {
953         m_currentSrcURL = (*m_currentStatSrc);
954         m_bURLDirty = true;
955         if (m_mode == CopyJob::Link) {
956             // Skip the "stating the source" stage, we don't need it for linking
957             m_currentDest = m_dest;
958             struct CopyInfo info;
959             info.permissions = -1;
960             info.size = KIO::invalidFilesize;
961             info.uSource = m_currentSrcURL;
962             info.uDest = m_currentDest;
963             // Append filename or dirname to destination URL, if allowed
964             if (destinationState == DEST_IS_DIR && !m_asMethod) {
965                 if (compareUrls(m_currentSrcURL, info.uDest)) {
966                     // This is the case of creating a real symlink
967                     info.uDest = addPathToUrl(info.uDest, m_currentSrcURL.fileName());
968                 } else {
969                     // Different protocols, we'll create a .desktop file
970                     // We have to change the extension anyway, so while we're at it,
971                     // name the file like the URL
972                     QByteArray encodedFilename = QFile::encodeName(m_currentSrcURL.toDisplayString());
973                     const int truncatePos = NAME_MAX - (info.uDest.toDisplayString().length() + 8); // length(.desktop) = 8
974                     if (truncatePos > 0) {
975                         encodedFilename.truncate(truncatePos);
976                     }
977                     const QString decodedFilename = QFile::decodeName(encodedFilename);
978                     info.uDest = addPathToUrl(info.uDest, KIO::encodeFileName(decodedFilename) + QLatin1String(".desktop"));
979                 }
980             }
981             files.append(info); // Files and any symlinks
982             statNextSrc(); // we could use a loop instead of a recursive call :)
983             return;
984         }
985 
986         // Let's see if we can skip stat'ing, for the case where a directory view has the info already
987         KIO::UDSEntry entry;
988         const KFileItem cachedItem = KCoreDirLister::cachedItemForUrl(m_currentSrcURL);
989         if (!cachedItem.isNull()) {
990             entry = cachedItem.entry();
991             if (destinationState != DEST_DOESNT_EXIST) { // only resolve src if we could resolve dest (#218719)
992                 bool dummyIsLocal;
993                 m_currentSrcURL = cachedItem.mostLocalUrl(&dummyIsLocal); // #183585
994             }
995         }
996 
997         // Don't go renaming right away if we need a stat() to find out the destination filename
998         const bool needStat =
999             KProtocolManager::fileNameUsedForCopying(m_currentSrcURL) == KProtocolInfo::FromUrl || destinationState != DEST_IS_DIR || m_asMethod;
1000         if (m_mode == CopyJob::Move && needStat) {
1001             // If moving, before going for the full stat+[list+]copy+del thing, try to rename
1002             // The logic is pretty similar to FileCopyJobPrivate::slotStart()
1003             if (compareUrls(m_currentSrcURL, m_dest)) {
1004                 startRenameJob(m_currentSrcURL);
1005                 return;
1006             } else if (m_currentSrcURL.isLocalFile() && KProtocolManager::canRenameFromFile(m_dest)) {
1007                 startRenameJob(m_dest);
1008                 return;
1009             } else if (m_dest.isLocalFile() && KProtocolManager::canRenameToFile(m_currentSrcURL)) {
1010                 startRenameJob(m_currentSrcURL);
1011                 return;
1012             }
1013         }
1014 
1015         // if the source file system doesn't support deleting, we do not even stat
1016         if (m_mode == CopyJob::Move && !KProtocolManager::supportsDeleting(m_currentSrcURL)) {
1017             QPointer<CopyJob> that = q;
1018             Q_EMIT q->warning(q, buildErrorString(ERR_CANNOT_DELETE, m_currentSrcURL.toDisplayString()));
1019             if (that) {
1020                 statNextSrc(); // we could use a loop instead of a recursive call :)
1021             }
1022             return;
1023         }
1024 
1025         m_bOnlyRenames = false;
1026 
1027         // Testing for entry.count()>0 here is not good enough; KFileItem inserts
1028         // entries for UDS_USER and UDS_GROUP even on initially empty UDSEntries (#192185)
1029         if (entry.contains(KIO::UDSEntry::UDS_NAME)) {
1030             qCDebug(KIO_COPYJOB_DEBUG) << "fast path! found info about" << m_currentSrcURL << "in KCoreDirLister";
1031             // sourceStated(entry, m_currentSrcURL); // don't recurse, see #319747, use queued invokeMethod instead
1032             QMetaObject::invokeMethod(q, "sourceStated", Qt::QueuedConnection, Q_ARG(KIO::UDSEntry, entry), Q_ARG(QUrl, m_currentSrcURL));
1033             return;
1034         }
1035 
1036         // Stat the next src url
1037         Job *job = KIO::statDetails(m_currentSrcURL, StatJob::SourceSide, KIO::StatDefaultDetails, KIO::HideProgressInfo);
1038         qCDebug(KIO_COPYJOB_DEBUG) << "KIO::stat on" << m_currentSrcURL;
1039         state = STATE_STATING;
1040         q->addSubjob(job);
1041         m_currentDestURL = m_dest;
1042         m_bURLDirty = true;
1043     } else {
1044         // Finished the stat'ing phase
1045         // First make sure that the totals were correctly emitted
1046         m_bURLDirty = true;
1047         slotReport();
1048 
1049         qCDebug(KIO_COPYJOB_DEBUG) << "Stating finished. To copy:" << m_totalSize << ", available:" << m_freeSpace;
1050 
1051         if (m_totalSize > m_freeSpace && m_freeSpace != static_cast<KIO::filesize_t>(-1)) {
1052             q->setError(ERR_DISK_FULL);
1053             q->setErrorText(m_currentSrcURL.toDisplayString());
1054             q->emitResult();
1055             return;
1056         }
1057 
1058 #if KIOCORE_BUILD_DEPRECATED_SINCE(5, 2)
1059         if (!dirs.isEmpty()) {
1060             Q_EMIT q->aboutToCreate(q, dirs);
1061         }
1062         if (!files.isEmpty()) {
1063             Q_EMIT q->aboutToCreate(q, files);
1064         }
1065 #endif
1066         // Check if we are copying a single file
1067         m_bSingleFileCopy = (files.count() == 1 && dirs.isEmpty());
1068         // Then start copying things
1069         state = STATE_CREATING_DIRS;
1070         createNextDir();
1071     }
1072 }
1073 
startRenameJob(const QUrl & slave_url)1074 void CopyJobPrivate::startRenameJob(const QUrl &slave_url)
1075 {
1076     Q_Q(CopyJob);
1077 
1078     // Silence KDirWatch notifications, otherwise performance is horrible
1079     if (m_currentSrcURL.isLocalFile()) {
1080         const QString parentDir = m_currentSrcURL.adjusted(QUrl::RemoveFilename).path();
1081         const auto [it, isInserted] = m_parentDirs.insert(parentDir);
1082         if (isInserted) {
1083             KDirWatch::self()->stopDirScan(parentDir);
1084         }
1085     }
1086 
1087     QUrl dest = m_dest;
1088     // Append filename or dirname to destination URL, if allowed
1089     if (destinationState == DEST_IS_DIR && !m_asMethod) {
1090         dest = addPathToUrl(dest, m_currentSrcURL.fileName());
1091     }
1092     m_currentDestURL = dest;
1093     qCDebug(KIO_COPYJOB_DEBUG) << m_currentSrcURL << "->" << dest << "trying direct rename first";
1094     if (state != STATE_RENAMING) {
1095         q->setTotalAmount(KJob::Files, m_srcList.count());
1096     }
1097     state = STATE_RENAMING;
1098 
1099     struct CopyInfo info;
1100     info.permissions = -1;
1101     info.size = KIO::invalidFilesize;
1102     info.uSource = m_currentSrcURL;
1103     info.uDest = dest;
1104 #if KIOCORE_BUILD_DEPRECATED_SINCE(5, 2)
1105     QList<CopyInfo> files;
1106     files.append(info);
1107     Q_EMIT q->aboutToCreate(q, files);
1108 #endif
1109 
1110     KIO_ARGS << m_currentSrcURL << dest << (qint8) false /*no overwrite*/;
1111     SimpleJob *newJob = SimpleJobPrivate::newJobNoUi(slave_url, CMD_RENAME, packedArgs);
1112     newJob->setParentJob(q);
1113     Scheduler::setJobPriority(newJob, 1);
1114     q->addSubjob(newJob);
1115     if (m_currentSrcURL.adjusted(QUrl::RemoveFilename) != dest.adjusted(QUrl::RemoveFilename)) { // For the user, moving isn't renaming. Only renaming is.
1116         m_bOnlyRenames = false;
1117     }
1118 }
1119 
startListing(const QUrl & src)1120 void CopyJobPrivate::startListing(const QUrl &src)
1121 {
1122     Q_Q(CopyJob);
1123     state = STATE_LISTING;
1124     m_bURLDirty = true;
1125     ListJob *newjob = listRecursive(src, KIO::HideProgressInfo);
1126     newjob->setUnrestricted(true);
1127     q->connect(newjob, &ListJob::entries, q, [this](KIO::Job *job, const KIO::UDSEntryList &list) {
1128         slotEntries(job, list);
1129     });
1130     q->connect(newjob, &ListJob::subError, q, [this](KIO::ListJob *job, KIO::ListJob *subJob) {
1131         slotSubError(job, subJob);
1132     });
1133     q->addSubjob(newjob);
1134 }
1135 
skip(const QUrl & sourceUrl,bool isDir)1136 void CopyJobPrivate::skip(const QUrl &sourceUrl, bool isDir)
1137 {
1138     QUrl dir(sourceUrl);
1139     if (!isDir) {
1140         // Skipping a file: make sure not to delete the parent dir (#208418)
1141         dir = dir.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash);
1142     }
1143     while (dirsToRemove.removeAll(dir) > 0) {
1144         // Do not rely on rmdir() on the parent directories aborting.
1145         // Exclude the parent dirs explicitly.
1146         dir = dir.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash);
1147     }
1148 }
1149 
shouldOverwriteDir(const QString & path) const1150 bool CopyJobPrivate::shouldOverwriteDir(const QString &path) const
1151 {
1152     if (m_bOverwriteAllDirs) {
1153         return true;
1154     }
1155     return m_overwriteList.contains(path);
1156 }
1157 
shouldOverwriteFile(const QString & path) const1158 bool CopyJobPrivate::shouldOverwriteFile(const QString &path) const
1159 {
1160     if (m_bOverwriteAllFiles) {
1161         return true;
1162     }
1163     return m_overwriteList.contains(path);
1164 }
1165 
shouldSkip(const QString & path) const1166 bool CopyJobPrivate::shouldSkip(const QString &path) const
1167 {
1168     for (const QString &skipPath : std::as_const(m_skipList)) {
1169         if (path.startsWith(skipPath)) {
1170             return true;
1171         }
1172     }
1173     return false;
1174 }
1175 
renameDirectory(const QList<CopyInfo>::iterator & it,const QUrl & newUrl)1176 void CopyJobPrivate::renameDirectory(const QList<CopyInfo>::iterator &it, const QUrl &newUrl)
1177 {
1178     Q_Q(CopyJob);
1179     Q_EMIT q->renamed(q, (*it).uDest, newUrl); // for e.g. KPropertiesDialog
1180 
1181     QString oldPath = (*it).uDest.path();
1182     if (!oldPath.endsWith(QLatin1Char('/'))) {
1183         oldPath += QLatin1Char('/');
1184     }
1185 
1186     // Change the current one and strip the trailing '/'
1187     (*it).uDest = newUrl.adjusted(QUrl::StripTrailingSlash);
1188 
1189     QString newPath = newUrl.path(); // With trailing slash
1190     if (!newPath.endsWith(QLatin1Char('/'))) {
1191         newPath += QLatin1Char('/');
1192     }
1193     QList<CopyInfo>::Iterator renamedirit = it;
1194     ++renamedirit;
1195     // Change the name of subdirectories inside the directory
1196     for (; renamedirit != dirs.end(); ++renamedirit) {
1197         QString path = (*renamedirit).uDest.path();
1198         if (path.startsWith(oldPath)) {
1199             QString n = path;
1200             n.replace(0, oldPath.length(), newPath);
1201             /*qDebug() << "dirs list:" << (*renamedirit).uSource.path()
1202                          << "was going to be" << path
1203                          << ", changed into" << n;*/
1204             (*renamedirit).uDest.setPath(n, QUrl::DecodedMode);
1205         }
1206     }
1207     // Change filenames inside the directory
1208     QList<CopyInfo>::Iterator renamefileit = files.begin();
1209     for (; renamefileit != files.end(); ++renamefileit) {
1210         QString path = (*renamefileit).uDest.path(QUrl::FullyDecoded);
1211         if (path.startsWith(oldPath)) {
1212             QString n = path;
1213             n.replace(0, oldPath.length(), newPath);
1214             /*qDebug() << "files list:" << (*renamefileit).uSource.path()
1215                          << "was going to be" << path
1216                          << ", changed into" << n;*/
1217             (*renamefileit).uDest.setPath(n, QUrl::DecodedMode);
1218         }
1219     }
1220 #if KIOCORE_BUILD_DEPRECATED_SINCE(5, 2)
1221     if (!dirs.isEmpty()) {
1222         Q_EMIT q->aboutToCreate(q, dirs);
1223     }
1224     if (!files.isEmpty()) {
1225         Q_EMIT q->aboutToCreate(q, files);
1226     }
1227 #endif
1228 }
1229 
slotResultCreatingDirs(KJob * job)1230 void CopyJobPrivate::slotResultCreatingDirs(KJob *job)
1231 {
1232     Q_Q(CopyJob);
1233     // The dir we are trying to create:
1234     QList<CopyInfo>::Iterator it = dirs.begin();
1235     // Was there an error creating a dir ?
1236     if (job->error()) {
1237         m_conflictError = job->error();
1238         if (m_conflictError == ERR_DIR_ALREADY_EXIST //
1239             || m_conflictError == ERR_FILE_ALREADY_EXIST) { // can't happen?
1240             QUrl oldURL = ((SimpleJob *)job)->url();
1241             // Should we skip automatically ?
1242             if (m_bAutoSkipDirs) {
1243                 // We don't want to copy files in this directory, so we put it on the skip list
1244                 QString path = oldURL.path();
1245                 if (!path.endsWith(QLatin1Char('/'))) {
1246                     path += QLatin1Char('/');
1247                 }
1248                 m_skipList.append(path);
1249                 skip(oldURL, true);
1250                 dirs.erase(it); // Move on to next dir
1251             } else {
1252                 // Did the user choose to overwrite already?
1253                 const QString destDir = (*it).uDest.path();
1254                 if (shouldOverwriteDir(destDir)) { // overwrite => just skip
1255                     Q_EMIT q->copyingDone(q, (*it).uSource, finalDestUrl((*it).uSource, (*it).uDest), (*it).mtime, true /* directory */, false /* renamed */);
1256                     dirs.erase(it); // Move on to next dir
1257                     ++m_processedDirs;
1258                 } else {
1259                     if (m_bAutoRenameDirs) {
1260                         const QUrl destDirectory = (*it).uDest.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash);
1261                         const QString newName = KFileUtils::suggestName(destDirectory, (*it).uDest.fileName());
1262                         QUrl newUrl(destDirectory);
1263                         newUrl.setPath(concatPaths(newUrl.path(), newName));
1264                         renameDirectory(it, newUrl);
1265                     } else {
1266                         if (!KIO::delegateExtension<AskUserActionInterface *>(q)) {
1267                             q->Job::slotResult(job); // will set the error and emit result(this)
1268                             return;
1269                         }
1270 
1271                         Q_ASSERT(((SimpleJob *)job)->url() == (*it).uDest);
1272                         q->removeSubjob(job);
1273                         Q_ASSERT(!q->hasSubjobs()); // We should have only one job at a time ...
1274 
1275                         // We need to stat the existing dir, to get its last-modification time
1276                         QUrl existingDest((*it).uDest);
1277                         SimpleJob *newJob = KIO::statDetails(existingDest, StatJob::DestinationSide, KIO::StatDefaultDetails, KIO::HideProgressInfo);
1278                         Scheduler::setJobPriority(newJob, 1);
1279                         qCDebug(KIO_COPYJOB_DEBUG) << "KIO::stat for resolving conflict on" << existingDest;
1280                         state = STATE_CONFLICT_CREATING_DIRS;
1281                         q->addSubjob(newJob);
1282                         return; // Don't move to next dir yet !
1283                     }
1284                 }
1285             }
1286         } else {
1287             // Severe error, abort
1288             q->Job::slotResult(job); // will set the error and emit result(this)
1289             return;
1290         }
1291     } else { // no error : remove from list, to move on to next dir
1292         // this is required for the undo feature
1293         Q_EMIT q->copyingDone(q, (*it).uSource, finalDestUrl((*it).uSource, (*it).uDest), (*it).mtime, true, false);
1294         m_directoriesCopied.push_back(*it);
1295         dirs.erase(it);
1296         ++m_processedDirs;
1297     }
1298 
1299     q->removeSubjob(job);
1300     Q_ASSERT(!q->hasSubjobs()); // We should have only one job at a time ...
1301     createNextDir();
1302 }
1303 
slotResultConflictCreatingDirs(KJob * job)1304 void CopyJobPrivate::slotResultConflictCreatingDirs(KJob *job)
1305 {
1306     Q_Q(CopyJob);
1307     // We come here after a conflict has been detected and we've stated the existing dir
1308 
1309     // The dir we were trying to create:
1310     QList<CopyInfo>::Iterator it = dirs.begin();
1311 
1312     const UDSEntry entry = ((KIO::StatJob *)job)->statResult();
1313 
1314     QDateTime destmtime;
1315     QDateTime destctime;
1316     const KIO::filesize_t destsize = entry.numberValue(KIO::UDSEntry::UDS_SIZE);
1317     const QString linkDest = entry.stringValue(KIO::UDSEntry::UDS_LINK_DEST);
1318 
1319     q->removeSubjob(job);
1320     Q_ASSERT(!q->hasSubjobs()); // We should have only one job at a time ...
1321 
1322     // Always multi and skip (since there are files after that)
1323     RenameDialog_Options options(RenameDialog_MultipleItems | RenameDialog_Skip | RenameDialog_DestIsDirectory);
1324     // Overwrite only if the existing thing is a dir (no chance with a file)
1325     if (m_conflictError == ERR_DIR_ALREADY_EXIST) {
1326         // We are in slotResultConflictCreatingDirs(), so the source is a dir
1327         options |= RenameDialog_SourceIsDirectory;
1328 
1329         if ((*it).uSource == (*it).uDest
1330             || ((*it).uSource.scheme() == (*it).uDest.scheme() && (*it).uSource.adjusted(QUrl::StripTrailingSlash).path() == linkDest)) {
1331             options |= RenameDialog_OverwriteItself;
1332         } else {
1333             options |= RenameDialog_Overwrite;
1334             destmtime = QDateTime::fromMSecsSinceEpoch(1000 * entry.numberValue(KIO::UDSEntry::UDS_MODIFICATION_TIME, -1), Qt::UTC);
1335             destctime = QDateTime::fromMSecsSinceEpoch(1000 * entry.numberValue(KIO::UDSEntry::UDS_CREATION_TIME, -1), Qt::UTC);
1336         }
1337     }
1338 
1339     if (m_reportTimer) {
1340         m_reportTimer->stop();
1341     }
1342 
1343     auto *askUserActionInterface = KIO::delegateExtension<KIO::AskUserActionInterface *>(q);
1344 
1345     auto renameSignal = &KIO::AskUserActionInterface::askUserRenameResult;
1346     QObject::connect(askUserActionInterface, renameSignal, q, [=](RenameDialog_Result result, const QUrl &newUrl, KJob *parentJob) {
1347         Q_ASSERT(parentJob == q);
1348         // Only receive askUserRenameResult once per rename dialog
1349         QObject::disconnect(askUserActionInterface, renameSignal, q, nullptr);
1350 
1351         if (m_reportTimer) {
1352             m_reportTimer->start(s_reportTimeout);
1353         }
1354 
1355         const QString existingDest = (*it).uDest.path();
1356 
1357         switch (result) {
1358         case Result_Cancel:
1359             q->setError(ERR_USER_CANCELED);
1360             q->emitResult();
1361             return;
1362         case Result_AutoRename:
1363             m_bAutoRenameDirs = true;
1364             // fall through
1365             Q_FALLTHROUGH();
1366         case Result_Rename:
1367             renameDirectory(it, newUrl);
1368             break;
1369         case Result_AutoSkip:
1370             m_bAutoSkipDirs = true;
1371             // fall through
1372             Q_FALLTHROUGH();
1373         case Result_Skip:
1374             m_skipList.append(existingDest);
1375             skip((*it).uSource, true);
1376             // Move on to next dir
1377             dirs.erase(it);
1378             ++m_processedDirs;
1379             break;
1380         case Result_Overwrite:
1381             m_overwriteList.insert(existingDest);
1382             Q_EMIT q->copyingDone(q, (*it).uSource, finalDestUrl((*it).uSource, (*it).uDest), (*it).mtime, true /* directory */, false /* renamed */);
1383             // Move on to next dir
1384             dirs.erase(it);
1385             ++m_processedDirs;
1386             break;
1387         case Result_OverwriteAll:
1388             m_bOverwriteAllDirs = true;
1389             Q_EMIT q->copyingDone(q, (*it).uSource, finalDestUrl((*it).uSource, (*it).uDest), (*it).mtime, true /* directory */, false /* renamed */);
1390             // Move on to next dir
1391             dirs.erase(it);
1392             ++m_processedDirs;
1393             break;
1394         default:
1395             Q_ASSERT(0);
1396         }
1397         state = STATE_CREATING_DIRS;
1398         createNextDir();
1399     });
1400 
1401     /* clang-format off */
1402     askUserActionInterface->askUserRename(q, i18n("Folder Already Exists"),
1403                                           (*it).uSource, (*it).uDest,
1404                                           options,
1405                                           (*it).size, destsize,
1406                                           (*it).ctime, destctime,
1407                                           (*it).mtime, destmtime);
1408     /* clang-format on */
1409 }
1410 
createNextDir()1411 void CopyJobPrivate::createNextDir()
1412 {
1413     Q_Q(CopyJob);
1414 
1415     // Take first dir to create out of list
1416     QList<CopyInfo>::Iterator it = dirs.begin();
1417     // Is this URL on the skip list or the overwrite list ?
1418     while (it != dirs.end()) {
1419         const QString dir = it->uDest.path();
1420         if (shouldSkip(dir)) {
1421             it = dirs.erase(it);
1422         } else {
1423             break;
1424         }
1425     }
1426 
1427     if (it != dirs.end()) { // any dir to create, finally ?
1428         if (it->uDest.isLocalFile()) {
1429             // uDest doesn't exist yet, check the filesystem of the parent dir
1430             const auto destFileSystem = KFileSystemType::fileSystemType(it->uDest.adjusted(QUrl::StripTrailingSlash | QUrl::RemoveFilename).toLocalFile());
1431             if (isFatOrNtfs(destFileSystem)) {
1432                 const QString dirName = it->uDest.adjusted(QUrl::StripTrailingSlash).fileName();
1433                 if (hasInvalidChars(dirName)) {
1434                     // We already asked the user?
1435                     if (m_autoReplaceInvalidChars) {
1436                         processCreateNextDir(it, KIO::Result_ReplaceInvalidChars);
1437                         return;
1438                     } else if (m_autoSkipDirsWithInvalidChars) {
1439                         processCreateNextDir(it, KIO::Result_Skip);
1440                         return;
1441                     }
1442 
1443                     const QString msg = invalidCharsSupportMsg(it->uDest.toDisplayString(QUrl::PreferLocalFile),
1444                                                                KFileSystemType::fileSystemName(destFileSystem),
1445                                                                true /* isDir */);
1446 
1447                     if (auto *askUserActionInterface = KIO::delegateExtension<KIO::AskUserActionInterface *>(q)) {
1448                         SkipDialog_Options options = KIO::SkipDialog_Replace_Invalid_Chars;
1449                         if (dirs.size() > 1) {
1450                             options |= SkipDialog_MultipleItems;
1451                         }
1452 
1453                         auto skipSignal = &KIO::AskUserActionInterface::askUserSkipResult;
1454                         QObject::connect(askUserActionInterface, skipSignal, q, [=](SkipDialog_Result result, KJob *parentJob) {
1455                             Q_ASSERT(parentJob == q);
1456 
1457                             // Only receive askUserSkipResult once per skip dialog
1458                             QObject::disconnect(askUserActionInterface, skipSignal, q, nullptr);
1459 
1460                             processCreateNextDir(it, result);
1461                         });
1462 
1463                         askUserActionInterface->askUserSkip(q, options, msg);
1464 
1465                         return;
1466                     } else { // No Job Ui delegate
1467                         qCWarning(KIO_COPYJOB_DEBUG) << msg;
1468                         q->emitResult();
1469                         return;
1470                     }
1471                 }
1472             }
1473         }
1474 
1475         processCreateNextDir(it, -1);
1476     } else { // we have finished creating dirs
1477         q->setProcessedAmount(KJob::Directories, m_processedDirs); // make sure final number appears
1478 
1479         if (m_mode == CopyJob::Move) {
1480             // Now we know which dirs hold the files we're going to delete.
1481             // To speed things up and prevent double-notification, we disable KDirWatch
1482             // on those dirs temporarily (using KDirWatch::self, that's the instance
1483             // used by e.g. kdirlister).
1484             for (const auto &dir : m_parentDirs) {
1485                 KDirWatch::self()->stopDirScan(dir);
1486             }
1487         }
1488 
1489         state = STATE_COPYING_FILES;
1490         ++m_processedFiles; // Ralf wants it to start at 1, not 0
1491         copyNextFile();
1492     }
1493 }
1494 
processCreateNextDir(const QList<CopyInfo>::Iterator & it,int result)1495 void CopyJobPrivate::processCreateNextDir(const QList<CopyInfo>::Iterator &it, int result)
1496 {
1497     Q_Q(CopyJob);
1498 
1499     switch (result) {
1500     case Result_Cancel:
1501         q->setError(ERR_USER_CANCELED);
1502         q->emitResult();
1503         return;
1504     case KIO::Result_ReplaceAllInvalidChars:
1505         m_autoReplaceInvalidChars = true;
1506         Q_FALLTHROUGH();
1507     case KIO::Result_ReplaceInvalidChars: {
1508         it->uDest = it->uDest.adjusted(QUrl::StripTrailingSlash);
1509         QString dirName = it->uDest.fileName();
1510         const int len = dirName.size();
1511         cleanMsdosDestName(dirName);
1512         QString path = it->uDest.path();
1513         path.replace(path.size() - len, len, dirName);
1514         it->uDest.setPath(path);
1515         break;
1516     }
1517     case KIO::Result_AutoSkip:
1518         m_autoSkipDirsWithInvalidChars = true;
1519         Q_FALLTHROUGH();
1520     case KIO::Result_Skip:
1521         m_skipList.append(it->uDest.path());
1522         skip(it->uSource, true);
1523         dirs.erase(it); // Move on to next dir
1524         ++m_processedDirs;
1525         createNextDir();
1526         return;
1527     default:
1528         break;
1529     }
1530 
1531     // Create the directory - with default permissions so that we can put files into it
1532     // TODO : change permissions once all is finished; but for stuff coming from CDROM it sucks...
1533     KIO::SimpleJob *newjob = KIO::mkdir(it->uDest, -1);
1534     newjob->setParentJob(q);
1535     Scheduler::setJobPriority(newjob, 1);
1536     if (shouldOverwriteFile(it->uDest.path())) { // if we are overwriting an existing file or symlink
1537         newjob->addMetaData(QStringLiteral("overwrite"), QStringLiteral("true"));
1538     }
1539 
1540     m_currentDestURL = it->uDest;
1541     m_bURLDirty = true;
1542 
1543     q->addSubjob(newjob);
1544 }
1545 
slotResultCopyingFiles(KJob * job)1546 void CopyJobPrivate::slotResultCopyingFiles(KJob *job)
1547 {
1548     Q_Q(CopyJob);
1549     // The file we were trying to copy:
1550     QList<CopyInfo>::Iterator it = files.begin();
1551     if (job->error()) {
1552         // Should we skip automatically ?
1553         if (m_bAutoSkipFiles) {
1554             skip((*it).uSource, false);
1555             m_fileProcessedSize = (*it).size;
1556             files.erase(it); // Move on to next file
1557         } else {
1558             m_conflictError = job->error(); // save for later
1559             // Existing dest ?
1560             if (m_conflictError == ERR_FILE_ALREADY_EXIST //
1561                 || m_conflictError == ERR_DIR_ALREADY_EXIST //
1562                 || m_conflictError == ERR_IDENTICAL_FILES) {
1563                 if (m_bAutoRenameFiles) {
1564                     QUrl destDirectory = (*it).uDest.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash);
1565                     const QString newName = KFileUtils::suggestName(destDirectory, (*it).uDest.fileName());
1566                     QUrl newDest(destDirectory);
1567                     newDest.setPath(concatPaths(newDest.path(), newName));
1568                     Q_EMIT q->renamed(q, (*it).uDest, newDest); // for e.g. kpropsdlg
1569                     (*it).uDest = newDest;
1570 
1571 #if KIOCORE_BUILD_DEPRECATED_SINCE(5, 2)
1572                     QList<CopyInfo> files;
1573                     files.append(*it);
1574                     Q_EMIT q->aboutToCreate(q, files);
1575 #endif
1576                 } else {
1577                     if (!KIO::delegateExtension<AskUserActionInterface *>(q)) {
1578                         q->Job::slotResult(job); // will set the error and emit result(this)
1579                         return;
1580                     }
1581 
1582                     q->removeSubjob(job);
1583                     Q_ASSERT(!q->hasSubjobs());
1584                     // We need to stat the existing file, to get its last-modification time
1585                     QUrl existingFile((*it).uDest);
1586                     SimpleJob *newJob =
1587                         KIO::statDetails(existingFile, StatJob::DestinationSide, KIO::StatDetail::StatBasic | KIO::StatDetail::StatTime, KIO::HideProgressInfo);
1588                     Scheduler::setJobPriority(newJob, 1);
1589                     qCDebug(KIO_COPYJOB_DEBUG) << "KIO::stat for resolving conflict on" << existingFile;
1590                     state = STATE_CONFLICT_COPYING_FILES;
1591                     q->addSubjob(newJob);
1592                     return; // Don't move to next file yet !
1593                 }
1594             } else {
1595                 if (m_bCurrentOperationIsLink && qobject_cast<KIO::DeleteJob *>(job)) {
1596                     // Very special case, see a few lines below
1597                     // We are deleting the source of a symlink we successfully moved... ignore error
1598                     m_fileProcessedSize = (*it).size;
1599                     ++m_processedFiles;
1600                     files.erase(it);
1601                 } else {
1602                     if (!KIO::delegateExtension<AskUserActionInterface *>(q)) {
1603                         q->Job::slotResult(job); // will set the error and emit result(this)
1604                         return;
1605                     }
1606 
1607                     // Go directly to the conflict resolution, there is nothing to stat
1608                     slotResultErrorCopyingFiles(job);
1609                     return;
1610                 }
1611             }
1612         }
1613     } else { // no error
1614         // Special case for moving links. That operation needs two jobs, unlike others.
1615         if (m_bCurrentOperationIsLink //
1616             && m_mode == CopyJob::Move //
1617             && !qobject_cast<KIO::DeleteJob *>(job) // Deleting source not already done
1618         ) {
1619             q->removeSubjob(job);
1620             Q_ASSERT(!q->hasSubjobs());
1621             // The only problem with this trick is that the error handling for this del operation
1622             // is not going to be right... see 'Very special case' above.
1623             KIO::Job *newjob = KIO::del((*it).uSource, HideProgressInfo);
1624             newjob->setParentJob(q);
1625             q->addSubjob(newjob);
1626             return; // Don't move to next file yet !
1627         }
1628 
1629         const QUrl finalUrl = finalDestUrl((*it).uSource, (*it).uDest);
1630 
1631         if (m_bCurrentOperationIsLink) {
1632             QString target = (m_mode == CopyJob::Link ? (*it).uSource.path() : (*it).linkDest);
1633             // required for the undo feature
1634             Q_EMIT q->copyingLinkDone(q, (*it).uSource, target, finalUrl);
1635         } else {
1636             // required for the undo feature
1637             Q_EMIT q->copyingDone(q, (*it).uSource, finalUrl, (*it).mtime, false, false);
1638             if (m_mode == CopyJob::Move) {
1639 #ifndef KIO_ANDROID_STUB
1640                 org::kde::KDirNotify::emitFileMoved((*it).uSource, finalUrl);
1641 #endif
1642             }
1643             m_successSrcList.append((*it).uSource);
1644             if (m_freeSpace != KIO::invalidFilesize && (*it).size != KIO::invalidFilesize) {
1645                 m_freeSpace -= (*it).size;
1646             }
1647         }
1648         // remove from list, to move on to next file
1649         files.erase(it);
1650         ++m_processedFiles;
1651     }
1652 
1653     // clear processed size for last file and add it to overall processed size
1654     m_processedSize += m_fileProcessedSize;
1655     m_fileProcessedSize = 0;
1656 
1657     qCDebug(KIO_COPYJOB_DEBUG) << files.count() << "files remaining";
1658 
1659     // Merge metadata from subjob
1660     KIO::Job *kiojob = qobject_cast<KIO::Job *>(job);
1661     Q_ASSERT(kiojob);
1662     m_incomingMetaData += kiojob->metaData();
1663     q->removeSubjob(job);
1664     Q_ASSERT(!q->hasSubjobs()); // We should have only one job at a time ...
1665     copyNextFile();
1666 }
1667 
slotResultErrorCopyingFiles(KJob * job)1668 void CopyJobPrivate::slotResultErrorCopyingFiles(KJob *job)
1669 {
1670     Q_Q(CopyJob);
1671     // We come here after a conflict has been detected and we've stated the existing file
1672     // The file we were trying to create:
1673     QList<CopyInfo>::Iterator it = files.begin();
1674 
1675     RenameDialog_Result res = Result_Cancel;
1676 
1677     if (m_reportTimer) {
1678         m_reportTimer->stop();
1679     }
1680 
1681     q->removeSubjob(job);
1682     Q_ASSERT(!q->hasSubjobs());
1683     auto *askUserActionInterface = KIO::delegateExtension<KIO::AskUserActionInterface *>(q);
1684 
1685     if (m_conflictError == ERR_FILE_ALREADY_EXIST //
1686         || m_conflictError == ERR_DIR_ALREADY_EXIST //
1687         || m_conflictError == ERR_IDENTICAL_FILES) {
1688         // Its modification time:
1689         const UDSEntry entry = static_cast<KIO::StatJob *>(job)->statResult();
1690 
1691         QDateTime destmtime;
1692         QDateTime destctime;
1693         const KIO::filesize_t destsize = entry.numberValue(KIO::UDSEntry::UDS_SIZE);
1694         const QString linkDest = entry.stringValue(KIO::UDSEntry::UDS_LINK_DEST);
1695 
1696         // Offer overwrite only if the existing thing is a file
1697         // If src==dest, use "overwrite-itself"
1698         RenameDialog_Options options;
1699         bool isDir = true;
1700 
1701         if (m_conflictError == ERR_DIR_ALREADY_EXIST) {
1702             options = RenameDialog_DestIsDirectory;
1703         } else {
1704             if ((*it).uSource == (*it).uDest
1705                 || ((*it).uSource.scheme() == (*it).uDest.scheme() && (*it).uSource.adjusted(QUrl::StripTrailingSlash).path() == linkDest)) {
1706                 options = RenameDialog_OverwriteItself;
1707             } else {
1708                 const qint64 destMTimeStamp = entry.numberValue(KIO::UDSEntry::UDS_MODIFICATION_TIME, -1);
1709                 if (m_bOverwriteWhenOlder && (*it).mtime.isValid() && destMTimeStamp != -1) {
1710                     if ((*it).mtime.currentSecsSinceEpoch() > destMTimeStamp) {
1711                         qCDebug(KIO_COPYJOB_DEBUG) << "dest is older, overwriting" << (*it).uDest;
1712                         res = Result_Overwrite;
1713                     } else {
1714                         qCDebug(KIO_COPYJOB_DEBUG) << "dest is newer, skipping" << (*it).uDest;
1715                         res = Result_Skip;
1716                     }
1717                 } else {
1718                     // These timestamps are used only when RenameDialog_Overwrite is set.
1719                     destmtime = QDateTime::fromMSecsSinceEpoch(1000 * destMTimeStamp, Qt::UTC);
1720                     destctime = QDateTime::fromMSecsSinceEpoch(1000 * entry.numberValue(KIO::UDSEntry::UDS_CREATION_TIME, -1), Qt::UTC);
1721 
1722                     options = RenameDialog_Overwrite;
1723                 }
1724             }
1725             isDir = false;
1726         }
1727 
1728         // if no preset value was set
1729         if (res == Result_Cancel) {
1730             if (!m_bSingleFileCopy) {
1731                 options = RenameDialog_Options(options | RenameDialog_MultipleItems | RenameDialog_Skip);
1732             }
1733 
1734             const QString caption = !isDir ? i18n("File Already Exists") : i18n("Already Exists as Folder");
1735 
1736             auto renameSignal = &KIO::AskUserActionInterface::askUserRenameResult;
1737             QObject::connect(askUserActionInterface, renameSignal, q, [=](RenameDialog_Result result, const QUrl &newUrl, KJob *parentJob) {
1738                 Q_ASSERT(parentJob == q);
1739                 // Only receive askUserRenameResult once per rename dialog
1740                 QObject::disconnect(askUserActionInterface, renameSignal, q, nullptr);
1741                 processFileRenameDialogResult(it, result, newUrl, destmtime);
1742             });
1743 
1744             /* clang-format off */
1745             askUserActionInterface->askUserRename(q, caption,
1746                                                   (*it).uSource, (*it).uDest,
1747                                                   options,
1748                                                   (*it).size, destsize,
1749                                                   (*it).ctime, destctime,
1750                                                   (*it).mtime, destmtime); /* clang-format on */
1751             return;
1752         }
1753     } else {
1754         if (job->error() == ERR_USER_CANCELED) {
1755             res = Result_Cancel;
1756         } else if (!askUserActionInterface) {
1757             q->Job::slotResult(job); // will set the error and emit result(this)
1758             return;
1759         } else {
1760             SkipDialog_Options options;
1761             if (files.count() > 1) {
1762                 options |= SkipDialog_MultipleItems;
1763             }
1764 
1765             auto skipSignal = &KIO::AskUserActionInterface::askUserSkipResult;
1766             QObject::connect(askUserActionInterface, skipSignal, q, [=](SkipDialog_Result result, KJob *parentJob) {
1767                 Q_ASSERT(parentJob == q);
1768                 // Only receive askUserSkipResult once per skip dialog
1769                 QObject::disconnect(askUserActionInterface, skipSignal, q, nullptr);
1770                 processFileRenameDialogResult(it, result, QUrl() /* no new url in skip */, QDateTime{});
1771             });
1772 
1773             askUserActionInterface->askUserSkip(q, options, job->errorString());
1774             return;
1775         }
1776     }
1777 
1778     processFileRenameDialogResult(it, res, QUrl{}, QDateTime{});
1779 }
1780 
processFileRenameDialogResult(const QList<CopyInfo>::Iterator & it,RenameDialog_Result result,const QUrl & newUrl,const QDateTime & destmtime)1781 void CopyJobPrivate::processFileRenameDialogResult(const QList<CopyInfo>::Iterator &it,
1782                                                    RenameDialog_Result result,
1783                                                    const QUrl &newUrl,
1784                                                    const QDateTime &destmtime)
1785 {
1786     Q_Q(CopyJob);
1787 
1788     if (m_reportTimer) {
1789         m_reportTimer->start(s_reportTimeout);
1790     }
1791 
1792     if (result == Result_OverwriteWhenOlder) {
1793         m_bOverwriteWhenOlder = true;
1794         if ((*it).mtime > destmtime) {
1795             qCDebug(KIO_COPYJOB_DEBUG) << "dest is older, overwriting" << (*it).uDest;
1796             result = Result_Overwrite;
1797         } else {
1798             qCDebug(KIO_COPYJOB_DEBUG) << "dest is newer, skipping" << (*it).uDest;
1799             result = Result_Skip;
1800         }
1801     }
1802 
1803     switch (result) {
1804     case Result_Cancel:
1805         q->setError(ERR_USER_CANCELED);
1806         q->emitResult();
1807         return;
1808     case Result_AutoRename:
1809         m_bAutoRenameFiles = true;
1810         // fall through
1811         Q_FALLTHROUGH();
1812     case Result_Rename: {
1813         Q_EMIT q->renamed(q, (*it).uDest, newUrl); // for e.g. kpropsdlg
1814         (*it).uDest = newUrl;
1815         m_bURLDirty = true;
1816 
1817 #if KIOCORE_BUILD_DEPRECATED_SINCE(5, 2)
1818         QList<CopyInfo> files;
1819         files.append(*it);
1820         Q_EMIT q->aboutToCreate(q, files);
1821 #endif
1822         break;
1823     }
1824     case Result_AutoSkip:
1825         m_bAutoSkipFiles = true;
1826         // fall through
1827         Q_FALLTHROUGH();
1828     case Result_Skip:
1829         // Move on to next file
1830         skip((*it).uSource, false);
1831         m_processedSize += (*it).size;
1832         files.erase(it);
1833         break;
1834     case Result_OverwriteAll:
1835         m_bOverwriteAllFiles = true;
1836         break;
1837     case Result_Overwrite:
1838         // Add to overwrite list, so that copyNextFile knows to overwrite
1839         m_overwriteList.insert((*it).uDest.path());
1840         break;
1841     case Result_Retry:
1842         // Do nothing, copy file again
1843         break;
1844     default:
1845         Q_ASSERT(0);
1846     }
1847     state = STATE_COPYING_FILES;
1848     copyNextFile();
1849 }
1850 
linkNextFile(const QUrl & uSource,const QUrl & uDest,JobFlags flags)1851 KIO::Job *CopyJobPrivate::linkNextFile(const QUrl &uSource, const QUrl &uDest, JobFlags flags)
1852 {
1853     qCDebug(KIO_COPYJOB_DEBUG) << "Linking";
1854     if (compareUrls(uSource, uDest)) {
1855         // This is the case of creating a real symlink
1856         KIO::SimpleJob *newJob = KIO::symlink(uSource.path(), uDest, flags | HideProgressInfo /*no GUI*/);
1857         newJob->setParentJob(q_func());
1858         Scheduler::setJobPriority(newJob, 1);
1859         qCDebug(KIO_COPYJOB_DEBUG) << "Linking target=" << uSource.path() << "link=" << uDest;
1860         // emit linking( this, uSource.path(), uDest );
1861         m_bCurrentOperationIsLink = true;
1862         m_currentSrcURL = uSource;
1863         m_currentDestURL = uDest;
1864         m_bURLDirty = true;
1865         // Observer::self()->slotCopying( this, uSource, uDest ); // should be slotLinking perhaps
1866         return newJob;
1867     } else {
1868         Q_Q(CopyJob);
1869         qCDebug(KIO_COPYJOB_DEBUG) << "Linking URL=" << uSource << "link=" << uDest;
1870         if (uDest.isLocalFile()) {
1871             // if the source is a devices url, handle it a littlebit special
1872 
1873             QString path = uDest.toLocalFile();
1874             qCDebug(KIO_COPYJOB_DEBUG) << "path=" << path;
1875             QFile f(path);
1876             if (f.open(QIODevice::ReadWrite)) {
1877                 f.close();
1878                 KDesktopFile desktopFile(path);
1879                 KConfigGroup config = desktopFile.desktopGroup();
1880                 QUrl url = uSource;
1881                 url.setPassword(QString());
1882                 config.writePathEntry("URL", url.toString());
1883                 config.writeEntry("Name", url.toString());
1884                 config.writeEntry("Type", QStringLiteral("Link"));
1885                 QString protocol = uSource.scheme();
1886                 if (protocol == QLatin1String("ftp")) {
1887                     config.writeEntry("Icon", QStringLiteral("folder-remote"));
1888                 } else if (protocol == QLatin1String("http") || protocol == QLatin1String("https")) {
1889                     config.writeEntry("Icon", QStringLiteral("text-html"));
1890                 } else if (protocol == QLatin1String("info")) {
1891                     config.writeEntry("Icon", QStringLiteral("text-x-texinfo"));
1892                 } else if (protocol == QLatin1String("mailto")) { // sven:
1893                     config.writeEntry("Icon", QStringLiteral("internet-mail")); // added mailto: support
1894                 } else if (protocol == QLatin1String("trash") && url.path().length() <= 1) { // trash:/ link
1895                     config.writeEntry("Name", i18n("Trash"));
1896                     config.writeEntry("Icon", QStringLiteral("user-trash-full"));
1897                     config.writeEntry("EmptyIcon", QStringLiteral("user-trash"));
1898                 } else {
1899                     config.writeEntry("Icon", QStringLiteral("unknown"));
1900                 }
1901                 config.sync();
1902                 files.erase(files.begin()); // done with this one, move on
1903                 ++m_processedFiles;
1904                 copyNextFile();
1905                 return nullptr;
1906             } else {
1907                 qCDebug(KIO_COPYJOB_DEBUG) << "ERR_CANNOT_OPEN_FOR_WRITING";
1908                 q->setError(ERR_CANNOT_OPEN_FOR_WRITING);
1909                 q->setErrorText(uDest.toLocalFile());
1910                 q->emitResult();
1911                 return nullptr;
1912             }
1913         } else {
1914             // Todo: not show "link" on remote dirs if the src urls are not from the same protocol+host+...
1915             q->setError(ERR_CANNOT_SYMLINK);
1916             q->setErrorText(uDest.toDisplayString());
1917             q->emitResult();
1918             return nullptr;
1919         }
1920     }
1921 }
1922 
handleMsdosFsQuirks(QList<CopyInfo>::Iterator it,KFileSystemType::Type fsType)1923 bool CopyJobPrivate::handleMsdosFsQuirks(QList<CopyInfo>::Iterator it, KFileSystemType::Type fsType)
1924 {
1925     Q_Q(CopyJob);
1926 
1927     QString msg;
1928     SkipDialog_Options options;
1929     SkipType skipType = NoSkipType;
1930 
1931     if (isFatFs(fsType) && !it->linkDest.isEmpty()) { // Copying a symlink
1932         skipType = SkipFatSymlinks;
1933         if (m_autoSkipFatSymlinks) { // Have we already asked the user?
1934             processCopyNextFile(it, KIO::Result_Skip, skipType);
1935             return true;
1936         }
1937         options = KIO::SkipDialog_Hide_Retry;
1938         msg = symlinkSupportMsg(it->uDest.toLocalFile(), KFileSystemType::fileSystemName(fsType));
1939     } else if (hasInvalidChars(it->uDest.fileName())) {
1940         skipType = SkipInvalidChars;
1941         if (m_autoReplaceInvalidChars) { // Have we already asked the user?
1942             processCopyNextFile(it, KIO::Result_ReplaceInvalidChars, skipType);
1943             return true;
1944         } else if (m_autoSkipFilesWithInvalidChars) { // Have we already asked the user?
1945             processCopyNextFile(it, KIO::Result_Skip, skipType);
1946             return true;
1947         }
1948 
1949         options = KIO::SkipDialog_Replace_Invalid_Chars;
1950         msg = invalidCharsSupportMsg(it->uDest.toDisplayString(QUrl::PreferLocalFile), KFileSystemType::fileSystemName(fsType));
1951     }
1952 
1953     if (!msg.isEmpty()) {
1954         if (auto *askUserActionInterface = KIO::delegateExtension<KIO::AskUserActionInterface *>(q)) {
1955             if (files.size() > 1) {
1956                 options |= SkipDialog_MultipleItems;
1957             }
1958 
1959             auto skipSignal = &KIO::AskUserActionInterface::askUserSkipResult;
1960             QObject::connect(askUserActionInterface, skipSignal, q, [=](SkipDialog_Result result, KJob *parentJob) {
1961                 Q_ASSERT(parentJob == q);
1962                 // Only receive askUserSkipResult once per skip dialog
1963                 QObject::disconnect(askUserActionInterface, skipSignal, q, nullptr);
1964 
1965                 processCopyNextFile(it, result, skipType);
1966             });
1967 
1968             askUserActionInterface->askUserSkip(q, options, msg);
1969 
1970             return true;
1971         } else { // No Job Ui delegate
1972             qCWarning(KIO_COPYJOB_DEBUG) << msg;
1973             q->emitResult();
1974             return true;
1975         }
1976     }
1977 
1978     return false; // Not handled, move on
1979 }
1980 
copyNextFile()1981 void CopyJobPrivate::copyNextFile()
1982 {
1983     Q_Q(CopyJob);
1984     bool bCopyFile = false;
1985     qCDebug(KIO_COPYJOB_DEBUG);
1986 
1987     bool isDestLocal = m_globalDest.isLocalFile();
1988 
1989     // Take the first file in the list
1990     QList<CopyInfo>::Iterator it = files.begin();
1991     // Is this URL on the skip list ?
1992     while (it != files.end() && !bCopyFile) {
1993         const QString destFile = (*it).uDest.path();
1994         bCopyFile = !shouldSkip(destFile);
1995         if (!bCopyFile) {
1996             it = files.erase(it);
1997         }
1998 
1999         if (it != files.end() && isDestLocal && (*it).size > 0xFFFFFFFF) { // 4GB-1
2000             const auto destFileSystem = KFileSystemType::fileSystemType(m_globalDest.toLocalFile());
2001             if (destFileSystem == KFileSystemType::Fat) {
2002                 q->setError(ERR_FILE_TOO_LARGE_FOR_FAT32);
2003                 q->setErrorText((*it).uDest.toDisplayString());
2004                 q->emitResult();
2005                 return;
2006             }
2007         }
2008     }
2009 
2010     if (bCopyFile) { // any file to create, finally ?
2011         if (isDestLocal) {
2012             const auto destFileSystem = KFileSystemType::fileSystemType(m_globalDest.toLocalFile());
2013             if (isFatOrNtfs(destFileSystem)) {
2014                 if (handleMsdosFsQuirks(it, destFileSystem)) {
2015                     return;
2016                 }
2017             }
2018         }
2019 
2020         processCopyNextFile(it, -1, NoSkipType);
2021     } else {
2022         // We're done
2023         qCDebug(KIO_COPYJOB_DEBUG) << "copyNextFile finished";
2024         --m_processedFiles; // undo the "start at 1" hack
2025         slotReport(); // display final numbers, important if progress dialog stays up
2026 
2027         deleteNextDir();
2028     }
2029 }
2030 
processCopyNextFile(const QList<CopyInfo>::Iterator & it,int result,SkipType skipType)2031 void CopyJobPrivate::processCopyNextFile(const QList<CopyInfo>::Iterator &it, int result, SkipType skipType)
2032 {
2033     Q_Q(CopyJob);
2034 
2035     switch (result) {
2036     case Result_Cancel:
2037         q->setError(ERR_USER_CANCELED);
2038         q->emitResult();
2039         return;
2040     case KIO::Result_ReplaceAllInvalidChars:
2041         m_autoReplaceInvalidChars = true;
2042         Q_FALLTHROUGH();
2043     case KIO::Result_ReplaceInvalidChars: {
2044         QString fileName = it->uDest.fileName();
2045         const int len = fileName.size();
2046         cleanMsdosDestName(fileName);
2047         QString path = it->uDest.path();
2048         path.replace(path.size() - len, len, fileName);
2049         it->uDest.setPath(path);
2050         break;
2051     }
2052     case KIO::Result_AutoSkip:
2053         if (skipType == SkipInvalidChars) {
2054             m_autoSkipFilesWithInvalidChars = true;
2055         } else if (skipType == SkipFatSymlinks) {
2056             m_autoSkipFatSymlinks = true;
2057         }
2058         Q_FALLTHROUGH();
2059     case KIO::Result_Skip:
2060         // Move on the next file
2061         files.erase(it);
2062         copyNextFile();
2063         return;
2064     default:
2065         break;
2066     }
2067 
2068     qCDebug(KIO_COPYJOB_DEBUG) << "preparing to copy" << (*it).uSource << (*it).size << m_freeSpace;
2069     if (m_freeSpace != KIO::invalidFilesize && (*it).size != KIO::invalidFilesize) {
2070         if (m_freeSpace < (*it).size) {
2071             q->setError(ERR_DISK_FULL);
2072             q->emitResult();
2073             return;
2074         }
2075     }
2076 
2077     const QUrl &uSource = (*it).uSource;
2078     const QUrl &uDest = (*it).uDest;
2079     // Do we set overwrite ?
2080     bool bOverwrite;
2081     const QString destFile = uDest.path();
2082     qCDebug(KIO_COPYJOB_DEBUG) << "copying" << destFile;
2083     if (uDest == uSource) {
2084         bOverwrite = false;
2085     } else {
2086         bOverwrite = shouldOverwriteFile(destFile);
2087     }
2088 
2089     // If source isn't local and target is local, we ignore the original permissions
2090     // Otherwise, files downloaded from HTTP end up with -r--r--r--
2091     const bool remoteSource = !KProtocolManager::supportsListing(uSource) || uSource.scheme() == QLatin1String("trash");
2092     int permissions = (*it).permissions;
2093     if (m_defaultPermissions || (remoteSource && uDest.isLocalFile())) {
2094         permissions = -1;
2095     }
2096     const JobFlags flags = bOverwrite ? Overwrite : DefaultFlags;
2097 
2098     m_bCurrentOperationIsLink = false;
2099     KIO::Job *newjob = nullptr;
2100     if (m_mode == CopyJob::Link) {
2101         // User requested that a symlink be made
2102         newjob = linkNextFile(uSource, uDest, flags);
2103         if (!newjob) {
2104             return;
2105         }
2106     } else if (!(*it).linkDest.isEmpty() && compareUrls(uSource, uDest))
2107     // Copying a symlink - only on the same protocol/host/etc. (#5601, downloading an FTP file through its link),
2108     {
2109         KIO::SimpleJob *newJob = KIO::symlink((*it).linkDest, uDest, flags | HideProgressInfo /*no GUI*/);
2110         newJob->setParentJob(q);
2111         Scheduler::setJobPriority(newJob, 1);
2112         newjob = newJob;
2113         qCDebug(KIO_COPYJOB_DEBUG) << "Linking target=" << (*it).linkDest << "link=" << uDest;
2114         m_currentSrcURL = QUrl::fromUserInput((*it).linkDest);
2115         m_currentDestURL = uDest;
2116         m_bURLDirty = true;
2117         // emit linking( this, (*it).linkDest, uDest );
2118         // Observer::self()->slotCopying( this, m_currentSrcURL, uDest ); // should be slotLinking perhaps
2119         m_bCurrentOperationIsLink = true;
2120         // NOTE: if we are moving stuff, the deletion of the source will be done in slotResultCopyingFiles
2121     } else if (m_mode == CopyJob::Move) { // Moving a file
2122         KIO::FileCopyJob *moveJob = KIO::file_move(uSource, uDest, permissions, flags | HideProgressInfo /*no GUI*/);
2123         moveJob->setParentJob(q);
2124         moveJob->setSourceSize((*it).size);
2125         moveJob->setModificationTime((*it).mtime); // #55804
2126         newjob = moveJob;
2127         qCDebug(KIO_COPYJOB_DEBUG) << "Moving" << uSource << "to" << uDest;
2128         // emit moving( this, uSource, uDest );
2129         m_currentSrcURL = uSource;
2130         m_currentDestURL = uDest;
2131         m_bURLDirty = true;
2132         // Observer::self()->slotMoving( this, uSource, uDest );
2133     } else { // Copying a file
2134         KIO::FileCopyJob *copyJob = KIO::file_copy(uSource, uDest, permissions, flags | HideProgressInfo /*no GUI*/);
2135         copyJob->setParentJob(q); // in case of rename dialog
2136         copyJob->setSourceSize((*it).size);
2137         copyJob->setModificationTime((*it).mtime);
2138         newjob = copyJob;
2139         qCDebug(KIO_COPYJOB_DEBUG) << "Copying" << uSource << "to" << uDest;
2140         m_currentSrcURL = uSource;
2141         m_currentDestURL = uDest;
2142         m_bURLDirty = true;
2143     }
2144     q->addSubjob(newjob);
2145     q->connect(newjob, &Job::processedSize, q, [this](KJob *job, qulonglong processedSize) {
2146         slotProcessedSize(job, processedSize);
2147     });
2148     q->connect(newjob, &Job::totalSize, q, [this](KJob *job, qulonglong totalSize) {
2149         slotTotalSize(job, totalSize);
2150     });
2151 }
2152 
deleteNextDir()2153 void CopyJobPrivate::deleteNextDir()
2154 {
2155     Q_Q(CopyJob);
2156     if (m_mode == CopyJob::Move && !dirsToRemove.isEmpty()) { // some dirs to delete ?
2157         state = STATE_DELETING_DIRS;
2158         m_bURLDirty = true;
2159         // Take first dir to delete out of list - last ones first !
2160         QList<QUrl>::Iterator it = --dirsToRemove.end();
2161         SimpleJob *job = KIO::rmdir(*it);
2162         job->setParentJob(q);
2163         Scheduler::setJobPriority(job, 1);
2164         dirsToRemove.erase(it);
2165         q->addSubjob(job);
2166     } else {
2167         // This step is done, move on
2168         state = STATE_SETTING_DIR_ATTRIBUTES;
2169         m_directoriesCopiedIterator = m_directoriesCopied.cbegin();
2170         setNextDirAttribute();
2171     }
2172 }
2173 
setNextDirAttribute()2174 void CopyJobPrivate::setNextDirAttribute()
2175 {
2176     Q_Q(CopyJob);
2177     while (m_directoriesCopiedIterator != m_directoriesCopied.cend() && !(*m_directoriesCopiedIterator).mtime.isValid()) {
2178         ++m_directoriesCopiedIterator;
2179     }
2180     if (m_directoriesCopiedIterator != m_directoriesCopied.cend()) {
2181         const QUrl url = (*m_directoriesCopiedIterator).uDest;
2182         const QDateTime dt = (*m_directoriesCopiedIterator).mtime;
2183         ++m_directoriesCopiedIterator;
2184 
2185         KIO::SimpleJob *job = KIO::setModificationTime(url, dt);
2186         job->setParentJob(q);
2187         Scheduler::setJobPriority(job, 1);
2188         q->addSubjob(job);
2189     } else {
2190         if (m_reportTimer) {
2191             m_reportTimer->stop();
2192         }
2193 
2194         q->emitResult();
2195     }
2196 }
2197 
emitResult()2198 void CopyJob::emitResult()
2199 {
2200     Q_D(CopyJob);
2201     // Before we go, tell the world about the changes that were made.
2202     // Even if some error made us abort midway, we might still have done
2203     // part of the job so we better update the views! (#118583)
2204     if (!d->m_bOnlyRenames) {
2205         // If only renaming happened, KDirNotify::FileRenamed was emitted by the rename jobs
2206         QUrl url(d->m_globalDest);
2207         if (d->m_globalDestinationState != DEST_IS_DIR || d->m_asMethod) {
2208             url = url.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash);
2209         }
2210         qCDebug(KIO_COPYJOB_DEBUG) << "KDirNotify'ing FilesAdded" << url;
2211 #ifndef KIO_ANDROID_STUB
2212         org::kde::KDirNotify::emitFilesAdded(url);
2213 #endif
2214 
2215         if (d->m_mode == CopyJob::Move && !d->m_successSrcList.isEmpty()) {
2216             qCDebug(KIO_COPYJOB_DEBUG) << "KDirNotify'ing FilesRemoved" << d->m_successSrcList;
2217 #ifndef KIO_ANDROID_STUB
2218             org::kde::KDirNotify::emitFilesRemoved(d->m_successSrcList);
2219 #endif
2220         }
2221     }
2222 
2223     // Re-enable watching on the dirs that held the deleted/moved files
2224     if (d->m_mode == CopyJob::Move) {
2225         for (const auto &dir : d->m_parentDirs) {
2226             KDirWatch::self()->restartDirScan(dir);
2227         }
2228     }
2229     Job::emitResult();
2230 }
2231 
slotProcessedSize(KJob *,qulonglong data_size)2232 void CopyJobPrivate::slotProcessedSize(KJob *, qulonglong data_size)
2233 {
2234     Q_Q(CopyJob);
2235     qCDebug(KIO_COPYJOB_DEBUG) << data_size;
2236     m_fileProcessedSize = data_size;
2237 
2238     if (m_processedSize + m_fileProcessedSize > m_totalSize) {
2239         // Example: download any attachment from bugs.kde.org
2240         m_totalSize = m_processedSize + m_fileProcessedSize;
2241         qCDebug(KIO_COPYJOB_DEBUG) << "Adjusting m_totalSize to" << m_totalSize;
2242         q->setTotalAmount(KJob::Bytes, m_totalSize); // safety
2243     }
2244     qCDebug(KIO_COPYJOB_DEBUG) << "emit processedSize" << (unsigned long)(m_processedSize + m_fileProcessedSize);
2245 }
2246 
slotTotalSize(KJob *,qulonglong size)2247 void CopyJobPrivate::slotTotalSize(KJob *, qulonglong size)
2248 {
2249     Q_Q(CopyJob);
2250     qCDebug(KIO_COPYJOB_DEBUG) << size;
2251     // Special case for copying a single file
2252     // This is because some protocols don't implement stat properly
2253     // (e.g. HTTP), and don't give us a size in some cases (redirection)
2254     // so we'd rather rely on the size given for the transfer
2255     if (m_bSingleFileCopy && size != m_totalSize) {
2256         qCDebug(KIO_COPYJOB_DEBUG) << "slotTotalSize: updating totalsize to" << size;
2257         m_totalSize = size;
2258         q->setTotalAmount(KJob::Bytes, size);
2259     }
2260 }
2261 
slotResultDeletingDirs(KJob * job)2262 void CopyJobPrivate::slotResultDeletingDirs(KJob *job)
2263 {
2264     Q_Q(CopyJob);
2265     if (job->error()) {
2266         // Couldn't remove directory. Well, perhaps it's not empty
2267         // because the user pressed Skip for a given file in it.
2268         // Let's not display "Could not remove dir ..." for each of those dir !
2269     } else {
2270         m_successSrcList.append(static_cast<KIO::SimpleJob *>(job)->url());
2271     }
2272     q->removeSubjob(job);
2273     Q_ASSERT(!q->hasSubjobs());
2274     deleteNextDir();
2275 }
2276 
slotResultSettingDirAttributes(KJob * job)2277 void CopyJobPrivate::slotResultSettingDirAttributes(KJob *job)
2278 {
2279     Q_Q(CopyJob);
2280     if (job->error()) {
2281         // Couldn't set directory attributes. Ignore the error, it can happen
2282         // with inferior file systems like VFAT.
2283         // Let's not display warnings for each dir like "cp -a" does.
2284     }
2285     q->removeSubjob(job);
2286     Q_ASSERT(!q->hasSubjobs());
2287     setNextDirAttribute();
2288 }
2289 
directRenamingFailed(const QUrl & dest)2290 void CopyJobPrivate::directRenamingFailed(const QUrl &dest)
2291 {
2292     Q_Q(CopyJob);
2293 
2294     qCDebug(KIO_COPYJOB_DEBUG) << "Couldn't rename" << m_currentSrcURL << "to" << dest << ", reverting to normal way, starting with stat";
2295     qCDebug(KIO_COPYJOB_DEBUG) << "KIO::stat on" << m_currentSrcURL;
2296 
2297     KIO::Job *job = KIO::statDetails(m_currentSrcURL, StatJob::SourceSide, KIO::StatDefaultDetails, KIO::HideProgressInfo);
2298     state = STATE_STATING;
2299     q->addSubjob(job);
2300     m_bOnlyRenames = false;
2301 }
2302 
2303 // We were trying to do a direct renaming, before even stat'ing
slotResultRenaming(KJob * job)2304 void CopyJobPrivate::slotResultRenaming(KJob *job)
2305 {
2306     Q_Q(CopyJob);
2307     int err = job->error();
2308     const QString errText = job->errorText();
2309     // Merge metadata from subjob
2310     KIO::Job *kiojob = qobject_cast<KIO::Job *>(job);
2311     Q_ASSERT(kiojob);
2312     m_incomingMetaData += kiojob->metaData();
2313     q->removeSubjob(job);
2314     Q_ASSERT(!q->hasSubjobs());
2315     // Determine dest again
2316     QUrl dest = m_dest;
2317     if (destinationState == DEST_IS_DIR && !m_asMethod) {
2318         dest = addPathToUrl(dest, m_currentSrcURL.fileName());
2319     }
2320     auto *askUserActionInterface = KIO::delegateExtension<KIO::AskUserActionInterface *>(q);
2321 
2322     if (err) {
2323         // This code is similar to CopyJobPrivate::slotResultErrorCopyingFiles
2324         // but here it's about the base src url being moved/renamed
2325         // (m_currentSrcURL) and its dest (m_dest), not about a single file.
2326         // It also means we already stated the dest, here.
2327         // On the other hand we haven't stated the src yet (we skipped doing it
2328         // to save time, since it's not necessary to rename directly!)...
2329 
2330         // Existing dest?
2331         if (err == ERR_DIR_ALREADY_EXIST || err == ERR_FILE_ALREADY_EXIST || err == ERR_IDENTICAL_FILES) {
2332             // Should we skip automatically ?
2333             bool isDir = (err == ERR_DIR_ALREADY_EXIST); // ## technically, isDir means "source is dir", not "dest is dir" #######
2334             if ((isDir && m_bAutoSkipDirs) || (!isDir && m_bAutoSkipFiles)) {
2335                 // Move on to next source url
2336                 ++m_filesHandledByDirectRename;
2337                 skipSrc(isDir);
2338                 return;
2339             } else if ((isDir && m_bOverwriteAllDirs) || (!isDir && m_bOverwriteAllFiles)) {
2340                 ; // nothing to do, stat+copy+del will overwrite
2341             } else if ((isDir && m_bAutoRenameDirs) || (!isDir && m_bAutoRenameFiles)) {
2342                 QUrl destDirectory = m_currentDestURL.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash); // m_currendDestURL includes filename
2343                 const QString newName = KFileUtils::suggestName(destDirectory, m_currentDestURL.fileName());
2344 
2345                 m_dest = destDirectory;
2346                 m_dest.setPath(concatPaths(m_dest.path(), newName));
2347                 Q_EMIT q->renamed(q, dest, m_dest);
2348                 KIO::Job *job = KIO::statDetails(m_dest, StatJob::DestinationSide, KIO::StatDefaultDetails, KIO::HideProgressInfo);
2349                 state = STATE_STATING;
2350                 destinationState = DEST_NOT_STATED;
2351                 q->addSubjob(job);
2352                 return;
2353             } else if (askUserActionInterface) {
2354                 // we lack mtime info for both the src (not stated)
2355                 // and the dest (stated but this info wasn't stored)
2356                 // Let's do it for local files, at least
2357                 KIO::filesize_t sizeSrc = KIO::invalidFilesize;
2358                 KIO::filesize_t sizeDest = KIO::invalidFilesize;
2359                 QDateTime ctimeSrc;
2360                 QDateTime ctimeDest;
2361                 QDateTime mtimeSrc;
2362                 QDateTime mtimeDest;
2363 
2364                 bool destIsDir = err == ERR_DIR_ALREADY_EXIST;
2365 
2366                 // ## TODO we need to stat the source using KIO::stat
2367                 // so that this code is properly network-transparent.
2368 
2369                 if (m_currentSrcURL.isLocalFile()) {
2370                     QFileInfo info(m_currentSrcURL.toLocalFile());
2371                     if (info.exists()) {
2372                         sizeSrc = info.size();
2373                         ctimeSrc = info.birthTime();
2374                         mtimeSrc = info.lastModified();
2375                         isDir = info.isDir();
2376                     }
2377                 }
2378                 if (dest.isLocalFile()) {
2379                     QFileInfo destInfo(dest.toLocalFile());
2380                     if (destInfo.exists()) {
2381                         sizeDest = destInfo.size();
2382                         ctimeDest = destInfo.birthTime();
2383                         mtimeDest = destInfo.lastModified();
2384                         destIsDir = destInfo.isDir();
2385                     }
2386                 }
2387 
2388                 // If src==dest, use "overwrite-itself"
2389                 RenameDialog_Options options = (m_currentSrcURL == dest) ? RenameDialog_OverwriteItself : RenameDialog_Overwrite;
2390                 if (!isDir && destIsDir) {
2391                     // We can't overwrite a dir with a file.
2392                     options = RenameDialog_Options();
2393                 }
2394 
2395                 if (m_srcList.count() > 1) {
2396                     options |= RenameDialog_Options(RenameDialog_MultipleItems | RenameDialog_Skip);
2397                 }
2398 
2399                 if (destIsDir) {
2400                     options |= RenameDialog_DestIsDirectory;
2401                 }
2402 
2403                 if (m_reportTimer) {
2404                     m_reportTimer->stop();
2405                 }
2406 
2407                 RenameDialog_Result r;
2408                 if (m_bOverwriteWhenOlder && mtimeSrc.isValid() && mtimeDest.isValid()) {
2409                     if (mtimeSrc > mtimeDest) {
2410                         qCDebug(KIO_COPYJOB_DEBUG) << "dest is older, overwriting" << dest;
2411                         r = Result_Overwrite;
2412                     } else {
2413                         qCDebug(KIO_COPYJOB_DEBUG) << "dest is newer, skipping" << dest;
2414                         r = Result_Skip;
2415                     }
2416 
2417                     processDirectRenamingConflictResult(r, isDir, destIsDir, mtimeSrc, mtimeDest, dest, QUrl{});
2418                     return;
2419                 } else {
2420                     auto renameSignal = &KIO::AskUserActionInterface::askUserRenameResult;
2421                     QObject::connect(askUserActionInterface, renameSignal, q, [=](RenameDialog_Result result, const QUrl &newUrl, KJob *parentJob) {
2422                         Q_ASSERT(parentJob == q);
2423                         // Only receive askUserRenameResult once per rename dialog
2424                         QObject::disconnect(askUserActionInterface, renameSignal, q, nullptr);
2425 
2426                         processDirectRenamingConflictResult(result, isDir, destIsDir, mtimeSrc, mtimeDest, dest, newUrl);
2427                     });
2428 
2429                     const QString caption = err != ERR_DIR_ALREADY_EXIST ? i18n("File Already Exists") : i18n("Already Exists as Folder");
2430 
2431                     /* clang-format off */
2432                     askUserActionInterface->askUserRename(q, caption,
2433                                                           m_currentSrcURL, dest,
2434                                                           options,
2435                                                           sizeSrc, sizeDest,
2436                                                           ctimeSrc, ctimeDest,
2437                                                           mtimeSrc, mtimeDest);
2438                     /* clang-format on */
2439 
2440                     return;
2441                 }
2442             } else if (err != KIO::ERR_UNSUPPORTED_ACTION) {
2443                 // Dest already exists, and job is not interactive -> abort with error
2444                 q->setError(err);
2445                 q->setErrorText(errText);
2446                 q->emitResult();
2447                 return;
2448             }
2449         } else if (err != KIO::ERR_UNSUPPORTED_ACTION) {
2450             qCDebug(KIO_COPYJOB_DEBUG) << "Couldn't rename" << m_currentSrcURL << "to" << dest << ", aborting";
2451             q->setError(err);
2452             q->setErrorText(errText);
2453             q->emitResult();
2454             return;
2455         }
2456 
2457         directRenamingFailed(dest);
2458         return;
2459     }
2460 
2461     // No error
2462     qCDebug(KIO_COPYJOB_DEBUG) << "Renaming succeeded, move on";
2463     ++m_processedFiles;
2464     ++m_filesHandledByDirectRename;
2465     // Emit copyingDone for FileUndoManager to remember what we did.
2466     // Use resolved URL m_currentSrcURL since that's what we just used for renaming. See bug 391606 and kio_desktop's testTrashAndUndo().
2467     const bool srcIsDir = false; // # TODO: we just don't know, since we never stat'ed it
2468     Q_EMIT q->copyingDone(q, m_currentSrcURL, finalDestUrl(m_currentSrcURL, dest), QDateTime() /*mtime unknown, and not needed*/, srcIsDir, true);
2469     m_successSrcList.append(*m_currentStatSrc);
2470     statNextSrc();
2471 }
2472 
processDirectRenamingConflictResult(RenameDialog_Result result,bool srcIsDir,bool destIsDir,const QDateTime & mtimeSrc,const QDateTime & mtimeDest,const QUrl & dest,const QUrl & newUrl)2473 void CopyJobPrivate::processDirectRenamingConflictResult(RenameDialog_Result result,
2474                                                          bool srcIsDir,
2475                                                          bool destIsDir,
2476                                                          const QDateTime &mtimeSrc,
2477                                                          const QDateTime &mtimeDest,
2478                                                          const QUrl &dest,
2479                                                          const QUrl &newUrl)
2480 {
2481     Q_Q(CopyJob);
2482 
2483     if (m_reportTimer) {
2484         m_reportTimer->start(s_reportTimeout);
2485     }
2486 
2487     if (result == Result_OverwriteWhenOlder) {
2488         m_bOverwriteWhenOlder = true;
2489         if (mtimeSrc > mtimeDest) {
2490             qCDebug(KIO_COPYJOB_DEBUG) << "dest is older, overwriting" << dest;
2491             result = Result_Overwrite;
2492         } else {
2493             qCDebug(KIO_COPYJOB_DEBUG) << "dest is newer, skipping" << dest;
2494             result = Result_Skip;
2495         }
2496     }
2497 
2498     switch (result) {
2499     case Result_Cancel: {
2500         q->setError(ERR_USER_CANCELED);
2501         q->emitResult();
2502         return;
2503     }
2504     case Result_AutoRename:
2505         if (srcIsDir) {
2506             m_bAutoRenameDirs = true;
2507         } else {
2508             m_bAutoRenameFiles = true;
2509         }
2510         // fall through
2511         Q_FALLTHROUGH();
2512     case Result_Rename: {
2513         // Set m_dest to the chosen destination
2514         // This is only for this src url; the next one will revert to m_globalDest
2515         m_dest = newUrl;
2516         Q_EMIT q->renamed(q, dest, m_dest); // For e.g. KPropertiesDialog
2517         KIO::Job *job = KIO::statDetails(m_dest, StatJob::DestinationSide, KIO::StatDefaultDetails, KIO::HideProgressInfo);
2518         state = STATE_STATING;
2519         destinationState = DEST_NOT_STATED;
2520         q->addSubjob(job);
2521         return;
2522     }
2523     case Result_AutoSkip:
2524         if (srcIsDir) {
2525             m_bAutoSkipDirs = true;
2526         } else {
2527             m_bAutoSkipFiles = true;
2528         }
2529         // fall through
2530         Q_FALLTHROUGH();
2531     case Result_Skip:
2532         // Move on to next url
2533         ++m_filesHandledByDirectRename;
2534         skipSrc(srcIsDir);
2535         return;
2536     case Result_OverwriteAll:
2537         if (destIsDir) {
2538             m_bOverwriteAllDirs = true;
2539         } else {
2540             m_bOverwriteAllFiles = true;
2541         }
2542         break;
2543     case Result_Overwrite:
2544         // Add to overwrite list
2545         // Note that we add dest, not m_dest.
2546         // This ensures that when moving several urls into a dir (m_dest),
2547         // we only overwrite for the current one, not for all.
2548         // When renaming a single file (m_asMethod), it makes no difference.
2549         qCDebug(KIO_COPYJOB_DEBUG) << "adding to overwrite list: " << dest.path();
2550         m_overwriteList.insert(dest.path());
2551         break;
2552     default:
2553         // Q_ASSERT( 0 );
2554         break;
2555     }
2556 
2557     directRenamingFailed(dest);
2558 }
2559 
slotResult(KJob * job)2560 void CopyJob::slotResult(KJob *job)
2561 {
2562     Q_D(CopyJob);
2563     qCDebug(KIO_COPYJOB_DEBUG) << "d->state=" << (int)d->state;
2564     // In each case, what we have to do is :
2565     // 1 - check for errors and treat them
2566     // 2 - removeSubjob(job);
2567     // 3 - decide what to do next
2568 
2569     switch (d->state) {
2570     case STATE_STATING: // We were trying to stat a src url or the dest
2571         d->slotResultStating(job);
2572         break;
2573     case STATE_RENAMING: { // We were trying to do a direct renaming, before even stat'ing
2574         d->slotResultRenaming(job);
2575         break;
2576     }
2577     case STATE_LISTING: // recursive listing finished
2578         qCDebug(KIO_COPYJOB_DEBUG) << "totalSize:" << (unsigned int)d->m_totalSize << "files:" << d->files.count() << "d->dirs:" << d->dirs.count();
2579         // Was there an error ?
2580         if (job->error()) {
2581             Job::slotResult(job); // will set the error and emit result(this)
2582             return;
2583         }
2584 
2585         removeSubjob(job);
2586         Q_ASSERT(!hasSubjobs());
2587 
2588         d->statNextSrc();
2589         break;
2590     case STATE_CREATING_DIRS:
2591         d->slotResultCreatingDirs(job);
2592         break;
2593     case STATE_CONFLICT_CREATING_DIRS:
2594         d->slotResultConflictCreatingDirs(job);
2595         break;
2596     case STATE_COPYING_FILES:
2597         d->slotResultCopyingFiles(job);
2598         break;
2599     case STATE_CONFLICT_COPYING_FILES:
2600         d->slotResultErrorCopyingFiles(job);
2601         break;
2602     case STATE_DELETING_DIRS:
2603         d->slotResultDeletingDirs(job);
2604         break;
2605     case STATE_SETTING_DIR_ATTRIBUTES:
2606         d->slotResultSettingDirAttributes(job);
2607         break;
2608     default:
2609         Q_ASSERT(0);
2610     }
2611 }
2612 
setDefaultPermissions(bool b)2613 void KIO::CopyJob::setDefaultPermissions(bool b)
2614 {
2615     d_func()->m_defaultPermissions = b;
2616 }
2617 
operationMode() const2618 KIO::CopyJob::CopyMode KIO::CopyJob::operationMode() const
2619 {
2620     return d_func()->m_mode;
2621 }
2622 
setAutoSkip(bool autoSkip)2623 void KIO::CopyJob::setAutoSkip(bool autoSkip)
2624 {
2625     d_func()->m_bAutoSkipFiles = autoSkip;
2626     d_func()->m_bAutoSkipDirs = autoSkip;
2627 }
2628 
setAutoRename(bool autoRename)2629 void KIO::CopyJob::setAutoRename(bool autoRename)
2630 {
2631     d_func()->m_bAutoRenameFiles = autoRename;
2632     d_func()->m_bAutoRenameDirs = autoRename;
2633 }
2634 
setWriteIntoExistingDirectories(bool overwriteAll)2635 void KIO::CopyJob::setWriteIntoExistingDirectories(bool overwriteAll) // #65926
2636 {
2637     d_func()->m_bOverwriteAllDirs = overwriteAll;
2638 }
2639 
copy(const QUrl & src,const QUrl & dest,JobFlags flags)2640 CopyJob *KIO::copy(const QUrl &src, const QUrl &dest, JobFlags flags)
2641 {
2642     qCDebug(KIO_COPYJOB_DEBUG) << "src=" << src << "dest=" << dest;
2643     QList<QUrl> srcList;
2644     srcList.append(src);
2645     return CopyJobPrivate::newJob(srcList, dest, CopyJob::Copy, false, flags);
2646 }
2647 
copyAs(const QUrl & src,const QUrl & dest,JobFlags flags)2648 CopyJob *KIO::copyAs(const QUrl &src, const QUrl &dest, JobFlags flags)
2649 {
2650     qCDebug(KIO_COPYJOB_DEBUG) << "src=" << src << "dest=" << dest;
2651     QList<QUrl> srcList;
2652     srcList.append(src);
2653     return CopyJobPrivate::newJob(srcList, dest, CopyJob::Copy, true, flags);
2654 }
2655 
copy(const QList<QUrl> & src,const QUrl & dest,JobFlags flags)2656 CopyJob *KIO::copy(const QList<QUrl> &src, const QUrl &dest, JobFlags flags)
2657 {
2658     qCDebug(KIO_COPYJOB_DEBUG) << src << dest;
2659     return CopyJobPrivate::newJob(src, dest, CopyJob::Copy, false, flags);
2660 }
2661 
move(const QUrl & src,const QUrl & dest,JobFlags flags)2662 CopyJob *KIO::move(const QUrl &src, const QUrl &dest, JobFlags flags)
2663 {
2664     qCDebug(KIO_COPYJOB_DEBUG) << src << dest;
2665     QList<QUrl> srcList;
2666     srcList.append(src);
2667     CopyJob *job = CopyJobPrivate::newJob(srcList, dest, CopyJob::Move, false, flags);
2668     if (job->uiDelegateExtension()) {
2669         job->uiDelegateExtension()->createClipboardUpdater(job, JobUiDelegateExtension::UpdateContent);
2670     }
2671     return job;
2672 }
2673 
moveAs(const QUrl & src,const QUrl & dest,JobFlags flags)2674 CopyJob *KIO::moveAs(const QUrl &src, const QUrl &dest, JobFlags flags)
2675 {
2676     qCDebug(KIO_COPYJOB_DEBUG) << src << dest;
2677     QList<QUrl> srcList;
2678     srcList.append(src);
2679     CopyJob *job = CopyJobPrivate::newJob(srcList, dest, CopyJob::Move, true, flags);
2680     if (job->uiDelegateExtension()) {
2681         job->uiDelegateExtension()->createClipboardUpdater(job, JobUiDelegateExtension::UpdateContent);
2682     }
2683     return job;
2684 }
2685 
move(const QList<QUrl> & src,const QUrl & dest,JobFlags flags)2686 CopyJob *KIO::move(const QList<QUrl> &src, const QUrl &dest, JobFlags flags)
2687 {
2688     qCDebug(KIO_COPYJOB_DEBUG) << src << dest;
2689     CopyJob *job = CopyJobPrivate::newJob(src, dest, CopyJob::Move, false, flags);
2690     if (job->uiDelegateExtension()) {
2691         job->uiDelegateExtension()->createClipboardUpdater(job, JobUiDelegateExtension::UpdateContent);
2692     }
2693     return job;
2694 }
2695 
link(const QUrl & src,const QUrl & destDir,JobFlags flags)2696 CopyJob *KIO::link(const QUrl &src, const QUrl &destDir, JobFlags flags)
2697 {
2698     QList<QUrl> srcList;
2699     srcList.append(src);
2700     return CopyJobPrivate::newJob(srcList, destDir, CopyJob::Link, false, flags);
2701 }
2702 
link(const QList<QUrl> & srcList,const QUrl & destDir,JobFlags flags)2703 CopyJob *KIO::link(const QList<QUrl> &srcList, const QUrl &destDir, JobFlags flags)
2704 {
2705     return CopyJobPrivate::newJob(srcList, destDir, CopyJob::Link, false, flags);
2706 }
2707 
linkAs(const QUrl & src,const QUrl & destDir,JobFlags flags)2708 CopyJob *KIO::linkAs(const QUrl &src, const QUrl &destDir, JobFlags flags)
2709 {
2710     QList<QUrl> srcList;
2711     srcList.append(src);
2712     return CopyJobPrivate::newJob(srcList, destDir, CopyJob::Link, true, flags);
2713 }
2714 
trash(const QUrl & src,JobFlags flags)2715 CopyJob *KIO::trash(const QUrl &src, JobFlags flags)
2716 {
2717     QList<QUrl> srcList;
2718     srcList.append(src);
2719     return CopyJobPrivate::newJob(srcList, QUrl(QStringLiteral("trash:/")), CopyJob::Move, false, flags);
2720 }
2721 
trash(const QList<QUrl> & srcList,JobFlags flags)2722 CopyJob *KIO::trash(const QList<QUrl> &srcList, JobFlags flags)
2723 {
2724     return CopyJobPrivate::newJob(srcList, QUrl(QStringLiteral("trash:/")), CopyJob::Move, false, flags);
2725 }
2726 
2727 #include "moc_copyjob.cpp"
2728