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