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

source code of kio/src/core/copyjob.cpp