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