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

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