File indexing completed on 2025-04-27 03:58:06
0001 /* ============================================================ 0002 * 0003 * This file is a part of digiKam project 0004 * https://www.digikam.org 0005 * 0006 * Date : 2008-12-10 0007 * Description : misc file operation methods 0008 * 0009 * SPDX-FileCopyrightText: 2014-2024 by Gilles Caulier <caulier dot gilles at gmail dot com> 0010 * SPDX-FileCopyrightText: 2006-2010 by Marcel Wiesweg <marcel dot wiesweg at gmx dot de> 0011 * 0012 * SPDX-License-Identifier: GPL-2.0-or-later 0013 * 0014 * ============================================================ */ 0015 0016 #include "dfileoperations.h" 0017 #include "digikam_config.h" 0018 0019 // C ANSI includes 0020 0021 #include <sys/types.h> 0022 #include <sys/stat.h> 0023 0024 // Qt includes 0025 0026 #include <QByteArray> 0027 #include <QProcess> 0028 #include <QDir> 0029 #include <QFile> 0030 #include <QFileInfo> 0031 #include <QSettings> 0032 #include <QMimeType> 0033 #include <QMimeDatabase> 0034 #include <QDesktopServices> 0035 #include <QDirIterator> 0036 #include <QStandardPaths> 0037 #include <qplatformdefs.h> 0038 #include <QRegularExpression> 0039 0040 #ifdef HAVE_DBUS 0041 # include <QDBusInterface> 0042 # include <QDBusPendingCall> 0043 #endif 0044 0045 // Local includes 0046 0047 #include "digikam_debug.h" 0048 #include "digikam_globals.h" 0049 #include "dservicemenu.h" 0050 #include "progressmanager.h" 0051 #include "metaenginesettings.h" 0052 0053 namespace Digikam 0054 { 0055 0056 bool DFileOperations::localFileRename(const QString& source, 0057 const QString& orgPath, 0058 const QString& destPath, 0059 bool ignoreSettings) 0060 { 0061 QString dest = destPath; 0062 0063 // check that we're not replacing a symlink 0064 0065 QFileInfo info(dest); 0066 0067 if (info.isSymLink()) 0068 { 0069 dest = info.symLinkTarget(); 0070 0071 qCDebug(DIGIKAM_GENERAL_LOG) << "Target filePath" << destPath 0072 << "is a symlink pointing to" << dest 0073 << ". Storing image there."; 0074 } 0075 0076 #ifndef Q_OS_WIN 0077 0078 // Store old permissions: 0079 // Just get the current umask. 0080 0081 mode_t curr_umask = umask(S_IREAD | S_IWRITE); 0082 0083 // Restore the umask. 0084 0085 umask(curr_umask); 0086 0087 // For new files respect the umask setting. 0088 0089 mode_t filePermissions = (S_IREAD | S_IWRITE | S_IROTH | S_IWOTH | S_IRGRP | S_IWGRP) & ~curr_umask; 0090 0091 // For existing files, use the mode of the original file. 0092 0093 QT_STATBUF stbuf; 0094 0095 if (QT_STAT(dest.toUtf8().constData(), &stbuf) == 0) 0096 { 0097 filePermissions = stbuf.st_mode; 0098 } 0099 0100 #endif // Q_OS_WIN 0101 0102 if (!ignoreSettings && !MetaEngineSettings::instance()->settings().updateFileTimeStamp) 0103 { 0104 copyModificationTime(source, orgPath); 0105 } 0106 0107 // remove dest file if it exist 0108 0109 if (orgPath != dest && QFile::exists(orgPath) && QFile::exists(dest)) 0110 { 0111 QFile::remove(dest); 0112 } 0113 0114 // rename tmp file to dest 0115 0116 if (!renameFile(orgPath, dest)) 0117 { 0118 return false; 0119 } 0120 0121 #ifndef Q_OS_WIN 0122 0123 // restore permissions 0124 0125 if (::chmod(dest.toUtf8().constData(), filePermissions) != 0) 0126 { 0127 qCWarning(DIGIKAM_GENERAL_LOG) << "Failed to restore file permissions for file" 0128 << dest; 0129 } 0130 0131 #endif // Q_OS_WIN 0132 0133 return true; 0134 } 0135 0136 void DFileOperations::openFilesWithDefaultApplication(const QList<QUrl>& urls) 0137 { 0138 if (urls.isEmpty()) 0139 { 0140 return; 0141 } 0142 0143 #ifdef Q_OS_LINUX 0144 0145 KService::List offers = DServiceMenu::servicesForOpenWith(urls); 0146 0147 if (!offers.isEmpty()) 0148 { 0149 KService::Ptr service = offers.first(); 0150 DServiceMenu::runFiles(service, urls); 0151 0152 return; 0153 } 0154 0155 #endif 0156 0157 Q_FOREACH (const QUrl& url, urls) 0158 { 0159 QDesktopServices::openUrl(url); 0160 } 0161 } 0162 0163 QUrl DFileOperations::getUniqueFileUrl(const QUrl& orgUrl, 0164 bool* const newurl) 0165 { 0166 if (newurl) 0167 { 0168 *newurl = false; 0169 } 0170 0171 int counter = 0; 0172 QUrl destUrl(orgUrl); 0173 QFileInfo fi(destUrl.toLocalFile()); 0174 QRegularExpression version(QRegularExpression::anchoredPattern(QLatin1String("(.+)_v(\\d+)"))); 0175 QString completeBaseName = fi.completeBaseName(); 0176 QRegularExpressionMatch match = version.match(completeBaseName); 0177 0178 if (match.hasMatch()) 0179 { 0180 completeBaseName = match.captured(1); 0181 counter = match.captured(2).toInt(); 0182 } 0183 0184 if (fi.exists()) 0185 { 0186 bool fileFound = false; 0187 0188 do 0189 { 0190 QFileInfo nfi(destUrl.toLocalFile()); 0191 0192 if (!nfi.exists()) 0193 { 0194 fileFound = false; 0195 0196 if (newurl) 0197 { 0198 *newurl = true; 0199 } 0200 } 0201 else 0202 { 0203 fileFound = true; 0204 destUrl = destUrl.adjusted(QUrl::RemoveFilename); 0205 destUrl.setPath(destUrl.path() + completeBaseName + 0206 QString::fromUtf8("_v%1.").arg(++counter) + fi.suffix()); 0207 } 0208 } 0209 while (fileFound); 0210 } 0211 0212 return destUrl; 0213 } 0214 0215 QUrl DFileOperations::getUniqueFolderUrl(const QUrl& orgUrl) 0216 { 0217 int counter = 0; 0218 QUrl destUrl(orgUrl); 0219 QFileInfo fi(destUrl.toLocalFile()); 0220 QRegularExpression version(QRegularExpression::anchoredPattern(QLatin1String("(.+)-(\\d+)"))); 0221 QString completeFileName = fi.fileName(); 0222 QRegularExpressionMatch match = version.match(completeFileName); 0223 0224 if (match.hasMatch()) 0225 { 0226 completeFileName = match.captured(1); 0227 counter = match.captured(2).toInt(); 0228 } 0229 0230 if (fi.exists()) 0231 { 0232 bool fileFound = false; 0233 0234 do 0235 { 0236 QFileInfo nfi(destUrl.toLocalFile()); 0237 0238 if (!nfi.exists()) 0239 { 0240 fileFound = false; 0241 } 0242 else 0243 { 0244 fileFound = true; 0245 destUrl = destUrl.adjusted(QUrl::RemoveFilename); 0246 destUrl.setPath(destUrl.path() + completeFileName + 0247 QString::fromUtf8("-%1").arg(++counter)); 0248 } 0249 } 0250 while (fileFound); 0251 } 0252 0253 return destUrl; 0254 } 0255 0256 void DFileOperations::openInFileManager(const QList<QUrl>& urls) 0257 { 0258 if (urls.isEmpty()) 0259 { 0260 return; 0261 } 0262 0263 bool similar = true; 0264 QUrl first = urls.first(); 0265 first = first.adjusted(QUrl::RemoveFilename); 0266 0267 Q_FOREACH (const QUrl& url, urls) 0268 { 0269 if (first != url.adjusted(QUrl::RemoveFilename)) 0270 { 0271 similar = false; 0272 break; 0273 } 0274 } 0275 0276 QList<QUrl> fileUrls; 0277 0278 if (similar) 0279 { 0280 fileUrls = urls; 0281 } 0282 else 0283 { 0284 fileUrls << urls.first(); 0285 } 0286 0287 QString path = fileUrls.first().toLocalFile(); 0288 0289 #ifdef Q_OS_WIN 0290 0291 QString dopusPath = findExecutable(QLatin1String("DOpus")); 0292 0293 if (!dopusPath.isEmpty()) 0294 { 0295 QFileInfo dopus(dopusPath); 0296 0297 if (dopus.exists()) 0298 { 0299 QFileInfo dopusrt(dopus.dir(), QLatin1String("dopusrt.exe")); 0300 0301 if (dopusrt.exists()) 0302 { 0303 QProcess process; 0304 process.setProgram(dopusrt.filePath()); 0305 process.setNativeArguments(QString::fromUtf8("/CMD Go \"%1\"") 0306 .arg(QDir::toNativeSeparators(path))); 0307 0308 if (process.startDetached()) 0309 { 0310 return; 0311 } 0312 } 0313 } 0314 } 0315 0316 QStringList args; 0317 QFileInfo info(path); 0318 0319 if (!info.isDir()) 0320 { 0321 args << QLatin1String("/select,"); 0322 } 0323 0324 args << QDir::toNativeSeparators(path); 0325 0326 if (QProcess::startDetached(QLatin1String("explorer"), args)) 0327 { 0328 return; 0329 } 0330 0331 #elif defined Q_OS_MACOS 0332 0333 QStringList args; 0334 args << QLatin1String("-e"); 0335 args << QLatin1String("tell application \"Finder\""); 0336 args << QLatin1String("-e"); 0337 args << QLatin1String("activate"); 0338 args << QLatin1String("-e"); 0339 args << QString::fromUtf8("select POSIX file \"%1\"").arg(path); 0340 args << QLatin1String("-e"); 0341 args << QLatin1String("end tell"); 0342 args << QLatin1String("-e"); 0343 args << QLatin1String("return"); 0344 0345 if (QProcess::execute(QLatin1String("/usr/bin/osascript"), args) == 0) 0346 { 0347 return; 0348 } 0349 0350 #elif defined HAVE_DBUS 0351 0352 QDBusInterface iface(QLatin1String("org.freedesktop.FileManager1"), 0353 QLatin1String("/org/freedesktop/FileManager1"), 0354 QLatin1String("org.freedesktop.FileManager1"), 0355 QDBusConnection::sessionBus()); 0356 0357 if (iface.isValid()) 0358 { 0359 QStringList uris; 0360 0361 Q_FOREACH (const QUrl& url, fileUrls) 0362 { 0363 uris << url.toString(); 0364 } 0365 0366 iface.asyncCall(QLatin1String("ShowItems"), uris, QString()); 0367 0368 return; 0369 } 0370 0371 #endif 0372 0373 QUrl url = fileUrls.first(); 0374 url = url.adjusted(QUrl::RemoveFilename | 0375 QUrl::StripTrailingSlash); 0376 0377 QDesktopServices::openUrl(url); 0378 } 0379 0380 bool DFileOperations::copyFolderRecursively(const QString& srcPath, 0381 const QString& dstPath, 0382 const QString& itemId, 0383 bool* const cancel, 0384 bool useDstPath) 0385 { 0386 QDir srcDir(srcPath); 0387 QString newCopyPath = dstPath; 0388 0389 if (!useDstPath) 0390 { 0391 newCopyPath += QLatin1Char('/') + srcDir.dirName(); 0392 } 0393 0394 if (!srcDir.mkpath(newCopyPath)) 0395 { 0396 return false; 0397 } 0398 0399 if (!itemId.isEmpty()) 0400 { 0401 int count = 0; 0402 0403 QDirIterator it(srcDir.path(), QDir::Files, 0404 QDirIterator::Subdirectories); 0405 0406 while (it.hasNext()) 0407 { 0408 it.next(); 0409 ++count; 0410 } 0411 0412 ProgressItem* const item = ProgressManager::instance()->findItembyId(itemId); 0413 0414 if (item) 0415 { 0416 item->incTotalItems(count); 0417 } 0418 } 0419 0420 Q_FOREACH (const QFileInfo& fileInfo, srcDir.entryInfoList(QDir::Files)) 0421 { 0422 QString copyPath = newCopyPath + QLatin1Char('/') + fileInfo.fileName(); 0423 0424 if (cancel && *cancel) 0425 { 0426 return false; 0427 } 0428 0429 if (!copyFile(fileInfo.filePath(), copyPath, cancel)) 0430 { 0431 return false; 0432 } 0433 0434 if (!itemId.isEmpty()) 0435 { 0436 ProgressItem* const item = ProgressManager::instance()->findItembyId(itemId); 0437 0438 if (item) 0439 { 0440 item->advance(1); 0441 } 0442 } 0443 } 0444 0445 Q_FOREACH (const QFileInfo& fileInfo, srcDir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot)) 0446 { 0447 if (!copyFolderRecursively(fileInfo.filePath(), newCopyPath, itemId, cancel, false)) 0448 { // cppcheck-suppress useStlAlgorithm 0449 return false; 0450 } 0451 } 0452 0453 return true; 0454 } 0455 0456 bool DFileOperations::copyFiles(const QStringList& srcPaths, 0457 const QString& dstPath) 0458 { 0459 Q_FOREACH (const QString& path, srcPaths) 0460 { 0461 QFileInfo fileInfo(path); 0462 QString copyPath = dstPath + QLatin1Char('/') + fileInfo.fileName(); 0463 0464 if (!copyFile(fileInfo.filePath(), copyPath)) 0465 { 0466 return false; 0467 } 0468 } 0469 0470 return true; 0471 } 0472 0473 bool DFileOperations::renameFile(const QString& srcFile, 0474 const QString& dstFile) 0475 { 0476 if (srcFile == dstFile) 0477 { 0478 return true; 0479 } 0480 0481 QFileInfo srcInfo(srcFile); 0482 QDateTime birDateTime = srcInfo.fileTime(QFileDevice::FileBirthTime); 0483 QDateTime accDateTime = srcInfo.fileTime(QFileDevice::FileAccessTime); 0484 QDateTime modDateTime = srcInfo.fileTime(QFileDevice::FileModificationTime); 0485 0486 bool ret = (!QFileInfo::exists(dstFile)); 0487 0488 if (ret) 0489 { 0490 ret = QFile::rename(srcFile, dstFile); 0491 0492 if (!ret && QFileInfo::exists(dstFile)) 0493 { 0494 QFile::remove(dstFile); 0495 } 0496 } 0497 0498 if (ret) 0499 { 0500 QFile modFile(dstFile); 0501 0502 if (modFile.open(QIODevice::WriteOnly | QIODevice::Append | QIODevice::ExistingOnly)) 0503 { 0504 if (modDateTime.isValid()) 0505 { 0506 modFile.setFileTime(modDateTime, QFileDevice::FileModificationTime); 0507 } 0508 0509 if (accDateTime.isValid()) 0510 { 0511 modFile.setFileTime(accDateTime, QFileDevice::FileAccessTime); 0512 } 0513 0514 if (birDateTime.isValid()) 0515 { 0516 modFile.setFileTime(birDateTime, QFileDevice::FileBirthTime); 0517 } 0518 0519 modFile.close(); 0520 0521 return ret; 0522 } 0523 0524 qCWarning(DIGIKAM_GENERAL_LOG) << "Failed to restore modification time for file" 0525 << dstFile; 0526 } 0527 0528 return ret; 0529 } 0530 0531 bool DFileOperations::copyFile(const QString& srcFile, 0532 const QString& dstFile, 0533 const bool* const cancel) 0534 { 0535 bool ret = true; 0536 QString tmpFile(dstFile); 0537 tmpFile += QLatin1String(".digikamtempfile.tmp"); 0538 0539 QFile sFile(srcFile); 0540 QFile dFile(tmpFile); 0541 0542 if (!sFile.open(QIODevice::ReadOnly)) 0543 { 0544 qCWarning(DIGIKAM_GENERAL_LOG) << "Failed to open source file for reading:" << srcFile; 0545 0546 return false; 0547 } 0548 0549 if (!dFile.open(QIODevice::WriteOnly | QIODevice::Unbuffered)) 0550 { 0551 qCWarning(DIGIKAM_GENERAL_LOG) << "Failed to open destination file for writing:" << tmpFile; 0552 0553 sFile.close(); 0554 0555 return false; 0556 } 0557 0558 const int MAX_IPC_SIZE = (1024 * 32); 0559 QByteArray buffer(MAX_IPC_SIZE, '\0'); 0560 qint64 len; 0561 0562 while (((len = sFile.read(buffer.data(), MAX_IPC_SIZE)) != 0)) 0563 { 0564 if ((cancel && *cancel) || (len == -1) || (dFile.write(buffer.data(), len) != len)) 0565 { 0566 ret = false; 0567 0568 break; 0569 } 0570 } 0571 0572 sFile.close(); 0573 dFile.close(); 0574 0575 if (ret) 0576 { 0577 ret = QFile::rename(tmpFile, dstFile); 0578 } 0579 0580 if (!ret) 0581 { 0582 QFile::remove(tmpFile); 0583 } 0584 0585 if (ret) 0586 { 0587 QFile::Permissions permissions = QFile::permissions(srcFile); 0588 QFile::setPermissions(dstFile, permissions); 0589 0590 copyModificationTime(srcFile, dstFile); 0591 } 0592 0593 return ret; 0594 } 0595 0596 bool DFileOperations::copyModificationTime(const QString& srcFile, 0597 const QString& dstFile) 0598 { 0599 QFileInfo srcInfo(srcFile); 0600 QDateTime birDateTime = srcInfo.fileTime(QFileDevice::FileBirthTime); 0601 QDateTime accDateTime = srcInfo.fileTime(QFileDevice::FileAccessTime); 0602 QDateTime modDateTime = srcInfo.fileTime(QFileDevice::FileModificationTime); 0603 0604 QFile modFile(dstFile); 0605 0606 if (modFile.open(QIODevice::WriteOnly | QIODevice::Append | QIODevice::ExistingOnly)) 0607 { 0608 if (modDateTime.isValid()) 0609 { 0610 modFile.setFileTime(modDateTime, QFileDevice::FileModificationTime); 0611 } 0612 0613 if (accDateTime.isValid()) 0614 { 0615 modFile.setFileTime(accDateTime, QFileDevice::FileAccessTime); 0616 } 0617 0618 if (birDateTime.isValid()) 0619 { 0620 modFile.setFileTime(birDateTime, QFileDevice::FileBirthTime); 0621 } 0622 0623 modFile.close(); 0624 0625 return true; 0626 } 0627 0628 qCWarning(DIGIKAM_GENERAL_LOG) << "Failed to restore modification time for file" 0629 << dstFile; 0630 0631 return false; 0632 } 0633 0634 bool DFileOperations::setModificationTime(const QString& srcFile, 0635 const QDateTime& dateTime) 0636 { 0637 if (dateTime.isValid()) 0638 { 0639 QFile modFile(srcFile); 0640 0641 if (modFile.open(QIODevice::WriteOnly | QIODevice::Append | QIODevice::ExistingOnly)) 0642 { 0643 modFile.setFileTime(dateTime, QFileDevice::FileModificationTime); 0644 modFile.close(); 0645 0646 return true; 0647 } 0648 } 0649 0650 qCWarning(DIGIKAM_GENERAL_LOG) << "Failed to set modification time for file" 0651 << srcFile; 0652 0653 return false; 0654 } 0655 0656 QString DFileOperations::findExecutable(const QString& name) 0657 { 0658 QString path; 0659 QString program = name; 0660 0661 #ifdef Q_OS_WIN 0662 0663 program.append(QLatin1String(".exe")); 0664 0665 QSettings settings(QString::fromUtf8("HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\" 0666 "CurrentVersion\\App Paths\\%1").arg(program), 0667 QSettings::NativeFormat); 0668 0669 path = settings.value(QLatin1String("Default"), QString()).toString(); 0670 0671 #endif 0672 0673 if (path.isEmpty()) 0674 { 0675 path = QStandardPaths::findExecutable(program); 0676 } 0677 0678 return path; 0679 } 0680 0681 bool DFileOperations::sidecarFiles(const QString& srcFile, 0682 const QString& dstFile, 0683 SidecarAction action) 0684 { 0685 QStringList sidecarExtensions; 0686 sidecarExtensions << QLatin1String("xmp"); 0687 sidecarExtensions << MetaEngineSettings::instance()->settings().sidecarExtensions; 0688 0689 QFileInfo srcInfo(srcFile); 0690 0691 Q_FOREACH (const QString& ext, sidecarExtensions) 0692 { 0693 QString suffix(QLatin1Char('.') + ext); 0694 0695 QFileInfo extInfo(srcInfo.filePath() + suffix); 0696 QFileInfo basInfo(srcInfo.path() + 0697 QLatin1Char('/') + 0698 srcInfo.completeBaseName() + suffix); 0699 0700 if (extInfo.exists()) 0701 { 0702 QFileInfo dstInfo(dstFile); 0703 QString destination = dstInfo.filePath() + suffix; 0704 0705 if (QFile::exists(destination)) 0706 { 0707 QFile::remove(destination); 0708 } 0709 0710 if (action == Rename) 0711 { 0712 if (!renameFile(extInfo.filePath(), destination)) 0713 { 0714 return false; 0715 } 0716 } 0717 else if (action == Copy) 0718 { 0719 if (!copyFile(extInfo.filePath(), destination)) 0720 { 0721 return false; 0722 } 0723 } 0724 0725 qCDebug(DIGIKAM_GENERAL_LOG) << "Detected a sidecar" << extInfo.filePath(); 0726 } 0727 0728 if (basInfo.exists()) 0729 { 0730 QFileInfo dstInfo(dstFile); 0731 QString destination = dstInfo.path() + 0732 QLatin1Char('/') + 0733 dstInfo.completeBaseName() + suffix; 0734 0735 if (QFile::exists(destination)) 0736 { 0737 QFile::remove(destination); 0738 } 0739 0740 if (action == Rename) 0741 { 0742 if (!renameFile(basInfo.filePath(), destination)) 0743 { 0744 return false; 0745 } 0746 } 0747 else if (action == Copy) 0748 { 0749 if (!copyFile(basInfo.filePath(), destination)) 0750 { 0751 return false; 0752 } 0753 } 0754 0755 qCDebug(DIGIKAM_GENERAL_LOG) << "Detected a sidecar" << basInfo.filePath(); 0756 } 0757 } 0758 0759 return true; 0760 } 0761 0762 } // namespace Digikam