File indexing completed on 2025-04-20 11:08:41
0001 // clang-format off 0002 /** 0003 * KDiff3 - Text Diff And Merge Tool 0004 * 0005 * SPDX-FileCopyrightText: 2021 Michael Reeves <reeves.87@gmail.com> 0006 * SPDX-License-Identifier: GPL-2.0-or-later 0007 * 0008 */ 0009 // clang-format on 0010 0011 #include "DefaultFileAccessJobHandler.h" 0012 0013 #include "compat.h" 0014 #include "defmac.h" 0015 #include "fileaccess.h" 0016 #include "IgnoreList.h" 0017 #include "Logging.h" 0018 #include "progress.h" 0019 #include "ProgressProxyExtender.h" 0020 #include "TypeUtils.h" 0021 0022 #include <algorithm> 0023 #include <list> 0024 #include <utility> 0025 0026 #include <QFileInfoList> 0027 #include <QDir> 0028 #include <QUrl> 0029 0030 #include <KIO/CopyJob> 0031 #include <KIO/FileCopyJob> 0032 #include <KIO/ListJob> 0033 #include <KIO/MkdirJob> 0034 #include <KIO/SimpleJob> 0035 #include <KIO/StatJob> 0036 #include <KIO/TransferJob> 0037 #include <KJob> 0038 #include <KLocalizedString> 0039 0040 #include <KIO/JobUiDelegate> 0041 #include <KMessageBox> 0042 0043 bool DefaultFileAccessJobHandler::stat(bool bWantToWrite) 0044 { 0045 m_bSuccess = false; 0046 #if HAS_KFKIO 0047 mFileAccess->setStatusText(QString()); 0048 0049 //For testing kf6 build. 0050 #if KF_VERSION < KF_VERSION_CHECK(5,240,0) 0051 KIO::StatJob* pStatJob = KIO::statDetails(mFileAccess->url(), 0052 bWantToWrite ? KIO::StatJob::DestinationSide : KIO::StatJob::SourceSide, 0053 KIO::StatDefaultDetails, KIO::HideProgressInfo); 0054 #else 0055 KIO::StatJob* pStatJob = KIO::stat(mFileAccess->url(), 0056 bWantToWrite ? KIO::StatJob::DestinationSide : KIO::StatJob::SourceSide, 0057 KIO::StatDefaultDetails, KIO::HideProgressInfo); 0058 0059 #endif 0060 chk_connect(pStatJob, &KIO::StatJob::result, this, &DefaultFileAccessJobHandler::slotStatResult); 0061 chk_connect(pStatJob, &KIO::StatJob::finished, this, &DefaultFileAccessJobHandler::slotJobEnded); 0062 0063 ProgressProxy::enterEventLoop(pStatJob, i18n("Getting file status: %1", mFileAccess->prettyAbsPath())); 0064 #endif 0065 return m_bSuccess; 0066 } 0067 0068 #if HAS_KFKIO 0069 void DefaultFileAccessJobHandler::slotStatResult(KJob* pJob) 0070 { 0071 qint32 err = pJob->error(); 0072 if(err != KJob::NoError) 0073 { 0074 qCDebug(kdiffFileAccess) << "slotStatResult: pJob->error() = " << pJob->error(); 0075 if(err != KIO::ERR_DOES_NOT_EXIST) 0076 { 0077 pJob->uiDelegate()->showErrorMessage(); 0078 m_bSuccess = false; 0079 mFileAccess->reset(); 0080 } 0081 else 0082 { 0083 mFileAccess->doError(); 0084 m_bSuccess = true; 0085 } 0086 } 0087 else 0088 { 0089 m_bSuccess = true; 0090 0091 const KIO::UDSEntry e = static_cast<KIO::StatJob*>(pJob)->statResult(); 0092 0093 mFileAccess->setFromUdsEntry(e, mFileAccess->parent()); 0094 m_bSuccess = mFileAccess->isValid(); 0095 } 0096 } 0097 #endif 0098 0099 bool DefaultFileAccessJobHandler::get(void* pDestBuffer, long maxLength) 0100 { 0101 ProgressProxyExtender pp; // Implicitly used in slotPercent() 0102 0103 if(maxLength > 0 && !ProgressProxy::wasCancelled()) 0104 { 0105 m_bSuccess = false; 0106 #if HAS_KFKIO 0107 KIO::TransferJob* pJob = KIO::get(mFileAccess->url(), KIO::NoReload); 0108 m_transferredBytes = 0; 0109 m_pTransferBuffer = (char*)pDestBuffer; 0110 m_maxLength = maxLength; 0111 mFileAccess->setStatusText(QString()); 0112 0113 chk_connect(pJob, &KIO::TransferJob::result, this, &DefaultFileAccessJobHandler::slotSimpleJobResult); 0114 chk_connect(pJob, &KIO::TransferJob::finished, this, &DefaultFileAccessJobHandler::slotJobEnded); 0115 chk_connect(pJob, &KIO::TransferJob::data, this, &DefaultFileAccessJobHandler::slotGetData); 0116 chk_connect(pJob, SIGNAL(percent(KJob*,ulong)), &pp, SLOT(slotPercent(KJob*,ulong))); 0117 0118 ProgressProxy::enterEventLoop(pJob, i18nc("Message for progress dialog %1 = path to file", "Reading file: %1", mFileAccess->prettyAbsPath())); 0119 #endif 0120 return m_bSuccess; 0121 } 0122 else 0123 return true; 0124 } 0125 0126 #if HAS_KFKIO 0127 void DefaultFileAccessJobHandler::slotGetData(KJob* pJob, const QByteArray& newData) 0128 { 0129 if(pJob->error() != KJob::NoError) 0130 { 0131 qCDebug(kdiffFileAccess) << "slotGetData: pJob->error() = " << pJob->error(); 0132 pJob->uiDelegate()->showErrorMessage(); 0133 } 0134 else 0135 { 0136 qint64 length = std::min(qint64(newData.size()), m_maxLength - m_transferredBytes); 0137 ::memcpy(m_pTransferBuffer + m_transferredBytes, newData.data(), newData.size()); 0138 m_transferredBytes += length; 0139 } 0140 } 0141 #endif 0142 0143 bool DefaultFileAccessJobHandler::put(const void* pSrcBuffer, long maxLength, bool bOverwrite, bool bResume, qint32 permissions) 0144 { 0145 ProgressProxyExtender pp; // Implicitly used in slotPercent() 0146 if(maxLength > 0) 0147 { 0148 m_bSuccess = false; 0149 #if HAS_KFKIO 0150 KIO::TransferJob* pJob = KIO::put(mFileAccess->url(), permissions, 0151 KIO::HideProgressInfo | (bOverwrite ? KIO::Overwrite : KIO::DefaultFlags) | (bResume ? KIO::Resume : KIO::DefaultFlags)); 0152 m_transferredBytes = 0; 0153 m_pTransferBuffer = (char*)pSrcBuffer; 0154 m_maxLength = maxLength; 0155 mFileAccess->setStatusText(QString()); 0156 0157 chk_connect(pJob, &KIO::TransferJob::result, this, &DefaultFileAccessJobHandler::slotPutJobResult); 0158 chk_connect(pJob, &KIO::TransferJob::finished, this, &DefaultFileAccessJobHandler::slotJobEnded); 0159 chk_connect(pJob, &KIO::TransferJob::dataReq, this, &DefaultFileAccessJobHandler::slotPutData); 0160 chk_connect(pJob, SIGNAL(percent(KJob*,ulong)), &pp, SLOT(slotPercent(KJob*,ulong))); 0161 0162 ProgressProxy::enterEventLoop(pJob, i18nc("Message for progress dialog %1 = path to file", "Writing file: %1", mFileAccess->prettyAbsPath())); 0163 #endif 0164 return m_bSuccess; 0165 } 0166 else 0167 return true; 0168 } 0169 0170 #if HAS_KFKIO 0171 void DefaultFileAccessJobHandler::slotPutData(KIO::Job* pJob, QByteArray& data) 0172 { 0173 if(pJob->error() != KJob::NoError) 0174 { 0175 qCDebug(kdiffFileAccess) << "slotPutData: pJob->error() = " << pJob->error(); 0176 pJob->uiDelegate()->showErrorMessage(); 0177 } 0178 else 0179 { 0180 /* 0181 Think twice before doing this in new code. 0182 The maxChunkSize must be able to fit a 32-bit int. Given that the fallowing is safe for qt5. 0183 Qt6 resolves this issue as it uses 64 bit sizes. 0184 */ 0185 qint64 maxChunkSize = 100000; 0186 qint64 length = std::min(maxChunkSize, m_maxLength - m_transferredBytes); 0187 if(length > 0) 0188 { 0189 data.resize((QtSizeType)length); 0190 if(data.size() == (QtSizeType)length) 0191 { 0192 ::memcpy(data.data(), m_pTransferBuffer + m_transferredBytes, data.size()); 0193 m_transferredBytes += length; 0194 } 0195 } 0196 else 0197 { 0198 KMessageBox::error(g_pProgressDialog, i18n("Out of memory")); 0199 data.resize(0); 0200 m_bSuccess = false; 0201 } 0202 } 0203 } 0204 0205 void DefaultFileAccessJobHandler::slotPutJobResult(KJob* pJob) 0206 { 0207 if(pJob->error() != KJob::NoError) 0208 { 0209 qCDebug(kdiffFileAccess) << "slotPutJobResult: pJob->error() = " << pJob->error(); 0210 pJob->uiDelegate()->showErrorMessage(); 0211 } 0212 else 0213 { 0214 m_bSuccess = (m_transferredBytes == m_maxLength); // Special success condition 0215 } 0216 } 0217 #endif 0218 0219 bool DefaultFileAccessJobHandler::mkDirImp(const QString& dirName) 0220 { 0221 if(dirName.isEmpty()) 0222 return false; 0223 0224 FileAccess dir(dirName); 0225 if(dir.isLocal()) 0226 { 0227 return QDir().mkdir(dir.absoluteFilePath()); 0228 } 0229 else 0230 { 0231 m_bSuccess = false; 0232 #if HAS_KFKIO 0233 KIO::SimpleJob* pJob = KIO::mkdir(dir.url()); 0234 chk_connect(pJob, &KIO::SimpleJob::result, this, &DefaultFileAccessJobHandler::slotSimpleJobResult); 0235 chk_connect(pJob, &KIO::SimpleJob::finished, this, &DefaultFileAccessJobHandler::slotJobEnded); 0236 0237 ProgressProxy::enterEventLoop(pJob, i18nc("Message for progress dialog %1 = path to file", "Making folder: %1", dirName)); 0238 #endif 0239 return m_bSuccess; 0240 } 0241 } 0242 0243 bool DefaultFileAccessJobHandler::rmDirImp(const QString& dirName) 0244 { 0245 if(dirName.isEmpty()) 0246 return false; 0247 0248 FileAccess fa(dirName); 0249 if(fa.isLocal()) 0250 { 0251 return QDir().rmdir(fa.absoluteFilePath()); 0252 } 0253 else 0254 { 0255 m_bSuccess = false; 0256 #if HAS_KFKIO 0257 KIO::SimpleJob* pJob = KIO::rmdir(fa.url()); 0258 chk_connect(pJob, &KIO::SimpleJob::result, this, &DefaultFileAccessJobHandler::slotSimpleJobResult); 0259 chk_connect(pJob, &KIO::SimpleJob::finished, this, &DefaultFileAccessJobHandler::slotJobEnded); 0260 0261 ProgressProxy::enterEventLoop(pJob, i18nc("Message for progress dialog %1 = path to file", "Removing folder: %1", dirName)); 0262 #endif 0263 return m_bSuccess; 0264 } 0265 } 0266 0267 bool DefaultFileAccessJobHandler::removeFile(const QUrl& fileName) 0268 { 0269 if(fileName.isEmpty()) 0270 return false; 0271 else 0272 { 0273 m_bSuccess = false; 0274 #if HAS_KFKIO 0275 KIO::SimpleJob* pJob = KIO::file_delete(fileName, KIO::HideProgressInfo); 0276 chk_connect(pJob, &KIO::SimpleJob::result, this, &DefaultFileAccessJobHandler::slotSimpleJobResult); 0277 chk_connect(pJob, &KIO::SimpleJob::finished, this, &DefaultFileAccessJobHandler::slotJobEnded); 0278 0279 ProgressProxy::enterEventLoop(pJob, i18nc("Message for progress dialog %1 = path to file", "Removing file: %1", FileAccess::prettyAbsPath(fileName))); 0280 #else 0281 if(FileAccess::isLocal(fileName)) 0282 m_bSuccess = QFile::remove(fileName) 0283 #endif 0284 return m_bSuccess; 0285 } 0286 } 0287 0288 bool DefaultFileAccessJobHandler::symLink(const QUrl& linkTarget, const QUrl& linkLocation) 0289 { 0290 if(linkTarget.isEmpty() || linkLocation.isEmpty()) 0291 return false; 0292 else 0293 { 0294 m_bSuccess = false; 0295 #if HAS_KFKIO 0296 KIO::CopyJob* pJob = KIO::link(linkTarget, linkLocation, KIO::HideProgressInfo); 0297 chk_connect(pJob, &KIO::CopyJob::result, this, &DefaultFileAccessJobHandler::slotSimpleJobResult); 0298 chk_connect(pJob, &KIO::CopyJob::finished, this, &DefaultFileAccessJobHandler::slotJobEnded); 0299 0300 ProgressProxy::enterEventLoop(pJob, 0301 i18n("Creating symbolic link: %1 -> %2", FileAccess::prettyAbsPath(linkLocation), FileAccess::prettyAbsPath(linkTarget))); 0302 #else 0303 //TODO: Not implemented. 0304 //if(FileAccess::isLocal(fileName)) 0305 // m_bSuccess = QFile::link(linkTarget, linkLocation) 0306 #endif 0307 return m_bSuccess; 0308 } 0309 } 0310 0311 bool DefaultFileAccessJobHandler::rename(const FileAccess& destFile) 0312 { 0313 if(destFile.fileName().isEmpty()) 0314 return false; 0315 0316 if(mFileAccess->isLocal() && destFile.isLocal()) 0317 { 0318 return QDir().rename(mFileAccess->absoluteFilePath(), destFile.absoluteFilePath()); 0319 } 0320 else 0321 { 0322 ProgressProxyExtender pp; 0323 qint32 permissions = -1; 0324 m_bSuccess = false; 0325 KIO::FileCopyJob* pJob = KIO::file_move(mFileAccess->url(), destFile.url(), permissions, KIO::HideProgressInfo); 0326 chk_connect(pJob, &KIO::FileCopyJob::result, this, &DefaultFileAccessJobHandler::slotSimpleJobResult); 0327 chk_connect(pJob, SIGNAL(percent(KJob*,ulong)), &pp, SLOT(slotPercent(KJob*,ulong))); 0328 chk_connect(pJob, &KIO::FileCopyJob::finished, this, &DefaultFileAccessJobHandler::slotJobEnded); 0329 0330 ProgressProxy::enterEventLoop(pJob, 0331 i18n("Renaming file: %1 -> %2", mFileAccess->prettyAbsPath(), destFile.prettyAbsPath())); 0332 return m_bSuccess; 0333 } 0334 } 0335 0336 void DefaultFileAccessJobHandler::slotJobEnded([[maybe_unused]] KJob* pJob) 0337 { 0338 ProgressProxy::exitEventLoop(); // Close the dialog, return from exec() 0339 } 0340 0341 void DefaultFileAccessJobHandler::slotSimpleJobResult(KJob* pJob) 0342 { 0343 if(pJob->error() != KJob::NoError) 0344 { 0345 qCDebug(kdiffFileAccess) << "slotSimpleJobResult: pJob->error() = " << pJob->error(); 0346 pJob->uiDelegate()->showErrorMessage(); 0347 } 0348 else 0349 { 0350 m_bSuccess = true; 0351 } 0352 } 0353 0354 // Copy local or remote files. 0355 bool DefaultFileAccessJobHandler::copyFile(const QString& inDest) 0356 { 0357 ProgressProxyExtender pp; 0358 FileAccess dest; 0359 dest.setFile(inDest); 0360 0361 mFileAccess->setStatusText(QString()); 0362 if(!mFileAccess->isNormal() || !dest.isNormal()) return false; 0363 0364 qint32 permissions = (mFileAccess->isExecutable() ? 0111 : 0) + (mFileAccess->isWritable() ? 0222 : 0) + (mFileAccess->isReadable() ? 0444 : 0); 0365 m_bSuccess = false; 0366 KIO::FileCopyJob* pJob = KIO::file_copy(mFileAccess->url(), dest.url(), permissions, KIO::HideProgressInfo|KIO::Overwrite); 0367 chk_connect(pJob, &KIO::FileCopyJob::result, this, &DefaultFileAccessJobHandler::slotSimpleJobResult); 0368 chk_connect(pJob, SIGNAL(percent(KJob*,ulong)), &pp, SLOT(slotPercent(KJob*,ulong))); 0369 chk_connect(pJob, &KIO::FileCopyJob::finished, this, &DefaultFileAccessJobHandler::slotJobEnded); 0370 0371 ProgressProxy::enterEventLoop(pJob, 0372 i18n("Copying file: %1 -> %2", mFileAccess->prettyAbsPath(), dest.prettyAbsPath())); 0373 0374 return m_bSuccess; 0375 // Note that the KIO-slave preserves the original date, if this is supported. 0376 } 0377 0378 bool DefaultFileAccessJobHandler::listDir(DirectoryList* pDirList, bool bRecursive, bool bFindHidden, const QString& filePattern, 0379 const QString& fileAntiPattern, const QString& dirAntiPattern, bool bFollowDirLinks, IgnoreList& ignoreList) 0380 { 0381 ProgressProxyExtender pp; 0382 m_pDirList = pDirList; 0383 m_pDirList->clear(); 0384 m_bFindHidden = bFindHidden; 0385 m_bRecursive = bRecursive; 0386 m_bFollowDirLinks = bFollowDirLinks; // Only relevant if bRecursive==true. 0387 m_fileAntiPattern = fileAntiPattern; 0388 m_filePattern = filePattern; 0389 m_dirAntiPattern = dirAntiPattern; 0390 0391 if(ProgressProxy::wasCancelled()) 0392 return true; // Cancelled is not an error. 0393 0394 ProgressProxy::setInformation(i18nc("Status message", "Reading folder: %1", mFileAccess->absoluteFilePath()), 0, false); 0395 qCInfo(kdiffFileAccess) << "Reading folder: " << mFileAccess->absoluteFilePath(); 0396 0397 if(mFileAccess->isLocal()) 0398 { 0399 m_bSuccess = true; 0400 QDir dir(mFileAccess->absoluteFilePath()); 0401 0402 dir.setSorting(QDir::Name | QDir::DirsFirst); 0403 if(bFindHidden) 0404 dir.setFilter(QDir::Files | QDir::Dirs | QDir::Hidden | QDir::System | QDir::NoDotAndDotDot); 0405 else 0406 dir.setFilter(QDir::Files | QDir::Dirs | QDir::System | QDir::NoDotAndDotDot); 0407 0408 const QFileInfoList fiList = dir.entryInfoList(); 0409 if(fiList.isEmpty()) 0410 { 0411 /* 0412 Sadly Qt provides no error information making this case ambiguous. 0413 A readability check is the best we can do. 0414 */ 0415 m_bSuccess = dir.isReadable(); 0416 } 0417 else 0418 { 0419 for(const QFileInfo& fi: fiList) // for each file... 0420 { 0421 if(ProgressProxy::wasCancelled()) 0422 break; 0423 0424 assert(fi.fileName() != "." && fi.fileName() != ".."); 0425 0426 FileAccess fa; 0427 0428 fa.setFile(mFileAccess, fi); 0429 pDirList->push_back(fa); 0430 } 0431 } 0432 } 0433 else 0434 { 0435 KIO::ListJob* pListJob = nullptr; 0436 #if KF_VERSION < KF_VERSION_CHECK(5, 240, 0) 0437 pListJob = KIO::listDir(mFileAccess->url(), KIO::HideProgressInfo, true /*bFindHidden*/); 0438 #else 0439 pListJob = KIO::listDir(mFileAccess->url(), KIO::HideProgressInfo, KIO::ListJob::ListFlag::IncludeHidden /*bFindHidden*/); 0440 #endif 0441 m_bSuccess = false; 0442 if(pListJob != nullptr) 0443 { 0444 chk_connect(pListJob, &KIO::ListJob::entries, this, &DefaultFileAccessJobHandler::slotListDirProcessNewEntries); 0445 chk_connect(pListJob, &KIO::ListJob::result, this, &DefaultFileAccessJobHandler::slotSimpleJobResult); 0446 chk_connect(pListJob, &KIO::ListJob::finished, this, &DefaultFileAccessJobHandler::slotJobEnded); 0447 chk_connect(pListJob, &KIO::ListJob::infoMessage, &pp, &ProgressProxyExtender::slotListDirInfoMessage); 0448 0449 // This line makes the transfer via fish unreliable.:-( 0450 /*if(mFileAccess->url().scheme() != u8"fish"){ 0451 chk_connect( pListJob, static_cast<void (KIO::ListJob::*)(KJob*,qint64)>(&KIO::ListJob::percent), &pp, &ProgressProxyExtender::slotPercent); 0452 }*/ 0453 0454 ProgressProxy::enterEventLoop(pListJob, 0455 i18n("Listing directory: %1", mFileAccess->prettyAbsPath())); 0456 } 0457 } 0458 0459 ignoreList.enterDir(mFileAccess->absoluteFilePath(), *pDirList); 0460 mFileAccess->filterList(mFileAccess->absoluteFilePath(), pDirList, filePattern, fileAntiPattern, dirAntiPattern, ignoreList); 0461 0462 if(bRecursive) 0463 { 0464 DirectoryList::iterator i; 0465 DirectoryList subDirsList; 0466 0467 for(i = m_pDirList->begin(); i != m_pDirList->end(); ++i) 0468 { 0469 assert(i->isValid()); 0470 if(i->isDir() && (!i->isSymLink() || m_bFollowDirLinks)) 0471 { 0472 DirectoryList dirList; 0473 i->listDir(&dirList, bRecursive, bFindHidden, 0474 filePattern, fileAntiPattern, dirAntiPattern, bFollowDirLinks, ignoreList); 0475 0476 // append data onto the main list 0477 subDirsList.splice(subDirsList.end(), dirList); 0478 } 0479 } 0480 0481 m_pDirList->splice(m_pDirList->end(), subDirsList); 0482 } 0483 0484 return m_bSuccess; 0485 } 0486 0487 void DefaultFileAccessJobHandler::slotListDirProcessNewEntries(KIO::Job*, const KIO::UDSEntryList& l) 0488 { 0489 //This function is called for non-local urls. Don't use QUrl::fromLocalFile here as it does not handle these. 0490 for(const KIO::UDSEntry& e: l) 0491 { 0492 FileAccess fa; 0493 0494 fa.setFromUdsEntry(e, mFileAccess); 0495 0496 //must be manually filtered KDE does not supply API for ignoring these. 0497 if(fa.fileName() != "." && fa.fileName() != ".." && fa.isValid()) 0498 { 0499 m_pDirList->push_back(std::move(fa)); 0500 } 0501 } 0502 }