File indexing completed on 2025-01-05 03:59:47

0001 /* ============================================================
0002  *
0003  * This file is a part of digiKam project
0004  * https://www.digikam.org
0005  *
0006  * Date        : 2004-12-21
0007  * Description : USB Mass Storage camera interface
0008  *
0009  * SPDX-FileCopyrightText: 2004-2005 by Renchi Raju <renchi dot raju at gmail dot com>
0010  * SPDX-FileCopyrightText: 2005-2024 by Gilles Caulier <caulier dot gilles at gmail dot com>
0011  *
0012  * SPDX-License-Identifier: GPL-2.0-or-later
0013  *
0014  * ============================================================ */
0015 
0016 #include "umscamera.h"
0017 
0018 // Qt includes
0019 
0020 #include <QDir>
0021 #include <QFile>
0022 #include <QFileInfo>
0023 #include <QDirIterator>
0024 #include <QTextDocument>
0025 #include <QtGlobal>
0026 #include <QScopedPointer>
0027 #include <QCryptographicHash>
0028 #include <qplatformdefs.h>
0029 
0030 // KDE includes
0031 
0032 #include <klocalizedstring.h>
0033 #include <kconfiggroup.h>
0034 #include <ksharedconfig.h>
0035 
0036 // Solid includes
0037 
0038 #if defined(Q_CC_CLANG)
0039 #   pragma clang diagnostic push
0040 #   pragma clang diagnostic ignored "-Wnonportable-include-path"
0041 #endif
0042 
0043 #include <solid/device.h>
0044 #include <solid/storageaccess.h>
0045 #include <solid/storagedrive.h>
0046 #include <solid/storagevolume.h>
0047 
0048 #if defined(Q_CC_CLANG)
0049 #   pragma clang diagnostic pop
0050 #endif
0051 
0052 // Local includes
0053 
0054 #include "drawdecoder.h"
0055 #include "digikam_debug.h"
0056 #include "digikam_config.h"
0057 #include "dimg.h"
0058 #include "dmetadata.h"
0059 #include "itemscanner.h"
0060 #include "dfileoperations.h"
0061 
0062 namespace Digikam
0063 {
0064 
0065 UMSCamera::UMSCamera(const QString& title, const QString& model,
0066                      const QString& port, const QString& path)
0067     : DKCamera(title, model, port, path),
0068       m_cancel(false)
0069 {
0070     getUUIDFromSolid();
0071 }
0072 
0073 UMSCamera::~UMSCamera()
0074 {
0075 }
0076 
0077 /// Method not supported by UMS camera.
0078 bool UMSCamera::getPreview(QImage& /*preview*/)
0079 {
0080     return false;
0081 }
0082 
0083 /// Method not supported by UMS camera.
0084 bool UMSCamera::capture(CamItemInfo& /*itemInfo*/)
0085 {
0086     return false;
0087 }
0088 
0089 DKCamera::CameraDriverType UMSCamera::cameraDriverType()
0090 {
0091     return DKCamera::UMSDriver;
0092 }
0093 
0094 QByteArray UMSCamera::cameraMD5ID()
0095 {
0096     QString camData;
0097 
0098     // Camera media UUID is used (if available) to improve fingerprint value
0099     // registration to database. We want to be sure that MD5 sum is really unique.
0100     // We don't use camera information here. media UUID is enough because it came be
0101     // mounted by a card reader or a camera. In this case, "already downloaded" flag will
0102     // be independent of the device used to mount memory card.
0103 
0104     camData.append(uuid());
0105     QCryptographicHash md5(QCryptographicHash::Md5);
0106     md5.addData(camData.toUtf8());
0107 
0108     return md5.result().toHex();
0109 }
0110 
0111 /// NOTE: implemented in gui, outside the camera thread.
0112 bool UMSCamera::getFreeSpace(qint64& /*bytesSize*/, qint64& /*bytesAvail*/)
0113 {
0114     return false;
0115 }
0116 
0117 bool UMSCamera::doConnect()
0118 {
0119     QFileInfo dir(m_path);
0120 
0121     if (!dir.exists() || !dir.isReadable() || !dir.isDir())
0122     {
0123         return false;
0124     }
0125 
0126     if (dir.isWritable())
0127     {
0128         m_deleteSupport = true;
0129         m_uploadSupport = true;
0130         m_mkDirSupport  = true;
0131         m_delDirSupport = true;
0132     }
0133     else
0134     {
0135         m_deleteSupport = false;
0136         m_uploadSupport = false;
0137         m_mkDirSupport  = false;
0138         m_delDirSupport = false;
0139     }
0140 
0141     m_thumbnailSupport    = true;   // UMS camera always support thumbnails.
0142     m_captureImageSupport = false;  // UMS camera never support capture mode.
0143 
0144     return true;
0145 }
0146 
0147 void UMSCamera::cancel()
0148 {
0149     // set the cancel flag
0150 
0151     m_cancel = true;
0152 }
0153 
0154 bool UMSCamera::getFolders(const QString& folder)
0155 {
0156     if (m_cancel)
0157     {
0158         return false;
0159     }
0160 
0161     QDirIterator it(folder, QDir::Dirs |
0162                             QDir::NoDotAndDotDot);
0163 
0164     QStringList subFolderList;
0165 
0166     while (it.hasNext() && !m_cancel)
0167     {
0168         subFolderList << it.next();
0169     }
0170 
0171     if (subFolderList.isEmpty())
0172     {
0173         return true;
0174     }
0175 
0176     Q_EMIT signalFolderList(subFolderList);
0177 
0178     return true;
0179 }
0180 
0181 bool UMSCamera::getItemsInfoList(const QString& folder, bool useMetadata, CamItemInfoList& infoList)
0182 {
0183     m_cancel = false;
0184     infoList.clear();
0185 
0186     if (!QFileInfo::exists(folder))
0187     {
0188         return false;
0189     }
0190 
0191     QDirIterator it(folder, QDir::Files |
0192                             QDir::NoDotAndDotDot);
0193 
0194     while (it.hasNext() && !m_cancel)
0195     {
0196         it.next();
0197         CamItemInfo info;
0198         getItemInfo(folder, it.fileName(), info, useMetadata);
0199         infoList.append(info);
0200     }
0201 
0202     return true;
0203 }
0204 
0205 void UMSCamera::getItemInfo(const QString& folder, const QString& itemName, CamItemInfo& info, bool useMetadata)
0206 {
0207     info.folder           = !folder.endsWith(QLatin1Char('/')) ? folder + QLatin1Char('/') : folder;
0208     info.name             = itemName;
0209 
0210     QFileInfo fi(info.folder + info.name);
0211     info.size             = fi.size();
0212     info.readPermissions  = fi.isReadable();
0213     info.writePermissions = fi.isWritable();
0214     info.mime             = mimeType(fi.suffix().toLower());
0215 
0216     if (!info.mime.isEmpty())
0217     {
0218         if (useMetadata)
0219         {
0220             // Try to use file metadata
0221 
0222             QScopedPointer<DMetadata> meta(new DMetadata);
0223             getMetadata(folder, itemName, *meta);
0224             fillItemInfoFromMetadata(info, *meta);
0225 
0226             // Fall back to file system info
0227 
0228             if (info.ctime.isNull())
0229             {
0230                 info.ctime = ItemScanner::creationDateFromFilesystem(fi);
0231             }
0232         }
0233         else
0234         {
0235             // Only use file system date
0236 
0237             info.ctime = ItemScanner::creationDateFromFilesystem(fi);
0238         }
0239     }
0240 
0241     // if we have an image, allow previews
0242     // TODO allow video previews at some point?
0243 /*
0244     if (info.mime.startsWith(QLatin1String("image/")) ||
0245     {
0246         info.mime.startsWith(QLatin1String("video/")))
0247     }
0248 */
0249     if (info.mime.startsWith(QLatin1String("image/")))
0250     {
0251         info.previewPossible = true;
0252     }
0253 }
0254 
0255 bool UMSCamera::getThumbnail(const QString& folder, const QString& itemName, QImage& thumbnail)
0256 {
0257     m_cancel     = false;
0258     QString path = !folder.endsWith(QLatin1Char('/')) ? folder + QLatin1Char('/') : folder;
0259     path        += itemName;
0260 
0261     // Try to get preview from Exif data (good quality). Can work with Raw files
0262 
0263     QScopedPointer<DMetadata> metadata(new DMetadata(path));
0264     metadata->getItemPreview(thumbnail);
0265 
0266     if (!thumbnail.isNull())
0267     {
0268         return true;
0269     }
0270 
0271     // RAW files : try to extract embedded thumbnail using RawEngine
0272 
0273     DRawDecoder::loadRawPreview(thumbnail, path);
0274 
0275     if (!thumbnail.isNull())
0276     {
0277         return true;
0278     }
0279 
0280     KSharedConfig::Ptr config  = KSharedConfig::openConfig();
0281     KConfigGroup group         = config->group(QLatin1String("Camera Settings"));
0282     bool turnHighQualityThumbs = group.readEntry(QLatin1String("TurnHighQualityThumbs"), false);
0283 
0284     // Try to get thumbnail from Exif data (poor quality).
0285 
0286     if (!turnHighQualityThumbs)
0287     {
0288         thumbnail = metadata->getExifThumbnail(true);
0289 
0290         if (!thumbnail.isNull())
0291         {
0292             return true;
0293         }
0294     }
0295 
0296     // THM files: try to get thumbnail from '.thm' files if we didn't manage to get
0297     // thumbnail from Exif. Any cameras provides *.thm files like JPEG files with RAW files.
0298     // Using this way is always speed up than ultimate loading using DImg.
0299     // Note: the thumbnail extracted with this method can be in poor quality.
0300     // 2006/27/01 - Gilles - Tested with my Minolta Dynax 5D USM camera.
0301 
0302     QFileInfo fi(path);
0303 
0304     if      (thumbnail.load(fi.path() + fi.baseName() + QLatin1String(".thm")))   // Lowercase
0305     {
0306         if (!thumbnail.isNull())
0307         {
0308             return true;
0309         }
0310     }
0311     else if (thumbnail.load(fi.path() + fi.baseName() + QLatin1String(".THM")))   // Uppercase
0312     {
0313         if (!thumbnail.isNull())
0314         {
0315             return true;
0316         }
0317     }
0318 
0319     // Finally, we trying to get thumbnail using DImg API (slow).
0320 
0321     qCDebug(DIGIKAM_IMPORTUI_LOG) << "Use DImg loader to get thumbnail from : " << path;
0322 
0323     DImg dimgThumb;
0324 
0325     // skip loading the data we don't need to speed it up.
0326 
0327     dimgThumb.load(path, false /*loadMetadata*/, false /*loadICCData*/, false /*loadUniqueHash*/, false /*loadHistory*/);
0328 
0329     if (!dimgThumb.isNull())
0330     {
0331         thumbnail = dimgThumb.copyQImage();
0332 
0333         return true;
0334     }
0335 
0336     return false;
0337 }
0338 
0339 bool UMSCamera::getMetadata(const QString& folder, const QString& itemName, DMetadata& meta)
0340 {
0341     QString path = !folder.endsWith(QLatin1Char('/')) ? folder + QLatin1Char('/') : folder;
0342     QFileInfo fi, thmlo, thmup;
0343     bool ret     = false;
0344 
0345     fi.setFile(path    + itemName);
0346     thmlo.setFile(path + fi.baseName() + QLatin1String(".thm"));
0347     thmup.setFile(path + fi.baseName() + QLatin1String(".THM"));
0348 
0349     if      (thmlo.exists())
0350     {
0351         // Try thumbnail sidecar files with lowercase extension.
0352 
0353         ret = meta.load(thmlo.filePath());
0354     }
0355     else if (thmup.exists())
0356     {
0357         // Try thumbnail sidecar files with uppercase extension.
0358 
0359         ret = meta.load(thmup.filePath());
0360     }
0361     else
0362     {
0363         // If no thumbnail sidecar file available, try to load image metadata for files.
0364 
0365         ret = meta.load(fi.filePath());
0366     }
0367 
0368     return ret;
0369 }
0370 
0371 bool UMSCamera::downloadItem(const QString& folder, const QString& itemName, const QString& saveFile)
0372 {
0373     m_cancel     = false;
0374     QString src  = !folder.endsWith(QLatin1Char('/')) ? folder + QLatin1Char('/') : folder;
0375     src         += itemName;
0376     QString dest = saveFile;
0377 
0378     QFile sFile(src);
0379     QFile dFile(dest);
0380 
0381     if (!sFile.open(QIODevice::ReadOnly))
0382     {
0383         qCWarning(DIGIKAM_IMPORTUI_LOG) << "Failed to open source file for reading:" << src;
0384 
0385         return false;
0386     }
0387 
0388     if (!dFile.open(QIODevice::WriteOnly | QIODevice::Unbuffered))
0389     {
0390         sFile.close();
0391         qCWarning(DIGIKAM_IMPORTUI_LOG) << "Failed to open destination file for writing:" << dest;
0392 
0393         return false;
0394     }
0395 
0396     const int  MAX_IPC_SIZE = (1024 * 32);
0397     QByteArray buffer(MAX_IPC_SIZE, '\0');
0398     qint64     len;
0399 
0400     while (((len = sFile.read(buffer.data(), MAX_IPC_SIZE)) != 0) && !m_cancel)
0401     {
0402         if ((len == -1) || (dFile.write(buffer.data(), len) != len))
0403         {
0404             sFile.close();
0405             dFile.close();
0406 
0407             return false;
0408         }
0409     }
0410 
0411     sFile.close();
0412     dFile.close();
0413 
0414     // Set the file modification time of the downloaded file to the original file.
0415     // NOTE: this behavior don't need to be managed through Setup/Metadata settings.
0416 
0417     dFile.setPermissions(sFile.permissions());
0418     DFileOperations::copyModificationTime(src, dest);
0419 
0420     return true;
0421 }
0422 
0423 bool UMSCamera::setLockItem(const QString& folder, const QString& itemName, bool lock)
0424 {
0425     QString src = !folder.endsWith(QLatin1Char('/')) ? folder + QLatin1Char('/') : folder;
0426     src        += itemName;
0427 
0428     if (lock)
0429     {
0430         // Lock the file to set read only flag
0431 
0432         if (::chmod(QFile::encodeName(src).constData(), S_IREAD) == -1)
0433         {
0434             return false;
0435         }
0436     }
0437     else
0438     {
0439         // Unlock the file to set read/write flag
0440 
0441         if (::chmod(QFile::encodeName(src).constData(), S_IREAD | S_IWRITE) == -1)
0442         {
0443             return false;
0444         }
0445     }
0446 
0447     return true;
0448 }
0449 
0450 bool UMSCamera::deleteItem(const QString& folder, const QString& itemName)
0451 {
0452     m_cancel     = false;
0453     QString path = !folder.endsWith(QLatin1Char('/')) ? folder + QLatin1Char('/') : folder;
0454 
0455     // Any camera provide THM (thumbnail) file with real image. We need to remove it also.
0456 
0457     QFileInfo fi(path + itemName);
0458 
0459     QFileInfo thmLo(path + fi.baseName() + QLatin1String(".thm"));          // Lowercase
0460 
0461     if (thmLo.exists())
0462     {
0463         QFile::remove(thmLo.filePath());
0464     }
0465 
0466     QFileInfo thmUp(path + fi.baseName() + QLatin1String(".THM"));          // Uppercase
0467 
0468     if (thmUp.exists())
0469     {
0470         QFile::remove(thmUp.filePath());
0471     }
0472 
0473     // Remove the real image.
0474 
0475     return QFile::remove(path + itemName);
0476 }
0477 
0478 bool UMSCamera::uploadItem(const QString& folder, const QString& itemName, const QString& localFile, CamItemInfo& info)
0479 {
0480     m_cancel     = false;
0481     QString dest = !folder.endsWith(QLatin1Char('/')) ? folder + QLatin1Char('/') : folder;
0482     dest        += itemName;
0483     QString src  = localFile;
0484 
0485     QFile sFile(src);
0486     QFile dFile(dest);
0487 
0488     if (!sFile.open(QIODevice::ReadOnly))
0489     {
0490         qCWarning(DIGIKAM_IMPORTUI_LOG) << "Failed to open source file for reading: " << src;
0491 
0492         return false;
0493     }
0494 
0495     if (!dFile.open(QIODevice::WriteOnly))
0496     {
0497         sFile.close();
0498         qCWarning(DIGIKAM_IMPORTUI_LOG) << "Failed to open destination file for writing: " << dest;
0499 
0500         return false;
0501     }
0502 
0503     const int MAX_IPC_SIZE = (1024 * 32);
0504 
0505     char buffer[MAX_IPC_SIZE];
0506 
0507     qint64 len;
0508 
0509     while (((len = sFile.read(buffer, MAX_IPC_SIZE)) != 0) && !m_cancel)
0510     {
0511         if ((len == -1) || (dFile.write(buffer, (quint64)len) == -1))
0512         {
0513             sFile.close();
0514             dFile.close();
0515 
0516             return false;
0517         }
0518     }
0519 
0520     sFile.close();
0521     dFile.close();
0522 
0523     // Set the file modification time of the uploaded file to original file.
0524     // NOTE: this behavior don't need to be managed through Setup/Metadata settings.
0525 
0526     DFileOperations::copyModificationTime(src, dest);
0527 
0528     // Get new camera item information.
0529 
0530     PhotoInfoContainer        pInfo;
0531     QScopedPointer<DMetadata> meta(new DMetadata);
0532     QFileInfo                 fi(dest);
0533     QString                   mime = mimeType(fi.suffix().toLower());
0534 
0535     if (!mime.isEmpty())
0536     {
0537         QSize     dims;
0538         QDateTime dt;
0539 
0540         // Try to load image metadata.
0541 
0542         meta->load(fi.filePath());
0543         dt    = meta->getItemDateTime();
0544         dims  = meta->getItemDimensions();
0545         pInfo = meta->getPhotographInformation();
0546 
0547         if (dt.isNull()) // fall back to file system info
0548         {
0549             dt = ItemScanner::creationDateFromFilesystem(fi);
0550         }
0551 
0552         info.name             = fi.fileName();
0553         info.folder           = !folder.endsWith(QLatin1Char('/')) ? folder + QLatin1Char('/') : folder;
0554         info.mime             = mime;
0555         info.ctime            = dt;
0556         info.size             = fi.size();
0557         info.width            = dims.width();
0558         info.height           = dims.height();
0559         info.downloaded       = CamItemInfo::DownloadUnknown;
0560         info.readPermissions  = fi.isReadable();
0561         info.writePermissions = fi.isWritable();
0562         info.photoInfo        = pInfo;
0563     }
0564 
0565     return true;
0566 }
0567 
0568 bool UMSCamera::cameraSummary(QString& summary)
0569 {
0570     summary =  QString(i18nc("@info", "\"Mounted Camera\" driver for USB/IEEE1394 mass storage cameras and "
0571                                       "Flash disk card readers.\n\n"));
0572 
0573     // we do not expect title/model/etc. to contain newlines,
0574     // so we just escape HTML characters
0575 
0576     summary += i18nc("@info List of device properties",
0577                      "Title: \"%1\"\n"
0578                      "Model: \"%2\"\n"
0579                      "Port: \"%3\"\n"
0580                      "Path: \"%4\"\n"
0581                      "UUID: \"%5\"\n\n",
0582                      title(), model(), port(), path(), uuid());
0583 
0584     summary += i18nc("@info List of supported device operations",
0585                      "Thumbnails: \"%1\"\n"
0586                      "Capture image: \"%2\"\n"
0587                      "Delete items: \"%3\"\n"
0588                      "Upload items: \"%4\"\n"
0589                      "Create directories: \"%5\"\n"
0590                      "Delete directories: \"%6\"\n\n",
0591                      thumbnailSupport()    ? i18nc("@info: ums backend feature", "yes") : i18nc("@info: ums backend feature", "no"),
0592                      captureImageSupport() ? i18nc("@info: ums backend feature", "yes") : i18nc("@info: ums backend feature", "no"),
0593                      deleteSupport()       ? i18nc("@info: ums backend feature", "yes") : i18nc("@info: ums backend feature", "no"),
0594                      uploadSupport()       ? i18nc("@info: ums backend feature", "yes") : i18nc("@info: ums backend feature", "no"),
0595                      mkDirSupport()        ? i18nc("@info: ums backend feature", "yes") : i18nc("@info: ums backend feature", "no"),
0596                      delDirSupport()       ? i18nc("@info: ums backend feature", "yes") : i18nc("@info: ums backend feature", "no"));
0597     return true;
0598 }
0599 
0600 bool UMSCamera::cameraManual(QString& manual)
0601 {
0602     manual = QString(i18nc("@info", "For more information about the \"Mounted Camera\" driver, "
0603                                     "please read the \"Supported Digital Still "
0604                                     "Cameras\" section in the digiKam manual."));
0605     return true;
0606 }
0607 
0608 bool UMSCamera::cameraAbout(QString& about)
0609 {
0610     about = QString(i18nc("@info", "The \"Mounted Camera\" driver is a simple interface to a camera disk "
0611                                    "mounted locally on your system.\n\n"
0612                                    "It does not use libgphoto2 drivers.\n\n"
0613                                    "To report any problems with this driver, please contact the digiKam team at:\n\n"
0614                                    "https://www.digikam.org/?q=contact"));
0615     return true;
0616 }
0617 
0618 void UMSCamera::getUUIDFromSolid()
0619 {
0620     QList<Solid::Device> devices = Solid::Device::listFromType(Solid::DeviceInterface::StorageAccess);
0621 
0622     Q_FOREACH (const Solid::Device& accessDevice, devices)
0623     {
0624         // check for StorageAccess
0625 
0626         if (!accessDevice.is<Solid::StorageAccess>())
0627         {
0628             continue;
0629         }
0630 
0631         const Solid::StorageAccess* const access = accessDevice.as<Solid::StorageAccess>();
0632 
0633         if (!access->isAccessible())
0634         {
0635             continue;
0636         }
0637 
0638         // check for StorageDrive
0639 
0640         Solid::Device driveDevice;
0641 
0642         for (Solid::Device currentDevice = accessDevice ;
0643              currentDevice.isValid() ;
0644              currentDevice = currentDevice.parent())
0645         {
0646             if (currentDevice.is<Solid::StorageDrive>())
0647             {
0648                 driveDevice = currentDevice;
0649                 break;
0650             }
0651         }
0652 
0653         if (!driveDevice.isValid())
0654         {
0655             continue;
0656         }
0657 
0658         // check for StorageVolume
0659 
0660         Solid::Device volumeDevice;
0661 
0662         for (Solid::Device currentDevice = accessDevice ;
0663              currentDevice.isValid() ;
0664              currentDevice = currentDevice.parent())
0665         {
0666             if (currentDevice.is<Solid::StorageVolume>())
0667             {
0668                 volumeDevice = currentDevice;
0669                 break;
0670             }
0671         }
0672 
0673         if (!volumeDevice.isValid())
0674         {
0675             continue;
0676         }
0677 
0678         Solid::StorageVolume* const volume = volumeDevice.as<Solid::StorageVolume>();
0679 
0680         if (m_path.startsWith(QDir::fromNativeSeparators(access->filePath())) &&
0681             (QDir::fromNativeSeparators(access->filePath()) != QLatin1String("/")))
0682         {
0683             m_uuid = volume->uuid();
0684         }
0685     }
0686 }
0687 
0688 } // namespace Digikam
0689 
0690 #include "moc_umscamera.cpp"