1 /*
2 This file is part of the KDE libraries
3 SPDX-FileCopyrightText: 2000 Stephan Kulow <coolo@kde.org>
4 SPDX-FileCopyrightText: 2000-2009 David Faure <faure@kde.org>
5
6 SPDX-License-Identifier: LGPL-2.0-or-later
7 */
8
9 #include "filecopyjob.h"
10 #include "askuseractioninterface.h"
11 #include "job_p.h"
12 #include "kprotocolmanager.h"
13 #include "scheduler.h"
14 #include "slave.h"
15 #include <kio/jobuidelegatefactory.h>
16
17 #include <KLocalizedString>
18
19 #include <QFile>
20 #include <QTimer>
21
22 using namespace KIO;
23
jobSlave(SimpleJob * job)24 static inline Slave *jobSlave(SimpleJob *job)
25 {
26 return SimpleJobPrivate::get(job)->m_slave;
27 }
28
29 /** @internal */
30 class KIO::FileCopyJobPrivate : public KIO::JobPrivate
31 {
32 public:
FileCopyJobPrivate(const QUrl & src,const QUrl & dest,int permissions,bool move,JobFlags flags)33 FileCopyJobPrivate(const QUrl &src, const QUrl &dest, int permissions, bool move, JobFlags flags)
34 : m_sourceSize(filesize_t(-1))
35 , m_src(src)
36 , m_dest(dest)
37 , m_moveJob(nullptr)
38 , m_copyJob(nullptr)
39 , m_delJob(nullptr)
40 , m_chmodJob(nullptr)
41 , m_getJob(nullptr)
42 , m_putJob(nullptr)
43 , m_permissions(permissions)
44 , m_move(move)
45 , m_mustChmod(0)
46 , m_bFileCopyInProgress(false)
47 , m_flags(flags)
48 {
49 }
50 KIO::filesize_t m_sourceSize;
51 QDateTime m_modificationTime;
52 QUrl m_src;
53 QUrl m_dest;
54 QByteArray m_buffer;
55 SimpleJob *m_moveJob;
56 SimpleJob *m_copyJob;
57 SimpleJob *m_delJob;
58 SimpleJob *m_chmodJob;
59 TransferJob *m_getJob;
60 TransferJob *m_putJob;
61 int m_permissions;
62 bool m_move : 1;
63 bool m_canResume : 1;
64 bool m_resumeAnswerSent : 1;
65 bool m_mustChmod : 1;
66 bool m_bFileCopyInProgress : 1;
67 JobFlags m_flags;
68
69 void startBestCopyMethod();
70 void startCopyJob();
71 void startCopyJob(const QUrl &slave_url);
72 void startRenameJob(const QUrl &slave_url);
73 void startDataPump();
74 void connectSubjob(SimpleJob *job);
75
76 void slotStart();
77 void slotData(KIO::Job *, const QByteArray &data);
78 void slotDataReq(KIO::Job *, QByteArray &data);
79 void slotMimetype(KIO::Job *, const QString &type);
80 /**
81 * Forward signal from subjob
82 * @param job the job that emitted this signal
83 * @param offset the offset to resume from
84 */
85 void slotCanResume(KIO::Job *job, KIO::filesize_t offset);
86 void processCanResumeResult(KIO::Job *job, RenameDialog_Result result, KIO::filesize_t offset);
87
Q_DECLARE_PUBLIC(FileCopyJob)88 Q_DECLARE_PUBLIC(FileCopyJob)
89
90 static inline FileCopyJob *newJob(const QUrl &src, const QUrl &dest, int permissions, bool move, JobFlags flags)
91 {
92 // qDebug() << src << "->" << dest;
93 FileCopyJob *job = new FileCopyJob(*new FileCopyJobPrivate(src, dest, permissions, move, flags));
94 job->setProperty("destUrl", dest.toString());
95 job->setUiDelegate(KIO::createDefaultJobUiDelegate());
96 if (!(flags & HideProgressInfo)) {
97 KIO::getJobTracker()->registerJob(job);
98 }
99 if (!(flags & NoPrivilegeExecution)) {
100 job->d_func()->m_privilegeExecutionEnabled = true;
101 job->d_func()->m_operationType = move ? Move : Copy;
102 }
103 return job;
104 }
105 };
106
isSrcDestSameSlaveProcess(const QUrl & src,const QUrl & dest)107 static bool isSrcDestSameSlaveProcess(const QUrl &src, const QUrl &dest)
108 {
109 /* clang-format off */
110 return src.scheme() == dest.scheme()
111 && src.host() == dest.host()
112 && src.port() == dest.port()
113 && src.userName() == dest.userName()
114 && src.password() == dest.password();
115 /* clang-format on */
116 }
117
118 /*
119 * The FileCopyJob works according to the famous Bavarian
120 * 'Alternating Bitburger Protocol': we either drink a beer or we
121 * we order a beer, but never both at the same time.
122 * Translated to io-slaves: We alternate between receiving a block of data
123 * and sending it away.
124 */
FileCopyJob(FileCopyJobPrivate & dd)125 FileCopyJob::FileCopyJob(FileCopyJobPrivate &dd)
126 : Job(dd)
127 {
128 Q_D(FileCopyJob);
129 QTimer::singleShot(0, this, [d]() {
130 d->slotStart();
131 });
132 }
133
slotStart()134 void FileCopyJobPrivate::slotStart()
135 {
136 Q_Q(FileCopyJob);
137 if (!m_move) {
138 JobPrivate::emitCopying(q, m_src, m_dest);
139 } else {
140 JobPrivate::emitMoving(q, m_src, m_dest);
141 }
142
143 if (m_move) {
144 // The if() below must be the same as the one in startBestCopyMethod
145 if (isSrcDestSameSlaveProcess(m_src, m_dest)) {
146 startRenameJob(m_src);
147 return;
148 } else if (m_src.isLocalFile() && KProtocolManager::canRenameFromFile(m_dest)) {
149 startRenameJob(m_dest);
150 return;
151 } else if (m_dest.isLocalFile() && KProtocolManager::canRenameToFile(m_src)) {
152 startRenameJob(m_src);
153 return;
154 }
155 // No fast-move available, use copy + del.
156 }
157 startBestCopyMethod();
158 }
159
startBestCopyMethod()160 void FileCopyJobPrivate::startBestCopyMethod()
161 {
162 if (isSrcDestSameSlaveProcess(m_src, m_dest)) {
163 startCopyJob();
164 } else if (m_src.isLocalFile() && KProtocolManager::canCopyFromFile(m_dest)) {
165 startCopyJob(m_dest);
166 } else if (m_dest.isLocalFile() && KProtocolManager::canCopyToFile(m_src) && !KIO::Scheduler::isSlaveOnHoldFor(m_src)) {
167 startCopyJob(m_src);
168 } else {
169 startDataPump();
170 }
171 }
172
~FileCopyJob()173 FileCopyJob::~FileCopyJob()
174 {
175 }
176
setSourceSize(KIO::filesize_t size)177 void FileCopyJob::setSourceSize(KIO::filesize_t size)
178 {
179 Q_D(FileCopyJob);
180 d->m_sourceSize = size;
181 if (size != (KIO::filesize_t)-1) {
182 setTotalAmount(KJob::Bytes, size);
183 }
184 }
185
setModificationTime(const QDateTime & mtime)186 void FileCopyJob::setModificationTime(const QDateTime &mtime)
187 {
188 Q_D(FileCopyJob);
189 d->m_modificationTime = mtime;
190 }
191
srcUrl() const192 QUrl FileCopyJob::srcUrl() const
193 {
194 return d_func()->m_src;
195 }
196
destUrl() const197 QUrl FileCopyJob::destUrl() const
198 {
199 return d_func()->m_dest;
200 }
201
startCopyJob()202 void FileCopyJobPrivate::startCopyJob()
203 {
204 startCopyJob(m_src);
205 }
206
startCopyJob(const QUrl & slave_url)207 void FileCopyJobPrivate::startCopyJob(const QUrl &slave_url)
208 {
209 Q_Q(FileCopyJob);
210 // qDebug();
211 KIO_ARGS << m_src << m_dest << m_permissions << (qint8)(m_flags & Overwrite);
212 auto job = new DirectCopyJob(slave_url, packedArgs);
213 m_copyJob = job;
214 m_copyJob->setParentJob(q);
215 if (m_modificationTime.isValid()) {
216 m_copyJob->addMetaData(QStringLiteral("modified"), m_modificationTime.toString(Qt::ISODate)); // #55804
217 }
218 q->addSubjob(m_copyJob);
219 connectSubjob(m_copyJob);
220 q->connect(job, &DirectCopyJob::canResume, q, [this](KIO::Job *job, KIO::filesize_t offset) {
221 slotCanResume(job, offset);
222 });
223 }
224
startRenameJob(const QUrl & slave_url)225 void FileCopyJobPrivate::startRenameJob(const QUrl &slave_url)
226 {
227 Q_Q(FileCopyJob);
228 m_mustChmod = true; // CMD_RENAME by itself doesn't change permissions
229 KIO_ARGS << m_src << m_dest << (qint8)(m_flags & Overwrite);
230 m_moveJob = SimpleJobPrivate::newJobNoUi(slave_url, CMD_RENAME, packedArgs);
231 m_moveJob->setParentJob(q);
232 if (m_modificationTime.isValid()) {
233 m_moveJob->addMetaData(QStringLiteral("modified"), m_modificationTime.toString(Qt::ISODate)); // #55804
234 }
235 q->addSubjob(m_moveJob);
236 connectSubjob(m_moveJob);
237 }
238
connectSubjob(SimpleJob * job)239 void FileCopyJobPrivate::connectSubjob(SimpleJob *job)
240 {
241 Q_Q(FileCopyJob);
242 q->connect(job, &KJob::totalSize, q, [q](KJob *job, qulonglong totalSize) {
243 Q_UNUSED(job);
244 if (totalSize != q->totalAmount(KJob::Bytes)) {
245 q->setTotalAmount(KJob::Bytes, totalSize);
246 }
247 });
248
249 q->connect(job, &KJob::processedSize, q, [q, this](KJob *job, qulonglong processedSize) {
250 if (job == m_copyJob) {
251 m_bFileCopyInProgress = processedSize > 0;
252 }
253 q->setProcessedAmount(KJob::Bytes, processedSize);
254 });
255
256 q->connect(job, &KJob::percentChanged, q, [q](KJob *, ulong percent) {
257 if (percent > q->percent()) {
258 q->setPercent(percent);
259 }
260 });
261
262 if (q->isSuspended()) {
263 job->suspend();
264 }
265 }
266
doSuspend()267 bool FileCopyJob::doSuspend()
268 {
269 Q_D(FileCopyJob);
270 if (d->m_moveJob) {
271 d->m_moveJob->suspend();
272 }
273
274 if (d->m_copyJob) {
275 d->m_copyJob->suspend();
276 }
277
278 if (d->m_getJob) {
279 d->m_getJob->suspend();
280 }
281
282 if (d->m_putJob) {
283 d->m_putJob->suspend();
284 }
285
286 Job::doSuspend();
287 return true;
288 }
289
doResume()290 bool FileCopyJob::doResume()
291 {
292 Q_D(FileCopyJob);
293 if (d->m_moveJob) {
294 d->m_moveJob->resume();
295 }
296
297 if (d->m_copyJob) {
298 d->m_copyJob->resume();
299 }
300
301 if (d->m_getJob) {
302 d->m_getJob->resume();
303 }
304
305 if (d->m_putJob) {
306 d->m_putJob->resume();
307 }
308
309 Job::doResume();
310 return true;
311 }
312
startDataPump()313 void FileCopyJobPrivate::startDataPump()
314 {
315 Q_Q(FileCopyJob);
316 // qDebug();
317
318 m_canResume = false;
319 m_resumeAnswerSent = false;
320 m_getJob = nullptr; // for now
321 m_putJob = put(m_dest, m_permissions, (m_flags | HideProgressInfo) /* no GUI */);
322 m_putJob->setParentJob(q);
323 // qDebug() << "m_putJob=" << m_putJob << "m_dest=" << m_dest;
324 if (m_modificationTime.isValid()) {
325 m_putJob->setModificationTime(m_modificationTime);
326 }
327
328 // The first thing the put job will tell us is whether we can
329 // resume or not (this is always emitted)
330 q->connect(m_putJob, &KIO::TransferJob::canResume, q, [this](KIO::Job *job, KIO::filesize_t offset) {
331 slotCanResume(job, offset);
332 });
333 q->connect(m_putJob, &KIO::TransferJob::dataReq, q, [this](KIO::Job *job, QByteArray &data) {
334 slotDataReq(job, data);
335 });
336 q->addSubjob(m_putJob);
337 }
338
slotCanResume(KIO::Job * job,KIO::filesize_t offset)339 void FileCopyJobPrivate::slotCanResume(KIO::Job *job, KIO::filesize_t offset)
340 {
341 Q_Q(FileCopyJob);
342
343 if (job == m_getJob) {
344 // Cool, the get job said ok, we can resume
345 m_canResume = true;
346 // qDebug() << "'can resume' from the GET job -> we can resume";
347
348 jobSlave(m_getJob)->setOffset(jobSlave(m_putJob)->offset());
349 return;
350 }
351
352 if (job == m_putJob || job == m_copyJob) {
353 // qDebug() << "'can resume' from PUT job. offset=" << KIO::number(offset);
354 if (offset == 0) {
355 m_resumeAnswerSent = true; // No need for an answer
356 } else {
357 KIO::Job *kioJob = q->parentJob() ? q->parentJob() : q;
358 auto *askUserActionInterface = KIO::delegateExtension<KIO::AskUserActionInterface *>(kioJob);
359 if (!KProtocolManager::autoResume() && !(m_flags & Overwrite) && askUserActionInterface) {
360 auto renameSignal = &AskUserActionInterface::askUserRenameResult;
361
362 q->connect(askUserActionInterface, renameSignal, q, [=](KIO::RenameDialog_Result result, const QUrl &, KJob *askJob) {
363 Q_ASSERT(kioJob == askJob);
364
365 // Only receive askUserRenameResult once per rename dialog
366 QObject::disconnect(askUserActionInterface, renameSignal, q, nullptr);
367
368 processCanResumeResult(job, result, offset);
369 });
370
371 // Ask confirmation about resuming previous transfer
372 askUserActionInterface->askUserRename(kioJob,
373 i18n("File Already Exists"),
374 m_src,
375 m_dest,
376 RenameDialog_Options(RenameDialog_Overwrite | RenameDialog_Resume | RenameDialog_NoRename),
377 m_sourceSize,
378 offset);
379 return;
380 }
381 }
382
383 processCanResumeResult(job, //
384 Result_Resume, // The default is to resume
385 offset);
386
387 return;
388 }
389
390 qCWarning(KIO_CORE) << "unknown job=" << job << "m_getJob=" << m_getJob << "m_putJob=" << m_putJob;
391 }
392
processCanResumeResult(KIO::Job * job,RenameDialog_Result result,KIO::filesize_t offset)393 void FileCopyJobPrivate::processCanResumeResult(KIO::Job *job, RenameDialog_Result result, KIO::filesize_t offset)
394 {
395 Q_Q(FileCopyJob);
396 if (result == Result_Overwrite || (m_flags & Overwrite)) {
397 offset = 0;
398 } else if (result == Result_Cancel) {
399 if (job == m_putJob) {
400 m_putJob->kill(FileCopyJob::Quietly);
401 q->removeSubjob(m_putJob);
402 m_putJob = nullptr;
403 } else {
404 m_copyJob->kill(FileCopyJob::Quietly);
405 q->removeSubjob(m_copyJob);
406 m_copyJob = nullptr;
407 }
408 q->setError(ERR_USER_CANCELED);
409 q->emitResult();
410 return;
411 }
412
413 if (job == m_copyJob) {
414 jobSlave(m_copyJob)->sendResumeAnswer(offset != 0);
415 return;
416 }
417
418 if (job == m_putJob) {
419 m_getJob = KIO::get(m_src, NoReload, HideProgressInfo /* no GUI */);
420 m_getJob->setParentJob(q);
421 // qDebug() << "m_getJob=" << m_getJob << m_src;
422 m_getJob->addMetaData(QStringLiteral("errorPage"), QStringLiteral("false"));
423 m_getJob->addMetaData(QStringLiteral("AllowCompressedPage"), QStringLiteral("false"));
424 // Set size in subjob. This helps if the slave doesn't emit totalSize.
425 if (m_sourceSize != (KIO::filesize_t)-1) {
426 m_getJob->setTotalAmount(KJob::Bytes, m_sourceSize);
427 }
428
429 if (offset) {
430 // qDebug() << "Setting metadata for resume to" << (unsigned long) offset;
431 m_getJob->addMetaData(QStringLiteral("range-start"), KIO::number(offset));
432
433 // Might or might not get emitted
434 q->connect(m_getJob, &KIO::TransferJob::canResume, q, [this](KIO::Job *job, KIO::filesize_t offset) {
435 slotCanResume(job, offset);
436 });
437 }
438 jobSlave(m_putJob)->setOffset(offset);
439
440 m_putJob->d_func()->internalSuspend();
441 q->addSubjob(m_getJob);
442 connectSubjob(m_getJob); // Progress info depends on get
443 m_getJob->d_func()->internalResume(); // Order a beer
444
445 q->connect(m_getJob, &KIO::TransferJob::data, q, [this](KIO::Job *job, const QByteArray &data) {
446 slotData(job, data);
447 });
448 q->connect(m_getJob, &KIO::TransferJob::mimeTypeFound, q, [this](KIO::Job *job, const QString &type) {
449 slotMimetype(job, type);
450 });
451 }
452 }
453
slotData(KIO::Job *,const QByteArray & data)454 void FileCopyJobPrivate::slotData(KIO::Job *, const QByteArray &data)
455 {
456 // qDebug() << "data size:" << data.size();
457 Q_ASSERT(m_putJob);
458 if (!m_putJob) {
459 return; // Don't crash
460 }
461 m_getJob->d_func()->internalSuspend();
462 m_putJob->d_func()->internalResume(); // Drink the beer
463 m_buffer += data;
464
465 // On the first set of data incoming, we tell the "put" slave about our
466 // decision about resuming
467 if (!m_resumeAnswerSent) {
468 m_resumeAnswerSent = true;
469 // qDebug() << "(first time) -> send resume answer " << m_canResume;
470 jobSlave(m_putJob)->sendResumeAnswer(m_canResume);
471 }
472 }
473
slotDataReq(KIO::Job *,QByteArray & data)474 void FileCopyJobPrivate::slotDataReq(KIO::Job *, QByteArray &data)
475 {
476 Q_Q(FileCopyJob);
477 // qDebug();
478 if (!m_resumeAnswerSent && !m_getJob) {
479 // This can't happen
480 q->setError(ERR_INTERNAL);
481 q->setErrorText(QStringLiteral("'Put' job did not send canResume or 'Get' job did not send data!"));
482 m_putJob->kill(FileCopyJob::Quietly);
483 q->removeSubjob(m_putJob);
484 m_putJob = nullptr;
485 q->emitResult();
486 return;
487 }
488 if (m_getJob) {
489 m_getJob->d_func()->internalResume(); // Order more beer
490 m_putJob->d_func()->internalSuspend();
491 }
492 data = m_buffer;
493 m_buffer = QByteArray();
494 }
495
slotMimetype(KIO::Job *,const QString & type)496 void FileCopyJobPrivate::slotMimetype(KIO::Job *, const QString &type)
497 {
498 Q_Q(FileCopyJob);
499 #if KIOCORE_BUILD_DEPRECATED_SINCE(5, 78)
500 Q_EMIT q->mimetype(q, type);
501 #endif
502 Q_EMIT q->mimeTypeFound(q, type);
503 }
504
slotResult(KJob * job)505 void FileCopyJob::slotResult(KJob *job)
506 {
507 Q_D(FileCopyJob);
508 // qDebug() << "this=" << this << "job=" << job;
509 removeSubjob(job);
510
511 // If result comes from copyjob then we are not writing anymore.
512 if (job == d->m_copyJob) {
513 d->m_bFileCopyInProgress = false;
514 }
515
516 // Did job have an error ?
517 if (job->error()) {
518 if ((job == d->m_moveJob) && (job->error() == ERR_UNSUPPORTED_ACTION)) {
519 d->m_moveJob = nullptr;
520 d->startBestCopyMethod();
521 return;
522 } else if ((job == d->m_copyJob) && (job->error() == ERR_UNSUPPORTED_ACTION)) {
523 d->m_copyJob = nullptr;
524 d->startDataPump();
525 return;
526 } else if (job == d->m_getJob) {
527 d->m_getJob = nullptr;
528 if (d->m_putJob) {
529 d->m_putJob->kill(Quietly);
530 removeSubjob(d->m_putJob);
531 }
532 } else if (job == d->m_putJob) {
533 d->m_putJob = nullptr;
534 if (d->m_getJob) {
535 d->m_getJob->kill(Quietly);
536 removeSubjob(d->m_getJob);
537 }
538 } else if (job == d->m_chmodJob) {
539 d->m_chmodJob = nullptr;
540 if (d->m_delJob) {
541 d->m_delJob->kill(Quietly);
542 removeSubjob(d->m_delJob);
543 }
544 } else if (job == d->m_delJob) {
545 d->m_delJob = nullptr;
546 if (d->m_chmodJob) {
547 d->m_chmodJob->kill(Quietly);
548 removeSubjob(d->m_chmodJob);
549 }
550 }
551 setError(job->error());
552 setErrorText(job->errorText());
553 emitResult();
554 return;
555 }
556
557 if (d->m_mustChmod) {
558 // If d->m_permissions == -1, keep the default permissions
559 if (d->m_permissions != -1) {
560 d->m_chmodJob = chmod(d->m_dest, d->m_permissions);
561 addSubjob(d->m_chmodJob);
562 }
563 d->m_mustChmod = false;
564 }
565
566 if (job == d->m_moveJob) {
567 d->m_moveJob = nullptr; // Finished
568 }
569
570 if (job == d->m_copyJob) {
571 d->m_copyJob = nullptr;
572 if (d->m_move) {
573 d->m_delJob = file_delete(d->m_src, HideProgressInfo /*no GUI*/); // Delete source
574 addSubjob(d->m_delJob);
575 }
576 }
577
578 if (job == d->m_getJob) {
579 // qDebug() << "m_getJob finished";
580 d->m_getJob = nullptr; // No action required
581 if (d->m_putJob) {
582 d->m_putJob->d_func()->internalResume();
583 }
584 }
585
586 if (job == d->m_putJob) {
587 // qDebug() << "m_putJob finished";
588 d->m_putJob = nullptr;
589 if (d->m_getJob) {
590 // The get job is still running, probably after emitting data(QByteArray())
591 // and before we receive its finished().
592 d->m_getJob->d_func()->internalResume();
593 }
594 if (d->m_move) {
595 d->m_delJob = file_delete(d->m_src, HideProgressInfo /*no GUI*/); // Delete source
596 addSubjob(d->m_delJob);
597 }
598 }
599
600 if (job == d->m_delJob) {
601 d->m_delJob = nullptr; // Finished
602 }
603
604 if (job == d->m_chmodJob) {
605 d->m_chmodJob = nullptr; // Finished
606 }
607
608 if (!hasSubjobs()) {
609 emitResult();
610 }
611 }
612
doKill()613 bool FileCopyJob::doKill()
614 {
615 #ifdef Q_OS_WIN
616 // TODO Use SetConsoleCtrlHandler on Windows or similar behaviour.
617 // https://stackoverflow.com/questions/2007516/is-there-a-posix-sigterm-alternative-on-windows-a-gentle-kill-for-console-ap
618 // https://danielkaes.wordpress.com/2009/06/04/how-to-catch-kill-events-with-python/
619 // https://phabricator.kde.org/D25117#566107
620
621 Q_D(FileCopyJob);
622
623 // If we are interrupted in the middle of file copying,
624 // we may end up with corrupted file at the destination.
625 // It is better to clean up this file. If a copy is being
626 // made as part of move operation then delete the dest only if
627 // source file is intact (m_delJob == NULL).
628 if (d->m_bFileCopyInProgress && d->m_copyJob && d->m_dest.isLocalFile()) {
629 if (d->m_flags & Overwrite) {
630 QFile::remove(d->m_dest.toLocalFile() + QStringLiteral(".part"));
631 } else {
632 QFile::remove(d->m_dest.toLocalFile());
633 }
634 }
635 #endif
636 return Job::doKill();
637 }
638
file_copy(const QUrl & src,const QUrl & dest,int permissions,JobFlags flags)639 FileCopyJob *KIO::file_copy(const QUrl &src, const QUrl &dest, int permissions, JobFlags flags)
640 {
641 return FileCopyJobPrivate::newJob(src, dest, permissions, false, flags);
642 }
643
file_move(const QUrl & src,const QUrl & dest,int permissions,JobFlags flags)644 FileCopyJob *KIO::file_move(const QUrl &src, const QUrl &dest, int permissions, JobFlags flags)
645 {
646 FileCopyJob *job = FileCopyJobPrivate::newJob(src, dest, permissions, true, flags);
647 if (job->uiDelegateExtension()) {
648 job->uiDelegateExtension()->createClipboardUpdater(job, JobUiDelegateExtension::UpdateContent);
649 }
650 return job;
651 }
652
653 #include "moc_filecopyjob.cpp"
654