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