File indexing completed on 2025-01-19 03:53:24

0001 /* ============================================================
0002  *
0003  * This file is a part of digiKam project
0004  * https://www.digikam.org
0005  *
0006  * Date        : 2007-04-09
0007  * Description : Collection location management - private containers.
0008  *
0009  * SPDX-FileCopyrightText: 2007-2009 by Marcel Wiesweg <marcel dot wiesweg at gmx dot de>
0010  *
0011  * SPDX-License-Identifier: GPL-2.0-or-later
0012  *
0013  * ============================================================ */
0014 
0015 #include "collectionmanager_p.h"
0016 
0017 namespace Digikam
0018 {
0019 
0020 CollectionManager::Private::Private(CollectionManager* const s)
0021     : changingDB  (false),
0022       watchEnabled(false),
0023       s           (s)
0024 {
0025     QObject::connect(s, SIGNAL(triggerUpdateVolumesList()),
0026                      s, SLOT(slotTriggerUpdateVolumesList()),
0027                      Qt::BlockingQueuedConnection);
0028 }
0029 
0030 QList<SolidVolumeInfo> CollectionManager::Private::listVolumes()
0031 {
0032     // Move calls to Solid to the main thread.
0033     // Solid was meant to be thread-safe, but it is not (KDE4.0),
0034     // calling from a non-UI thread leads to a reversible
0035     // lock-up of variable length.
0036 
0037     if (QThread::currentThread() == QCoreApplication::instance()->thread())
0038     {
0039         return actuallyListVolumes();
0040     }
0041     else
0042     {
0043         // Q_EMIT a blocking queued signal to move call to main thread
0044 
0045         Q_EMIT s->triggerUpdateVolumesList();
0046 
0047         return volumesListCache;
0048     }
0049 }
0050 
0051 void CollectionManager::Private::slotTriggerUpdateVolumesList()
0052 {
0053     volumesListCache = actuallyListVolumes();
0054 }
0055 
0056 QList<SolidVolumeInfo> CollectionManager::Private::actuallyListVolumes()
0057 {
0058     QList<SolidVolumeInfo> volumes;
0059 
0060     //qCDebug(DIGIKAM_DATABASE_LOG) << "listFromType";
0061 
0062     QList<Solid::Device> devices = Solid::Device::listFromType(Solid::DeviceInterface::StorageAccess);
0063 
0064     //qCDebug(DIGIKAM_DATABASE_LOG) << "got listFromType";
0065 
0066     udisToWatch.clear();
0067 
0068     Q_FOREACH (const Solid::Device& accessDevice, devices)
0069     {
0070         // check for StorageAccess
0071 
0072         if (!accessDevice.is<Solid::StorageAccess>())
0073         {
0074             continue;
0075         }
0076 
0077         // mark as a device of principal interest
0078 
0079         udisToWatch << accessDevice.udi();
0080 
0081         const Solid::StorageAccess* access = accessDevice.as<Solid::StorageAccess>();
0082 
0083         // watch mount status (remove previous connections)
0084 
0085         QObject::disconnect(access, SIGNAL(accessibilityChanged(bool,QString)),
0086                             s, SLOT(accessibilityChanged(bool,QString)));
0087 
0088         QObject::connect(access, SIGNAL(accessibilityChanged(bool,QString)),
0089                          s, SLOT(accessibilityChanged(bool,QString)));
0090 
0091         if (!access->isAccessible())
0092         {
0093             continue;
0094         }
0095 
0096         // check for StorageDrive
0097 
0098         Solid::Device driveDevice;
0099 
0100         for (Solid::Device currentDevice = accessDevice ;
0101              currentDevice.isValid() ; currentDevice = currentDevice.parent())
0102         {
0103             if (currentDevice.is<Solid::StorageDrive>())
0104             {
0105                 driveDevice = currentDevice;
0106                 break;
0107             }
0108         }
0109 
0110 /*
0111         We cannot require a drive, some logical volumes may not have "one" drive as parent
0112         See bug 273369
0113 
0114         if (!driveDevice.isValid())
0115         {
0116             continue;
0117         }
0118 */
0119 
0120         Solid::StorageDrive* drive = driveDevice.as<Solid::StorageDrive>();
0121 
0122         // check for StorageVolume
0123 
0124         Solid::Device volumeDevice;
0125 
0126         for (Solid::Device currentDevice = accessDevice;
0127              currentDevice.isValid() ; currentDevice = currentDevice.parent())
0128         {
0129             if (currentDevice.is<Solid::StorageVolume>())
0130             {
0131                 volumeDevice = currentDevice;
0132                 break;
0133             }
0134         }
0135 
0136         if (!volumeDevice.isValid())
0137         {
0138             continue;
0139         }
0140 
0141         Solid::StorageVolume* const volume = volumeDevice.as<Solid::StorageVolume>();
0142 
0143         SolidVolumeInfo info;
0144         info.udi       = accessDevice.udi();
0145         info.path      = QDir::fromNativeSeparators(access->filePath());
0146         info.isMounted = access->isAccessible();
0147 
0148         if (!info.path.isEmpty() && !info.path.endsWith(QLatin1Char('/')))
0149         {
0150             info.path += QLatin1Char('/');
0151         }
0152 
0153         info.uuid  = volume->uuid();
0154         info.label = volume->label();
0155 
0156         if (drive)
0157         {
0158             info.isRemovable = (drive->isHotpluggable() || drive->isRemovable());
0159         }
0160         else
0161         {
0162             // impossible to know, but probably not hotpluggable (see comment above)
0163 
0164             info.isRemovable = false;
0165         }
0166 
0167         info.isOpticalDisc = volumeDevice.is<Solid::OpticalDisc>();
0168 
0169         volumes << info;
0170     }
0171 
0172     // This is the central place where the watch is enabled
0173 
0174     watchEnabled = true;
0175 
0176     return volumes;
0177 }
0178 
0179 QString CollectionManager::Private::volumeIdentifier(const SolidVolumeInfo& volume)
0180 {
0181     QUrl url;
0182     url.setScheme(QLatin1String("volumeid"));
0183 
0184     // On changing these, please update the checkLocation() code
0185 
0186     bool identifyByUUID      = !volume.uuid.isEmpty();
0187     bool identifyByLabel     = !identifyByUUID && !volume.label.isEmpty() && (volume.isOpticalDisc || volume.isRemovable);
0188     bool addDirectoryHash    = identifyByLabel && volume.isOpticalDisc;
0189     bool identifyByMountPath = !identifyByUUID && !identifyByLabel;
0190 
0191     if (identifyByUUID)
0192     {
0193         QUrlQuery q(url);
0194         q.addQueryItem(QLatin1String("uuid"), volume.uuid);
0195         url.setQuery(q);
0196     }
0197 
0198     if (identifyByLabel)
0199     {
0200         QUrlQuery q(url);
0201         q.addQueryItem(QLatin1String("label"), volume.label);
0202         url.setQuery(q);
0203     }
0204 
0205     if (addDirectoryHash)
0206     {
0207         // for CDs, we store a hash of the root directory. May be useful.
0208 
0209         QString dirHash = directoryHash(volume.path);
0210 
0211         if (!dirHash.isNull())
0212         {
0213             QUrlQuery q(url);
0214             q.addQueryItem(QLatin1String("directoryhash"), dirHash);
0215             url.setQuery(q);
0216         }
0217     }
0218 
0219     if (identifyByMountPath)
0220     {
0221         QUrlQuery q(url);
0222         q.addQueryItem(QLatin1String("mountpath"), volume.path);
0223         url.setQuery(q);
0224     }
0225 
0226     return url.url();
0227 }
0228 
0229 QString CollectionManager::Private::volumeIdentifier(const QString& path)
0230 {
0231     QUrl url;
0232     url.setScheme(QLatin1String("volumeid"));
0233 
0234     QUrlQuery q(url);
0235     q.addQueryItem(QLatin1String("path"), path);
0236     url.setQuery(q);
0237 
0238     return url.url();
0239 }
0240 
0241 QString CollectionManager::Private::networkShareIdentifier(const QStringList& paths)
0242 {
0243     QUrl url;
0244     url.setScheme(QLatin1String("networkshareid"));
0245 
0246     QUrlQuery q(url);
0247 
0248     Q_FOREACH (const QString& path, paths)
0249     {
0250         q.addQueryItem(QLatin1String("mountpath"), path);
0251     }
0252 
0253     url.setQuery(q);
0254 
0255     return url.url();
0256 }
0257 
0258 QString CollectionManager::Private::pathFromIdentifier(const AlbumRootLocation* location)
0259 {
0260     QUrl url(location->identifier);
0261 
0262     if (url.scheme() != QLatin1String("volumeid"))
0263     {
0264         return QString();
0265     }
0266 
0267     return QUrlQuery(url).queryItemValue(QLatin1String("path"));
0268 }
0269 
0270 QStringList CollectionManager::Private::networkShareMountPathsFromIdentifier(const AlbumRootLocation* location)
0271 {
0272     // using a QUrl because QUrl cannot handle duplicate query items
0273 
0274     QUrl url(location->identifier);
0275 
0276     if (url.scheme() != QLatin1String("networkshareid"))
0277     {
0278         return QStringList();
0279     }
0280 
0281     return QUrlQuery(url).allQueryItemValues(QLatin1String("mountpath"));
0282 }
0283 
0284 QString CollectionManager::Private::directoryHash(const QString& path)
0285 {
0286     QDir dir(path);
0287 
0288     if (dir.isReadable())
0289     {
0290         QStringList entries = dir.entryList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot);
0291         QCryptographicHash md5(QCryptographicHash::Md5);
0292 
0293         Q_FOREACH (const QString& entry, entries)
0294         {
0295             md5.addData(entry.toUtf8());
0296         }
0297 
0298         return QString::fromUtf8(md5.result().toHex());
0299     }
0300 
0301     return QString();
0302 }
0303 
0304 SolidVolumeInfo CollectionManager::Private::findVolumeForLocation(const AlbumRootLocation* location,
0305                                                                   const QList<SolidVolumeInfo>& volumes)
0306 {
0307     QString queryItem;
0308     QUrl url(location->identifier);
0309 
0310     if (url.scheme() != QLatin1String("volumeid"))
0311     {
0312         return SolidVolumeInfo();
0313     }
0314 
0315     if (!(queryItem = QUrlQuery(url).queryItemValue(QLatin1String("fileuuid"))).isNull())
0316     {
0317         QString uuid = QUrlQuery(url).queryItemValue(QLatin1String("uuid"));
0318         QList<SolidVolumeInfo> candidateVolumes;
0319 
0320         Q_FOREACH (const SolidVolumeInfo& volume, volumes)
0321         {
0322             QString volPath = volume.path;
0323             volPath.chop(1);
0324             QString colPath = volPath + location->specificPath;
0325 
0326             if (volume.isMounted)
0327             {
0328                 if (queryItem == getCollectionUUID(colPath))
0329                 {
0330                     if (!uuid.isNull() && (volume.uuid.compare(uuid, Qt::CaseInsensitive) == 0))
0331                     {
0332                         return volume;
0333                     }
0334                     else
0335                     {
0336                         qCDebug(DIGIKAM_DATABASE_LOG) << "Partition uuid possibly changed from"
0337                                                       << volPath;
0338                         candidateVolumes << volume;
0339                     }
0340                 }
0341             }
0342         }
0343 
0344         if (candidateVolumes.size() == 1)
0345         {
0346             return candidateVolumes.first();
0347         }
0348     }
0349 
0350     if (!(queryItem = QUrlQuery(url).queryItemValue(QLatin1String("uuid"))).isNull())
0351     {
0352         Q_FOREACH (const SolidVolumeInfo& volume, volumes)
0353         {
0354             if (volume.uuid.compare(queryItem, Qt::CaseInsensitive) == 0)
0355             {    // cppcheck-suppress useStlAlgorithm
0356                 return volume;
0357             }
0358         }
0359 
0360         return SolidVolumeInfo();
0361     }
0362     else if (!(queryItem = QUrlQuery(url).queryItemValue(QLatin1String("label"))).isNull())
0363     {
0364         // This one is a bit more difficult, as we take into account the possibility
0365         // that the label is not unique, and we take some care to make it work anyway.
0366 
0367         // find all available volumes with the given label (usually one)
0368 
0369         QList<SolidVolumeInfo> candidateVolumes;
0370 
0371         Q_FOREACH (const SolidVolumeInfo& volume, volumes)
0372         {
0373             if (volume.label == queryItem)
0374             {
0375                 candidateVolumes << volume;
0376             }
0377         }
0378 
0379         if (candidateVolumes.isEmpty())
0380         {
0381             return SolidVolumeInfo();
0382         }
0383 
0384         // find out of there is another location with the same label (usually not)
0385 
0386         bool hasOtherLocation = false;
0387 
0388         Q_FOREACH (AlbumRootLocation* const otherLocation, locations)
0389         {
0390             if (otherLocation == location)
0391             {
0392                 continue;
0393             }
0394 
0395             QUrl otherUrl(otherLocation->identifier);
0396 
0397             if ((otherUrl.scheme() == QLatin1String("volumeid")) &&
0398                 (QUrlQuery(otherUrl).queryItemValue(QLatin1String("label")) == queryItem))
0399             {
0400                 hasOtherLocation = true;
0401                 break;
0402             }
0403         }
0404 
0405         // the usual, easy case
0406 
0407         if ((candidateVolumes.size() == 1) && !hasOtherLocation)
0408         {
0409             return candidateVolumes.first();
0410         }
0411         else
0412         {
0413             // not unique: try to use the directoryhash
0414 
0415             QString dirHash = QUrlQuery(url).queryItemValue(QLatin1String("directoryhash"));
0416 
0417             // bail out if not provided
0418 
0419             if (dirHash.isNull())
0420             {
0421                 qCDebug(DIGIKAM_DATABASE_LOG) << "No directory hash specified for the non-unique Label"
0422                                               << queryItem << "Resorting to returning the first match.";
0423                 return candidateVolumes.first();
0424             }
0425 
0426             // match against directory hash
0427 
0428             Q_FOREACH (const SolidVolumeInfo& volume, candidateVolumes)
0429             {
0430                 QString volumeDirHash = directoryHash(volume.path);
0431 
0432                 if (volumeDirHash == dirHash)
0433                 {
0434                     return volume;
0435                 }
0436             }
0437         }
0438 
0439         return SolidVolumeInfo();
0440     }
0441     else if (!(queryItem = QUrlQuery(url).queryItemValue(QLatin1String("mountpath"))).isNull())
0442     {
0443         Q_FOREACH (const SolidVolumeInfo& volume, volumes)
0444         {
0445             if (volume.isMounted && (volume.path == queryItem))
0446             {    // cppcheck-suppress useStlAlgorithm
0447                 return volume;
0448             }
0449         }
0450 
0451         return SolidVolumeInfo();
0452     }
0453 
0454     return SolidVolumeInfo();
0455 }
0456 
0457 QString CollectionManager::Private::technicalDescription(const AlbumRootLocation* albumLoc)
0458 {
0459     QUrl url(albumLoc->identifier);
0460     QString queryItem;
0461 
0462     if      (url.scheme() == QLatin1String("volumeid"))
0463     {
0464         if      (!(queryItem = QUrlQuery(url).queryItemValue(QLatin1String("uuid"))).isNull())
0465         {
0466             return i18nc("@info",
0467                          "\"relative path\" on harddisk partition with \"UUID\"\n"
0468                          "Folder \"%1\" on the volume with the id \"%2\"",
0469                          QDir::toNativeSeparators(albumLoc->specificPath),
0470                          queryItem);
0471         }
0472         else if (!(queryItem = QUrlQuery(url).queryItemValue(QLatin1String("label"))).isNull())
0473         {
0474             return i18nc("@info",
0475                          "\"relative path\" on harddisk partition with \"label\"\n"
0476                          "Folder \"%1\" on the volume labeled \"%2\"",
0477                          QDir::toNativeSeparators(albumLoc->specificPath),
0478                          queryItem);
0479         }
0480         else if (!(queryItem = QUrlQuery(url).queryItemValue(QLatin1String("mountpath"))).isNull())
0481         {
0482             return QString::fromUtf8("\"%1\"").arg(queryItem);
0483         }
0484     }
0485     else if (url.scheme() == QLatin1String("networkshareid"))
0486     {
0487         if (!(queryItem =  QUrlQuery(url).queryItemValue(QLatin1String("mountpath"))).isNull())
0488         {
0489             return i18nc("@info", "Shared directory mounted at \"%1\"", QDir::toNativeSeparators(queryItem));
0490         }
0491     }
0492 
0493     return QString();
0494 }
0495 
0496 SolidVolumeInfo CollectionManager::Private::findVolumeForUrl(const QUrl& fileUrl,
0497                                                              const QList<SolidVolumeInfo>& volumes)
0498 {
0499     SolidVolumeInfo volume;
0500 
0501     // v.path is specified to have a trailing slash. path needs one as well.
0502 
0503     QString path    = fileUrl.toLocalFile() + QLatin1Char('/');
0504     int volumeMatch = 0;
0505 
0506     //FIXME: Network shares! Here we get only the volume of the mount path...
0507     // This is probably not really clean. But Solid does not help us.
0508 
0509     Q_FOREACH (const SolidVolumeInfo& v, volumes)
0510     {
0511         if (v.isMounted && !v.path.isEmpty() && path.startsWith(v.path))
0512         {
0513             int length = v.path.length();
0514 
0515             if (length > volumeMatch)
0516             {
0517                 volumeMatch = v.path.length();
0518                 volume      = v;
0519             }
0520         }
0521     }
0522 
0523     if (!volumeMatch)
0524     {
0525         qCDebug(DIGIKAM_DATABASE_LOG) << "Failed to detect a storage volume for path " << path << " with Solid";
0526     }
0527 
0528     return volume;
0529 }
0530 
0531 bool CollectionManager::Private::checkIfExists(const QString& filePath, QList<CollectionLocation> assumeDeleted)
0532 {
0533     const QUrl filePathUrl = QUrl::fromLocalFile(filePath);
0534 
0535     QReadLocker readLocker(&lock);
0536 
0537     Q_FOREACH (AlbumRootLocation* const location, locations)
0538     {
0539         const QUrl locationPathUrl = QUrl::fromLocalFile(location->albumRootPath());
0540 /*
0541         qCDebug(DIGIKAM_DATABASE_LOG) << filePathUrl << locationPathUrl;
0542 */
0543         // make sure filePathUrl is neither a child nor a parent
0544         // of an existing collection
0545 
0546         if (!locationPathUrl.isEmpty() &&
0547             (filePathUrl.isParentOf(locationPathUrl) ||
0548              locationPathUrl.isParentOf(filePathUrl)))
0549         {
0550             bool isDeleted = false;
0551 
0552             Q_FOREACH (const CollectionLocation& deletedLoc, assumeDeleted)
0553             {
0554                 if (deletedLoc.id() == location->id())
0555                 {   // cppcheck-suppress useStlAlgorithm
0556                     isDeleted = true;
0557                     break;
0558                 }
0559             }
0560 
0561             if (!isDeleted)
0562             {
0563                 return true;
0564             }
0565         }
0566     }
0567 
0568     return false;
0569 }
0570 
0571 QString CollectionManager::Private::getCollectionUUID(const QString& path)
0572 {
0573     QString uuid;
0574     QFileInfo info(path);
0575     const int uuidSize = 36;
0576 
0577     if (!info.exists() || !info.isReadable())
0578     {
0579         return uuid;
0580     }
0581 
0582     QString uuidFile = info.filePath()           +
0583                        QLatin1String("/.dtrash") +
0584                        QLatin1String("/digikam.uuid");
0585 
0586     if  (!QFile::exists(uuidFile))
0587     {
0588         return uuid;
0589     }
0590 
0591     QFile readFile(uuidFile);
0592 
0593     if (!readFile.open(QIODevice::ReadOnly))
0594     {
0595         return uuid;
0596     }
0597 
0598     uuid = QString::fromLatin1(readFile.read(uuidSize));
0599 
0600     if (uuid.size() != uuidSize)
0601     {
0602         uuid.clear();
0603     }
0604     else
0605     {
0606         qCDebug(DIGIKAM_DATABASE_LOG) << "Found Location" << path
0607                                       << "with file uuid" << (uuid.left(8) + QLatin1String("..."));
0608     }
0609 
0610     return uuid;
0611 }
0612 
0613 bool CollectionManager::Private::checkCollectionUUID(AlbumRootLocation* const location, const QString& path)
0614 {
0615     QFileInfo info(path);
0616     const int uuidSize = 36;
0617     QUrl url(location->identifier);
0618     const QString uuidQuery(QLatin1String("fileuuid"));
0619 
0620     if (!info.exists() || !info.isWritable())
0621     {
0622         return false;
0623     }
0624 
0625     QString uuidPath = info.filePath()    +
0626                        QLatin1String("/.dtrash");
0627     QString uuidFile = uuidPath;
0628     uuidFile        += QLatin1String("/digikam.uuid");
0629 
0630     if (QFileInfo::exists(uuidFile))
0631     {
0632         QUrlQuery q(url);
0633 
0634         if (q.queryItemValue(uuidQuery).isNull())
0635         {
0636             QString uuid = getCollectionUUID(path);
0637 
0638             if (!uuid.isNull())
0639             {
0640                 q.addQueryItem(uuidQuery, uuid);
0641                 url.setQuery(q);
0642 
0643                 location->identifier = url.url();
0644 
0645                 return true;
0646             }
0647         }
0648 
0649         return false;
0650     }
0651 
0652     if (!QFileInfo::exists(uuidPath))
0653     {
0654         QDir().mkpath(uuidPath);
0655     }
0656 
0657     QFile writeFile(uuidFile);
0658 
0659     if (!writeFile.open(QIODevice::WriteOnly))
0660     {
0661         return false;
0662     }
0663 
0664     QString uuid = QUuid::createUuid().toString().mid(1, uuidSize);
0665 
0666     if (writeFile.write(uuid.toLatin1()) != uuidSize)
0667     {
0668         return false;
0669     }
0670 
0671     writeFile.close();
0672     writeFile.setPermissions(QFileDevice::ReadOwner  |
0673                              QFileDevice::ReadGroup  |
0674                              QFileDevice::ReadOther  |
0675                              QFileDevice::WriteOwner |
0676                              QFileDevice::WriteGroup);
0677 
0678     QUrlQuery q(url);
0679     q.removeQueryItem(uuidQuery);
0680     q.addQueryItem(uuidQuery, uuid);
0681     url.setQuery(q);
0682 
0683     location->identifier = url.url();
0684 
0685     return true;
0686 }
0687 
0688 } // namespace Digikam