File indexing completed on 2024-10-06 06:41:01
0001 /* 0002 This file is part of the KDE libraries 0003 SPDX-FileCopyrightText: 2000 Stephan Kulow <coolo@kde.org> 0004 SPDX-FileCopyrightText: 2000-2006 David Faure <faure@kde.org> 0005 SPDX-FileCopyrightText: 2000 Waldo Bastian <bastian@kde.org> 0006 SPDX-FileCopyrightText: 2021 Ahmad Samir <a.samirh78@gmail.com> 0007 0008 SPDX-License-Identifier: LGPL-2.0-or-later 0009 */ 0010 0011 #include "copyjob.h" 0012 #include "../utils_p.h" 0013 #include "deletejob.h" 0014 #include "filecopyjob.h" 0015 #include "global.h" 0016 #include "job.h" // buildErrorString 0017 #include "kcoredirlister.h" 0018 #include "kfileitem.h" 0019 #include "kiocoredebug.h" 0020 #include "kioglobal_p.h" 0021 #include "listjob.h" 0022 #include "mkdirjob.h" 0023 #include "statjob.h" 0024 #include <cerrno> 0025 0026 #include <KConfigGroup> 0027 #include <KDesktopFile> 0028 #include <KLocalizedString> 0029 0030 #include "kprotocolmanager.h" 0031 #include "worker_p.h" 0032 #include <KDirWatch> 0033 0034 #include "askuseractioninterface.h" 0035 #include <jobuidelegateextension.h> 0036 #include <kio/jobuidelegatefactory.h> 0037 0038 #include <kdirnotify.h> 0039 0040 #ifdef Q_OS_UNIX 0041 #include <utime.h> 0042 #endif 0043 0044 #include <QDateTime> 0045 #include <QFile> 0046 #include <QFileInfo> 0047 #include <QPointer> 0048 #include <QTemporaryFile> 0049 #include <QTimeZone> 0050 #include <QTimer> 0051 0052 #include <sys/stat.h> // mode_t 0053 0054 #include "job_p.h" 0055 #include <KFileSystemType> 0056 #include <KFileUtils> 0057 #include <KIO/FileSystemFreeSpaceJob> 0058 0059 #include <list> 0060 #include <set> 0061 0062 #include <QLoggingCategory> 0063 Q_DECLARE_LOGGING_CATEGORY(KIO_COPYJOB_DEBUG) 0064 Q_LOGGING_CATEGORY(KIO_COPYJOB_DEBUG, "kf.kio.core.copyjob", QtWarningMsg) 0065 0066 using namespace KIO; 0067 0068 // this will update the report dialog with 5 Hz, I think this is fast enough, aleXXX 0069 static constexpr int s_reportTimeout = 200; 0070 0071 #if !defined(NAME_MAX) 0072 #if defined(_MAX_FNAME) 0073 static constexpr int NAME_MAX = _MAX_FNAME; // For Windows 0074 #else 0075 static constexpr NAME_MAX = 0; 0076 #endif 0077 #endif 0078 0079 enum DestinationState { 0080 DEST_NOT_STATED, 0081 DEST_IS_DIR, 0082 DEST_IS_FILE, 0083 DEST_DOESNT_EXIST, 0084 }; 0085 0086 /** 0087 * States: 0088 * STATE_INITIAL the constructor was called 0089 * STATE_STATING for the dest 0090 * statCurrentSrc then does, for each src url: 0091 * STATE_RENAMING if direct rename looks possible 0092 * (on already exists, and user chooses rename, TODO: go to STATE_RENAMING again) 0093 * STATE_STATING 0094 * and then, if dir -> STATE_LISTING (filling 'd->dirs' and 'd->files') 0095 * STATE_CREATING_DIRS (createNextDir, iterating over 'd->dirs') 0096 * if conflict: STATE_CONFLICT_CREATING_DIRS 0097 * STATE_COPYING_FILES (copyNextFile, iterating over 'd->files') 0098 * if conflict: STATE_CONFLICT_COPYING_FILES 0099 * STATE_DELETING_DIRS (deleteNextDir) (if moving) 0100 * STATE_SETTING_DIR_ATTRIBUTES (setNextDirAttribute, iterating over d->m_directoriesCopied) 0101 * done. 0102 */ 0103 enum CopyJobState { 0104 STATE_INITIAL, 0105 STATE_STATING, 0106 STATE_RENAMING, 0107 STATE_LISTING, 0108 STATE_CREATING_DIRS, 0109 STATE_CONFLICT_CREATING_DIRS, 0110 STATE_COPYING_FILES, 0111 STATE_CONFLICT_COPYING_FILES, 0112 STATE_DELETING_DIRS, 0113 STATE_SETTING_DIR_ATTRIBUTES, 0114 }; 0115 0116 static QUrl addPathToUrl(const QUrl &url, const QString &relPath) 0117 { 0118 QUrl u(url); 0119 u.setPath(Utils::concatPaths(url.path(), relPath)); 0120 return u; 0121 } 0122 0123 static bool compareUrls(const QUrl &srcUrl, const QUrl &destUrl) 0124 { 0125 /* clang-format off */ 0126 return srcUrl.scheme() == destUrl.scheme() 0127 && srcUrl.host() == destUrl.host() 0128 && srcUrl.port() == destUrl.port() 0129 && srcUrl.userName() == destUrl.userName() 0130 && srcUrl.password() == destUrl.password(); 0131 /* clang-format on */ 0132 } 0133 0134 // https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file#naming-conventions 0135 static const char s_msdosInvalidChars[] = R"(<>:"/\|?*)"; 0136 0137 static bool hasInvalidChars(const QString &dest) 0138 { 0139 return std::any_of(std::begin(s_msdosInvalidChars), std::end(s_msdosInvalidChars), [=](const char c) { 0140 return dest.contains(QLatin1Char(c)); 0141 }); 0142 } 0143 0144 static void cleanMsdosDestName(QString &name) 0145 { 0146 for (const char c : s_msdosInvalidChars) { 0147 name.replace(QLatin1Char(c), QLatin1String("_")); 0148 } 0149 } 0150 0151 static bool isFatFs(KFileSystemType::Type fsType) 0152 { 0153 return fsType == KFileSystemType::Fat || fsType == KFileSystemType::Exfat; 0154 } 0155 0156 static bool isFatOrNtfs(KFileSystemType::Type fsType) 0157 { 0158 return fsType == KFileSystemType::Ntfs || isFatFs(fsType); 0159 } 0160 0161 static QString symlinkSupportMsg(const QString &path, const QString &fsName) 0162 { 0163 const QString msg = i18nc( 0164 "The first arg is the path to the symlink that couldn't be created, the second" 0165 "arg is the filesystem type (e.g. vfat, exfat)", 0166 "Could not create symlink \"%1\".\n" 0167 "The destination filesystem (%2) doesn't support symlinks.", 0168 path, 0169 fsName); 0170 return msg; 0171 } 0172 0173 static QString invalidCharsSupportMsg(const QString &path, const QString &fsName, bool isDir = false) 0174 { 0175 QString msg; 0176 if (isDir) { 0177 msg = i18n( 0178 "Could not create \"%1\".\n" 0179 "The destination filesystem (%2) disallows the following characters in folder names: %3\n" 0180 "Selecting Replace will replace any invalid characters (in the destination folder name) with an underscore \"_\".", 0181 path, 0182 fsName, 0183 QLatin1String(s_msdosInvalidChars)); 0184 } else { 0185 msg = i18n( 0186 "Could not create \"%1\".\n" 0187 "The destination filesystem (%2) disallows the following characters in file names: %3\n" 0188 "Selecting Replace will replace any invalid characters (in the destination file name) with an underscore \"_\".", 0189 path, 0190 fsName, 0191 QLatin1String(s_msdosInvalidChars)); 0192 } 0193 0194 return msg; 0195 } 0196 0197 /** @internal */ 0198 struct CopyInfo { 0199 QUrl uSource; 0200 QUrl uDest; 0201 QString linkDest; // for symlinks only 0202 int permissions; 0203 QDateTime ctime; 0204 QDateTime mtime; 0205 KIO::filesize_t size; // 0 for dirs 0206 }; 0207 0208 /** @internal */ 0209 class KIO::CopyJobPrivate : public KIO::JobPrivate 0210 { 0211 public: 0212 CopyJobPrivate(const QList<QUrl> &src, const QUrl &dest, CopyJob::CopyMode mode, bool asMethod) 0213 : m_globalDest(dest) 0214 , m_globalDestinationState(DEST_NOT_STATED) 0215 , m_defaultPermissions(false) 0216 , m_bURLDirty(false) 0217 , m_mode(mode) 0218 , m_asMethod(asMethod) 0219 , destinationState(DEST_NOT_STATED) 0220 , state(STATE_INITIAL) 0221 , m_freeSpace(-1) 0222 , m_totalSize(0) 0223 , m_processedSize(0) 0224 , m_fileProcessedSize(0) 0225 , m_filesHandledByDirectRename(0) 0226 , m_processedFiles(0) 0227 , m_processedDirs(0) 0228 , m_srcList(src) 0229 , m_currentStatSrc(m_srcList.constBegin()) 0230 , m_bCurrentOperationIsLink(false) 0231 , m_bSingleFileCopy(false) 0232 , m_bOnlyRenames(mode == CopyJob::Move) 0233 , m_dest(dest) 0234 , m_bAutoRenameFiles(false) 0235 , m_bAutoRenameDirs(false) 0236 , m_bAutoSkipFiles(false) 0237 , m_bAutoSkipDirs(false) 0238 , m_bOverwriteAllFiles(false) 0239 , m_bOverwriteAllDirs(false) 0240 , m_bOverwriteWhenOlder(false) 0241 , m_conflictError(0) 0242 , m_reportTimer(nullptr) 0243 { 0244 } 0245 0246 // This is the dest URL that was initially given to CopyJob 0247 // It is copied into m_dest, which can be changed for a given src URL 0248 // (when using the RENAME dialog in slotResult), 0249 // and which will be reset for the next src URL. 0250 QUrl m_globalDest; 0251 // The state info about that global dest 0252 DestinationState m_globalDestinationState; 0253 // See setDefaultPermissions 0254 bool m_defaultPermissions; 0255 // Whether URLs changed (and need to be emitted by the next slotReport call) 0256 bool m_bURLDirty; 0257 // Used after copying all the files into the dirs, to set mtime (TODO: and permissions?) 0258 // after the copy is done 0259 std::list<CopyInfo> m_directoriesCopied; 0260 std::list<CopyInfo>::const_iterator m_directoriesCopiedIterator; 0261 0262 CopyJob::CopyMode m_mode; 0263 bool m_asMethod; // See copyAs() method 0264 DestinationState destinationState; 0265 CopyJobState state; 0266 0267 KIO::filesize_t m_freeSpace; 0268 0269 KIO::filesize_t m_totalSize; 0270 KIO::filesize_t m_processedSize; 0271 KIO::filesize_t m_fileProcessedSize; 0272 int m_filesHandledByDirectRename; 0273 int m_processedFiles; 0274 int m_processedDirs; 0275 QList<CopyInfo> files; 0276 QList<CopyInfo> dirs; 0277 // List of dirs that will be copied then deleted when CopyMode is Move 0278 QList<QUrl> dirsToRemove; 0279 QList<QUrl> m_srcList; 0280 QList<QUrl> m_successSrcList; // Entries in m_srcList that have successfully been moved 0281 QList<QUrl>::const_iterator m_currentStatSrc; 0282 bool m_bCurrentSrcIsDir; 0283 bool m_bCurrentOperationIsLink; 0284 bool m_bSingleFileCopy; 0285 bool m_bOnlyRenames; 0286 QUrl m_dest; 0287 QUrl m_currentDest; // set during listing, used by slotEntries 0288 // 0289 QStringList m_skipList; 0290 QSet<QString> m_overwriteList; 0291 bool m_bAutoRenameFiles; 0292 bool m_bAutoRenameDirs; 0293 bool m_bAutoSkipFiles; 0294 bool m_bAutoSkipDirs; 0295 bool m_bOverwriteAllFiles; 0296 bool m_bOverwriteAllDirs; 0297 bool m_bOverwriteWhenOlder; 0298 0299 bool m_autoSkipDirsWithInvalidChars = false; 0300 bool m_autoSkipFilesWithInvalidChars = false; 0301 bool m_autoReplaceInvalidChars = false; 0302 0303 bool m_autoSkipFatSymlinks = false; 0304 0305 enum SkipType { 0306 // No skip dialog is involved 0307 NoSkipType = 0, 0308 // SkipDialog is asking about invalid chars in destination file/dir names 0309 SkipInvalidChars, 0310 // SkipDialog is asking about how to handle symlinks why copying to a 0311 // filesystem that doesn't support symlinks 0312 SkipFatSymlinks, 0313 }; 0314 0315 int m_conflictError; 0316 0317 QTimer *m_reportTimer; 0318 0319 // The current src url being stat'ed or copied 0320 // During the stat phase, this is initially equal to *m_currentStatSrc but it can be resolved to a local file equivalent (#188903). 0321 QUrl m_currentSrcURL; 0322 QUrl m_currentDestURL; 0323 0324 std::set<QString> m_parentDirs; 0325 bool m_ignoreSourcePermissions = false; 0326 0327 void statCurrentSrc(); 0328 void statNextSrc(); 0329 0330 // Those aren't slots but submethods for slotResult. 0331 void slotResultStating(KJob *job); 0332 void startListing(const QUrl &src); 0333 0334 void slotResultCreatingDirs(KJob *job); 0335 void slotResultConflictCreatingDirs(KJob *job); 0336 void createNextDir(); 0337 void processCreateNextDir(const QList<CopyInfo>::Iterator &it, int result); 0338 0339 void slotResultCopyingFiles(KJob *job); 0340 void slotResultErrorCopyingFiles(KJob *job); 0341 void processFileRenameDialogResult(const QList<CopyInfo>::Iterator &it, RenameDialog_Result result, const QUrl &newUrl, const QDateTime &destmtime); 0342 0343 // KIO::Job* linkNextFile( const QUrl& uSource, const QUrl& uDest, bool overwrite ); 0344 KIO::Job *linkNextFile(const QUrl &uSource, const QUrl &uDest, JobFlags flags); 0345 // MsDos filesystems don't allow certain characters in filenames, and VFAT and ExFAT 0346 // don't support symlinks, this method detects those conditions and tries to handle it 0347 bool handleMsdosFsQuirks(QList<CopyInfo>::Iterator it, KFileSystemType::Type fsType); 0348 void copyNextFile(); 0349 void processCopyNextFile(const QList<CopyInfo>::Iterator &it, int result, SkipType skipType); 0350 0351 void slotResultDeletingDirs(KJob *job); 0352 void deleteNextDir(); 0353 void sourceStated(const UDSEntry &entry, const QUrl &sourceUrl); 0354 // Removes a dir from the "dirsToRemove" list 0355 void skip(const QUrl &sourceURL, bool isDir); 0356 0357 void slotResultRenaming(KJob *job); 0358 void directRenamingFailed(const QUrl &dest); 0359 void processDirectRenamingConflictResult(RenameDialog_Result result, 0360 bool srcIsDir, 0361 bool destIsDir, 0362 const QDateTime &mtimeSrc, 0363 const QDateTime &mtimeDest, 0364 const QUrl &dest, 0365 const QUrl &newUrl); 0366 0367 void slotResultSettingDirAttributes(KJob *job); 0368 void setNextDirAttribute(); 0369 0370 void startRenameJob(const QUrl &workerUrl); 0371 bool shouldOverwriteDir(const QString &path) const; 0372 bool shouldOverwriteFile(const QString &path) const; 0373 bool shouldSkip(const QString &path) const; 0374 void skipSrc(bool isDir); 0375 void renameDirectory(const QList<CopyInfo>::iterator &it, const QUrl &newUrl); 0376 QUrl finalDestUrl(const QUrl &src, const QUrl &dest) const; 0377 0378 void slotStart(); 0379 void slotEntries(KIO::Job *, const KIO::UDSEntryList &list); 0380 void slotSubError(KIO::ListJob *job, KIO::ListJob *subJob); 0381 void addCopyInfoFromUDSEntry(const UDSEntry &entry, const QUrl &srcUrl, bool srcIsDir, const QUrl ¤tDest); 0382 /** 0383 * Forward signal from subjob 0384 */ 0385 void slotProcessedSize(KJob *, qulonglong data_size); 0386 /** 0387 * Forward signal from subjob 0388 * @param size the total size 0389 */ 0390 void slotTotalSize(KJob *, qulonglong size); 0391 0392 void slotReport(); 0393 0394 Q_DECLARE_PUBLIC(CopyJob) 0395 0396 static inline CopyJob *newJob(const QList<QUrl> &src, const QUrl &dest, CopyJob::CopyMode mode, bool asMethod, JobFlags flags) 0397 { 0398 CopyJob *job = new CopyJob(*new CopyJobPrivate(src, dest, mode, asMethod)); 0399 job->setUiDelegate(KIO::createDefaultJobUiDelegate()); 0400 if (!(flags & HideProgressInfo)) { 0401 KIO::getJobTracker()->registerJob(job); 0402 } 0403 if (flags & KIO::Overwrite) { 0404 job->d_func()->m_bOverwriteAllDirs = true; 0405 job->d_func()->m_bOverwriteAllFiles = true; 0406 } 0407 if (!(flags & KIO::NoPrivilegeExecution)) { 0408 job->d_func()->m_privilegeExecutionEnabled = true; 0409 FileOperationType copyType; 0410 switch (mode) { 0411 case CopyJob::Copy: 0412 copyType = Copy; 0413 break; 0414 case CopyJob::Move: 0415 copyType = Move; 0416 break; 0417 case CopyJob::Link: 0418 copyType = Symlink; 0419 break; 0420 default: 0421 Q_UNREACHABLE(); 0422 } 0423 job->d_func()->m_operationType = copyType; 0424 } 0425 return job; 0426 } 0427 }; 0428 0429 CopyJob::CopyJob(CopyJobPrivate &dd) 0430 : Job(dd) 0431 { 0432 Q_D(CopyJob); 0433 setProperty("destUrl", d_func()->m_dest.toString()); 0434 QTimer::singleShot(0, this, [d]() { 0435 d->slotStart(); 0436 }); 0437 qRegisterMetaType<KIO::UDSEntry>(); 0438 } 0439 0440 CopyJob::~CopyJob() 0441 { 0442 } 0443 0444 QList<QUrl> CopyJob::srcUrls() const 0445 { 0446 return d_func()->m_srcList; 0447 } 0448 0449 QUrl CopyJob::destUrl() const 0450 { 0451 return d_func()->m_dest; 0452 } 0453 0454 void CopyJobPrivate::slotStart() 0455 { 0456 Q_Q(CopyJob); 0457 if (q->isSuspended()) { 0458 return; 0459 } 0460 0461 if (m_mode == CopyJob::CopyMode::Move) { 0462 for (const QUrl &url : std::as_const(m_srcList)) { 0463 if (m_dest.scheme() == url.scheme() && m_dest.host() == url.host()) { 0464 const QString srcPath = Utils::slashAppended(url.path()); 0465 if (m_dest.path().startsWith(srcPath)) { 0466 q->setError(KIO::ERR_CANNOT_MOVE_INTO_ITSELF); 0467 q->emitResult(); 0468 return; 0469 } 0470 } 0471 } 0472 } 0473 0474 if (m_mode == CopyJob::CopyMode::Link && m_globalDest.isLocalFile()) { 0475 const QString destPath = m_globalDest.toLocalFile(); 0476 const auto destFs = KFileSystemType::fileSystemType(destPath); 0477 if (isFatFs(destFs)) { 0478 q->setError(ERR_SYMLINKS_NOT_SUPPORTED); 0479 const QString errText = destPath + QLatin1String(" [") + KFileSystemType::fileSystemName(destFs) + QLatin1Char(']'); 0480 q->setErrorText(errText); 0481 q->emitResult(); 0482 return; 0483 } 0484 } 0485 0486 /** 0487 We call the functions directly instead of using signals. 0488 Calling a function via a signal takes approx. 65 times the time 0489 compared to calling it directly (at least on my machine). aleXXX 0490 */ 0491 m_reportTimer = new QTimer(q); 0492 0493 q->connect(m_reportTimer, &QTimer::timeout, q, [this]() { 0494 slotReport(); 0495 }); 0496 m_reportTimer->start(s_reportTimeout); 0497 0498 // Stat the dest 0499 state = STATE_STATING; 0500 const QUrl dest = m_asMethod ? m_dest.adjusted(QUrl::RemoveFilename) : m_dest; 0501 // We need isDir() and UDS_LOCAL_PATH (for workers who set it). Let's assume the latter is part of StatBasic too. 0502 KIO::Job *job = KIO::stat(dest, StatJob::DestinationSide, KIO::StatBasic | KIO::StatResolveSymlink, KIO::HideProgressInfo); 0503 qCDebug(KIO_COPYJOB_DEBUG) << "CopyJob: stating the dest" << dest; 0504 q->addSubjob(job); 0505 } 0506 0507 // For unit test purposes 0508 KIOCORE_EXPORT bool kio_resolve_local_urls = true; 0509 0510 void CopyJobPrivate::slotResultStating(KJob *job) 0511 { 0512 Q_Q(CopyJob); 0513 qCDebug(KIO_COPYJOB_DEBUG); 0514 // Was there an error while stating the src ? 0515 if (job->error() && destinationState != DEST_NOT_STATED) { 0516 const QUrl srcurl = static_cast<SimpleJob *>(job)->url(); 0517 if (!srcurl.isLocalFile()) { 0518 // Probably : src doesn't exist. Well, over some protocols (e.g. FTP) 0519 // this info isn't really reliable (thanks to MS FTP servers). 0520 // We'll assume a file, and try to download anyway. 0521 qCDebug(KIO_COPYJOB_DEBUG) << "Error while stating source. Activating hack"; 0522 q->removeSubjob(job); 0523 Q_ASSERT(!q->hasSubjobs()); // We should have only one job at a time ... 0524 struct CopyInfo info; 0525 info.permissions = (mode_t)-1; 0526 info.size = KIO::invalidFilesize; 0527 info.uSource = srcurl; 0528 info.uDest = m_dest; 0529 // Append filename or dirname to destination URL, if allowed 0530 if (destinationState == DEST_IS_DIR && !m_asMethod) { 0531 const QString fileName = srcurl.scheme() == QLatin1String("data") ? QStringLiteral("data") : srcurl.fileName(); // #379093 0532 info.uDest = addPathToUrl(info.uDest, fileName); 0533 } 0534 0535 files.append(info); 0536 statNextSrc(); 0537 return; 0538 } 0539 // Local file. If stat fails, the file definitely doesn't exist. 0540 // yes, q->Job::, because we don't want to call our override 0541 q->Job::slotResult(job); // will set the error and emit result(this) 0542 return; 0543 } 0544 0545 // Keep copy of the stat result 0546 auto statJob = static_cast<StatJob *>(job); 0547 const UDSEntry entry = statJob->statResult(); 0548 0549 if (destinationState == DEST_NOT_STATED) { 0550 const bool isGlobalDest = m_dest == m_globalDest; 0551 0552 // we were stating the dest 0553 if (job->error()) { 0554 destinationState = DEST_DOESNT_EXIST; 0555 qCDebug(KIO_COPYJOB_DEBUG) << "dest does not exist"; 0556 } else { 0557 const bool isDir = entry.isDir(); 0558 0559 // Check for writability, before spending time stat'ing everything (#141564). 0560 // This assumes all KIO workers set permissions correctly... 0561 const int permissions = entry.numberValue(KIO::UDSEntry::UDS_ACCESS, -1); 0562 const bool isWritable = (permissions != -1) && (permissions & S_IWUSR); 0563 if (!m_privilegeExecutionEnabled && !isWritable) { 0564 const QUrl dest = m_asMethod ? m_dest.adjusted(QUrl::RemoveFilename) : m_dest; 0565 q->setError(ERR_WRITE_ACCESS_DENIED); 0566 q->setErrorText(dest.toDisplayString(QUrl::PreferLocalFile)); 0567 q->emitResult(); 0568 return; 0569 } 0570 0571 // Treat symlinks to dirs as dirs here, so no test on isLink 0572 destinationState = isDir ? DEST_IS_DIR : DEST_IS_FILE; 0573 qCDebug(KIO_COPYJOB_DEBUG) << "dest is dir:" << isDir; 0574 0575 if (isGlobalDest) { 0576 m_globalDestinationState = destinationState; 0577 } 0578 0579 const QString sLocalPath = entry.stringValue(KIO::UDSEntry::UDS_LOCAL_PATH); 0580 if (!sLocalPath.isEmpty() && kio_resolve_local_urls && statJob->url().scheme() != QStringLiteral("trash")) { 0581 const QString fileName = m_dest.fileName(); 0582 m_dest = QUrl::fromLocalFile(sLocalPath); 0583 if (m_asMethod) { 0584 m_dest = addPathToUrl(m_dest, fileName); 0585 } 0586 qCDebug(KIO_COPYJOB_DEBUG) << "Setting m_dest to the local path:" << sLocalPath; 0587 if (isGlobalDest) { 0588 m_globalDest = m_dest; 0589 } 0590 } 0591 } 0592 0593 q->removeSubjob(job); 0594 Q_ASSERT(!q->hasSubjobs()); 0595 0596 // In copy-as mode, we want to check the directory to which we're 0597 // copying. The target file or directory does not exist yet, which 0598 // might confuse FileSystemFreeSpaceJob. 0599 const QUrl existingDest = m_asMethod ? m_dest.adjusted(QUrl::RemoveFilename) : m_dest; 0600 KIO::FileSystemFreeSpaceJob *spaceJob = KIO::fileSystemFreeSpace(existingDest); 0601 q->connect(spaceJob, &KJob::result, q, [this, existingDest, spaceJob]() { 0602 if (!spaceJob->error()) { 0603 m_freeSpace = spaceJob->availableSize(); 0604 } else { 0605 qCDebug(KIO_COPYJOB_DEBUG) << "Couldn't determine free space information for" << existingDest; 0606 } 0607 // After knowing what the dest is, we can start stat'ing the first src. 0608 statCurrentSrc(); 0609 }); 0610 return; 0611 } else { 0612 sourceStated(entry, static_cast<SimpleJob *>(job)->url()); 0613 q->removeSubjob(job); 0614 } 0615 } 0616 0617 void CopyJobPrivate::sourceStated(const UDSEntry &entry, const QUrl &sourceUrl) 0618 { 0619 const QString sLocalPath = sourceUrl.scheme() != QStringLiteral("trash") ? entry.stringValue(KIO::UDSEntry::UDS_LOCAL_PATH) : QString(); 0620 const bool isDir = entry.isDir(); 0621 0622 // We were stating the current source URL 0623 // Is it a file or a dir ? 0624 0625 // There 6 cases, and all end up calling addCopyInfoFromUDSEntry first : 0626 // 1 - src is a dir, destination is a directory, 0627 // slotEntries will append the source-dir-name to the destination 0628 // 2 - src is a dir, destination is a file -- will offer to overwrite, later on. 0629 // 3 - src is a dir, destination doesn't exist, then it's the destination dirname, 0630 // so slotEntries will use it as destination. 0631 0632 // 4 - src is a file, destination is a directory, 0633 // slotEntries will append the filename to the destination. 0634 // 5 - src is a file, destination is a file, m_dest is the exact destination name 0635 // 6 - src is a file, destination doesn't exist, m_dest is the exact destination name 0636 0637 QUrl srcurl; 0638 if (!sLocalPath.isEmpty() && destinationState != DEST_DOESNT_EXIST) { 0639 qCDebug(KIO_COPYJOB_DEBUG) << "Using sLocalPath. destinationState=" << destinationState; 0640 // Prefer the local path -- but only if we were able to stat() the dest. 0641 // Otherwise, renaming a desktop:/ url would copy from src=file to dest=desktop (#218719) 0642 srcurl = QUrl::fromLocalFile(sLocalPath); 0643 } else { 0644 srcurl = sourceUrl; 0645 } 0646 addCopyInfoFromUDSEntry(entry, srcurl, false, m_dest); 0647 0648 m_currentDest = m_dest; 0649 m_bCurrentSrcIsDir = false; 0650 0651 if (isDir // 0652 && !entry.isLink() // treat symlinks as files (no recursion) 0653 && m_mode != CopyJob::Link) { // No recursion in Link mode either. 0654 qCDebug(KIO_COPYJOB_DEBUG) << "Source is a directory"; 0655 0656 if (srcurl.isLocalFile()) { 0657 const QString parentDir = srcurl.adjusted(QUrl::StripTrailingSlash).toLocalFile(); 0658 m_parentDirs.insert(parentDir); 0659 } 0660 0661 m_bCurrentSrcIsDir = true; // used by slotEntries 0662 if (destinationState == DEST_IS_DIR) { // (case 1) 0663 if (!m_asMethod) { 0664 // Use <desturl>/<directory_copied> as destination, from now on 0665 QString directory = srcurl.fileName(); 0666 const QString sName = entry.stringValue(KIO::UDSEntry::UDS_NAME); 0667 KProtocolInfo::FileNameUsedForCopying fnu = KProtocolManager::fileNameUsedForCopying(srcurl); 0668 if (fnu == KProtocolInfo::Name) { 0669 if (!sName.isEmpty()) { 0670 directory = sName; 0671 } 0672 } else if (fnu == KProtocolInfo::DisplayName) { 0673 const QString dispName = entry.stringValue(KIO::UDSEntry::UDS_DISPLAY_NAME); 0674 if (!dispName.isEmpty()) { 0675 directory = dispName; 0676 } else if (!sName.isEmpty()) { 0677 directory = sName; 0678 } 0679 } 0680 m_currentDest = addPathToUrl(m_currentDest, directory); 0681 } 0682 } else { // (case 3) 0683 // otherwise dest is new name for toplevel dir 0684 // so the destination exists, in fact, from now on. 0685 // (This even works with other src urls in the list, since the 0686 // dir has effectively been created) 0687 destinationState = DEST_IS_DIR; 0688 if (m_dest == m_globalDest) { 0689 m_globalDestinationState = destinationState; 0690 } 0691 } 0692 0693 startListing(srcurl); 0694 } else { 0695 qCDebug(KIO_COPYJOB_DEBUG) << "Source is a file (or a symlink), or we are linking -> no recursive listing"; 0696 0697 if (srcurl.isLocalFile()) { 0698 const QString parentDir = srcurl.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).path(); 0699 m_parentDirs.insert(parentDir); 0700 } 0701 0702 statNextSrc(); 0703 } 0704 } 0705 0706 bool CopyJob::doSuspend() 0707 { 0708 Q_D(CopyJob); 0709 d->slotReport(); 0710 return Job::doSuspend(); 0711 } 0712 0713 bool CopyJob::doResume() 0714 { 0715 Q_D(CopyJob); 0716 switch (d->state) { 0717 case STATE_INITIAL: 0718 QTimer::singleShot(0, this, [d]() { 0719 d->slotStart(); 0720 }); 0721 break; 0722 default: 0723 // not implemented 0724 break; 0725 } 0726 return Job::doResume(); 0727 } 0728 0729 void CopyJobPrivate::slotReport() 0730 { 0731 Q_Q(CopyJob); 0732 if (q->isSuspended()) { 0733 return; 0734 } 0735 0736 // If showProgressInfo was set, progressId() is > 0. 0737 switch (state) { 0738 case STATE_RENAMING: 0739 if (m_bURLDirty) { 0740 m_bURLDirty = false; 0741 Q_ASSERT(m_mode == CopyJob::Move); 0742 emitMoving(q, m_currentSrcURL, m_currentDestURL); 0743 Q_EMIT q->moving(q, m_currentSrcURL, m_currentDestURL); 0744 } 0745 // "N" files renamed shouldn't include skipped files 0746 q->setProcessedAmount(KJob::Files, m_processedFiles); 0747 // % value should include skipped files 0748 q->emitPercent(m_filesHandledByDirectRename, q->totalAmount(KJob::Files)); 0749 break; 0750 0751 case STATE_COPYING_FILES: 0752 q->setProcessedAmount(KJob::Files, m_processedFiles); 0753 q->setProcessedAmount(KJob::Bytes, m_processedSize + m_fileProcessedSize); 0754 if (m_bURLDirty) { 0755 // Only emit urls when they changed. This saves time, and fixes #66281 0756 m_bURLDirty = false; 0757 if (m_mode == CopyJob::Move) { 0758 emitMoving(q, m_currentSrcURL, m_currentDestURL); 0759 Q_EMIT q->moving(q, m_currentSrcURL, m_currentDestURL); 0760 } else if (m_mode == CopyJob::Link) { 0761 emitCopying(q, m_currentSrcURL, m_currentDestURL); // we don't have a delegate->linking 0762 Q_EMIT q->linking(q, m_currentSrcURL.path(), m_currentDestURL); 0763 } else { 0764 emitCopying(q, m_currentSrcURL, m_currentDestURL); 0765 Q_EMIT q->copying(q, m_currentSrcURL, m_currentDestURL); 0766 } 0767 } 0768 break; 0769 0770 case STATE_CREATING_DIRS: 0771 q->setProcessedAmount(KJob::Directories, m_processedDirs); 0772 if (m_bURLDirty) { 0773 m_bURLDirty = false; 0774 Q_EMIT q->creatingDir(q, m_currentDestURL); 0775 emitCreatingDir(q, m_currentDestURL); 0776 } 0777 break; 0778 0779 case STATE_STATING: 0780 case STATE_LISTING: 0781 if (m_bURLDirty) { 0782 m_bURLDirty = false; 0783 if (m_mode == CopyJob::Move) { 0784 emitMoving(q, m_currentSrcURL, m_currentDestURL); 0785 } else { 0786 emitCopying(q, m_currentSrcURL, m_currentDestURL); 0787 } 0788 } 0789 q->setProgressUnit(KJob::Bytes); 0790 q->setTotalAmount(KJob::Bytes, m_totalSize); 0791 q->setTotalAmount(KJob::Files, files.count() + m_filesHandledByDirectRename); 0792 q->setTotalAmount(KJob::Directories, dirs.count()); 0793 break; 0794 0795 default: 0796 break; 0797 } 0798 } 0799 0800 void CopyJobPrivate::slotEntries(KIO::Job *job, const UDSEntryList &list) 0801 { 0802 // Q_Q(CopyJob); 0803 UDSEntryList::ConstIterator it = list.constBegin(); 0804 UDSEntryList::ConstIterator end = list.constEnd(); 0805 for (; it != end; ++it) { 0806 const UDSEntry &entry = *it; 0807 addCopyInfoFromUDSEntry(entry, static_cast<SimpleJob *>(job)->url(), m_bCurrentSrcIsDir, m_currentDest); 0808 } 0809 } 0810 0811 void CopyJobPrivate::slotSubError(ListJob *job, ListJob *subJob) 0812 { 0813 const QUrl &url = subJob->url(); 0814 qCWarning(KIO_CORE) << url << subJob->errorString(); 0815 0816 Q_Q(CopyJob); 0817 0818 Q_EMIT q->warning(job, subJob->errorString()); 0819 skip(url, true); 0820 } 0821 0822 void CopyJobPrivate::addCopyInfoFromUDSEntry(const UDSEntry &entry, const QUrl &srcUrl, bool srcIsDir, const QUrl ¤tDest) 0823 { 0824 struct CopyInfo info; 0825 info.permissions = entry.numberValue(KIO::UDSEntry::UDS_ACCESS, -1); 0826 const auto timeVal = entry.numberValue(KIO::UDSEntry::UDS_MODIFICATION_TIME, -1); 0827 if (timeVal != -1) { 0828 info.mtime = QDateTime::fromSecsSinceEpoch(timeVal, QTimeZone::UTC); 0829 } 0830 info.ctime = QDateTime::fromSecsSinceEpoch(entry.numberValue(KIO::UDSEntry::UDS_CREATION_TIME, -1), QTimeZone::UTC); 0831 info.size = static_cast<KIO::filesize_t>(entry.numberValue(KIO::UDSEntry::UDS_SIZE, -1)); 0832 const bool isDir = entry.isDir(); 0833 0834 if (!isDir && info.size != KIO::invalidFilesize) { 0835 m_totalSize += info.size; 0836 } 0837 0838 // recursive listing, displayName can be a/b/c/d 0839 const QString fileName = entry.stringValue(KIO::UDSEntry::UDS_NAME); 0840 const QString urlStr = entry.stringValue(KIO::UDSEntry::UDS_URL); 0841 QUrl url; 0842 if (!urlStr.isEmpty()) { 0843 url = QUrl(urlStr); 0844 } 0845 QString localPath = srcUrl.scheme() != QStringLiteral("trash") ? entry.stringValue(KIO::UDSEntry::UDS_LOCAL_PATH) : QString(); 0846 info.linkDest = entry.stringValue(KIO::UDSEntry::UDS_LINK_DEST); 0847 0848 if (fileName != QLatin1String("..") && fileName != QLatin1String(".")) { 0849 const bool hasCustomURL = !url.isEmpty() || !localPath.isEmpty(); 0850 if (!hasCustomURL) { 0851 // Make URL from displayName 0852 url = srcUrl; 0853 if (srcIsDir) { // Only if src is a directory. Otherwise uSource is fine as is 0854 qCDebug(KIO_COPYJOB_DEBUG) << "adding path" << fileName; 0855 url = addPathToUrl(url, fileName); 0856 } 0857 } 0858 qCDebug(KIO_COPYJOB_DEBUG) << "fileName=" << fileName << "url=" << url; 0859 if (!localPath.isEmpty() && kio_resolve_local_urls && destinationState != DEST_DOESNT_EXIST) { 0860 url = QUrl::fromLocalFile(localPath); 0861 } 0862 0863 info.uSource = url; 0864 info.uDest = currentDest; 0865 qCDebug(KIO_COPYJOB_DEBUG) << "uSource=" << info.uSource << "uDest(1)=" << info.uDest; 0866 // Append filename or dirname to destination URL, if allowed 0867 if (destinationState == DEST_IS_DIR && 0868 // "copy/move as <foo>" means 'foo' is the dest for the base srcurl 0869 // (passed here during stating) but not its children (during listing) 0870 (!(m_asMethod && state == STATE_STATING))) { 0871 QString destFileName; 0872 KProtocolInfo::FileNameUsedForCopying fnu = KProtocolManager::fileNameUsedForCopying(url); 0873 if (hasCustomURL && fnu == KProtocolInfo::FromUrl) { 0874 // destFileName = url.fileName(); // Doesn't work for recursive listing 0875 // Count the number of prefixes used by the recursive listjob 0876 int numberOfSlashes = fileName.count(QLatin1Char('/')); // don't make this a find()! 0877 QString path = url.path(); 0878 int pos = 0; 0879 for (int n = 0; n < numberOfSlashes + 1; ++n) { 0880 pos = path.lastIndexOf(QLatin1Char('/'), pos - 1); 0881 if (pos == -1) { // error 0882 qCWarning(KIO_CORE) << "KIO worker bug: not enough slashes in UDS_URL" << path << "- looking for" << numberOfSlashes << "slashes"; 0883 break; 0884 } 0885 } 0886 if (pos >= 0) { 0887 destFileName = path.mid(pos + 1); 0888 } 0889 0890 } else if (fnu == KProtocolInfo::Name) { // destination filename taken from UDS_NAME 0891 destFileName = fileName; 0892 } else { // from display name (with fallback to name) 0893 const QString displayName = entry.stringValue(KIO::UDSEntry::UDS_DISPLAY_NAME); 0894 destFileName = displayName.isEmpty() ? fileName : displayName; 0895 } 0896 0897 // Here we _really_ have to add some filename to the dest. 0898 // Otherwise, we end up with e.g. dest=..../Desktop/ itself. 0899 // (This can happen when dropping a link to a webpage with no path) 0900 if (destFileName.isEmpty()) { 0901 destFileName = KIO::encodeFileName(info.uSource.toDisplayString()); 0902 } 0903 0904 qCDebug(KIO_COPYJOB_DEBUG) << " adding destFileName=" << destFileName; 0905 info.uDest = addPathToUrl(info.uDest, destFileName); 0906 } 0907 qCDebug(KIO_COPYJOB_DEBUG) << " uDest(2)=" << info.uDest; 0908 qCDebug(KIO_COPYJOB_DEBUG) << " " << info.uSource << "->" << info.uDest; 0909 if (info.linkDest.isEmpty() && isDir && m_mode != CopyJob::Link) { // Dir 0910 dirs.append(info); // Directories 0911 if (m_mode == CopyJob::Move) { 0912 dirsToRemove.append(info.uSource); 0913 } 0914 } else { 0915 files.append(info); // Files and any symlinks 0916 } 0917 } 0918 } 0919 0920 // Adjust for kio_trash choosing its own dest url... 0921 QUrl CopyJobPrivate::finalDestUrl(const QUrl &src, const QUrl &dest) const 0922 { 0923 Q_Q(const CopyJob); 0924 if (dest.scheme() == QLatin1String("trash")) { 0925 const QMap<QString, QString> &metaData = q->metaData(); 0926 QMap<QString, QString>::ConstIterator it = metaData.find(QLatin1String("trashURL-") + src.path()); 0927 if (it != metaData.constEnd()) { 0928 qCDebug(KIO_COPYJOB_DEBUG) << "finalDestUrl=" << it.value(); 0929 return QUrl(it.value()); 0930 } 0931 } 0932 return dest; 0933 } 0934 0935 void CopyJobPrivate::skipSrc(bool isDir) 0936 { 0937 m_dest = m_globalDest; 0938 destinationState = m_globalDestinationState; 0939 skip(*m_currentStatSrc, isDir); 0940 ++m_currentStatSrc; 0941 statCurrentSrc(); 0942 } 0943 0944 void CopyJobPrivate::statNextSrc() 0945 { 0946 /* Revert to the global destination, the one that applies to all source urls. 0947 * 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. 0948 * d->m_dest is /foo/b for b, but we have to revert to /d for item c and following. 0949 */ 0950 m_dest = m_globalDest; 0951 qCDebug(KIO_COPYJOB_DEBUG) << "Setting m_dest to" << m_dest; 0952 destinationState = m_globalDestinationState; 0953 ++m_currentStatSrc; 0954 statCurrentSrc(); 0955 } 0956 0957 void CopyJobPrivate::statCurrentSrc() 0958 { 0959 Q_Q(CopyJob); 0960 if (m_currentStatSrc != m_srcList.constEnd()) { 0961 m_currentSrcURL = (*m_currentStatSrc); 0962 m_bURLDirty = true; 0963 m_ignoreSourcePermissions = !KProtocolManager::supportsListing(m_currentSrcURL) || m_currentSrcURL.scheme() == QLatin1String("trash"); 0964 0965 if (m_mode == CopyJob::Link) { 0966 // Skip the "stating the source" stage, we don't need it for linking 0967 m_currentDest = m_dest; 0968 struct CopyInfo info; 0969 info.permissions = -1; 0970 info.size = KIO::invalidFilesize; 0971 info.uSource = m_currentSrcURL; 0972 info.uDest = m_currentDest; 0973 // Append filename or dirname to destination URL, if allowed 0974 if (destinationState == DEST_IS_DIR && !m_asMethod) { 0975 if (compareUrls(m_currentSrcURL, info.uDest)) { 0976 // This is the case of creating a real symlink 0977 info.uDest = addPathToUrl(info.uDest, m_currentSrcURL.fileName()); 0978 } else { 0979 // Different protocols, we'll create a .desktop file 0980 // We have to change the extension anyway, so while we're at it, 0981 // name the file like the URL 0982 QByteArray encodedFilename = QFile::encodeName(m_currentSrcURL.toDisplayString()); 0983 const int truncatePos = NAME_MAX - (info.uDest.toDisplayString().length() + 8); // length(.desktop) = 8 0984 if (truncatePos > 0) { 0985 encodedFilename.truncate(truncatePos); 0986 } 0987 const QString decodedFilename = QFile::decodeName(encodedFilename); 0988 info.uDest = addPathToUrl(info.uDest, KIO::encodeFileName(decodedFilename) + QLatin1String(".desktop")); 0989 } 0990 } 0991 files.append(info); // Files and any symlinks 0992 statNextSrc(); // we could use a loop instead of a recursive call :) 0993 return; 0994 } 0995 0996 // Let's see if we can skip stat'ing, for the case where a directory view has the info already 0997 KIO::UDSEntry entry; 0998 const KFileItem cachedItem = KCoreDirLister::cachedItemForUrl(m_currentSrcURL); 0999 if (!cachedItem.isNull()) { 1000 entry = cachedItem.entry(); 1001 if (destinationState != DEST_DOESNT_EXIST 1002 && m_currentSrcURL.scheme() != QStringLiteral("trash")) { // only resolve src if we could resolve dest (#218719) 1003 1004 m_currentSrcURL = cachedItem.mostLocalUrl(); // #183585 1005 } 1006 } 1007 1008 // Don't go renaming right away if we need a stat() to find out the destination filename 1009 const bool needStat = 1010 KProtocolManager::fileNameUsedForCopying(m_currentSrcURL) == KProtocolInfo::FromUrl || destinationState != DEST_IS_DIR || m_asMethod; 1011 if (m_mode == CopyJob::Move && needStat) { 1012 // If moving, before going for the full stat+[list+]copy+del thing, try to rename 1013 // The logic is pretty similar to FileCopyJobPrivate::slotStart() 1014 if (compareUrls(m_currentSrcURL, m_dest)) { 1015 startRenameJob(m_currentSrcURL); 1016 return; 1017 } else if (m_currentSrcURL.isLocalFile() && KProtocolManager::canRenameFromFile(m_dest)) { 1018 startRenameJob(m_dest); 1019 return; 1020 } else if (m_dest.isLocalFile() && KProtocolManager::canRenameToFile(m_currentSrcURL)) { 1021 startRenameJob(m_currentSrcURL); 1022 return; 1023 } 1024 } 1025 1026 // if the source file system doesn't support deleting, we do not even stat 1027 if (m_mode == CopyJob::Move && !KProtocolManager::supportsDeleting(m_currentSrcURL)) { 1028 QPointer<CopyJob> that = q; 1029 Q_EMIT q->warning(q, buildErrorString(ERR_CANNOT_DELETE, m_currentSrcURL.toDisplayString())); 1030 if (that) { 1031 statNextSrc(); // we could use a loop instead of a recursive call :) 1032 } 1033 return; 1034 } 1035 1036 m_bOnlyRenames = false; 1037 1038 // Testing for entry.count()>0 here is not good enough; KFileItem inserts 1039 // entries for UDS_USER and UDS_GROUP even on initially empty UDSEntries (#192185) 1040 if (entry.contains(KIO::UDSEntry::UDS_NAME)) { 1041 qCDebug(KIO_COPYJOB_DEBUG) << "fast path! found info about" << m_currentSrcURL << "in KCoreDirLister"; 1042 // sourceStated(entry, m_currentSrcURL); // don't recurse, see #319747, use queued invokeMethod instead 1043 auto srcStatedFunc = [this, entry]() { 1044 sourceStated(entry, m_currentSrcURL); 1045 }; 1046 QMetaObject::invokeMethod(q, srcStatedFunc, Qt::QueuedConnection); 1047 return; 1048 } 1049 1050 // Stat the next src url 1051 Job *job = KIO::stat(m_currentSrcURL, KIO::HideProgressInfo); 1052 qCDebug(KIO_COPYJOB_DEBUG) << "KIO::stat on" << m_currentSrcURL; 1053 state = STATE_STATING; 1054 q->addSubjob(job); 1055 m_currentDestURL = m_dest; 1056 m_bURLDirty = true; 1057 } else { 1058 // Finished the stat'ing phase 1059 // First make sure that the totals were correctly emitted 1060 m_bURLDirty = true; 1061 slotReport(); 1062 1063 qCDebug(KIO_COPYJOB_DEBUG) << "Stating finished. To copy:" << m_totalSize << ", available:" << m_freeSpace; 1064 1065 if (m_totalSize > m_freeSpace && m_freeSpace != static_cast<KIO::filesize_t>(-1)) { 1066 q->setError(ERR_DISK_FULL); 1067 q->setErrorText(m_currentSrcURL.toDisplayString()); 1068 q->emitResult(); 1069 return; 1070 } 1071 1072 // Check if we are copying a single file 1073 m_bSingleFileCopy = (files.count() == 1 && dirs.isEmpty()); 1074 // Then start copying things 1075 state = STATE_CREATING_DIRS; 1076 createNextDir(); 1077 } 1078 } 1079 1080 void CopyJobPrivate::startRenameJob(const QUrl &workerUrl) 1081 { 1082 Q_Q(CopyJob); 1083 1084 // Silence KDirWatch notifications, otherwise performance is horrible 1085 if (m_currentSrcURL.isLocalFile()) { 1086 const QString parentDir = m_currentSrcURL.adjusted(QUrl::RemoveFilename).path(); 1087 const auto [it, isInserted] = m_parentDirs.insert(parentDir); 1088 if (isInserted) { 1089 KDirWatch::self()->stopDirScan(parentDir); 1090 } 1091 } 1092 1093 QUrl dest = m_dest; 1094 // Append filename or dirname to destination URL, if allowed 1095 if (destinationState == DEST_IS_DIR && !m_asMethod) { 1096 dest = addPathToUrl(dest, m_currentSrcURL.fileName()); 1097 } 1098 m_currentDestURL = dest; 1099 qCDebug(KIO_COPYJOB_DEBUG) << m_currentSrcURL << "->" << dest << "trying direct rename first"; 1100 if (state != STATE_RENAMING) { 1101 q->setTotalAmount(KJob::Files, m_srcList.count()); 1102 } 1103 state = STATE_RENAMING; 1104 1105 struct CopyInfo info; 1106 info.permissions = -1; 1107 info.size = KIO::invalidFilesize; 1108 info.uSource = m_currentSrcURL; 1109 info.uDest = dest; 1110 1111 KIO_ARGS << m_currentSrcURL << dest << (qint8) false /*no overwrite*/; 1112 SimpleJob *newJob = SimpleJobPrivate::newJobNoUi(workerUrl, CMD_RENAME, packedArgs); 1113 newJob->setParentJob(q); 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 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 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 1150 bool CopyJobPrivate::shouldOverwriteDir(const QString &path) const 1151 { 1152 if (m_bOverwriteAllDirs) { 1153 return true; 1154 } 1155 return m_overwriteList.contains(path); 1156 } 1157 1158 bool CopyJobPrivate::shouldOverwriteFile(const QString &path) const 1159 { 1160 if (m_bOverwriteAllFiles) { 1161 return true; 1162 } 1163 return m_overwriteList.contains(path); 1164 } 1165 1166 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 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 const QString oldPath = Utils::slashAppended((*it).uDest.path()); 1182 1183 // Change the current one and strip the trailing '/' 1184 (*it).uDest = newUrl.adjusted(QUrl::StripTrailingSlash); 1185 1186 const QString newPath = Utils::slashAppended(newUrl.path()); // With trailing slash 1187 1188 QList<CopyInfo>::Iterator renamedirit = it; 1189 ++renamedirit; 1190 // Change the name of subdirectories inside the directory 1191 for (; renamedirit != dirs.end(); ++renamedirit) { 1192 QString path = (*renamedirit).uDest.path(); 1193 if (path.startsWith(oldPath)) { 1194 QString n = path; 1195 n.replace(0, oldPath.length(), newPath); 1196 /*qDebug() << "dirs list:" << (*renamedirit).uSource.path() 1197 << "was going to be" << path 1198 << ", changed into" << n;*/ 1199 (*renamedirit).uDest.setPath(n, QUrl::DecodedMode); 1200 } 1201 } 1202 // Change filenames inside the directory 1203 QList<CopyInfo>::Iterator renamefileit = files.begin(); 1204 for (; renamefileit != files.end(); ++renamefileit) { 1205 QString path = (*renamefileit).uDest.path(QUrl::FullyDecoded); 1206 if (path.startsWith(oldPath)) { 1207 QString n = path; 1208 n.replace(0, oldPath.length(), newPath); 1209 /*qDebug() << "files list:" << (*renamefileit).uSource.path() 1210 << "was going to be" << path 1211 << ", changed into" << n;*/ 1212 (*renamefileit).uDest.setPath(n, QUrl::DecodedMode); 1213 } 1214 } 1215 } 1216 1217 void CopyJobPrivate::slotResultCreatingDirs(KJob *job) 1218 { 1219 Q_Q(CopyJob); 1220 // The dir we are trying to create: 1221 QList<CopyInfo>::Iterator it = dirs.begin(); 1222 // Was there an error creating a dir ? 1223 if (job->error()) { 1224 m_conflictError = job->error(); 1225 if (m_conflictError == ERR_DIR_ALREADY_EXIST // 1226 || m_conflictError == ERR_FILE_ALREADY_EXIST) { // can't happen? 1227 QUrl oldURL = ((SimpleJob *)job)->url(); 1228 // Should we skip automatically ? 1229 if (m_bAutoSkipDirs) { 1230 // We don't want to copy files in this directory, so we put it on the skip list 1231 const QString path = Utils::slashAppended(oldURL.path()); 1232 m_skipList.append(path); 1233 skip(oldURL, true); 1234 dirs.erase(it); // Move on to next dir 1235 } else { 1236 // Did the user choose to overwrite already? 1237 const QString destDir = (*it).uDest.path(); 1238 if (shouldOverwriteDir(destDir)) { // overwrite => just skip 1239 Q_EMIT q->copyingDone(q, (*it).uSource, finalDestUrl((*it).uSource, (*it).uDest), (*it).mtime, true /* directory */, false /* renamed */); 1240 dirs.erase(it); // Move on to next dir 1241 ++m_processedDirs; 1242 } else { 1243 if (m_bAutoRenameDirs) { 1244 const QUrl destDirectory = (*it).uDest.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash); 1245 const QString newName = KFileUtils::suggestName(destDirectory, (*it).uDest.fileName()); 1246 QUrl newUrl(destDirectory); 1247 newUrl.setPath(Utils::concatPaths(newUrl.path(), newName)); 1248 renameDirectory(it, newUrl); 1249 } else { 1250 if (!KIO::delegateExtension<AskUserActionInterface *>(q)) { 1251 q->Job::slotResult(job); // will set the error and emit result(this) 1252 return; 1253 } 1254 1255 Q_ASSERT(((SimpleJob *)job)->url() == (*it).uDest); 1256 q->removeSubjob(job); 1257 Q_ASSERT(!q->hasSubjobs()); // We should have only one job at a time ... 1258 1259 // We need to stat the existing dir, to get its last-modification time 1260 QUrl existingDest((*it).uDest); 1261 SimpleJob *newJob = KIO::stat(existingDest, StatJob::DestinationSide, KIO::StatDefaultDetails, KIO::HideProgressInfo); 1262 qCDebug(KIO_COPYJOB_DEBUG) << "KIO::stat for resolving conflict on" << existingDest; 1263 state = STATE_CONFLICT_CREATING_DIRS; 1264 q->addSubjob(newJob); 1265 return; // Don't move to next dir yet ! 1266 } 1267 } 1268 } 1269 } else { 1270 // Severe error, abort 1271 q->Job::slotResult(job); // will set the error and emit result(this) 1272 return; 1273 } 1274 } else { // no error : remove from list, to move on to next dir 1275 // this is required for the undo feature 1276 Q_EMIT q->copyingDone(q, (*it).uSource, finalDestUrl((*it).uSource, (*it).uDest), (*it).mtime, true, false); 1277 m_directoriesCopied.push_back(*it); 1278 dirs.erase(it); 1279 ++m_processedDirs; 1280 } 1281 1282 q->removeSubjob(job); 1283 Q_ASSERT(!q->hasSubjobs()); // We should have only one job at a time ... 1284 createNextDir(); 1285 } 1286 1287 void CopyJobPrivate::slotResultConflictCreatingDirs(KJob *job) 1288 { 1289 Q_Q(CopyJob); 1290 // We come here after a conflict has been detected and we've stated the existing dir 1291 1292 // The dir we were trying to create: 1293 QList<CopyInfo>::Iterator it = dirs.begin(); 1294 1295 const UDSEntry entry = ((KIO::StatJob *)job)->statResult(); 1296 1297 QDateTime destmtime; 1298 QDateTime destctime; 1299 const KIO::filesize_t destsize = entry.numberValue(KIO::UDSEntry::UDS_SIZE); 1300 const QString linkDest = entry.stringValue(KIO::UDSEntry::UDS_LINK_DEST); 1301 1302 q->removeSubjob(job); 1303 Q_ASSERT(!q->hasSubjobs()); // We should have only one job at a time ... 1304 1305 // Always multi and skip (since there are files after that) 1306 RenameDialog_Options options(RenameDialog_MultipleItems | RenameDialog_Skip | RenameDialog_DestIsDirectory); 1307 // Overwrite only if the existing thing is a dir (no chance with a file) 1308 if (m_conflictError == ERR_DIR_ALREADY_EXIST) { 1309 // We are in slotResultConflictCreatingDirs(), so the source is a dir 1310 options |= RenameDialog_SourceIsDirectory; 1311 1312 if ((*it).uSource == (*it).uDest 1313 || ((*it).uSource.scheme() == (*it).uDest.scheme() && (*it).uSource.adjusted(QUrl::StripTrailingSlash).path() == linkDest)) { 1314 options |= RenameDialog_OverwriteItself; 1315 } else { 1316 options |= RenameDialog_Overwrite; 1317 destmtime = QDateTime::fromSecsSinceEpoch(entry.numberValue(KIO::UDSEntry::UDS_MODIFICATION_TIME, -1), QTimeZone::UTC); 1318 destctime = QDateTime::fromSecsSinceEpoch(entry.numberValue(KIO::UDSEntry::UDS_CREATION_TIME, -1), QTimeZone::UTC); 1319 } 1320 } 1321 1322 if (m_reportTimer) { 1323 m_reportTimer->stop(); 1324 } 1325 1326 auto *askUserActionInterface = KIO::delegateExtension<KIO::AskUserActionInterface *>(q); 1327 1328 auto renameSignal = &KIO::AskUserActionInterface::askUserRenameResult; 1329 QObject::connect(askUserActionInterface, renameSignal, q, [=](RenameDialog_Result result, const QUrl &newUrl, KJob *parentJob) { 1330 Q_ASSERT(parentJob == q); 1331 // Only receive askUserRenameResult once per rename dialog 1332 QObject::disconnect(askUserActionInterface, renameSignal, q, nullptr); 1333 1334 if (m_reportTimer) { 1335 m_reportTimer->start(s_reportTimeout); 1336 } 1337 1338 const QString existingDest = (*it).uDest.path(); 1339 1340 switch (result) { 1341 case Result_Cancel: 1342 q->setError(ERR_USER_CANCELED); 1343 q->emitResult(); 1344 return; 1345 case Result_AutoRename: 1346 m_bAutoRenameDirs = true; 1347 // fall through 1348 Q_FALLTHROUGH(); 1349 case Result_Rename: 1350 renameDirectory(it, newUrl); 1351 break; 1352 case Result_AutoSkip: 1353 m_bAutoSkipDirs = true; 1354 // fall through 1355 Q_FALLTHROUGH(); 1356 case Result_Skip: 1357 m_skipList.append(Utils::slashAppended(existingDest)); 1358 skip((*it).uSource, true); 1359 // Move on to next dir 1360 dirs.erase(it); 1361 ++m_processedDirs; 1362 break; 1363 case Result_Overwrite: 1364 m_overwriteList.insert(existingDest); 1365 Q_EMIT q->copyingDone(q, (*it).uSource, finalDestUrl((*it).uSource, (*it).uDest), (*it).mtime, true /* directory */, false /* renamed */); 1366 // Move on to next dir 1367 dirs.erase(it); 1368 ++m_processedDirs; 1369 break; 1370 case Result_OverwriteAll: 1371 m_bOverwriteAllDirs = true; 1372 Q_EMIT q->copyingDone(q, (*it).uSource, finalDestUrl((*it).uSource, (*it).uDest), (*it).mtime, true /* directory */, false /* renamed */); 1373 // Move on to next dir 1374 dirs.erase(it); 1375 ++m_processedDirs; 1376 break; 1377 default: 1378 Q_ASSERT(0); 1379 } 1380 state = STATE_CREATING_DIRS; 1381 createNextDir(); 1382 }); 1383 1384 /* clang-format off */ 1385 askUserActionInterface->askUserRename(q, i18n("Folder Already Exists"), 1386 (*it).uSource, (*it).uDest, 1387 options, 1388 (*it).size, destsize, 1389 (*it).ctime, destctime, 1390 (*it).mtime, destmtime); 1391 /* clang-format on */ 1392 } 1393 1394 void CopyJobPrivate::createNextDir() 1395 { 1396 Q_Q(CopyJob); 1397 1398 // Take first dir to create out of list 1399 QList<CopyInfo>::Iterator it = dirs.begin(); 1400 // Is this URL on the skip list or the overwrite list ? 1401 while (it != dirs.end()) { 1402 const QString dir = it->uDest.path(); 1403 if (shouldSkip(dir)) { 1404 it = dirs.erase(it); 1405 } else { 1406 break; 1407 } 1408 } 1409 1410 if (it != dirs.end()) { // any dir to create, finally ? 1411 if (it->uDest.isLocalFile()) { 1412 // uDest doesn't exist yet, check the filesystem of the parent dir 1413 const auto destFileSystem = KFileSystemType::fileSystemType(it->uDest.adjusted(QUrl::StripTrailingSlash | QUrl::RemoveFilename).toLocalFile()); 1414 if (isFatOrNtfs(destFileSystem)) { 1415 const QString dirName = it->uDest.adjusted(QUrl::StripTrailingSlash).fileName(); 1416 if (hasInvalidChars(dirName)) { 1417 // We already asked the user? 1418 if (m_autoReplaceInvalidChars) { 1419 processCreateNextDir(it, KIO::Result_ReplaceInvalidChars); 1420 return; 1421 } else if (m_autoSkipDirsWithInvalidChars) { 1422 processCreateNextDir(it, KIO::Result_Skip); 1423 return; 1424 } 1425 1426 const QString msg = invalidCharsSupportMsg(it->uDest.toDisplayString(QUrl::PreferLocalFile), 1427 KFileSystemType::fileSystemName(destFileSystem), 1428 true /* isDir */); 1429 1430 if (auto *askUserActionInterface = KIO::delegateExtension<KIO::AskUserActionInterface *>(q)) { 1431 SkipDialog_Options options = KIO::SkipDialog_Replace_Invalid_Chars; 1432 if (dirs.size() > 1) { 1433 options |= SkipDialog_MultipleItems; 1434 } 1435 1436 auto skipSignal = &KIO::AskUserActionInterface::askUserSkipResult; 1437 QObject::connect(askUserActionInterface, skipSignal, q, [=](SkipDialog_Result result, KJob *parentJob) { 1438 Q_ASSERT(parentJob == q); 1439 1440 // Only receive askUserSkipResult once per skip dialog 1441 QObject::disconnect(askUserActionInterface, skipSignal, q, nullptr); 1442 1443 processCreateNextDir(it, result); 1444 }); 1445 1446 askUserActionInterface->askUserSkip(q, options, msg); 1447 1448 return; 1449 } else { // No Job Ui delegate 1450 qCWarning(KIO_COPYJOB_DEBUG) << msg; 1451 q->emitResult(); 1452 return; 1453 } 1454 } 1455 } 1456 } 1457 1458 processCreateNextDir(it, -1); 1459 } else { // we have finished creating dirs 1460 q->setProcessedAmount(KJob::Directories, m_processedDirs); // make sure final number appears 1461 1462 if (m_mode == CopyJob::Move) { 1463 // Now we know which dirs hold the files we're going to delete. 1464 // To speed things up and prevent double-notification, we disable KDirWatch 1465 // on those dirs temporarily (using KDirWatch::self, that's the instance 1466 // used by e.g. kdirlister). 1467 for (const auto &dir : m_parentDirs) { 1468 KDirWatch::self()->stopDirScan(dir); 1469 } 1470 } 1471 1472 state = STATE_COPYING_FILES; 1473 ++m_processedFiles; // Ralf wants it to start at 1, not 0 1474 copyNextFile(); 1475 } 1476 } 1477 1478 void CopyJobPrivate::processCreateNextDir(const QList<CopyInfo>::Iterator &it, int result) 1479 { 1480 Q_Q(CopyJob); 1481 1482 switch (result) { 1483 case Result_Cancel: 1484 q->setError(ERR_USER_CANCELED); 1485 q->emitResult(); 1486 return; 1487 case KIO::Result_ReplaceAllInvalidChars: 1488 m_autoReplaceInvalidChars = true; 1489 Q_FALLTHROUGH(); 1490 case KIO::Result_ReplaceInvalidChars: { 1491 it->uDest = it->uDest.adjusted(QUrl::StripTrailingSlash); 1492 QString dirName = it->uDest.fileName(); 1493 const int len = dirName.size(); 1494 cleanMsdosDestName(dirName); 1495 QString path = it->uDest.path(); 1496 path.replace(path.size() - len, len, dirName); 1497 it->uDest.setPath(path); 1498 break; 1499 } 1500 case KIO::Result_AutoSkip: 1501 m_autoSkipDirsWithInvalidChars = true; 1502 Q_FALLTHROUGH(); 1503 case KIO::Result_Skip: 1504 m_skipList.append(Utils::slashAppended(it->uDest.path())); 1505 skip(it->uSource, true); 1506 dirs.erase(it); // Move on to next dir 1507 ++m_processedDirs; 1508 createNextDir(); 1509 return; 1510 default: 1511 break; 1512 } 1513 1514 // Create the directory - with default permissions so that we can put files into it 1515 // TODO : change permissions once all is finished; but for stuff coming from CDROM it sucks... 1516 KIO::SimpleJob *newjob = KIO::mkdir(it->uDest, -1); 1517 newjob->setParentJob(q); 1518 if (shouldOverwriteFile(it->uDest.path())) { // if we are overwriting an existing file or symlink 1519 newjob->addMetaData(QStringLiteral("overwrite"), QStringLiteral("true")); 1520 } 1521 1522 m_currentDestURL = it->uDest; 1523 m_bURLDirty = true; 1524 1525 q->addSubjob(newjob); 1526 } 1527 1528 void CopyJobPrivate::slotResultCopyingFiles(KJob *job) 1529 { 1530 Q_Q(CopyJob); 1531 // The file we were trying to copy: 1532 QList<CopyInfo>::Iterator it = files.begin(); 1533 if (job->error()) { 1534 // Should we skip automatically ? 1535 if (m_bAutoSkipFiles) { 1536 skip((*it).uSource, false); 1537 m_fileProcessedSize = (*it).size; 1538 files.erase(it); // Move on to next file 1539 } else { 1540 m_conflictError = job->error(); // save for later 1541 // Existing dest ? 1542 if (m_conflictError == ERR_FILE_ALREADY_EXIST // 1543 || m_conflictError == ERR_DIR_ALREADY_EXIST // 1544 || m_conflictError == ERR_IDENTICAL_FILES) { 1545 if (m_bAutoRenameFiles) { 1546 QUrl destDirectory = (*it).uDest.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash); 1547 const QString newName = KFileUtils::suggestName(destDirectory, (*it).uDest.fileName()); 1548 QUrl newDest(destDirectory); 1549 newDest.setPath(Utils::concatPaths(newDest.path(), newName)); 1550 Q_EMIT q->renamed(q, (*it).uDest, newDest); // for e.g. kpropsdlg 1551 (*it).uDest = newDest; 1552 } else { 1553 if (!KIO::delegateExtension<AskUserActionInterface *>(q)) { 1554 q->Job::slotResult(job); // will set the error and emit result(this) 1555 return; 1556 } 1557 1558 q->removeSubjob(job); 1559 Q_ASSERT(!q->hasSubjobs()); 1560 // We need to stat the existing file, to get its last-modification time 1561 QUrl existingFile((*it).uDest); 1562 SimpleJob *newJob = 1563 KIO::stat(existingFile, StatJob::DestinationSide, KIO::StatDetail::StatBasic | KIO::StatDetail::StatTime, KIO::HideProgressInfo); 1564 qCDebug(KIO_COPYJOB_DEBUG) << "KIO::stat for resolving conflict on" << existingFile; 1565 state = STATE_CONFLICT_COPYING_FILES; 1566 q->addSubjob(newJob); 1567 return; // Don't move to next file yet ! 1568 } 1569 } else { 1570 if (m_bCurrentOperationIsLink && qobject_cast<KIO::DeleteJob *>(job)) { 1571 // Very special case, see a few lines below 1572 // We are deleting the source of a symlink we successfully moved... ignore error 1573 m_fileProcessedSize = (*it).size; 1574 ++m_processedFiles; 1575 files.erase(it); 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 // Go directly to the conflict resolution, there is nothing to stat 1583 slotResultErrorCopyingFiles(job); 1584 return; 1585 } 1586 } 1587 } 1588 } else { // no error 1589 // Special case for moving links. That operation needs two jobs, unlike others. 1590 if (m_bCurrentOperationIsLink // 1591 && m_mode == CopyJob::Move // 1592 && !qobject_cast<KIO::DeleteJob *>(job) // Deleting source not already done 1593 ) { 1594 q->removeSubjob(job); 1595 Q_ASSERT(!q->hasSubjobs()); 1596 // The only problem with this trick is that the error handling for this del operation 1597 // is not going to be right... see 'Very special case' above. 1598 KIO::Job *newjob = KIO::del((*it).uSource, HideProgressInfo); 1599 newjob->setParentJob(q); 1600 q->addSubjob(newjob); 1601 return; // Don't move to next file yet ! 1602 } 1603 1604 const QUrl finalUrl = finalDestUrl((*it).uSource, (*it).uDest); 1605 1606 if (m_bCurrentOperationIsLink) { 1607 QString target = (m_mode == CopyJob::Link ? (*it).uSource.path() : (*it).linkDest); 1608 // required for the undo feature 1609 Q_EMIT q->copyingLinkDone(q, (*it).uSource, target, finalUrl); 1610 } else { 1611 // required for the undo feature 1612 Q_EMIT q->copyingDone(q, (*it).uSource, finalUrl, (*it).mtime, false, false); 1613 if (m_mode == CopyJob::Move) { 1614 #ifndef KIO_ANDROID_STUB 1615 org::kde::KDirNotify::emitFileMoved((*it).uSource, finalUrl); 1616 #endif 1617 } 1618 m_successSrcList.append((*it).uSource); 1619 if (m_freeSpace != KIO::invalidFilesize && (*it).size != KIO::invalidFilesize) { 1620 m_freeSpace -= (*it).size; 1621 } 1622 } 1623 // remove from list, to move on to next file 1624 files.erase(it); 1625 ++m_processedFiles; 1626 } 1627 1628 // clear processed size for last file and add it to overall processed size 1629 m_processedSize += m_fileProcessedSize; 1630 m_fileProcessedSize = 0; 1631 1632 qCDebug(KIO_COPYJOB_DEBUG) << files.count() << "files remaining"; 1633 1634 // Merge metadata from subjob 1635 KIO::Job *kiojob = qobject_cast<KIO::Job *>(job); 1636 Q_ASSERT(kiojob); 1637 m_incomingMetaData += kiojob->metaData(); 1638 q->removeSubjob(job); 1639 Q_ASSERT(!q->hasSubjobs()); // We should have only one job at a time ... 1640 copyNextFile(); 1641 } 1642 1643 void CopyJobPrivate::slotResultErrorCopyingFiles(KJob *job) 1644 { 1645 Q_Q(CopyJob); 1646 // We come here after a conflict has been detected and we've stated the existing file 1647 // The file we were trying to create: 1648 QList<CopyInfo>::Iterator it = files.begin(); 1649 1650 RenameDialog_Result res = Result_Cancel; 1651 1652 if (m_reportTimer) { 1653 m_reportTimer->stop(); 1654 } 1655 1656 q->removeSubjob(job); 1657 Q_ASSERT(!q->hasSubjobs()); 1658 auto *askUserActionInterface = KIO::delegateExtension<KIO::AskUserActionInterface *>(q); 1659 1660 if (m_conflictError == ERR_FILE_ALREADY_EXIST // 1661 || m_conflictError == ERR_DIR_ALREADY_EXIST // 1662 || m_conflictError == ERR_IDENTICAL_FILES) { 1663 // Its modification time: 1664 const UDSEntry entry = static_cast<KIO::StatJob *>(job)->statResult(); 1665 1666 QDateTime destmtime; 1667 QDateTime destctime; 1668 const KIO::filesize_t destsize = entry.numberValue(KIO::UDSEntry::UDS_SIZE); 1669 const QString linkDest = entry.stringValue(KIO::UDSEntry::UDS_LINK_DEST); 1670 1671 // Offer overwrite only if the existing thing is a file 1672 // If src==dest, use "overwrite-itself" 1673 RenameDialog_Options options; 1674 bool isDir = true; 1675 1676 if (m_conflictError == ERR_DIR_ALREADY_EXIST) { 1677 options = RenameDialog_DestIsDirectory; 1678 } else { 1679 if ((*it).uSource == (*it).uDest 1680 || ((*it).uSource.scheme() == (*it).uDest.scheme() && (*it).uSource.adjusted(QUrl::StripTrailingSlash).path() == linkDest)) { 1681 options = RenameDialog_OverwriteItself; 1682 } else { 1683 const qint64 destMTimeStamp = entry.numberValue(KIO::UDSEntry::UDS_MODIFICATION_TIME, -1); 1684 if (m_bOverwriteWhenOlder && (*it).mtime.isValid() && destMTimeStamp != -1) { 1685 if ((*it).mtime.currentSecsSinceEpoch() > destMTimeStamp) { 1686 qCDebug(KIO_COPYJOB_DEBUG) << "dest is older, overwriting" << (*it).uDest; 1687 res = Result_Overwrite; 1688 } else { 1689 qCDebug(KIO_COPYJOB_DEBUG) << "dest is newer, skipping" << (*it).uDest; 1690 res = Result_Skip; 1691 } 1692 } else { 1693 // These timestamps are used only when RenameDialog_Overwrite is set. 1694 destmtime = QDateTime::fromSecsSinceEpoch(destMTimeStamp, QTimeZone::UTC); 1695 destctime = QDateTime::fromSecsSinceEpoch(entry.numberValue(KIO::UDSEntry::UDS_CREATION_TIME, -1), QTimeZone::UTC); 1696 1697 options = RenameDialog_Overwrite; 1698 } 1699 } 1700 isDir = false; 1701 } 1702 1703 // if no preset value was set 1704 if (res == Result_Cancel) { 1705 if (!m_bSingleFileCopy) { 1706 options = RenameDialog_Options(options | RenameDialog_MultipleItems | RenameDialog_Skip); 1707 } 1708 1709 const QString title = !isDir ? i18n("File Already Exists") : i18n("Already Exists as Folder"); 1710 1711 auto renameSignal = &KIO::AskUserActionInterface::askUserRenameResult; 1712 QObject::connect(askUserActionInterface, renameSignal, q, [=](RenameDialog_Result result, const QUrl &newUrl, KJob *parentJob) { 1713 Q_ASSERT(parentJob == q); 1714 // Only receive askUserRenameResult once per rename dialog 1715 QObject::disconnect(askUserActionInterface, renameSignal, q, nullptr); 1716 processFileRenameDialogResult(it, result, newUrl, destmtime); 1717 }); 1718 1719 /* clang-format off */ 1720 askUserActionInterface->askUserRename(q, title, 1721 (*it).uSource, (*it).uDest, 1722 options, 1723 (*it).size, destsize, 1724 (*it).ctime, destctime, 1725 (*it).mtime, destmtime); /* clang-format on */ 1726 return; 1727 } 1728 } else { 1729 if (job->error() == ERR_USER_CANCELED) { 1730 res = Result_Cancel; 1731 } else if (!askUserActionInterface) { 1732 q->Job::slotResult(job); // will set the error and emit result(this) 1733 return; 1734 } else { 1735 SkipDialog_Options options; 1736 if (files.count() > 1) { 1737 options |= SkipDialog_MultipleItems; 1738 } 1739 1740 auto skipSignal = &KIO::AskUserActionInterface::askUserSkipResult; 1741 QObject::connect(askUserActionInterface, skipSignal, q, [=](SkipDialog_Result result, KJob *parentJob) { 1742 Q_ASSERT(parentJob == q); 1743 // Only receive askUserSkipResult once per skip dialog 1744 QObject::disconnect(askUserActionInterface, skipSignal, q, nullptr); 1745 processFileRenameDialogResult(it, result, QUrl() /* no new url in skip */, QDateTime{}); 1746 }); 1747 1748 askUserActionInterface->askUserSkip(q, options, job->errorString()); 1749 return; 1750 } 1751 } 1752 1753 processFileRenameDialogResult(it, res, QUrl{}, QDateTime{}); 1754 } 1755 1756 void CopyJobPrivate::processFileRenameDialogResult(const QList<CopyInfo>::Iterator &it, 1757 RenameDialog_Result result, 1758 const QUrl &newUrl, 1759 const QDateTime &destmtime) 1760 { 1761 Q_Q(CopyJob); 1762 1763 if (m_reportTimer) { 1764 m_reportTimer->start(s_reportTimeout); 1765 } 1766 1767 if (result == Result_OverwriteWhenOlder) { 1768 m_bOverwriteWhenOlder = true; 1769 if ((*it).mtime > destmtime) { 1770 qCDebug(KIO_COPYJOB_DEBUG) << "dest is older, overwriting" << (*it).uDest; 1771 result = Result_Overwrite; 1772 } else { 1773 qCDebug(KIO_COPYJOB_DEBUG) << "dest is newer, skipping" << (*it).uDest; 1774 result = Result_Skip; 1775 } 1776 } 1777 1778 switch (result) { 1779 case Result_Cancel: 1780 q->setError(ERR_USER_CANCELED); 1781 q->emitResult(); 1782 return; 1783 case Result_AutoRename: 1784 m_bAutoRenameFiles = true; 1785 // fall through 1786 Q_FALLTHROUGH(); 1787 case Result_Rename: { 1788 Q_EMIT q->renamed(q, (*it).uDest, newUrl); // for e.g. kpropsdlg 1789 (*it).uDest = newUrl; 1790 m_bURLDirty = true; 1791 break; 1792 } 1793 case Result_AutoSkip: 1794 m_bAutoSkipFiles = true; 1795 // fall through 1796 Q_FALLTHROUGH(); 1797 case Result_Skip: 1798 // Move on to next file 1799 skip((*it).uSource, false); 1800 m_processedSize += (*it).size; 1801 files.erase(it); 1802 break; 1803 case Result_OverwriteAll: 1804 m_bOverwriteAllFiles = true; 1805 break; 1806 case Result_Overwrite: 1807 // Add to overwrite list, so that copyNextFile knows to overwrite 1808 m_overwriteList.insert((*it).uDest.path()); 1809 break; 1810 case Result_Retry: 1811 // Do nothing, copy file again 1812 break; 1813 default: 1814 Q_ASSERT(0); 1815 } 1816 state = STATE_COPYING_FILES; 1817 copyNextFile(); 1818 } 1819 1820 KIO::Job *CopyJobPrivate::linkNextFile(const QUrl &uSource, const QUrl &uDest, JobFlags flags) 1821 { 1822 qCDebug(KIO_COPYJOB_DEBUG) << "Linking"; 1823 if (compareUrls(uSource, uDest)) { 1824 // This is the case of creating a real symlink 1825 KIO::SimpleJob *newJob = KIO::symlink(uSource.path(), uDest, flags | HideProgressInfo /*no GUI*/); 1826 newJob->setParentJob(q_func()); 1827 qCDebug(KIO_COPYJOB_DEBUG) << "Linking target=" << uSource.path() << "link=" << uDest; 1828 // emit linking( this, uSource.path(), uDest ); 1829 m_bCurrentOperationIsLink = true; 1830 m_currentSrcURL = uSource; 1831 m_currentDestURL = uDest; 1832 m_bURLDirty = true; 1833 // Observer::self()->slotCopying( this, uSource, uDest ); // should be slotLinking perhaps 1834 return newJob; 1835 } else { 1836 Q_Q(CopyJob); 1837 qCDebug(KIO_COPYJOB_DEBUG) << "Linking URL=" << uSource << "link=" << uDest; 1838 if (uDest.isLocalFile()) { 1839 // if the source is a devices url, handle it a littlebit special 1840 1841 QString path = uDest.toLocalFile(); 1842 qCDebug(KIO_COPYJOB_DEBUG) << "path=" << path; 1843 QFile f(path); 1844 if (f.open(QIODevice::ReadWrite)) { 1845 f.close(); 1846 KDesktopFile desktopFile(path); 1847 KConfigGroup config = desktopFile.desktopGroup(); 1848 QUrl url = uSource; 1849 url.setPassword(QString()); 1850 config.writePathEntry("URL", url.toString()); 1851 config.writeEntry("Name", url.toString()); 1852 config.writeEntry("Type", QStringLiteral("Link")); 1853 QString protocol = uSource.scheme(); 1854 if (protocol == QLatin1String("ftp")) { 1855 config.writeEntry("Icon", QStringLiteral("folder-remote")); 1856 } else if (protocol == QLatin1String("http") || protocol == QLatin1String("https")) { 1857 config.writeEntry("Icon", QStringLiteral("text-html")); 1858 } else if (protocol == QLatin1String("info")) { 1859 config.writeEntry("Icon", QStringLiteral("text-x-texinfo")); 1860 } else if (protocol == QLatin1String("mailto")) { // sven: 1861 config.writeEntry("Icon", QStringLiteral("internet-mail")); // added mailto: support 1862 } else if (protocol == QLatin1String("trash") && url.path().length() <= 1) { // trash:/ link 1863 config.writeEntry("Name", i18n("Trash")); 1864 config.writeEntry("Icon", QStringLiteral("user-trash-full")); 1865 config.writeEntry("EmptyIcon", QStringLiteral("user-trash")); 1866 } else { 1867 config.writeEntry("Icon", QStringLiteral("unknown")); 1868 } 1869 config.sync(); 1870 files.erase(files.begin()); // done with this one, move on 1871 ++m_processedFiles; 1872 copyNextFile(); 1873 return nullptr; 1874 } else { 1875 qCDebug(KIO_COPYJOB_DEBUG) << "ERR_CANNOT_OPEN_FOR_WRITING"; 1876 q->setError(ERR_CANNOT_OPEN_FOR_WRITING); 1877 q->setErrorText(uDest.toLocalFile()); 1878 q->emitResult(); 1879 return nullptr; 1880 } 1881 } else { 1882 // Todo: not show "link" on remote dirs if the src urls are not from the same protocol+host+... 1883 q->setError(ERR_CANNOT_SYMLINK); 1884 q->setErrorText(uDest.toDisplayString()); 1885 q->emitResult(); 1886 return nullptr; 1887 } 1888 } 1889 } 1890 1891 bool CopyJobPrivate::handleMsdosFsQuirks(QList<CopyInfo>::Iterator it, KFileSystemType::Type fsType) 1892 { 1893 Q_Q(CopyJob); 1894 1895 QString msg; 1896 SkipDialog_Options options; 1897 SkipType skipType = NoSkipType; 1898 1899 if (isFatFs(fsType) && !it->linkDest.isEmpty()) { // Copying a symlink 1900 skipType = SkipFatSymlinks; 1901 if (m_autoSkipFatSymlinks) { // Have we already asked the user? 1902 processCopyNextFile(it, KIO::Result_Skip, skipType); 1903 return true; 1904 } 1905 options = KIO::SkipDialog_Hide_Retry; 1906 msg = symlinkSupportMsg(it->uDest.toLocalFile(), KFileSystemType::fileSystemName(fsType)); 1907 } else if (hasInvalidChars(it->uDest.fileName())) { 1908 skipType = SkipInvalidChars; 1909 if (m_autoReplaceInvalidChars) { // Have we already asked the user? 1910 processCopyNextFile(it, KIO::Result_ReplaceInvalidChars, skipType); 1911 return true; 1912 } else if (m_autoSkipFilesWithInvalidChars) { // Have we already asked the user? 1913 processCopyNextFile(it, KIO::Result_Skip, skipType); 1914 return true; 1915 } 1916 1917 options = KIO::SkipDialog_Replace_Invalid_Chars; 1918 msg = invalidCharsSupportMsg(it->uDest.toDisplayString(QUrl::PreferLocalFile), KFileSystemType::fileSystemName(fsType)); 1919 } 1920 1921 if (!msg.isEmpty()) { 1922 if (auto *askUserActionInterface = KIO::delegateExtension<KIO::AskUserActionInterface *>(q)) { 1923 if (files.size() > 1) { 1924 options |= SkipDialog_MultipleItems; 1925 } 1926 1927 auto skipSignal = &KIO::AskUserActionInterface::askUserSkipResult; 1928 QObject::connect(askUserActionInterface, skipSignal, q, [=](SkipDialog_Result result, KJob *parentJob) { 1929 Q_ASSERT(parentJob == q); 1930 // Only receive askUserSkipResult once per skip dialog 1931 QObject::disconnect(askUserActionInterface, skipSignal, q, nullptr); 1932 1933 processCopyNextFile(it, result, skipType); 1934 }); 1935 1936 askUserActionInterface->askUserSkip(q, options, msg); 1937 1938 return true; 1939 } else { // No Job Ui delegate 1940 qCWarning(KIO_COPYJOB_DEBUG) << msg; 1941 q->emitResult(); 1942 return true; 1943 } 1944 } 1945 1946 return false; // Not handled, move on 1947 } 1948 1949 void CopyJobPrivate::copyNextFile() 1950 { 1951 Q_Q(CopyJob); 1952 bool bCopyFile = false; 1953 qCDebug(KIO_COPYJOB_DEBUG); 1954 1955 bool isDestLocal = m_globalDest.isLocalFile(); 1956 1957 // Take the first file in the list 1958 QList<CopyInfo>::Iterator it = files.begin(); 1959 // Is this URL on the skip list ? 1960 while (it != files.end() && !bCopyFile) { 1961 const QString destFile = (*it).uDest.path(); 1962 bCopyFile = !shouldSkip(destFile); 1963 if (!bCopyFile) { 1964 it = files.erase(it); 1965 } 1966 1967 if (it != files.end() && isDestLocal && (*it).size > 0xFFFFFFFF) { // 4GB-1 1968 const auto destFileSystem = KFileSystemType::fileSystemType(m_globalDest.toLocalFile()); 1969 if (destFileSystem == KFileSystemType::Fat) { 1970 q->setError(ERR_FILE_TOO_LARGE_FOR_FAT32); 1971 q->setErrorText((*it).uDest.toDisplayString()); 1972 q->emitResult(); 1973 return; 1974 } 1975 } 1976 } 1977 1978 if (bCopyFile) { // any file to create, finally ? 1979 if (isDestLocal) { 1980 const auto destFileSystem = KFileSystemType::fileSystemType(m_globalDest.toLocalFile()); 1981 if (isFatOrNtfs(destFileSystem)) { 1982 if (handleMsdosFsQuirks(it, destFileSystem)) { 1983 return; 1984 } 1985 } 1986 } 1987 1988 processCopyNextFile(it, -1, NoSkipType); 1989 } else { 1990 // We're done 1991 qCDebug(KIO_COPYJOB_DEBUG) << "copyNextFile finished"; 1992 --m_processedFiles; // undo the "start at 1" hack 1993 slotReport(); // display final numbers, important if progress dialog stays up 1994 1995 deleteNextDir(); 1996 } 1997 } 1998 1999 void CopyJobPrivate::processCopyNextFile(const QList<CopyInfo>::Iterator &it, int result, SkipType skipType) 2000 { 2001 Q_Q(CopyJob); 2002 2003 switch (result) { 2004 case Result_Cancel: 2005 q->setError(ERR_USER_CANCELED); 2006 q->emitResult(); 2007 return; 2008 case KIO::Result_ReplaceAllInvalidChars: 2009 m_autoReplaceInvalidChars = true; 2010 Q_FALLTHROUGH(); 2011 case KIO::Result_ReplaceInvalidChars: { 2012 QString fileName = it->uDest.fileName(); 2013 const int len = fileName.size(); 2014 cleanMsdosDestName(fileName); 2015 QString path = it->uDest.path(); 2016 path.replace(path.size() - len, len, fileName); 2017 it->uDest.setPath(path); 2018 break; 2019 } 2020 case KIO::Result_AutoSkip: 2021 if (skipType == SkipInvalidChars) { 2022 m_autoSkipFilesWithInvalidChars = true; 2023 } else if (skipType == SkipFatSymlinks) { 2024 m_autoSkipFatSymlinks = true; 2025 } 2026 Q_FALLTHROUGH(); 2027 case KIO::Result_Skip: 2028 // Move on the next file 2029 files.erase(it); 2030 copyNextFile(); 2031 return; 2032 default: 2033 break; 2034 } 2035 2036 qCDebug(KIO_COPYJOB_DEBUG) << "preparing to copy" << (*it).uSource << (*it).size << m_freeSpace; 2037 if (m_freeSpace != KIO::invalidFilesize && (*it).size != KIO::invalidFilesize) { 2038 if (m_freeSpace < (*it).size) { 2039 q->setError(ERR_DISK_FULL); 2040 q->emitResult(); 2041 return; 2042 } 2043 } 2044 2045 const QUrl &uSource = (*it).uSource; 2046 const QUrl &uDest = (*it).uDest; 2047 // Do we set overwrite ? 2048 bool bOverwrite; 2049 const QString destFile = uDest.path(); 2050 qCDebug(KIO_COPYJOB_DEBUG) << "copying" << destFile; 2051 if (uDest == uSource) { 2052 bOverwrite = false; 2053 } else { 2054 bOverwrite = shouldOverwriteFile(destFile); 2055 } 2056 2057 // If source isn't local and target is local, we ignore the original permissions 2058 // Otherwise, files downloaded from HTTP end up with -r--r--r-- 2059 int permissions = (*it).permissions; 2060 if (m_defaultPermissions || (m_ignoreSourcePermissions && uDest.isLocalFile())) { 2061 permissions = -1; 2062 } 2063 const JobFlags flags = bOverwrite ? Overwrite : DefaultFlags; 2064 2065 m_bCurrentOperationIsLink = false; 2066 KIO::Job *newjob = nullptr; 2067 if (m_mode == CopyJob::Link) { 2068 // User requested that a symlink be made 2069 newjob = linkNextFile(uSource, uDest, flags); 2070 if (!newjob) { 2071 return; 2072 } 2073 } else if (!(*it).linkDest.isEmpty() && compareUrls(uSource, uDest)) 2074 // Copying a symlink - only on the same protocol/host/etc. (#5601, downloading an FTP file through its link), 2075 { 2076 KIO::SimpleJob *newJob = KIO::symlink((*it).linkDest, uDest, flags | HideProgressInfo /*no GUI*/); 2077 newJob->setParentJob(q); 2078 newjob = newJob; 2079 qCDebug(KIO_COPYJOB_DEBUG) << "Linking target=" << (*it).linkDest << "link=" << uDest; 2080 m_currentSrcURL = QUrl::fromUserInput((*it).linkDest); 2081 m_currentDestURL = uDest; 2082 m_bURLDirty = true; 2083 // emit linking( this, (*it).linkDest, uDest ); 2084 // Observer::self()->slotCopying( this, m_currentSrcURL, uDest ); // should be slotLinking perhaps 2085 m_bCurrentOperationIsLink = true; 2086 // NOTE: if we are moving stuff, the deletion of the source will be done in slotResultCopyingFiles 2087 } else if (m_mode == CopyJob::Move) { // Moving a file 2088 KIO::FileCopyJob *moveJob = KIO::file_move(uSource, uDest, permissions, flags | HideProgressInfo /*no GUI*/); 2089 moveJob->setParentJob(q); 2090 moveJob->setSourceSize((*it).size); 2091 moveJob->setModificationTime((*it).mtime); // #55804 2092 newjob = moveJob; 2093 qCDebug(KIO_COPYJOB_DEBUG) << "Moving" << uSource << "to" << uDest; 2094 // emit moving( this, uSource, uDest ); 2095 m_currentSrcURL = uSource; 2096 m_currentDestURL = uDest; 2097 m_bURLDirty = true; 2098 // Observer::self()->slotMoving( this, uSource, uDest ); 2099 } else { // Copying a file 2100 KIO::FileCopyJob *copyJob = KIO::file_copy(uSource, uDest, permissions, flags | HideProgressInfo /*no GUI*/); 2101 copyJob->setParentJob(q); // in case of rename dialog 2102 copyJob->setSourceSize((*it).size); 2103 copyJob->setModificationTime((*it).mtime); 2104 newjob = copyJob; 2105 qCDebug(KIO_COPYJOB_DEBUG) << "Copying" << uSource << "to" << uDest; 2106 m_currentSrcURL = uSource; 2107 m_currentDestURL = uDest; 2108 m_bURLDirty = true; 2109 } 2110 q->addSubjob(newjob); 2111 q->connect(newjob, &Job::processedSize, q, [this](KJob *job, qulonglong processedSize) { 2112 slotProcessedSize(job, processedSize); 2113 }); 2114 q->connect(newjob, &Job::totalSize, q, [this](KJob *job, qulonglong totalSize) { 2115 slotTotalSize(job, totalSize); 2116 }); 2117 } 2118 2119 void CopyJobPrivate::deleteNextDir() 2120 { 2121 Q_Q(CopyJob); 2122 if (m_mode == CopyJob::Move && !dirsToRemove.isEmpty()) { // some dirs to delete ? 2123 state = STATE_DELETING_DIRS; 2124 m_bURLDirty = true; 2125 // Take first dir to delete out of list - last ones first ! 2126 QList<QUrl>::Iterator it = --dirsToRemove.end(); 2127 SimpleJob *job = KIO::rmdir(*it); 2128 job->setParentJob(q); 2129 dirsToRemove.erase(it); 2130 q->addSubjob(job); 2131 } else { 2132 // This step is done, move on 2133 state = STATE_SETTING_DIR_ATTRIBUTES; 2134 m_directoriesCopiedIterator = m_directoriesCopied.cbegin(); 2135 setNextDirAttribute(); 2136 } 2137 } 2138 2139 void CopyJobPrivate::setNextDirAttribute() 2140 { 2141 Q_Q(CopyJob); 2142 while (m_directoriesCopiedIterator != m_directoriesCopied.cend() && !(*m_directoriesCopiedIterator).mtime.isValid()) { 2143 ++m_directoriesCopiedIterator; 2144 } 2145 if (m_directoriesCopiedIterator != m_directoriesCopied.cend()) { 2146 const QUrl url = (*m_directoriesCopiedIterator).uDest; 2147 const QDateTime dt = (*m_directoriesCopiedIterator).mtime; 2148 ++m_directoriesCopiedIterator; 2149 2150 KIO::SimpleJob *job = KIO::setModificationTime(url, dt); 2151 job->setParentJob(q); 2152 q->addSubjob(job); 2153 } else { 2154 if (m_reportTimer) { 2155 m_reportTimer->stop(); 2156 } 2157 2158 q->emitResult(); 2159 } 2160 } 2161 2162 void CopyJob::emitResult() 2163 { 2164 Q_D(CopyJob); 2165 // Before we go, tell the world about the changes that were made. 2166 // Even if some error made us abort midway, we might still have done 2167 // part of the job so we better update the views! (#118583) 2168 if (!d->m_bOnlyRenames) { 2169 // If only renaming happened, KDirNotify::FileRenamed was emitted by the rename jobs 2170 QUrl url(d->m_globalDest); 2171 if (d->m_globalDestinationState != DEST_IS_DIR || d->m_asMethod) { 2172 url = url.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash); 2173 } 2174 qCDebug(KIO_COPYJOB_DEBUG) << "KDirNotify'ing FilesAdded" << url; 2175 #ifndef KIO_ANDROID_STUB 2176 org::kde::KDirNotify::emitFilesAdded(url); 2177 #endif 2178 2179 if (d->m_mode == CopyJob::Move && !d->m_successSrcList.isEmpty()) { 2180 qCDebug(KIO_COPYJOB_DEBUG) << "KDirNotify'ing FilesRemoved" << d->m_successSrcList; 2181 #ifndef KIO_ANDROID_STUB 2182 org::kde::KDirNotify::emitFilesRemoved(d->m_successSrcList); 2183 #endif 2184 } 2185 } 2186 2187 // Re-enable watching on the dirs that held the deleted/moved files 2188 if (d->m_mode == CopyJob::Move) { 2189 for (const auto &dir : d->m_parentDirs) { 2190 KDirWatch::self()->restartDirScan(dir); 2191 } 2192 } 2193 Job::emitResult(); 2194 } 2195 2196 void CopyJobPrivate::slotProcessedSize(KJob *, qulonglong data_size) 2197 { 2198 Q_Q(CopyJob); 2199 qCDebug(KIO_COPYJOB_DEBUG) << data_size; 2200 m_fileProcessedSize = data_size; 2201 2202 if (m_processedSize + m_fileProcessedSize > m_totalSize) { 2203 // Example: download any attachment from bugs.kde.org 2204 m_totalSize = m_processedSize + m_fileProcessedSize; 2205 qCDebug(KIO_COPYJOB_DEBUG) << "Adjusting m_totalSize to" << m_totalSize; 2206 q->setTotalAmount(KJob::Bytes, m_totalSize); // safety 2207 } 2208 qCDebug(KIO_COPYJOB_DEBUG) << "emit processedSize" << (unsigned long)(m_processedSize + m_fileProcessedSize); 2209 } 2210 2211 void CopyJobPrivate::slotTotalSize(KJob *, qulonglong size) 2212 { 2213 Q_Q(CopyJob); 2214 qCDebug(KIO_COPYJOB_DEBUG) << size; 2215 // Special case for copying a single file 2216 // This is because some protocols don't implement stat properly 2217 // (e.g. HTTP), and don't give us a size in some cases (redirection) 2218 // so we'd rather rely on the size given for the transfer 2219 if (m_bSingleFileCopy && size != m_totalSize) { 2220 qCDebug(KIO_COPYJOB_DEBUG) << "slotTotalSize: updating totalsize to" << size; 2221 m_totalSize = size; 2222 q->setTotalAmount(KJob::Bytes, size); 2223 } 2224 } 2225 2226 void CopyJobPrivate::slotResultDeletingDirs(KJob *job) 2227 { 2228 Q_Q(CopyJob); 2229 if (job->error()) { 2230 // Couldn't remove directory. Well, perhaps it's not empty 2231 // because the user pressed Skip for a given file in it. 2232 // Let's not display "Could not remove dir ..." for each of those dir ! 2233 } else { 2234 m_successSrcList.append(static_cast<KIO::SimpleJob *>(job)->url()); 2235 } 2236 q->removeSubjob(job); 2237 Q_ASSERT(!q->hasSubjobs()); 2238 deleteNextDir(); 2239 } 2240 2241 void CopyJobPrivate::slotResultSettingDirAttributes(KJob *job) 2242 { 2243 Q_Q(CopyJob); 2244 if (job->error()) { 2245 // Couldn't set directory attributes. Ignore the error, it can happen 2246 // with inferior file systems like VFAT. 2247 // Let's not display warnings for each dir like "cp -a" does. 2248 } 2249 q->removeSubjob(job); 2250 Q_ASSERT(!q->hasSubjobs()); 2251 setNextDirAttribute(); 2252 } 2253 2254 void CopyJobPrivate::directRenamingFailed(const QUrl &dest) 2255 { 2256 Q_Q(CopyJob); 2257 2258 qCDebug(KIO_COPYJOB_DEBUG) << "Couldn't rename" << m_currentSrcURL << "to" << dest << ", reverting to normal way, starting with stat"; 2259 qCDebug(KIO_COPYJOB_DEBUG) << "KIO::stat on" << m_currentSrcURL; 2260 2261 KIO::Job *job = KIO::stat(m_currentSrcURL, KIO::HideProgressInfo); 2262 state = STATE_STATING; 2263 q->addSubjob(job); 2264 m_bOnlyRenames = false; 2265 } 2266 2267 // We were trying to do a direct renaming, before even stat'ing 2268 void CopyJobPrivate::slotResultRenaming(KJob *job) 2269 { 2270 Q_Q(CopyJob); 2271 int err = job->error(); 2272 const QString errText = job->errorText(); 2273 // Merge metadata from subjob 2274 KIO::Job *kiojob = qobject_cast<KIO::Job *>(job); 2275 Q_ASSERT(kiojob); 2276 m_incomingMetaData += kiojob->metaData(); 2277 q->removeSubjob(job); 2278 Q_ASSERT(!q->hasSubjobs()); 2279 // Determine dest again 2280 QUrl dest = m_dest; 2281 if (destinationState == DEST_IS_DIR && !m_asMethod) { 2282 dest = addPathToUrl(dest, m_currentSrcURL.fileName()); 2283 } 2284 auto *askUserActionInterface = KIO::delegateExtension<KIO::AskUserActionInterface *>(q); 2285 2286 if (err) { 2287 // This code is similar to CopyJobPrivate::slotResultErrorCopyingFiles 2288 // but here it's about the base src url being moved/renamed 2289 // (m_currentSrcURL) and its dest (m_dest), not about a single file. 2290 // It also means we already stated the dest, here. 2291 // On the other hand we haven't stated the src yet (we skipped doing it 2292 // to save time, since it's not necessary to rename directly!)... 2293 2294 // Existing dest? 2295 if (err == ERR_DIR_ALREADY_EXIST || err == ERR_FILE_ALREADY_EXIST || err == ERR_IDENTICAL_FILES) { 2296 // Should we skip automatically ? 2297 bool isDir = (err == ERR_DIR_ALREADY_EXIST); // ## technically, isDir means "source is dir", not "dest is dir" ####### 2298 if ((isDir && m_bAutoSkipDirs) || (!isDir && m_bAutoSkipFiles)) { 2299 // Move on to next source url 2300 ++m_filesHandledByDirectRename; 2301 skipSrc(isDir); 2302 return; 2303 } else if ((isDir && m_bOverwriteAllDirs) || (!isDir && m_bOverwriteAllFiles)) { 2304 ; // nothing to do, stat+copy+del will overwrite 2305 } else if ((isDir && m_bAutoRenameDirs) || (!isDir && m_bAutoRenameFiles)) { 2306 QUrl destDirectory = m_currentDestURL.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash); // m_currendDestURL includes filename 2307 const QString newName = KFileUtils::suggestName(destDirectory, m_currentDestURL.fileName()); 2308 2309 m_dest = destDirectory; 2310 m_dest.setPath(Utils::concatPaths(m_dest.path(), newName)); 2311 Q_EMIT q->renamed(q, dest, m_dest); 2312 KIO::Job *job = KIO::stat(m_dest, StatJob::DestinationSide, KIO::StatDefaultDetails, KIO::HideProgressInfo); 2313 state = STATE_STATING; 2314 destinationState = DEST_NOT_STATED; 2315 q->addSubjob(job); 2316 return; 2317 } else if (askUserActionInterface) { 2318 // we lack mtime info for both the src (not stated) 2319 // and the dest (stated but this info wasn't stored) 2320 // Let's do it for local files, at least 2321 KIO::filesize_t sizeSrc = KIO::invalidFilesize; 2322 KIO::filesize_t sizeDest = KIO::invalidFilesize; 2323 QDateTime ctimeSrc; 2324 QDateTime ctimeDest; 2325 QDateTime mtimeSrc; 2326 QDateTime mtimeDest; 2327 2328 bool destIsDir = err == ERR_DIR_ALREADY_EXIST; 2329 2330 // ## TODO we need to stat the source using KIO::stat 2331 // so that this code is properly network-transparent. 2332 2333 if (m_currentSrcURL.isLocalFile()) { 2334 QFileInfo info(m_currentSrcURL.toLocalFile()); 2335 if (info.exists()) { 2336 sizeSrc = info.size(); 2337 ctimeSrc = info.birthTime(); 2338 mtimeSrc = info.lastModified(); 2339 isDir = info.isDir(); 2340 } 2341 } 2342 if (dest.isLocalFile()) { 2343 QFileInfo destInfo(dest.toLocalFile()); 2344 if (destInfo.exists()) { 2345 sizeDest = destInfo.size(); 2346 ctimeDest = destInfo.birthTime(); 2347 mtimeDest = destInfo.lastModified(); 2348 destIsDir = destInfo.isDir(); 2349 } 2350 } 2351 2352 // If src==dest, use "overwrite-itself" 2353 RenameDialog_Options options = (m_currentSrcURL == dest) ? RenameDialog_OverwriteItself : RenameDialog_Overwrite; 2354 if (!isDir && destIsDir) { 2355 // We can't overwrite a dir with a file. 2356 options = RenameDialog_Options(); 2357 } 2358 2359 if (m_srcList.count() > 1) { 2360 options |= RenameDialog_Options(RenameDialog_MultipleItems | RenameDialog_Skip); 2361 } 2362 2363 if (destIsDir) { 2364 options |= RenameDialog_DestIsDirectory; 2365 } 2366 2367 if (m_reportTimer) { 2368 m_reportTimer->stop(); 2369 } 2370 2371 RenameDialog_Result r; 2372 if (m_bOverwriteWhenOlder && mtimeSrc.isValid() && mtimeDest.isValid()) { 2373 if (mtimeSrc > mtimeDest) { 2374 qCDebug(KIO_COPYJOB_DEBUG) << "dest is older, overwriting" << dest; 2375 r = Result_Overwrite; 2376 } else { 2377 qCDebug(KIO_COPYJOB_DEBUG) << "dest is newer, skipping" << dest; 2378 r = Result_Skip; 2379 } 2380 2381 processDirectRenamingConflictResult(r, isDir, destIsDir, mtimeSrc, mtimeDest, dest, QUrl{}); 2382 return; 2383 } else { 2384 auto renameSignal = &KIO::AskUserActionInterface::askUserRenameResult; 2385 QObject::connect(askUserActionInterface, renameSignal, q, [=](RenameDialog_Result result, const QUrl &newUrl, KJob *parentJob) { 2386 Q_ASSERT(parentJob == q); 2387 // Only receive askUserRenameResult once per rename dialog 2388 QObject::disconnect(askUserActionInterface, renameSignal, q, nullptr); 2389 2390 processDirectRenamingConflictResult(result, isDir, destIsDir, mtimeSrc, mtimeDest, dest, newUrl); 2391 }); 2392 2393 const QString title = err != ERR_DIR_ALREADY_EXIST ? i18n("File Already Exists") : i18n("Already Exists as Folder"); 2394 2395 /* clang-format off */ 2396 askUserActionInterface->askUserRename(q, title, 2397 m_currentSrcURL, dest, 2398 options, 2399 sizeSrc, sizeDest, 2400 ctimeSrc, ctimeDest, 2401 mtimeSrc, mtimeDest); 2402 /* clang-format on */ 2403 2404 return; 2405 } 2406 } else if (err != KIO::ERR_UNSUPPORTED_ACTION) { 2407 // Dest already exists, and job is not interactive -> abort with error 2408 q->setError(err); 2409 q->setErrorText(errText); 2410 q->emitResult(); 2411 return; 2412 } 2413 } else if (err != KIO::ERR_UNSUPPORTED_ACTION) { 2414 qCDebug(KIO_COPYJOB_DEBUG) << "Couldn't rename" << m_currentSrcURL << "to" << dest << ", aborting"; 2415 q->setError(err); 2416 q->setErrorText(errText); 2417 q->emitResult(); 2418 return; 2419 } 2420 2421 directRenamingFailed(dest); 2422 return; 2423 } 2424 2425 // No error 2426 qCDebug(KIO_COPYJOB_DEBUG) << "Renaming succeeded, move on"; 2427 ++m_processedFiles; 2428 ++m_filesHandledByDirectRename; 2429 // Emit copyingDone for FileUndoManager to remember what we did. 2430 // Use resolved URL m_currentSrcURL since that's what we just used for renaming. See bug 391606 and kio_desktop's testTrashAndUndo(). 2431 const bool srcIsDir = false; // # TODO: we just don't know, since we never stat'ed it 2432 Q_EMIT q->copyingDone(q, m_currentSrcURL, finalDestUrl(m_currentSrcURL, dest), QDateTime() /*mtime unknown, and not needed*/, srcIsDir, true); 2433 m_successSrcList.append(*m_currentStatSrc); 2434 statNextSrc(); 2435 } 2436 2437 void CopyJobPrivate::processDirectRenamingConflictResult(RenameDialog_Result result, 2438 bool srcIsDir, 2439 bool destIsDir, 2440 const QDateTime &mtimeSrc, 2441 const QDateTime &mtimeDest, 2442 const QUrl &dest, 2443 const QUrl &newUrl) 2444 { 2445 Q_Q(CopyJob); 2446 2447 if (m_reportTimer) { 2448 m_reportTimer->start(s_reportTimeout); 2449 } 2450 2451 if (result == Result_OverwriteWhenOlder) { 2452 m_bOverwriteWhenOlder = true; 2453 if (mtimeSrc > mtimeDest) { 2454 qCDebug(KIO_COPYJOB_DEBUG) << "dest is older, overwriting" << dest; 2455 result = Result_Overwrite; 2456 } else { 2457 qCDebug(KIO_COPYJOB_DEBUG) << "dest is newer, skipping" << dest; 2458 result = Result_Skip; 2459 } 2460 } 2461 2462 switch (result) { 2463 case Result_Cancel: { 2464 q->setError(ERR_USER_CANCELED); 2465 q->emitResult(); 2466 return; 2467 } 2468 case Result_AutoRename: 2469 if (srcIsDir) { 2470 m_bAutoRenameDirs = true; 2471 } else { 2472 m_bAutoRenameFiles = true; 2473 } 2474 // fall through 2475 Q_FALLTHROUGH(); 2476 case Result_Rename: { 2477 // Set m_dest to the chosen destination 2478 // This is only for this src url; the next one will revert to m_globalDest 2479 m_dest = newUrl; 2480 Q_EMIT q->renamed(q, dest, m_dest); // For e.g. KPropertiesDialog 2481 KIO::Job *job = KIO::stat(m_dest, StatJob::DestinationSide, KIO::StatDefaultDetails, KIO::HideProgressInfo); 2482 state = STATE_STATING; 2483 destinationState = DEST_NOT_STATED; 2484 q->addSubjob(job); 2485 return; 2486 } 2487 case Result_AutoSkip: 2488 if (srcIsDir) { 2489 m_bAutoSkipDirs = true; 2490 } else { 2491 m_bAutoSkipFiles = true; 2492 } 2493 // fall through 2494 Q_FALLTHROUGH(); 2495 case Result_Skip: 2496 // Move on to next url 2497 ++m_filesHandledByDirectRename; 2498 skipSrc(srcIsDir); 2499 return; 2500 case Result_OverwriteAll: 2501 if (destIsDir) { 2502 m_bOverwriteAllDirs = true; 2503 } else { 2504 m_bOverwriteAllFiles = true; 2505 } 2506 break; 2507 case Result_Overwrite: 2508 // Add to overwrite list 2509 // Note that we add dest, not m_dest. 2510 // This ensures that when moving several urls into a dir (m_dest), 2511 // we only overwrite for the current one, not for all. 2512 // When renaming a single file (m_asMethod), it makes no difference. 2513 qCDebug(KIO_COPYJOB_DEBUG) << "adding to overwrite list: " << dest.path(); 2514 m_overwriteList.insert(dest.path()); 2515 break; 2516 default: 2517 // Q_ASSERT( 0 ); 2518 break; 2519 } 2520 2521 directRenamingFailed(dest); 2522 } 2523 2524 void CopyJob::slotResult(KJob *job) 2525 { 2526 Q_D(CopyJob); 2527 qCDebug(KIO_COPYJOB_DEBUG) << "d->state=" << (int)d->state; 2528 // In each case, what we have to do is : 2529 // 1 - check for errors and treat them 2530 // 2 - removeSubjob(job); 2531 // 3 - decide what to do next 2532 2533 switch (d->state) { 2534 case STATE_STATING: // We were trying to stat a src url or the dest 2535 d->slotResultStating(job); 2536 break; 2537 case STATE_RENAMING: { // We were trying to do a direct renaming, before even stat'ing 2538 d->slotResultRenaming(job); 2539 break; 2540 } 2541 case STATE_LISTING: // recursive listing finished 2542 qCDebug(KIO_COPYJOB_DEBUG) << "totalSize:" << (unsigned int)d->m_totalSize << "files:" << d->files.count() << "d->dirs:" << d->dirs.count(); 2543 // Was there an error ? 2544 if (job->error()) { 2545 Job::slotResult(job); // will set the error and emit result(this) 2546 return; 2547 } 2548 2549 removeSubjob(job); 2550 Q_ASSERT(!hasSubjobs()); 2551 2552 d->statNextSrc(); 2553 break; 2554 case STATE_CREATING_DIRS: 2555 d->slotResultCreatingDirs(job); 2556 break; 2557 case STATE_CONFLICT_CREATING_DIRS: 2558 d->slotResultConflictCreatingDirs(job); 2559 break; 2560 case STATE_COPYING_FILES: 2561 d->slotResultCopyingFiles(job); 2562 break; 2563 case STATE_CONFLICT_COPYING_FILES: 2564 d->slotResultErrorCopyingFiles(job); 2565 break; 2566 case STATE_DELETING_DIRS: 2567 d->slotResultDeletingDirs(job); 2568 break; 2569 case STATE_SETTING_DIR_ATTRIBUTES: 2570 d->slotResultSettingDirAttributes(job); 2571 break; 2572 default: 2573 Q_ASSERT(0); 2574 } 2575 } 2576 2577 void KIO::CopyJob::setDefaultPermissions(bool b) 2578 { 2579 d_func()->m_defaultPermissions = b; 2580 } 2581 2582 KIO::CopyJob::CopyMode KIO::CopyJob::operationMode() const 2583 { 2584 return d_func()->m_mode; 2585 } 2586 2587 void KIO::CopyJob::setAutoSkip(bool autoSkip) 2588 { 2589 d_func()->m_bAutoSkipFiles = autoSkip; 2590 d_func()->m_bAutoSkipDirs = autoSkip; 2591 } 2592 2593 void KIO::CopyJob::setAutoRename(bool autoRename) 2594 { 2595 d_func()->m_bAutoRenameFiles = autoRename; 2596 d_func()->m_bAutoRenameDirs = autoRename; 2597 } 2598 2599 void KIO::CopyJob::setWriteIntoExistingDirectories(bool overwriteAll) // #65926 2600 { 2601 d_func()->m_bOverwriteAllDirs = overwriteAll; 2602 } 2603 2604 CopyJob *KIO::copy(const QUrl &src, const QUrl &dest, JobFlags flags) 2605 { 2606 qCDebug(KIO_COPYJOB_DEBUG) << "src=" << src << "dest=" << dest; 2607 QList<QUrl> srcList; 2608 srcList.append(src); 2609 return CopyJobPrivate::newJob(srcList, dest, CopyJob::Copy, false, flags); 2610 } 2611 2612 CopyJob *KIO::copyAs(const QUrl &src, const QUrl &dest, JobFlags flags) 2613 { 2614 qCDebug(KIO_COPYJOB_DEBUG) << "src=" << src << "dest=" << dest; 2615 QList<QUrl> srcList; 2616 srcList.append(src); 2617 return CopyJobPrivate::newJob(srcList, dest, CopyJob::Copy, true, flags); 2618 } 2619 2620 CopyJob *KIO::copy(const QList<QUrl> &src, const QUrl &dest, JobFlags flags) 2621 { 2622 qCDebug(KIO_COPYJOB_DEBUG) << src << dest; 2623 return CopyJobPrivate::newJob(src, dest, CopyJob::Copy, false, flags); 2624 } 2625 2626 CopyJob *KIO::move(const QUrl &src, const QUrl &dest, JobFlags flags) 2627 { 2628 qCDebug(KIO_COPYJOB_DEBUG) << src << dest; 2629 QList<QUrl> srcList; 2630 srcList.append(src); 2631 CopyJob *job = CopyJobPrivate::newJob(srcList, dest, CopyJob::Move, false, flags); 2632 if (job->uiDelegateExtension()) { 2633 job->uiDelegateExtension()->createClipboardUpdater(job, JobUiDelegateExtension::UpdateContent); 2634 } 2635 return job; 2636 } 2637 2638 CopyJob *KIO::moveAs(const QUrl &src, const QUrl &dest, JobFlags flags) 2639 { 2640 qCDebug(KIO_COPYJOB_DEBUG) << src << dest; 2641 QList<QUrl> srcList; 2642 srcList.append(src); 2643 CopyJob *job = CopyJobPrivate::newJob(srcList, dest, CopyJob::Move, true, flags); 2644 if (job->uiDelegateExtension()) { 2645 job->uiDelegateExtension()->createClipboardUpdater(job, JobUiDelegateExtension::UpdateContent); 2646 } 2647 return job; 2648 } 2649 2650 CopyJob *KIO::move(const QList<QUrl> &src, const QUrl &dest, JobFlags flags) 2651 { 2652 qCDebug(KIO_COPYJOB_DEBUG) << src << dest; 2653 CopyJob *job = CopyJobPrivate::newJob(src, dest, CopyJob::Move, false, flags); 2654 if (job->uiDelegateExtension()) { 2655 job->uiDelegateExtension()->createClipboardUpdater(job, JobUiDelegateExtension::UpdateContent); 2656 } 2657 return job; 2658 } 2659 2660 CopyJob *KIO::link(const QUrl &src, const QUrl &destDir, JobFlags flags) 2661 { 2662 QList<QUrl> srcList; 2663 srcList.append(src); 2664 return CopyJobPrivate::newJob(srcList, destDir, CopyJob::Link, false, flags); 2665 } 2666 2667 CopyJob *KIO::link(const QList<QUrl> &srcList, const QUrl &destDir, JobFlags flags) 2668 { 2669 return CopyJobPrivate::newJob(srcList, destDir, CopyJob::Link, false, flags); 2670 } 2671 2672 CopyJob *KIO::linkAs(const QUrl &src, const QUrl &destDir, JobFlags flags) 2673 { 2674 QList<QUrl> srcList; 2675 srcList.append(src); 2676 return CopyJobPrivate::newJob(srcList, destDir, CopyJob::Link, true, flags); 2677 } 2678 2679 CopyJob *KIO::trash(const QUrl &src, JobFlags flags) 2680 { 2681 QList<QUrl> srcList; 2682 srcList.append(src); 2683 return CopyJobPrivate::newJob(srcList, QUrl(QStringLiteral("trash:/")), CopyJob::Move, false, flags); 2684 } 2685 2686 CopyJob *KIO::trash(const QList<QUrl> &srcList, JobFlags flags) 2687 { 2688 return CopyJobPrivate::newJob(srcList, QUrl(QStringLiteral("trash:/")), CopyJob::Move, false, flags); 2689 } 2690 2691 #include "moc_copyjob.cpp"