diff --git a/NEWS b/NEWS index 137501e535..6b004e39b8 100644 --- a/NEWS +++ b/NEWS @@ -1,241 +1,242 @@ digiKam 6.0.0 - Release date: 2018-xx-xx ***************************************************************************************************** NEW FEATURES: ImageEditor : New clone tool to replace old CImg in-painting tool. ImageEditor : Add Web services Import and Export tools. Showfoto : Add Web services Import and Export tools. LightTable : Add Web services Import and Export tools. Database : Similarity database has been moved to a dedicated file to not bloat core database with computed finger-prints. This will speed-up query in core database in case of Similarity feature is used. Database : New video metadata parser based on ffmpeg to populate database. Search : Add video support to find files based on properties registered on database. General : Add QWebEngine support. General : Update internal Libraw to last 0.18.11. General : Fix all Krazy static analyzer reports. Tags : Add possibility to merge tags by drag & drop. HTML Gallery : New Vanilla theme to emulate export to HTML from Adobe LightRoom. HTML Gallery : New Blue Frame theme. HTML Gallery : New Kiosk-Mode theme. ***************************************************************************************************** BUGFIXES: 001 ==> 172650 - No export tools available. 002 ==> 149591 - The export menu is blank. 003 ==> 300424 - Export tools not detected. 004 ==> 327743 - MediaWiki export not displayed. 005 ==> 348146 - Export tools configuration module. 006 ==> 243275 - Crashing when calling configuration. 007 ==> 257134 - Crashes when entering its settings. 008 ==> 230666 - Crash during start. 009 ==> 306698 - Crashes after update to KDE. 010 ==> 207640 - Crashes immediately at startup, sometimes at closing. 011 ==> 245628 - Crash when enabling/disabling Facebook import/export. 012 ==> 097317 - sigsegv [New Thread 1100241184 (LWP 5665)]. 013 ==> 245776 - Crashes when selecting Settings. 014 ==> 245775 - Crashes even without export tools installed. 015 ==> 254283 - Crash as soon as i click settings. 016 ==> 255733 - crash when reopening configuration dialog. 017 ==> 262050 - Crash after new install and scan to mysql database. 018 ==> 263871 - Crashed after searching for duplicates. 019 ==> 268242 - Crashes after I clicked on 'Settings'. 020 ==> 276882 - Add export tool buttons to toolbar. 021 ==> 284801 - Export to picasaweb crashes. 022 ==> 277669 - Crash after files moved. 023 ==> 281250 - Crash after disabling Export tools. 024 ==> 202637 - Crash trying to start export tool. 025 ==> 306693 - after update to KDE host application crashes on startup. 026 ==> 282781 - Crashes when reopening configuration dialog after disabling export tools. 027 ==> 285499 - Crash when settings window opened. 028 ==> 297629 - Crashed when importing from Nikon P7000. 029 ==> 303338 - Crashes when clicking "send to" button. 030 ==> 307619 - Refuses to load Export tools. 031 ==> 311812 - Export tools not loading, SO version not defined. 032 ==> 313186 - Crashes on attempt to use the "Send to" menu. 033 ==> 313356 - Crashed when clicking the "send to" button. 034 ==> 313577 - Crashes when pressing the "send to" button. 035 ==> 313890 - Crash when clicking "Send to...". 036 ==> 315033 - Crashes on pressing Send To... button. 037 ==> 315914 - The facebook tool crashes everytime on initialization. 038 ==> 326556 - Export tools are not loaded when starting host application for second time. 039 ==> 095175 - crash on loading, signal 11 SIGSEGV. 040 ==> 175844 - Crashes at startup loading Export tools. 041 ==> 306511 - Crash during start. 042 ==> 234021 - Crash on loading. 043 ==> 219772 - Opening the application causes crash. 044 ==> 294173 - Crash after Image resize start. 045 ==> 306881 - Crashed when attempting to open Export tools. 046 ==> 306495 - Crash changing settings linux ubuntu lucid. 047 ==> 306497 - Crash after changing settings segmentation fault possible 2nd report? 048 ==> 185470 - "Import from facebook" is listed twice in import menu. 049 ==> 334045 - MediaWiki option not available in Export menu when plugin activated. 050 ==> 142112 - Can't save on my webspace with ShowFoto. 051 ==> 167417 - Showfoto cannot save files of CIFS mount. 052 ==> 125164 - Flickr export tool should respect host application selection. 053 ==> 238927 - Host application quits when uploading to Flickr. 054 ==> 326740 - Selection of tools is set to default after each update. 055 ==> 233063 - Add progress indicator when moving or copy files [patch]. 056 ==> 361829 - Rotated MP4 video with "Orientation" flag are not played back in the correct rotation angle. 057 ==> 329854 - digiKam doesn't rotate video thumbnail. 058 ==> 376661 - When importing ~200,000 video files Digikam crashes in about 2-5 seconds of starting. 059 ==> 377072 - Cannot read video metadata. 060 ==> 374453 - Extract right video date from MP4 files metadata. 061 ==> 377177 - Geolocation / GPS information doesn't appear in video metadata. 062 ==> 383588 - Imported video files have time shifted exactly 2 hours later. 063 ==> 380847 - MP4 video not importing with correct date. 064 ==> 373682 - geolocalisation filter does not take care of the videos geolocalisation tags. 065 ==> 340925 - digiKam crash when start it. 066 ==> 331506 - digiKam crashes on startup. 067 ==> 335816 - Crash when trying to add a big collection. 068 ==> 353295 - digiKam repeatedly crashes while importing pictures. 069 ==> 341433 - Crash when opening digiKam application. 070 ==> 375562 - digiKam crashes while scanning images into sqlite database. 071 ==> 334782 - Crash while doing nothing. 072 ==> 362672 - Crash on start of digiKam. 073 ==> 341023 - Crash after startup during check for updated images. 074 ==> 386891 - Crashed while adding pictures. 075 ==> 342666 - digiKam crashes during find new items. 076 ==> 341274 - digiKam crash on startup. 077 ==> 343708 - Crash when scanning album. 078 ==> 332721 - Crash when reading a certain MP4 video file. 079 ==> 343736 - Crashes when rebuilding thumbnails from database. 080 ==> 346356 - digiKam crashes when adding 90.000 pictures to library. 081 ==> 343714 - digiKam crash when scanning for new items. 082 ==> 341091 - digiKam crashes when updating the MySQL database of a a hudge photo collection. 083 ==> 340879 - digiKam crashes after getting unexpected but reasonable output from libexiv2. 084 ==> 342712 - Crash on collection scanning. 085 ==> 356704 - digiKam still crashes while scanning a new photo directory and subdirs. 086 ==> 339269 - Segfault when opening a folder that contains unknown file types (mov, avi, xcf). 087 ==> 364635 - digiKam crashes on startup. 088 ==> 357356 - digiKam crash on startup while scanning photos. 089 ==> 341554 - digiKam crashed by Data-Import from NFS. 090 ==> 345457 - digiKam crashes at "loading tools". 091 ==> 349635 - Crash of digiKam - moving album. 092 ==> 342604 - digiKam crash. 093 ==> 331450 - Signal 8 on album opening. 094 ==> 342030 - digiKam crashes when checking an AVI video file using exiv2. 095 ==> 352777 - Crash during scan. 096 ==> 352944 - digiKam crashes on start. 097 ==> 343643 - digiKam crashes while perform initial scanning of custom photo folder. 098 ==> 342000 - digiKam crash when opening folder with Videos (Album or SD Card import). 099 ==> 353447 - digiKam crashes when scanning files. 100 ==> 346807 - Crashes on startup. 101 ==> 364639 - digiKam crashed while opening database. 102 ==> 341504 - Crash while using application. 103 ==> 367691 - Searching for pictures crashes at 30% every time. 104 ==> 334604 - Crash after changing disk partions. 105 ==> 351689 - Crash on opening digiKam. 106 ==> 149267 - digiKam crashes after finding avi and so on. 107 ==> 170387 - Add movies management. 108 ==> 369629 - digiKam does not use GPS data from video files. 109 ==> 367880 - Nexus 5X videos show up upside-down in digiKam. 110 ==> 330116 - digiKam does not take care about GPS info stored in MP4 video files. 111 ==> 339150 - digikam crashes when trying to display video file. 112 ==> 344406 - Crash at start. 113 ==> 339909 - digiKam Segmentation Fault on open. 114 ==> 343231 - Crash at scanning for new fotos. 115 ==> 340373 - Crash on scanning video directory. 116 ==> 134679 - Video and audio files are not imported. 117 ==> 375357 - No video preview. 118 ==> 261773 - Batch renaming does not complete when MP4 video file is processed. 119 ==> 185915 - Album View: "Created" time of video set to "00:00". 120 ==> 303218 - digiKam import crashes when you select video files. 121 ==> 374241 - Bad video date rename. 122 ==> 375646 - Be able to scan only photo, not video and audio. 123 ==> 262499 - Cannot rename .AVI files. 124 ==> 199261 - Import avi movies from sdcard wrong date and no thumbnail. 125 ==> 181521 - NEF's in descending order, AVI in ascending order in import from SD-card. 126 ==> 392019 - Two persons can point to the same face tag in pictures. 127 ==> 392013 - Metadata explorer does not show XMP face rectangles. 128 ==> 389508 - Dates Side Menu Is Not Updated Automatically After Exif Date Change [patch]. 129 ==> 331864 - Merge Tags with same name when moving to same hierarchy level. 130 ==> 347302 - Reassign name to face. 131 ==> 391747 - BQM Tool "Remove Metadata" doesn't remove all metadata from image. 132 ==> 285683 - Already imported pictures not recognized after daylight savings time. 133 ==> 392309 - Icons are pixelated when my display scale factor is 1.2 134 ==> 392405 - Function 'getImageIdsFromArea' argument order different. 135 ==> 386224 - Metadata is not updated when moving tags. 136 ==> 370245 - Be able to rename tags which have been setted in pictures. 137 ==> 374470 - Deleted tags are not removed from file metadata. 138 ==> 374516 - Persons metadata are not updated after a tag removed. 139 ==> 392436 - Count Files in directory. 140 ==> 363859 - digiKam core port from QWebKit to QWebEngine [patch]. 141 ==> 392427 - Cannot add collection on network drive. 142 ==> 392022 - Position of a face tag appears on top of bottom of the list, instead of being sorted alphabetically. 143 ==> 372763 - Rename does not give options on Conflict. 144 ==> 391533 - Feature request: add "NOT" tag matching condition in "Filters" panel. 145 ==> 381222 - digiKam crash on fuzzy search. 146 ==> 386275 - Crash caused by QtAV. 147 ==> 372342 - Face tag area is very short [patch]. 148 ==> 391348 - People Side Menu Shows Only Faces Not People Tagged Images. 149 ==> 385630 - Views Requiring Maps Takes ~30s to Launch. 150 ==> 192908 - Allow to split icon-view in order to show multiple albums at the same time. 151 ==> 339088 - GIT master: crash when clicking through images in preview, with face recognition running in background. 152 ==> 341605 - Crash if I attempt to use left-sidebar tags tab. 153 ==> 227266 - Handle Video Date from metadata. 154 ==> 227259 - Needs to Edit Video Date. 155 ==> 373284 - digiKam crashed with SIGSEGV in QSortFilterProxyModel::parent(). 156 ==> 384807 - digikam 5.7.0 AppImage bundle : provide a more recent ffmpeg version for video support. 157 ==> 391835 - Deleted pictures still appear in group. 158 ==> 387483 - Elegant theme: Selected frame colors swapped [patch]. 159 ==> 375424 - Thumbnails are not being removed from AlbumsView after moving the images to Trash. 160 ==> 368796 - Problem with Exif-tags: ImageDescription and UserComment. 161 ==> 392417 - AppImage (5.9.0-01-x86-64) does not support "--install" cli parameter. 162 ==> 392922 - digikam-6.0.0 fail to start. 163 ==> 391399 - Not possible to add location bookmarks in Digikam >5.6.0. 164 ==> 380876 - Tags in Digikam DB maintained after being removed from file and file re-scanned. 165 ==> 392017 - Merging, renaming and removing face tags. 166 ==> 352711 - Externally removed tags are not removed from digiKam. 167 ==> 393108 - Tags not always visible when selecting multiple pictures in a group. 168 ==> 392656 - Selecting a root album for face scan doesn't include subfolders, but rather scans an unexpected album set. 169 ==> 329438 - Rename function with Date & Time does not work with NTFS. 170 ==> 376473 - Can"t set empty IPTC country code when using metadata templates. 171 ==> 380289 - Cannot write to Albums residing on NFS. 172 ==> 384465 - With Compact Flash Card Created date in thumbnails is wrong. 173 ==> 381958 - Cannot add additional collection. 174 ==> 383747 - "Rotate only by setting a flag" Changes Image Instead. 175 ==> 387977 - No icon only view of "Extras sidebar": sidebar taking up a lot of space. 176 ==> 277502 - All versions of version set always displayed in Album view [patch]. 177 ==> 393283 - Caption not updating Exif.Image.ImageDescription field. 178 ==> 391060 - Crashes on undo of very large tif. 179 ==> 366305 - Add a message at startup about the lack of temporary space to perform Undo operations. 180 ==> 366391 - Rotating an image seems to forget to reset the orientation flag. 181 ==> 393654 - Not able to select gpx file. 182 ==> 367596 - Sub-folder count images but don't show them (unsupported JPEG file?). 183 ==> 379922 - Digikam won't remove tags set by Windows Explorer. 184 ==> 379081 - GPS data are in file but geolocation indicator is not shown and map view empty. 185 ==> 354819 - Specific pictures not showing up in digikam. 186 ==> 393855 - MySQL/MariaDB upgrade fails. 187 ==> 384603 - Camera Creation Date not set from EXIF data. 188 ==> 386959 - Properties view: wrong creation date [patch]. 189 ==> 393970 - No mts video thumbnails. 190 ==> 393728 - Reread metadata from Video uses sidecar only. 191 ==> 393925 - UpdateSchemaFromV7ToV9 fails due to duplicate key in album_2. 192 ==> 393773 - showfoto crashes when geotagging. 193 ==> 388199 - No context menu to copy coordinates from map. 194 ==> 393399 - Windows defender freaks out in windows 10 and Edge. 195 ==> 392134 - SIGSEGV While Scanning Faces [patch]. 196 ==> 394168 - OSM Search Yields No Results. 197 ==> 377719 - Cannot rename file with overwrite [patch]. 198 ==> 388002 - remove kio related legacy [patch] 199 ==> 394242 - Import settings unneccesarily asks to overwrite image database, and crashes when I decline. 200 ==> 394278 - A slideshow theme for kiosk mode. 201 ==> 340389 - digiKam crashes while editing pictures for color balancing on OSX [patch]. 202 ==> 394413 - Unify group handling [patch]. 203 ==> 394573 - Revers geodata from open street map does not work. 204 ==> 394590 - Feature request: being able to filter on all metadatas fields. 205 ==> 394671 - Distortion on Panasonic DMC-LX15. 206 ==> 393205 - Advanced rename very slow. 207 ==> 382474 - Thumbnail regeneration. 208 ==> 394865 - digikam suspicious crash on exit. 209 ==> 390541 - Tooltip background cannot be changed. 210 ==> 391521 - "Tool-tip" box difficult to read due to default color scheme. -211 ==> +211 ==> 377849 - Albums disappear when the network is interrupted. +212 ==> diff --git a/core/libs/database/collection/collectionmanager.cpp b/core/libs/database/collection/collectionmanager.cpp index dcf34e8644..46bb787399 100644 --- a/core/libs/database/collection/collectionmanager.cpp +++ b/core/libs/database/collection/collectionmanager.cpp @@ -1,1796 +1,1803 @@ /* ============================================================ * * This file is a part of digiKam project * http://www.digikam.org * * Date : 2007-04-09 * Description : Collection location management * * Copyright (C) 2007-2009 by Marcel Wiesweg * * This program is free software; you can redistribute it * and/or modify it under the terms of the GNU General * Public License as published by the Free Software Foundation; * either version 2, or (at your option) * any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * ============================================================ */ #include "collectionmanager.h" // Qt includes #include #include #include #include #include // KDE includes #include #include #include #include #include #include #include #include #include // Local includes #include "digikam_debug.h" #include "coredbaccess.h" #include "coredbchangesets.h" #include "coredbtransaction.h" #include "coredb.h" #include "collectionscanner.h" #include "collectionlocation.h" namespace Digikam { class AlbumRootLocation : public CollectionLocation { public: AlbumRootLocation() : available(false), hidden(false) { } explicit AlbumRootLocation(const AlbumRootInfo& info) { qCDebug(DIGIKAM_DATABASE_LOG) << "Creating new Location " << info.specificPath << " uuid " << info.identifier; m_id = info.id; m_type = (Type)info.type; specificPath = info.specificPath; identifier = info.identifier; m_label = info.label; m_path.clear(); setStatus((CollectionLocation::Status)info.status); } void setStatusFromFlags() { if (hidden) { m_status = CollectionLocation::LocationHidden; } else { if (available) { m_status = CollectionLocation::LocationAvailable; } else { m_status = CollectionLocation::LocationUnavailable; } } } void setStatus(CollectionLocation::Status s) { m_status = s; // status is exclusive, and Hidden wins // but really both states are independent // - a hidden location might or might not be available if (m_status == CollectionLocation::LocationAvailable) { available = true; hidden = false; } else if (m_status == CollectionLocation::LocationHidden) { available = false; hidden = true; } else // Unavailable, Null, Deleted { available = false; hidden = false; } } void setId(int id) { m_id = id; } void setAbsolutePath(const QString& path) { m_path = path; } void setType(Type type) { m_type = type; } void setLabel(const QString& label) { m_label = label; } public: QString identifier; QString specificPath; bool available; bool hidden; }; // ------------------------------------------------- class SolidVolumeInfo { public: SolidVolumeInfo() : isRemovable(false), isOpticalDisc(false), isMounted(false) { } bool isNull() const { return path.isNull(); } public: QString udi; // Solid device UDI of the StorageAccess device QString path; // mount path of volume, with trailing slash QString uuid; // UUID as from Solid QString label; // volume label (think of CDs) bool isRemovable; // may be removed bool isOpticalDisc; // is an optical disk device as CD/DVD/BR bool isMounted; // is mounted on File System. }; // ------------------------------------------------- class CollectionManagerPrivate { public: explicit CollectionManagerPrivate(CollectionManager* s); // hack for Solid's threading problems QList actuallyListVolumes(); void slotTriggerUpdateVolumesList(); QList volumesListCache; /// Access Solid and return a list of storage volumes QList listVolumes(); /** * Find from a given list (usually the result of listVolumes) the volume * corresponding to the location */ SolidVolumeInfo findVolumeForLocation(const AlbumRootLocation* location, const QList volumes); /** * Find from a given list (usually the result of listVolumes) the volume * on which the file path specified by the url is located. */ SolidVolumeInfo findVolumeForUrl(const QUrl& fileUrl, const QList volumes); /// Create the volume identifier for the given volume info static QString volumeIdentifier(const SolidVolumeInfo& info); /// Create a volume identifier based on the path only QString volumeIdentifier(const QString& path); /// Create a network share identifier based on the mountpath QString networkShareIdentifier(const QString& path); /// Return the path, if location has a path-only identifier. Else returns a null string. QString pathFromIdentifier(const AlbumRootLocation* location); /// Return the path, if location has a path-only identifier. Else returns a null string. QStringList networkShareMountPathsFromIdentifier(const AlbumRootLocation* location); /// Create an MD5 hash of the top-level entries (file names, not file content) of the given path static QString directoryHash(const QString& path); /// Check if a location for specified path exists, assuming the given list of locations was deleted bool checkIfExists(const QString& path, QList assumeDeleted); /// Make a user presentable description, regardless of current location status QString technicalDescription(const AlbumRootLocation* location); public: QMap locations; bool changingDB; QStringList udisToWatch; bool watchEnabled; CollectionManager* s; }; // ------------------------------------------------- class ChangingDB { public: explicit ChangingDB(CollectionManagerPrivate* d) : d(d) { d->changingDB = true; } ~ChangingDB() { d->changingDB = false; } public: CollectionManagerPrivate* const d; }; } // namespace Digikam // This is because of the private slot; we'd want a collectionmanager_p.h #include "collectionmanager.h" // krazy:exclude=includes namespace Digikam { CollectionManagerPrivate::CollectionManagerPrivate(CollectionManager* s) : changingDB(false), watchEnabled(false), s(s) { QObject::connect(s, SIGNAL(triggerUpdateVolumesList()), s, SLOT(slotTriggerUpdateVolumesList()), Qt::BlockingQueuedConnection); } QList CollectionManagerPrivate::listVolumes() { // Move calls to Solid to the main thread. // Solid was meant to be thread-safe, but it is not (KDE4.0), // calling from a non-UI thread leads to a reversible // lock-up of variable length. if (QThread::currentThread() == QCoreApplication::instance()->thread()) { return actuallyListVolumes(); } else { // emit a blocking queued signal to move call to main thread emit(s->triggerUpdateVolumesList()); return volumesListCache; } } void CollectionManagerPrivate::slotTriggerUpdateVolumesList() { volumesListCache = actuallyListVolumes(); } QList CollectionManagerPrivate::actuallyListVolumes() { QList volumes; //qCDebug(DIGIKAM_DATABASE_LOG) << "listFromType"; QList devices = Solid::Device::listFromType(Solid::DeviceInterface::StorageAccess); //qCDebug(DIGIKAM_DATABASE_LOG) << "got listFromType"; udisToWatch.clear(); foreach(const Solid::Device& accessDevice, devices) { // check for StorageAccess if (!accessDevice.is()) { continue; } // mark as a device of principal interest udisToWatch << accessDevice.udi(); const Solid::StorageAccess* access = accessDevice.as(); // watch mount status (remove previous connections) QObject::disconnect(access, SIGNAL(accessibilityChanged(bool,QString)), s, SLOT(accessibilityChanged(bool,QString))); QObject::connect(access, SIGNAL(accessibilityChanged(bool,QString)), s, SLOT(accessibilityChanged(bool,QString))); if (!access->isAccessible()) { continue; } // check for StorageDrive Solid::Device driveDevice; for (Solid::Device currentDevice = accessDevice; currentDevice.isValid(); currentDevice = currentDevice.parent()) { if (currentDevice.is()) { driveDevice = currentDevice; break; } } /* * We cannot require a drive, some logical volumes may not have "one" drive as parent * See bug 273369 if (!driveDevice.isValid()) { continue; } */ Solid::StorageDrive* drive = driveDevice.as(); // check for StorageVolume Solid::Device volumeDevice; for (Solid::Device currentDevice = accessDevice; currentDevice.isValid(); currentDevice = currentDevice.parent()) { if (currentDevice.is()) { volumeDevice = currentDevice; break; } } if (!volumeDevice.isValid()) { continue; } Solid::StorageVolume* const volume = volumeDevice.as(); SolidVolumeInfo info; info.udi = accessDevice.udi(); info.path = QDir::fromNativeSeparators(access->filePath()); info.isMounted = access->isAccessible(); if (!info.path.isEmpty() && !info.path.endsWith(QLatin1Char('/'))) { info.path += QLatin1Char('/'); } info.uuid = volume->uuid(); info.label = volume->label(); if (drive) { info.isRemovable = drive->isHotpluggable() || drive->isRemovable(); } else { // impossible to know, but probably not hotpluggable (see comment above) info.isRemovable = false; } info.isOpticalDisc = volumeDevice.is(); volumes << info; } // This is the central place where the watch is enabled watchEnabled = true; return volumes; } QString CollectionManagerPrivate::volumeIdentifier(const SolidVolumeInfo& volume) { QUrl url; url.setScheme(QLatin1String("volumeid")); // On changing these, please update the checkLocation() code bool identifyByUUID = !volume.uuid.isEmpty(); bool identifyByLabel = !identifyByUUID && !volume.label.isEmpty() && (volume.isOpticalDisc || volume.isRemovable); bool addDirectoryHash = identifyByLabel && volume.isOpticalDisc; bool identifyByMountPath = !identifyByUUID && !identifyByLabel; if (identifyByUUID) { QUrlQuery q(url); q.addQueryItem(QLatin1String("uuid"), volume.uuid); url.setQuery(q); } if (identifyByLabel) { QUrlQuery q(url); q.addQueryItem(QLatin1String("label"), volume.label); url.setQuery(q); } if (addDirectoryHash) { // for CDs, we store a hash of the root directory. May be useful. QString dirHash = directoryHash(volume.path); if (!dirHash.isNull()) { QUrlQuery q(url); q.addQueryItem(QLatin1String("directoryhash"), dirHash); url.setQuery(q); } } if (identifyByMountPath) { QUrlQuery q(url); q.addQueryItem(QLatin1String("mountpath"), volume.path); url.setQuery(q); } return url.url(); } QString CollectionManagerPrivate::volumeIdentifier(const QString& path) { QUrl url; url.setScheme(QLatin1String("volumeid")); QUrlQuery q(url); q.addQueryItem(QLatin1String("path"), path); url.setQuery(q); return url.url(); } QString CollectionManagerPrivate::networkShareIdentifier(const QString& path) { QUrl url; url.setScheme(QLatin1String("networkshareid")); QUrlQuery q(url); q.addQueryItem(QLatin1String("mountpath"), path); url.setQuery(q); return url.url(); } QString CollectionManagerPrivate::pathFromIdentifier(const AlbumRootLocation* location) { QUrl url(location->identifier); if (url.scheme() != QLatin1String("volumeid")) { return QString(); } return QUrlQuery(url).queryItemValue(QLatin1String("path")); } QStringList CollectionManagerPrivate::networkShareMountPathsFromIdentifier(const AlbumRootLocation* location) { // using a QUrl because QUrl cannot handle duplicate query items QUrl url(location->identifier); if (url.scheme() != QLatin1String("networkshareid")) { return QStringList(); } return QUrlQuery(url).allQueryItemValues(QLatin1String("mountpath")); } QString CollectionManagerPrivate::directoryHash(const QString& path) { QDir dir(path); if (dir.isReadable()) { QStringList entries = dir.entryList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot); QCryptographicHash md5(QCryptographicHash::Md5); foreach(const QString& entry, entries) { md5.addData(entry.toUtf8()); } return QString::fromUtf8(md5.result().toHex()); } return QString(); } SolidVolumeInfo CollectionManagerPrivate::findVolumeForLocation(const AlbumRootLocation* location, const QList volumes) { QUrl url(location->identifier); QString queryItem; if (url.scheme() != QLatin1String("volumeid")) { return SolidVolumeInfo(); } if (!(queryItem = QUrlQuery(url).queryItemValue(QLatin1String("uuid"))).isNull()) { foreach(const SolidVolumeInfo& volume, volumes) { if (volume.uuid.compare(queryItem, Qt::CaseInsensitive) == 0) { return volume; } } return SolidVolumeInfo(); } else if (!(queryItem = QUrlQuery(url).queryItemValue(QLatin1String("label"))).isNull()) { // This one is a bit more difficult, as we take into account the possibility // that the label is not unique, and we take some care to make it work anyway. // find all available volumes with the given label (usually one) QList candidateVolumes; foreach(const SolidVolumeInfo& volume, volumes) { if (volume.label == queryItem) { candidateVolumes << volume; } } if (candidateVolumes.isEmpty()) { return SolidVolumeInfo(); } // find out of there is another location with the same label (usually not) bool hasOtherLocation = false; foreach(AlbumRootLocation* const otherLocation, locations) { if (otherLocation == location) { continue; } QUrl otherUrl(otherLocation->identifier); if (otherUrl.scheme() == QLatin1String("volumeid") && QUrlQuery(otherUrl).queryItemValue(QLatin1String("label")) == queryItem) { hasOtherLocation = true; break; } } // the usual, easy case if (candidateVolumes.size() == 1 && !hasOtherLocation) { return candidateVolumes.first(); } else { // not unique: try to use the directoryhash QString dirHash = QUrlQuery(url).queryItemValue(QLatin1String("directoryhash")); // bail out if not provided if (dirHash.isNull()) { qCDebug(DIGIKAM_DATABASE_LOG) << "No directory hash specified for the non-unique Label" << queryItem << "Resorting to returning the first match."; return candidateVolumes.first(); } // match against directory hash foreach(const SolidVolumeInfo& volume, candidateVolumes) { QString volumeDirHash = directoryHash(volume.path); if (volumeDirHash == dirHash) { return volume; } } } return SolidVolumeInfo(); } else if (!(queryItem = QUrlQuery(url).queryItemValue(QLatin1String("mountpath"))).isNull()) { foreach(const SolidVolumeInfo& volume, volumes) { if (volume.isMounted && volume.path == queryItem) { return volume; } } return SolidVolumeInfo(); } return SolidVolumeInfo(); } QString CollectionManagerPrivate::technicalDescription(const AlbumRootLocation* albumLoc) { QUrl url(albumLoc->identifier); QString queryItem; if (url.scheme() == QLatin1String("volumeid")) { if (!(queryItem = QUrlQuery(url).queryItemValue(QLatin1String("uuid"))).isNull()) { return i18nc("\"relative path\" on harddisk partition with \"UUID\"", "Folder \"%1\" on the volume with the id \"%2\"", QDir::toNativeSeparators(albumLoc->specificPath), queryItem); } else if (!(queryItem = QUrlQuery(url).queryItemValue(QLatin1String("label"))).isNull()) { return i18nc("\"relative path\" on harddisk partition with \"label\"", "Folder \"%1\" on the volume labeled \"%2\"", QDir::toNativeSeparators(albumLoc->specificPath), queryItem); } else if (!(queryItem = QUrlQuery(url).queryItemValue(QLatin1String("mountpath"))).isNull()) { return QString::fromUtf8("\"%1\"").arg(queryItem); } } else if (url.scheme() == QLatin1String("networkshareid")) { if (!(queryItem = QUrlQuery(url).queryItemValue(QLatin1String("mountpath"))).isNull()) { return i18nc("@info", "Shared directory mounted at %1", QDir::toNativeSeparators(queryItem)); } } return QString(); } SolidVolumeInfo CollectionManagerPrivate::findVolumeForUrl(const QUrl& fileUrl, const QList volumes) { SolidVolumeInfo volume; // v.path is specified to have a trailing slash. path needs one as well. QString path = fileUrl.toLocalFile() + QLatin1String("/"); int volumeMatch = 0; //FIXME: Network shares! Here we get only the volume of the mount path... // This is probably not really clean. But Solid does not help us. foreach(const SolidVolumeInfo& v, volumes) { if (v.isMounted && !v.path.isEmpty() && path.startsWith(v.path)) { int length = v.path.length(); if (length > volumeMatch) { volumeMatch = v.path.length(); volume = v; } } } if (!volumeMatch) { qCDebug(DIGIKAM_DATABASE_LOG) << "Failed to detect a storage volume for path " << path << " with Solid"; } return volume; } bool CollectionManagerPrivate::checkIfExists(const QString& filePath, QList assumeDeleted) { const QUrl filePathUrl = QUrl::fromLocalFile(filePath); CoreDbAccess access; foreach(AlbumRootLocation* const location, locations) { const QUrl locationPathUrl = QUrl::fromLocalFile(location->albumRootPath()); //qCDebug(DIGIKAM_DATABASE_LOG) << filePathUrl << locationPathUrl; // make sure filePathUrl is neither a child nor a parent // of an existing collection if (!locationPathUrl.isEmpty() && (filePathUrl.isParentOf(locationPathUrl) || locationPathUrl.isParentOf(filePathUrl)) ) { bool isDeleted = false; foreach(const CollectionLocation& deletedLoc, assumeDeleted) { if (deletedLoc.id() == location->id()) { isDeleted = true; break; } } if (!isDeleted) { return true; } } } return false; } // ------------------------------------------------- CollectionManager* CollectionManager::m_instance = 0; CollectionManager* CollectionManager::instance() { if (!m_instance) { m_instance = new CollectionManager; } return m_instance; } void CollectionManager::cleanUp() { delete m_instance; m_instance = 0; } CollectionManager::CollectionManager() : d(new CollectionManagerPrivate(this)) { qRegisterMetaType("CollectionLocation"); connect(Solid::DeviceNotifier::instance(), SIGNAL(deviceAdded(QString)), this, SLOT(deviceAdded(QString))); connect(Solid::DeviceNotifier::instance(), SIGNAL(deviceRemoved(QString)), this, SLOT(deviceRemoved(QString))); // CoreDbWatch slot is connected at construction of CoreDbWatch, which may be later. } CollectionManager::~CollectionManager() { qDeleteAll(d->locations.values()); delete d; } void CollectionManager::refresh() { { // if called from the CoreDbAccess constructor itself, it will // hold a flag to prevent endless recursion CoreDbAccess access; clear_locked(); } updateLocations(); } void CollectionManager::setWatchDisabled() { d->watchEnabled = false; } CollectionLocation CollectionManager::addLocation(const QUrl& fileUrl, const QString& label) { qCDebug(DIGIKAM_DATABASE_LOG) << "addLocation " << fileUrl; QString path = fileUrl.adjusted(QUrl::StripTrailingSlash).toLocalFile(); if (!locationForPath(path).isNull()) { return CollectionLocation(); } QList volumes = d->listVolumes(); SolidVolumeInfo volume = d->findVolumeForUrl(fileUrl, volumes); if (!volume.isNull()) { CoreDbAccess access; // volume.path has a trailing slash. We want to split in front of this. QString specificPath = path.mid(volume.path.length() - 1); AlbumRoot::Type type; if (volume.isRemovable) { type = AlbumRoot::VolumeRemovable; } else { type = AlbumRoot::VolumeHardWired; } ChangingDB changing(d); access.db()->addAlbumRoot(type, d->volumeIdentifier(volume), specificPath, label); } else { // Empty volumes indicates that Solid is not working correctly. if (volumes.isEmpty()) { qCDebug(DIGIKAM_DATABASE_LOG) << "Solid did not return any storage volumes on your system."; qCDebug(DIGIKAM_DATABASE_LOG) << "This indicates a missing implementation or a problem with your installation"; qCDebug(DIGIKAM_DATABASE_LOG) << "On Linux, check that Solid and HAL are working correctly." "Problems with RAID partitions have been reported, if you have RAID this error may be normal."; qCDebug(DIGIKAM_DATABASE_LOG) << "On Windows, Solid may not be fully implemented, if you are running Windows this error may be normal."; } // fall back qCWarning(DIGIKAM_DATABASE_LOG) << "Unable to identify a path with Solid. Adding the location with path only."; ChangingDB changing(d); CoreDbAccess().db()->addAlbumRoot(AlbumRoot::VolumeHardWired, d->volumeIdentifier(path), QLatin1String("/"), label); } // Do not emit the locationAdded signal here, it is done in updateLocations() updateLocations(); return locationForPath(path); } CollectionLocation CollectionManager::addNetworkLocation(const QUrl& fileUrl, const QString& label) { qCDebug(DIGIKAM_DATABASE_LOG) << "addLocation " << fileUrl; QString path = fileUrl.adjusted(QUrl::StripTrailingSlash).toLocalFile(); if (!locationForPath(path).isNull()) { return CollectionLocation(); } ChangingDB changing(d); CoreDbAccess().db()->addAlbumRoot(AlbumRoot::Network, d->networkShareIdentifier(path), QLatin1String("/"), label); // Do not emit the locationAdded signal here, it is done in updateLocations() updateLocations(); return locationForPath(path); } CollectionManager::LocationCheckResult CollectionManager::checkLocation(const QUrl& fileUrl, QList assumeDeleted, QString* message, QString* iconName) { if (!fileUrl.isLocalFile()) { if (message) { *message = i18n("Sorry, digiKam does not support remote URLs as collections."); } if (iconName) { *iconName = QLatin1String("dialog-error"); } return LocationNotAllowed; } QString path = fileUrl.adjusted(QUrl::StripTrailingSlash).toLocalFile(); QDir dir(path); if (!dir.isReadable()) { if (message) { *message = i18n("The selected folder does not exist or is not readable"); } if (iconName) { *iconName = QLatin1String("dialog-error"); } return LocationNotAllowed; } if (d->checkIfExists(path, assumeDeleted)) { if (message) { *message = i18n("There is already a collection containing the folder \"%1\"", QDir::toNativeSeparators(path)); } if (iconName) { *iconName = QLatin1String("dialog-error"); } return LocationNotAllowed; } QList volumes = d->listVolumes(); SolidVolumeInfo volume = d->findVolumeForUrl(fileUrl, volumes); if (!volume.isNull()) { if (!volume.uuid.isEmpty()) { if (volume.isRemovable) { if (message) { *message = i18n("The storage media can be uniquely identified."); } if (iconName) { *iconName = QLatin1String("drive-removable-media"); } } else { if (message) { *message = i18n("The collection is located on your harddisk"); } if (iconName) { *iconName = QLatin1String("drive-harddisk"); } } return LocationAllRight; } else if (!volume.label.isEmpty() && (volume.isOpticalDisc || volume.isRemovable)) { if (volume.isOpticalDisc) { bool hasOtherLocation = false; foreach(AlbumRootLocation* const otherLocation, d->locations) { QUrl otherUrl(otherLocation->identifier); if (otherUrl.scheme() == QLatin1String("volumeid") && QUrlQuery(otherUrl).queryItemValue(QLatin1String("label")) == volume.label) { hasOtherLocation = true; break; } } if (iconName) { *iconName = QLatin1String("media-optical"); } if (hasOtherLocation) { if (message) *message = i18n("This is a CD/DVD, which is identified by the label " "that you can set in your CD burning application. " "There is already another entry with the same label. " "The two will be distinguished by the files in the top directory, " "so please do not append files to the CD, or it will not be recognized. " "In the future, please set a unique label on your CDs and DVDs " "if you intend to use them with digiKam."); return LocationHasProblems; } else { if (message) *message = i18n("This is a CD/DVD. It will be identified by the label (\"%1\")" "that you have set in your CD burning application. " "If you create further CDs for use with digikam in the future, " "please remember to give them a unique label as well.", volume.label); return LocationAllRight; } } else { // Which situation? HasProblems or AllRight? if (message) *message = i18n("This is a removable storage medium that will be identified by its label (\"%1\")", volume.label); if (iconName) { *iconName = QLatin1String("drive-removable-media"); } return LocationAllRight; } } else { if (message) *message = i18n("This entry will only be identified by the path where it is found on your system (\"%1\"). " "No more specific means of identification (UUID, label) is available.", QDir::toNativeSeparators(volume.path)); if (iconName) { *iconName = QLatin1String("drive-removale-media"); } return LocationHasProblems; } } else { if (message) *message = i18n("It is not possible on your system to identify the storage medium of this path. " "It will be added using the file path as the only identifier. " "This will work well for your local hard disk."); if (iconName) { *iconName = QLatin1String("folder-important"); } return LocationHasProblems; } } CollectionManager::LocationCheckResult CollectionManager::checkNetworkLocation(const QUrl& fileUrl, QList assumeDeleted, QString* message, QString* iconName) { if (!fileUrl.isLocalFile()) { if (message) { if (fileUrl.scheme() == QLatin1String("smb")) *message = i18n("You need to locally mount your Samba share. " "Sorry, digiKam does currently not support smb:// URLs. "); else *message = i18n("Your network storage must be set up to be accessible " "as files and folders through the operating system. " "DigiKam does not support remote URLs."); } if (iconName) { *iconName = QLatin1String("dialog-error"); } return LocationNotAllowed; } QString path = fileUrl.adjusted(QUrl::StripTrailingSlash).toLocalFile(); QDir dir(path); if (!dir.isReadable()) { if (message) { *message = i18n("The selected folder does not exist or is not readable"); } if (iconName) { *iconName = QLatin1String("dialog-error"); } return LocationNotAllowed; } if (d->checkIfExists(path, assumeDeleted)) { if (message) { *message = i18n("There is already a collection for a network share with the same path."); } if (iconName) { *iconName = QLatin1String("dialog-error"); } return LocationNotAllowed; } if (message) *message = i18n("The network share will be identified by the path you selected. " "If the path is empty, the share will be considered unavailable."); if (iconName) { *iconName = QLatin1String("network-wired-activated"); } return LocationAllRight; } void CollectionManager::removeLocation(const CollectionLocation& location) { { CoreDbAccess access; AlbumRootLocation* const albumLoc = d->locations.value(location.id()); if (!albumLoc) { return; } // Ensure that all albums are set to orphan and no images will be permanently deleted, // as would do only calling deleteAlbumRoot by a Trigger QList albumIds = access.db()->getAlbumsOnAlbumRoot(albumLoc->id()); ChangingDB changing(d); CollectionScanner scanner; CoreDbTransaction transaction(&access); scanner.safelyRemoveAlbums(albumIds); access.db()->deleteAlbumRoot(albumLoc->id()); } // Do not emit the locationRemoved signal here, it is done in updateLocations() updateLocations(); } QList CollectionManager::checkHardWiredLocations() { QList disappearedLocations; QList volumes = d->listVolumes(); CoreDbAccess access; foreach(AlbumRootLocation* const location, d->locations) { // Hardwired and unavailable? if (location->type() == CollectionLocation::TypeVolumeHardWired && location->status() == CollectionLocation::LocationUnavailable) { disappearedLocations << *location; } } return disappearedLocations; } void CollectionManager::migrationCandidates(const CollectionLocation& location, QString* const description, QStringList* const candidateIdentifiers, QStringList* const candidateDescriptions) { description->clear(); candidateIdentifiers->clear(); candidateDescriptions->clear(); QList volumes = d->listVolumes(); CoreDbAccess access; AlbumRootLocation* const albumLoc = d->locations.value(location.id()); if (!albumLoc) { return; } *description = d->technicalDescription(albumLoc); // Find possible new volumes where the specific path is found. foreach(const SolidVolumeInfo& info, volumes) { if (info.isMounted && !info.path.isEmpty()) { QDir dir(info.path + albumLoc->specificPath); if (dir.exists()) { *candidateIdentifiers << d->volumeIdentifier(info); *candidateDescriptions << dir.absolutePath(); } } } } void CollectionManager::migrateToVolume(const CollectionLocation& location, const QString& identifier) { CoreDbAccess access; AlbumRootLocation* const albumLoc = d->locations.value(location.id()); if (!albumLoc) { return; } // update db ChangingDB db(d); access.db()->migrateAlbumRoot(albumLoc->id(), identifier); albumLoc->identifier = identifier; updateLocations(); } void CollectionManager::setLabel(const CollectionLocation& location, const QString& label) { CoreDbAccess access; AlbumRootLocation* const albumLoc = d->locations.value(location.id()); if (!albumLoc) { return; } // update db ChangingDB db(d); access.db()->setAlbumRootLabel(albumLoc->id(), label); // update local structure albumLoc->setLabel(label); emit locationPropertiesChanged(*albumLoc); } void CollectionManager::changeType(const CollectionLocation& location, int type) { CoreDbAccess access; AlbumRootLocation* const albumLoc = d->locations.value(location.id()); if (!albumLoc) { return; } // update db ChangingDB db(d); access.db()->changeAlbumRootType(albumLoc->id(), (AlbumRoot::Type)type); // update local structure albumLoc->setType((CollectionLocation::Type)type); emit locationPropertiesChanged(*albumLoc); } QList CollectionManager::allLocations() { CoreDbAccess access; QList list; foreach(AlbumRootLocation* const location, d->locations) { list << *location; } return list; } QList CollectionManager::allAvailableLocations() { CoreDbAccess access; QList list; foreach(AlbumRootLocation* const location, d->locations) { if (location->status() == CollectionLocation::LocationAvailable) { list << *location; } } return list; } QStringList CollectionManager::allAvailableAlbumRootPaths() { CoreDbAccess access; QStringList list; foreach(AlbumRootLocation* const location, d->locations) { if (location->status() == CollectionLocation::LocationAvailable) { list << location->albumRootPath(); } } return list; } CollectionLocation CollectionManager::locationForAlbumRootId(int id) { CoreDbAccess access; AlbumRootLocation* const location = d->locations.value(id); if (location) { return *location; } else { return CollectionLocation(); } } CollectionLocation CollectionManager::locationForAlbumRoot(const QUrl& fileUrl) { return locationForAlbumRootPath(fileUrl.adjusted(QUrl::StripTrailingSlash).toLocalFile()); } CollectionLocation CollectionManager::locationForAlbumRootPath(const QString& albumRootPath) { + if (!QFileInfo::exists(albumRootPath)) + { + qCWarning(DIGIKAM_DATABASE_LOG) << "Album root path not exist" << albumRootPath; + qCWarning(DIGIKAM_DATABASE_LOG) << "Drive or network connection broken?"; + + updateLocations(); + } + CoreDbAccess access; - QString path = albumRootPath; foreach(AlbumRootLocation* const location, d->locations) { - if (location->albumRootPath() == path) + if (location->albumRootPath() == albumRootPath) { return *location; } } return CollectionLocation(); } CollectionLocation CollectionManager::locationForUrl(const QUrl& fileUrl) { return locationForPath(fileUrl.adjusted(QUrl::StripTrailingSlash).toLocalFile()); } CollectionLocation CollectionManager::locationForPath(const QString& givenPath) { CoreDbAccess access; foreach(AlbumRootLocation* const location, d->locations) { QString rootPath = location->albumRootPath(); QString filePath = QDir::fromNativeSeparators(givenPath); if (!rootPath.isEmpty() && filePath.startsWith(rootPath)) { // see also bug #221155 for extra checks if (filePath == rootPath || filePath.startsWith(rootPath + QLatin1Char('/'))) { return *location; } } } return CollectionLocation(); } QString CollectionManager::albumRootPath(int id) { CoreDbAccess access; CollectionLocation* const location = d->locations.value(id); if (location && location->status() == CollectionLocation::LocationAvailable) { return location->albumRootPath(); } return QString(); } QString CollectionManager::albumRootLabel(int id) { CoreDbAccess access; CollectionLocation* const location = d->locations.value(id); if (location && location->status() == CollectionLocation::LocationAvailable) { return location->label(); } return QString(); } QUrl CollectionManager::albumRoot(const QUrl& fileUrl) { return QUrl::fromLocalFile(albumRootPath(fileUrl.adjusted(QUrl::StripTrailingSlash).toLocalFile())); } QString CollectionManager::albumRootPath(const QUrl& fileUrl) { return albumRootPath(fileUrl.adjusted(QUrl::StripTrailingSlash).toLocalFile()); } QString CollectionManager::albumRootPath(const QString& givenPath) { CoreDbAccess access; foreach(AlbumRootLocation* const location, d->locations) { QString rootPath = location->albumRootPath(); QString filePath = QDir::fromNativeSeparators(givenPath); if (!rootPath.isEmpty() && filePath.startsWith(rootPath)) { // see also bug #221155 for extra checks if (filePath == rootPath || filePath.startsWith(rootPath + QLatin1Char('/'))) { return location->albumRootPath(); } } } return QString(); } bool CollectionManager::isAlbumRoot(const QUrl& fileUrl) { return isAlbumRoot(fileUrl.adjusted(QUrl::StripTrailingSlash).toLocalFile()); } bool CollectionManager::isAlbumRoot(const QString& filePath) { CoreDbAccess access; foreach(AlbumRootLocation* const location, d->locations) { if (filePath == location->albumRootPath()) { return true; } } return false; } QString CollectionManager::album(const QUrl& fileUrl) { return album(fileUrl.adjusted(QUrl::StripTrailingSlash).toLocalFile()); } QString CollectionManager::album(const QString& filePath) { CoreDbAccess access; foreach(AlbumRootLocation* const location, d->locations) { QString absolutePath = location->albumRootPath(); if (absolutePath.isEmpty()) { continue; } QString firstPart = filePath.left(absolutePath.length()); if (firstPart == absolutePath) { if (filePath == absolutePath || (filePath.length() == absolutePath.length() + 1 && filePath.right(1) == QLatin1String("/"))) { return QLatin1String("/"); } else { QString album = filePath.mid(absolutePath.length()); if (album.endsWith(QLatin1Char('/'))) { album.chop(1); } return album; } } } return QString(); } QString CollectionManager::album(const CollectionLocation& location, const QUrl& fileUrl) { return album(location, fileUrl.adjusted(QUrl::StripTrailingSlash).toLocalFile()); } QString CollectionManager::album(const CollectionLocation& location, const QString& filePath) { if (location.isNull()) { return QString(); } QString absolutePath = location.albumRootPath(); if (filePath == absolutePath) { return QLatin1String("/"); } else { QString album = filePath.mid(absolutePath.length()); if (album.endsWith(QLatin1Char('/'))) { album.chop(1); } return album; } } QUrl CollectionManager::oneAlbumRoot() { return QUrl::fromLocalFile(oneAlbumRootPath()); } QString CollectionManager::oneAlbumRootPath() { CoreDbAccess access; foreach(AlbumRootLocation* const location, d->locations) { if (location->status() == CollectionLocation::LocationAvailable) { return location->albumRootPath(); } } return QString(); } void CollectionManager::deviceAdded(const QString& udi) { if (!d->watchEnabled) { return; } Solid::Device device(udi); if (device.is()) { updateLocations(); } } void CollectionManager::deviceRemoved(const QString& udi) { if (!d->watchEnabled) { return; } // we can't access the Solid::Device to check because it is removed CoreDbAccess access; if (!d->udisToWatch.contains(udi)) { return; } updateLocations(); } void CollectionManager::accessibilityChanged(bool accessible, const QString& udi) { Q_UNUSED(accessible); Q_UNUSED(udi); updateLocations(); } void CollectionManager::updateLocations() { // get information from Solid QList volumes; { // Absolutely ensure that the db mutex is not held when emitting the blocking queued signal! Deadlock! CoreDbAccessUnlock unlock; volumes = d->listVolumes(); } { CoreDbAccess access; // read information from database QList infos = access.db()->getAlbumRoots(); // synchronize map with database QMap locs = d->locations; d->locations.clear(); foreach(const AlbumRootInfo& info, infos) { if (locs.contains(info.id)) { d->locations[info.id] = locs.value(info.id); locs.remove(info.id); } else { d->locations[info.id] = new AlbumRootLocation(info); } } // delete old locations foreach(AlbumRootLocation* const location, locs) { CollectionLocation::Status oldStatus = location->status(); location->setStatus(CollectionLocation::LocationDeleted); emit locationStatusChanged(*location, oldStatus); delete location; } // update status with current access state, store old status in list QList oldStatus; foreach(AlbumRootLocation* const location, d->locations) { oldStatus << location->status(); bool available = false; QString absolutePath; if (location->type() == CollectionLocation::TypeNetwork) { foreach(const QString& path, d->networkShareMountPathsFromIdentifier(location)) { QDir dir(path); available = dir.isReadable() && dir.entryList(QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot).count() > 0; absolutePath = path; if (available) { break; } } } else { SolidVolumeInfo info = d->findVolumeForLocation(location, volumes); if (!info.isNull()) { available = info.isMounted; QString volumePath = info.path; // volume.path has a trailing slash (and this is good) // but specific path has a leading slash, so remove it volumePath.chop(1); // volumePath is the mount point of the volume; // specific path is the path on the file system of the volume. absolutePath = volumePath + location->specificPath; } else { QString path = d->pathFromIdentifier(location); if (!path.isNull()) { available = true; // Here we have the absolute path as definition of the volume. // specificPath is "/" as per convention, but ignored, // absolute path shall not have a trailing slash. absolutePath = path; } } } // set values in location // Don't touch location->status, do not interfere with "hidden" setting location->available = available; location->setAbsolutePath(absolutePath); qCDebug(DIGIKAM_DATABASE_LOG) << "location for " << absolutePath << " is available " << available; // set the status depending on "hidden" and "available" location->setStatusFromFlags(); } // emit status changes (and new locations) int i = 0; foreach(AlbumRootLocation* const location, d->locations) { if (oldStatus.at(i) != location->status()) { emit locationStatusChanged(*location, oldStatus.at(i)); } ++i; } } } void CollectionManager::clear_locked() { // Internal method: Called with mutex locked // Cave: Difficult recursions with CoreDbAccess constructor and setParameters foreach(AlbumRootLocation* const location, d->locations) { CollectionLocation::Status oldStatus = location->status(); location->setStatus(CollectionLocation::LocationDeleted); emit locationStatusChanged(*location, oldStatus); delete location; } d->locations.clear(); } void CollectionManager::slotAlbumRootChange(const AlbumRootChangeset& changeset) { if (d->changingDB) { return; } switch (changeset.operation()) { case AlbumRootChangeset::Added: case AlbumRootChangeset::Deleted: updateLocations(); break; case AlbumRootChangeset::PropertiesChanged: // label has changed { CollectionLocation toBeEmitted; { CoreDbAccess access; AlbumRootLocation* const location = d->locations.value(changeset.albumRootId()); if (location) { QList infos = access.db()->getAlbumRoots(); foreach(const AlbumRootInfo& info, infos) { if (info.id == location->id()) { location->setLabel(info.label); toBeEmitted = *location; break; } } } } if (!toBeEmitted.isNull()) { emit locationPropertiesChanged(toBeEmitted); } } break; case AlbumRootChangeset::Unknown: break; } } } // namespace Digikam #include "moc_collectionmanager.cpp" diff --git a/core/libs/database/collection/collectionscanner.cpp b/core/libs/database/collection/collectionscanner.cpp index 9de355e4f3..7292833b41 100644 --- a/core/libs/database/collection/collectionscanner.cpp +++ b/core/libs/database/collection/collectionscanner.cpp @@ -1,2138 +1,2132 @@ /* ============================================================ * * This file is a part of digiKam project * http://www.digikam.org * * Date : 2007-03-21 * Description : Collection scanning to database. * * Copyright (C) 2005 by Renchi Raju * Copyright (C) 2005-2006 by Tom Albers * Copyright (C) 2007-2011 by Marcel Wiesweg * Copyright (C) 2009-2018 by Gilles Caulier * * This program is free software; you can redistribute it * and/or modify it under the terms of the GNU General * Public License as published by the Free Software Foundation; * either version 2, or (at your option) * any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * ============================================================ */ #include "collectionscanner.h" // C++ includes #include #include #include // Qt includes #include #include #include #include #include #include #include #include // Local includes #include "rawfiles.h" #include "digikam_debug.h" #include "coredb.h" #include "collectionmanager.h" #include "collectionlocation.h" #include "collectionscannerhints.h" #include "collectionscannerobserver.h" #include "coredbaccess.h" #include "coredbbackend.h" #include "coredbtransaction.h" #include "coredboperationgroup.h" #include "imagecomments.h" #include "imagecopyright.h" #include "imageinfo.h" #include "imagescanner.h" #include "metadatasettings.h" #include "tagscache.h" #include "thumbsdbaccess.h" #include "thumbsdb.h" namespace Digikam { class NewlyAppearedFile { public: NewlyAppearedFile() : albumId(0) { } NewlyAppearedFile(int albumId, const QString& fileName) : albumId(albumId), fileName(fileName) { } bool operator==(const NewlyAppearedFile& other) const { return (albumId == other.albumId) && (fileName == other.fileName); } public: int albumId; QString fileName; }; // -------------------------------------------------------------------- inline uint qHash(const NewlyAppearedFile& file) { return ::qHash(file.albumId) ^ ::qHash(file.fileName); } // -------------------------------------------------------------------- static bool modificationDateEquals(const QDateTime& a, const QDateTime& b) { if (a != b) { // allow a "modify window" of one second. // FAT filesystems store the modify date in 2-second resolution. int diff = a.secsTo(b); if (abs(diff) > 1) { return false; } } return true; } // -------------------------------------------------------------------- class CollectionScannerHintContainerImplementation : public CollectionScannerHintContainer { public: virtual void recordHints(const QList& hints); virtual void recordHints(const QList& hints); virtual void recordHints(const QList& hints); virtual void recordHint(const ItemMetadataAdjustmentHint& hint); virtual void clear(); bool hasAnyNormalHint(qlonglong id) { QReadLocker locker(&lock); return modifiedItemHints.contains(id) || rescanItemHints.contains(id) || metadataAboutToAdjustHints.contains(id) || metadataAdjustedHints.contains(id); } bool hasAlbumHints() { QReadLocker locker(&lock); return !albumHints.isEmpty(); } bool hasModificationHint(qlonglong id) { QReadLocker locker(&lock); return modifiedItemHints.contains(id); } bool hasRescanHint(qlonglong id) { QReadLocker locker(&lock); return rescanItemHints.contains(id); } bool hasMetadataAboutToAdjustHint(qlonglong id) { QReadLocker locker(&lock); return metadataAboutToAdjustHints.contains(id); } bool hasMetadataAdjustedHint(qlonglong id) { QReadLocker locker(&lock); return metadataAdjustedHints.contains(id); } public: QReadWriteLock lock; QHash albumHints; QHash itemHints; QSet modifiedItemHints; QSet rescanItemHints; QHash metadataAboutToAdjustHints; QHash metadataAdjustedHints; }; void CollectionScannerHintContainerImplementation::recordHints(const QList& hints) { QWriteLocker locker(&lock); foreach(const AlbumCopyMoveHint& hint, hints) { // automagic casting to src and dst albumHints[hint] = hint; } } void CollectionScannerHintContainerImplementation::recordHints(const QList& hints) { QWriteLocker locker(&lock); foreach(const ItemCopyMoveHint& hint, hints) { QList ids = hint.srcIds(); QStringList dstNames = hint.dstNames(); for (int i=0; i& hints) { QWriteLocker locker(&lock); foreach(const ItemChangeHint& hint, hints) { const QList& ids = hint.ids(); for (int i=0; i::iterator it = metadataAboutToAdjustHints.find(hint.id()); if (it == metadataAboutToAdjustHints.end()) { return; } QDateTime date = it.value(); metadataAboutToAdjustHints.erase(it); metadataAdjustedHints[hint.id()] = hint.modificationDate(); } else // Aborted { QWriteLocker locker(&lock); QDateTime formerDate = metadataAboutToAdjustHints.take(hint.id()); } } void CollectionScannerHintContainerImplementation::clear() { QWriteLocker locker(&lock); albumHints.clear(); itemHints.clear(); modifiedItemHints.clear(); rescanItemHints.clear(); metadataAboutToAdjustHints.clear(); metadataAdjustedHints.clear(); } // -------------------------------------------------------------------- class CollectionScanner::Private { public: explicit Private() : wantSignals(false), needTotalFiles(false), hints(0), updatingHashHint(false), recordHistoryIds(false), deferredFileScanning(false), observer(0) { } public: void resetRemovedItemsTime() { removedItemsTime = QDateTime(); } void removedItems() { removedItemsTime = QDateTime::currentDateTime(); } inline bool checkObserver() { if (observer) { return observer->continueQuery(); } return true; } inline bool checkDeferred(const QFileInfo& info) { if (deferredFileScanning) { deferredAlbumPaths << info.path(); return true; } return false; } void finishScanner(ImageScanner& scanner); public: QSet nameFilters; QSet imageFilterSet; QSet videoFilterSet; QSet audioFilterSet; QList scannedAlbums; bool wantSignals; bool needTotalFiles; QDateTime removedItemsTime; CollectionScannerHintContainerImplementation* hints; QHash establishedSourceAlbums; bool updatingHashHint; bool recordHistoryIds; QSet needResolveHistorySet; QSet needTaggingHistorySet; bool deferredFileScanning; QSet deferredAlbumPaths; CollectionScannerObserver* observer; }; void CollectionScanner::Private::finishScanner(ImageScanner& scanner) { // Perform the actual write operation to the database { CoreDbOperationGroup group; scanner.commit(); } if (recordHistoryIds && scanner.hasHistoryToResolve()) { needResolveHistorySet << scanner.id(); } } // -------------------------------------------------------------------------- CollectionScanner::CollectionScanner() : d(new Private) { } CollectionScanner::~CollectionScanner() { delete d; } void CollectionScanner::setSignalsEnabled(bool on) { d->wantSignals = on; } void CollectionScanner::setNeedFileCount(bool on) { d->needTotalFiles = on; } CollectionScannerHintContainer* CollectionScanner::createHintContainer() { return new CollectionScannerHintContainerImplementation; } void CollectionScanner::setHintContainer(CollectionScannerHintContainer* const container) { // the API specs require the object given here to be created by createContainer, so we can cast. d->hints = static_cast(container); } void CollectionScanner::setUpdateHashHint(bool hint) { d->updatingHashHint = hint; } void CollectionScanner::loadNameFilters() { if (!d->nameFilters.isEmpty()) { return; } QStringList imageFilter, audioFilter, videoFilter; CoreDbAccess().db()->getFilterSettings(&imageFilter, &videoFilter, &audioFilter); // three sets to find category of a file d->imageFilterSet = imageFilter.toSet(); d->audioFilterSet = audioFilter.toSet(); d->videoFilterSet = videoFilter.toSet(); d->nameFilters = d->imageFilterSet + d->audioFilterSet + d->videoFilterSet; } void CollectionScanner::setObserver(CollectionScannerObserver* const observer) { d->observer = observer; } void CollectionScanner::setDeferredFileScanning(bool defer) { d->deferredFileScanning = defer; } QStringList CollectionScanner::deferredAlbumPaths() const { return d->deferredAlbumPaths.toList(); } void CollectionScanner::completeScan() { QTime time; time.start(); emit startCompleteScan(); // lock database CoreDbTransaction transaction; mainEntryPoint(true); d->resetRemovedItemsTime(); //TODO: Implement a mechanism to watch for album root changes while we keep this list QList allLocations = CollectionManager::instance()->allAvailableLocations(); if (d->wantSignals && d->needTotalFiles) { // count for progress info int count = 0; foreach(const CollectionLocation& location, allLocations) { count += countItemsInFolder(location.albumRootPath()); } emit totalFilesToScan(count); } if (!d->checkObserver()) { emit cancelled(); return; } // if we have no hints to follow, clean up all stale albums if (!d->hints || !d->hints->hasAlbumHints()) { CoreDbAccess().db()->deleteStaleAlbums(); } scanForStaleAlbums(allLocations); if (!d->checkObserver()) { emit cancelled(); return; } if (d->wantSignals) { emit startScanningAlbumRoots(); } foreach(const CollectionLocation& location, allLocations) { scanAlbumRoot(location); } // do not continue to clean up without a complete scan! if (!d->checkObserver()) { emit cancelled(); return; } if (d->deferredFileScanning) { qCDebug(DIGIKAM_DATABASE_LOG) << "Complete scan (file scanning deferred) took:" << time.elapsed() << "msecs."; emit finishedCompleteScan(); return; } completeScanCleanupPart(); qCDebug(DIGIKAM_DATABASE_LOG) << "Complete scan took:" << time.elapsed() << "msecs."; } void CollectionScanner::finishCompleteScan(const QStringList& albumPaths) { emit startCompleteScan(); { CoreDbTransaction transaction; mainEntryPoint(true); d->resetRemovedItemsTime(); } if (!d->checkObserver()) { emit cancelled(); return; } if (d->wantSignals) { emit startScanningAlbumRoots(); } // remove subalbums from list if parent album is already contained QStringList sortedPaths = albumPaths; std::sort(sortedPaths.begin(), sortedPaths.end()); QStringList::iterator it, it2; for (it = sortedPaths.begin(); it != sortedPaths.end(); ) { // remove all following entries as long as they have the same beginning (= are subalbums) for (it2 = it + 1; it2 != sortedPaths.end() && it2->startsWith(*it); ) { it2 = sortedPaths.erase(it2); } it = it2; } if (d->wantSignals && d->needTotalFiles) { // count for progress info int count = 0; foreach(const QString& path, sortedPaths) { count += countItemsInFolder(path); } emit totalFilesToScan(count); } foreach(const QString& path, sortedPaths) { CollectionLocation location = CollectionManager::instance()->locationForPath(path); QString album = CollectionManager::instance()->album(path); if (album == QLatin1String("/")) { scanAlbumRoot(location); } else { scanAlbum(location, album); } } // do not continue to clean up without a complete scan! if (!d->checkObserver()) { emit cancelled(); return; } CoreDbTransaction transaction; completeScanCleanupPart(); } void CollectionScanner::completeScanCleanupPart() { completeHistoryScanning(); updateRemovedItemsTime(); // Items may be set to status removed, without being definitely deleted. // This deletion shall be done after a certain time, as checked by checkedDeleteRemoved if (checkDeleteRemoved()) { // Mark items that are old enough and have the status trashed as obsolete // Only do this in a complete scan! CoreDbAccess access; QList trashedItems = access.db()->getImageIds(DatabaseItem::Status::Trashed); foreach(const qlonglong& item, trashedItems) { access.db()->setItemStatus(item, DatabaseItem::Status::Obsolete); } resetDeleteRemovedSettings(); } else { // increment the count of complete scans during which removed items were not deleted incrementDeleteRemovedCompleteScanCount(); } markDatabaseAsScanned(); emit finishedCompleteScan(); } void CollectionScanner::partialScan(const QString& filePath) { QString albumRoot = CollectionManager::instance()->albumRootPath(filePath); QString album = CollectionManager::instance()->album(filePath); partialScan(albumRoot, album); } void CollectionScanner::partialScan(const QString& albumRoot, const QString& album) { if (albumRoot.isNull() || album.isEmpty()) { // If you want to scan the album root, pass "/" qCWarning(DIGIKAM_DATABASE_LOG) << "partialScan(QString, QString) called with invalid values"; return; } - if (!QFileInfo::exists(albumRoot)) - { - qCWarning(DIGIKAM_DATABASE_LOG) << "The album root path does not exist - scan are canceled"; - return; - } - /* if (CoreDbAccess().backend()->isInTransaction()) { // Install ScanController::instance()->suspendCollectionScan around your CoreDbTransaction qCDebug(DIGIKAM_DATABASE_LOG) << "Detected an active database transaction when starting a collection scan. " "Please report this error."; return; } */ mainEntryPoint(false); d->resetRemovedItemsTime(); CollectionLocation location = CollectionManager::instance()->locationForAlbumRootPath(albumRoot); if (location.isNull()) { qCWarning(DIGIKAM_DATABASE_LOG) << "Did not find a CollectionLocation for album root path " << albumRoot; return; } // if we have no hints to follow, clean up all stale albums // Hint: Rethink with next major db update if (!d->hints || !d->hints->hasAlbumHints()) { CoreDbAccess().db()->deleteStaleAlbums(); } // Usually, we can restrict stale album scanning to our own location. // But when there are album hints from a second location to this location, // also scan the second location QSet locationIdsToScan; locationIdsToScan << location.id(); if (d->hints) { QReadLocker locker(&d->hints->lock); QHash::const_iterator it; for (it = d->hints->albumHints.constBegin(); it != d->hints->albumHints.constEnd(); ++it) { if (it.key().albumRootId == location.id()) { locationIdsToScan << it.key().albumRootId; } } } scanForStaleAlbums(locationIdsToScan.toList()); if (!d->checkObserver()) { emit cancelled(); return; } if (album == QLatin1String("/")) { scanAlbumRoot(location); } else { scanAlbum(location, album); } finishHistoryScanning(); if (!d->checkObserver()) { emit cancelled(); return; } updateRemovedItemsTime(); } qlonglong CollectionScanner::scanFile(const QString& filePath, FileScanMode mode) { QFileInfo info(filePath); QString dirPath = info.path(); // strip off filename QString albumRoot = CollectionManager::instance()->albumRootPath(dirPath); if (albumRoot.isNull()) { return -1; } QString album = CollectionManager::instance()->album(dirPath); return scanFile(albumRoot, album, info.fileName(), mode); } qlonglong CollectionScanner::scanFile(const QString& albumRoot, const QString& album, const QString& fileName, FileScanMode mode) { if (album.isEmpty() || fileName.isEmpty()) { qCWarning(DIGIKAM_DATABASE_LOG) << "scanFile(QString, QString, QString) called with empty album or empty filename"; return -1; } CollectionLocation location = CollectionManager::instance()->locationForAlbumRootPath(albumRoot); if (location.isNull()) { qCWarning(DIGIKAM_DATABASE_LOG) << "Did not find a CollectionLocation for album root path " << albumRoot; return -1; } QDir dir(location.albumRootPath() + album); QFileInfo fi(dir, fileName); if (!fi.exists()) { qCWarning(DIGIKAM_DATABASE_LOG) << "File given to scan does not exist" << albumRoot << album << fileName; return -1; } int albumId = checkAlbum(location, album); qlonglong imageId = CoreDbAccess().db()->getImageId(albumId, fileName); imageId = scanFile(fi, albumId, imageId, mode); return imageId; } void CollectionScanner::scanFile(const ImageInfo& info, FileScanMode mode) { if (info.isNull()) { return; } QFileInfo fi(info.filePath()); scanFile(fi, info.albumId(), info.id(), mode); } qlonglong CollectionScanner::scanFile(const QFileInfo& fi, int albumId, qlonglong imageId, FileScanMode mode) { mainEntryPoint(false); if (imageId == -1) { switch (mode) { case NormalScan: case ModifiedScan: imageId = scanNewFile(fi, albumId); break; case Rescan: imageId = scanNewFileFullScan(fi, albumId); break; } } else { ItemScanInfo scanInfo = CoreDbAccess().db()->getItemScanInfo(imageId); switch (mode) { case NormalScan: scanFileNormal(fi, scanInfo); break; case ModifiedScan: scanModifiedFile(fi, scanInfo); break; case Rescan: rescanFile(fi, scanInfo); break; } } finishHistoryScanning(); return imageId; } void CollectionScanner::mainEntryPoint(bool complete) { loadNameFilters(); d->recordHistoryIds = !complete; } void CollectionScanner::scanAlbumRoot(const CollectionLocation& location) { if (d->wantSignals) { emit startScanningAlbumRoot(location.albumRootPath()); } /* QDir dir(location.albumRootPath()); QStringList fileList(dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot)); for (QStringList::iterator fileIt = fileList.begin(); fileIt != fileList.end(); ++fileIt) { scanAlbum(location, '/' + (*fileIt)); } */ // scan album that covers the root directory of this album root, // all contained albums, and their subalbums recursively. scanAlbum(location, QLatin1String("/")); if (d->wantSignals) { emit finishedScanningAlbumRoot(location.albumRootPath()); } } void CollectionScanner::scanForStaleAlbums(const QList& locations) { QList locationIdsToScan; foreach(const CollectionLocation& location, locations) { locationIdsToScan << location.id(); } scanForStaleAlbums(locationIdsToScan); } void CollectionScanner::scanForStaleAlbums(const QList& locationIdsToScan) { if (d->wantSignals) { emit startScanningForStaleAlbums(); } QList albumList = CoreDbAccess().db()->getAlbumShortInfos(); QList toBeDeleted; /* // See bug #231598 QHash albumRoots; foreach(const CollectionLocation& location, locations) { albumRoots[location.id()] = location; } */ QList::const_iterator it; for (it = albumList.constBegin(); it != albumList.constEnd(); ++it) { if (!locationIdsToScan.contains((*it).albumRootId)) { continue; } CollectionLocation location = CollectionManager::instance()->locationForAlbumRootId((*it).albumRootId); // Only handle albums on available locations if (location.isAvailable()) { QFileInfo fileInfo(location.albumRootPath() + (*it).relativePath); // let digikam think that ignored directories got deleted // (if they already exist in the database, this will delete them) if (!fileInfo.exists() || !fileInfo.isDir() || ignoredDirectoryContainsFileName(fileInfo.fileName())) { toBeDeleted << (*it).id; d->scannedAlbums << (*it).id; } } } // At this point, it is important to handle album renames. // We can still copy over album attributes later, but we cannot identify // the former album of removed images. // Just renaming the album is also much cheaper than rescanning all files. if (!toBeDeleted.isEmpty() && d->hints) { // shallow copy for reading without caring for locks QHash albumHints; { QReadLocker locker(&d->hints->lock); albumHints = d->hints->albumHints; } // go through all album copy/move hints QHash::const_iterator it; int toBeDeletedIndex; for (it = albumHints.constBegin(); it != albumHints.constEnd(); ++it) { // if the src entry of a hint is found in toBeDeleted, we have a move/rename, no copy. Handle these here. toBeDeletedIndex = toBeDeleted.indexOf(it.value().albumId); // We must double check that not, for some reason, the target album has already been scanned. QList::const_iterator it2; for (it2 = albumList.constBegin(); it2 != albumList.constEnd(); ++it2) { if (it2->albumRootId == it.key().albumRootId && it2->relativePath == it.key().relativePath) { toBeDeletedIndex = -1; break; } } if (toBeDeletedIndex != -1) { // check for existence of target CollectionLocation location = CollectionManager::instance()->locationForAlbumRootId(it.key().albumRootId); if (location.isAvailable()) { QFileInfo fileInfo(location.albumRootPath() + it.key().relativePath); // Make sure ignored directories are not used in renaming operations if (fileInfo.exists() && fileInfo.isDir() && ignoredDirectoryContainsFileName(fileInfo.fileName())) { // Just set a new root/relativePath to the album. Further scanning will care for all cases or error. CoreDbAccess().db()->renameAlbum(it.value().albumId, it.key().albumRootId, it.key().relativePath); // No need any more to delete the album toBeDeleted.removeAt(toBeDeletedIndex); } } } } } safelyRemoveAlbums(toBeDeleted); if (d->wantSignals) { emit finishedScanningForStaleAlbums(); } } void CollectionScanner::safelyRemoveAlbums(const QList& albumIds) { // Remove the items (orphan items, detach them from the album, but keep entries for a certain time) // Make album orphan (no album root, keep entries until next application start) CoreDbAccess access; CoreDbTransaction transaction(&access); foreach(int albumId, albumIds) { QList ids = access.db()->getItemIDsInAlbum(albumId); access.db()->removeItemsFromAlbum(albumId, ids); access.db()->makeStaleAlbum(albumId); itemsWereRemoved(ids); } } int CollectionScanner::checkAlbum(const CollectionLocation& location, const QString& album) { // get album id if album exists int albumID = CoreDbAccess().db()->getAlbumForPath(location.id(), album, false); d->establishedSourceAlbums.remove(albumID); // create if necessary if (albumID == -1) { QFileInfo fi(location.albumRootPath() + album); albumID = CoreDbAccess().db()->addAlbum(location.id(), album, QString(), fi.lastModified().date(), QString()); // have album this one was copied from? if (d->hints) { CollectionScannerHints::Album src; { QReadLocker locker(&d->hints->lock); src = d->hints->albumHints.value(CollectionScannerHints::DstPath(location.id(), album)); } if (!src.isNull()) { //qCDebug(DIGIKAM_DATABASE_LOG) << "Identified album" << src.albumId << "as source of new album" << fi.filePath(); CoreDbAccess().db()->copyAlbumProperties(src.albumId, albumID); d->establishedSourceAlbums[albumID] = src.albumId; } } } return albumID; } void CollectionScanner::scanAlbum(const CollectionLocation& location, const QString& album) { // + Adds album if it does not yet exist in the db. // + Recursively scans subalbums of album. // + Adds files if they do not yet exist in the db. // + Marks stale files as removed QDir dir(location.albumRootPath() + album); if ( !dir.exists() || !dir.isReadable() ) { qCWarning(DIGIKAM_DATABASE_LOG) << "Folder does not exist or is not readable: " << dir.path(); return; } if (d->wantSignals) { emit startScanningAlbum(location.albumRootPath(), album); } int albumID = checkAlbum(location, album); QList scanInfos = CoreDbAccess().db()->getItemScanInfos(albumID); // create a hash filename -> index in list QHash fileNameIndexHash; QSet itemIdSet; for (int i = 0; i < scanInfos.size(); ++i) { fileNameIndexHash[scanInfos.at(i).itemName] = i; itemIdSet << scanInfos.at(i).id; } const QFileInfoList list = dir.entryInfoList(QDir::AllDirs | QDir::Files | QDir::NoDotAndDotDot, QDir::Name | QDir::DirsLast); QFileInfoList::const_iterator fi; int counter = -1; for (fi = list.constBegin(); fi != list.constEnd(); ++fi) { if (!d->checkObserver()) { return; // return directly, do not go to cleanup code after loop! } ++counter; if (d->wantSignals && counter && (counter % 100 == 0)) { emit scannedFiles(counter); counter = 0; } if (fi->isFile()) { // filter with name filter QString suffix = fi->suffix().toLower(); if (!d->nameFilters.contains(suffix)) { continue; } // ignore new files in subdirectories of ignored directories if(pathContainsIgnoredDirectory(fi->dir().path())) { continue; } int index = fileNameIndexHash.value(fi->fileName(), -1); if (index != -1) { // mark item as "seen" itemIdSet.remove(scanInfos.at(index).id); scanFileNormal(*fi, scanInfos.at(index)); } // ignore temp files we created ourselves else if (fi->completeSuffix().contains(QLatin1String("digikamtempfile."))) { continue; } else { //qCDebug(DIGIKAM_DATABASE_LOG) << "Adding item " << fi->fileName(); scanNewFile(*fi, albumID); // emit signals for scanned files with much higher granularity if (d->wantSignals && counter && (counter % 2 == 0)) { emit scannedFiles(counter); counter = 0; } } } else if (fi->isDir()) { #ifdef Q_OS_WIN //Hide album that starts with a dot, as under Linux. if (fi->fileName().startsWith(QLatin1Char('.'))) { continue; } #endif QString subalbum; if (ignoredDirectoryContainsFileName(fi->fileName())) { continue; } if (album == QLatin1String("/")) { subalbum = QLatin1Char('/') + fi->fileName(); } else { subalbum = album + QLatin1Char('/') + fi->fileName(); } scanAlbum(location, subalbum); } } if (d->wantSignals && counter) { emit scannedFiles(counter); } // Mark items in the db which we did not see on disk. if (!itemIdSet.isEmpty()) { QList ids = itemIdSet.toList(); CoreDbOperationGroup group; CoreDbAccess().db()->removeItems(ids, QList() << albumID); itemsWereRemoved(ids); } // mark album as scanned d->scannedAlbums << albumID; if (d->wantSignals) { emit finishedScanningAlbum(location.albumRootPath(), album, list.count()); } } void CollectionScanner::scanFileNormal(const QFileInfo& fi, const ItemScanInfo& scanInfo) { bool hasAnyHint = d->hints && d->hints->hasAnyNormalHint(scanInfo.id); // if the date is null, this signals a full rescan if (scanInfo.modificationDate.isNull() || (hasAnyHint && d->hints->hasRescanHint(scanInfo.id)) ) { if (hasAnyHint) { QWriteLocker locker(&d->hints->lock); d->hints->rescanItemHints.remove(scanInfo.id); } rescanFile(fi, scanInfo); return; } else if (hasAnyHint && d->hints->hasModificationHint(scanInfo.id)) { { QWriteLocker locker(&d->hints->lock); d->hints->modifiedItemHints.remove(scanInfo.id); } scanModifiedFile(fi, scanInfo); return; } else if (hasAnyHint) // metadata adjustment hints { if (d->hints->hasMetadataAboutToAdjustHint(scanInfo.id)) { // postpone scan return; } else // hasMetadataAdjustedHint { { QWriteLocker locker(&d->hints->lock); d->hints->metadataAdjustedHints.remove(scanInfo.id); } scanFileUpdateHashReuseThumbnail(fi, scanInfo, true); return; } } else if (d->updatingHashHint) { // if the file need not be scanned because of modification, update the hash if (modificationDateEquals(fi.lastModified(), scanInfo.modificationDate) && fi.size() == scanInfo.fileSize) { scanFileUpdateHashReuseThumbnail(fi, scanInfo, false); return; } } if (!modificationDateEquals(fi.lastModified(), scanInfo.modificationDate) || fi.size() != scanInfo.fileSize) { if (MetadataSettings::instance()->settings().rescanImageIfModified) { rescanFile(fi, scanInfo); } else { scanModifiedFile(fi, scanInfo); } } } qlonglong CollectionScanner::scanNewFile(const QFileInfo& info, int albumId) { if (d->checkDeferred(info)) { return -1; } ImageScanner scanner(info); scanner.setCategory(category(info)); // Check copy/move hints for single items qlonglong srcId = 0; if (d->hints) { QReadLocker locker(&d->hints->lock); srcId = d->hints->itemHints.value(NewlyAppearedFile(albumId, info.fileName())); } if (srcId != 0) { scanner.copiedFrom(albumId, srcId); } else { // Check copy/move hints for whole albums int srcAlbum = d->establishedSourceAlbums.value(albumId); if (srcAlbum) { // if we have one source album, find out if there is a file with the same name srcId = CoreDbAccess().db()->getImageId(srcAlbum, info.fileName()); } if (srcId != 0) { scanner.copiedFrom(albumId, srcId); } else { // Establishing identity with the unique hsah scanner.newFile(albumId); } } d->finishScanner(scanner); return scanner.id(); } qlonglong CollectionScanner::scanNewFileFullScan(const QFileInfo& info, int albumId) { if (d->checkDeferred(info)) { return -1; } ImageScanner scanner(info); scanner.setCategory(category(info)); scanner.newFileFullScan(albumId); d->finishScanner(scanner); return scanner.id(); } void CollectionScanner::scanModifiedFile(const QFileInfo& info, const ItemScanInfo& scanInfo) { if (d->checkDeferred(info)) { return; } ImageScanner scanner(info, scanInfo); scanner.setCategory(category(info)); scanner.fileModified(); d->finishScanner(scanner); } void CollectionScanner::scanFileUpdateHashReuseThumbnail(const QFileInfo& info, const ItemScanInfo& scanInfo, bool fileWasEdited) { QString oldHash = scanInfo.uniqueHash; qlonglong oldSize = scanInfo.fileSize; // same code as scanModifiedFile ImageScanner scanner(info, scanInfo); scanner.setCategory(category(info)); scanner.fileModified(); QString newHash = scanner.itemScanInfo().uniqueHash; qlonglong newSize = scanner.itemScanInfo().fileSize; if (ThumbsDbAccess::isInitialized()) { if (fileWasEdited) { // The file was edited in such a way that we know that the pixel content did not change, so we can reuse the thumbnail. // We need to add a link to the thumbnail data with the new hash/file size _and_ adjust // the file modification date in the data table. ThumbsDbInfo thumbDbInfo = ThumbsDbAccess().db()->findByHash(oldHash, oldSize); if (thumbDbInfo.id != -1) { ThumbsDbAccess().db()->insertUniqueHash(newHash, newSize, thumbDbInfo.id); ThumbsDbAccess().db()->updateModificationDate(thumbDbInfo.id, scanner.itemScanInfo().modificationDate); // TODO: also update details thumbnails (by file path and URL scheme) } } else { ThumbsDbAccess().db()->replaceUniqueHash(oldHash, oldSize, newHash, newSize); } } d->finishScanner(scanner); } void CollectionScanner::rescanFile(const QFileInfo& info, const ItemScanInfo& scanInfo) { if (d->checkDeferred(info)) { return; } ImageScanner scanner(info, scanInfo); scanner.setCategory(category(info)); scanner.rescan(); d->finishScanner(scanner); } void CollectionScanner::copyFileProperties(const ImageInfo& source, const ImageInfo& d) { if (source.isNull() || d.isNull()) { return; } ImageInfo dest(d); CoreDbOperationGroup group; qCDebug(DIGIKAM_DATABASE_LOG) << "Copying properties from" << source.id() << "to" << dest.id(); // Rating, creation dates DatabaseFields::ImageInformation imageInfoFields = DatabaseFields::Rating | DatabaseFields::CreationDate | DatabaseFields::DigitizationDate; QVariantList imageInfos = CoreDbAccess().db()->getImageInformation(source.id(), imageInfoFields); if (!imageInfos.isEmpty()) { CoreDbAccess().db()->changeImageInformation(dest.id(), imageInfos, imageInfoFields); } // Copy public tags foreach (int tagId, TagsCache::instance()->publicTags(source.tagIds())) { dest.setTag(tagId); } // Copy color and pick label dest.setPickLabel(source.pickLabel()); dest.setColorLabel(source.colorLabel()); // important: skip other internal tags, such a history tags. Therefore CoreDB::copyImageTags is not to be used. // GPS data QVariantList positionData = CoreDbAccess().db()->getImagePosition(source.id(), DatabaseFields::ImagePositionsAll); if (!positionData.isEmpty()) { CoreDbAccess().db()->addImagePosition(dest.id(), positionData, DatabaseFields::ImagePositionsAll); } // Comments { CoreDbAccess access; ImageComments commentsSource(access, source.id()); ImageComments commentsDest(access, dest.id()); commentsDest.replaceFrom(commentsSource); commentsDest.apply(access); } // Copyright info ImageCopyright copyrightDest(dest.id()); copyrightDest.replaceFrom(ImageCopyright(source.id())); // Image Properties CoreDbAccess().db()->copyImageProperties(source.id(), dest.id()); } void CollectionScanner::itemsWereRemoved(const QList& removedIds) { // set time stamp d->removedItems(); // manage relations QList relatedImages = CoreDbAccess().db()->getOneRelatedImageEach(removedIds, DatabaseRelation::DerivedFrom); qCDebug(DIGIKAM_DATABASE_LOG) << "Removed items:" << removedIds << "related items:" << relatedImages; if (d->recordHistoryIds) { foreach(const qlonglong& id, relatedImages) { d->needTaggingHistorySet << id; } } else { int needTaggingTag = TagsCache::instance()->getOrCreateInternalTag(InternalTagName::needTaggingHistoryGraph()); CoreDbAccess().db()->addTagsToItems(relatedImages, QList() << needTaggingTag); } } void CollectionScanner::completeHistoryScanning() { // scan tagged images int needResolvingTag = TagsCache::instance()->getOrCreateInternalTag(InternalTagName::needResolvingHistory()); int needTaggingTag = TagsCache::instance()->getOrCreateInternalTag(InternalTagName::needTaggingHistoryGraph()); QList ids = CoreDbAccess().db()->getItemIDsInTag(needResolvingTag); historyScanningStage2(ids); ids = CoreDbAccess().db()->getItemIDsInTag(needTaggingTag); qCDebug(DIGIKAM_DATABASE_LOG) << "items to tag" << ids; historyScanningStage3(ids); } void CollectionScanner::finishHistoryScanning() { // scan recorded ids QList ids; // stage 2 ids = d->needResolveHistorySet.toList(); d->needResolveHistorySet.clear(); historyScanningStage2(ids); if (!d->checkObserver()) { return; } // stage 3 ids = d->needTaggingHistorySet.toList(); d->needTaggingHistorySet.clear(); historyScanningStage3(ids); } void CollectionScanner::historyScanningStage2(const QList& ids) { foreach(const qlonglong& id, ids) { if (!d->checkObserver()) { return; } CoreDbOperationGroup group; if (d->recordHistoryIds) { QList needTaggingIds; ImageScanner::resolveImageHistory(id, &needTaggingIds); foreach(const qlonglong& needTag, needTaggingIds) { d->needTaggingHistorySet << needTag; } } else { ImageScanner::resolveImageHistory(id); } } } void CollectionScanner::historyScanningStage3(const QList& ids) { foreach(const qlonglong& id, ids) { if (!d->checkObserver()) { return; } CoreDbOperationGroup group; ImageScanner::tagImageHistoryGraph(id); } } int CollectionScanner::countItemsInFolder(const QString& directory) { int items = 0; QDir dir( directory ); if ( !dir.exists() || !dir.isReadable() ) { return 0; } QFileInfoList list = dir.entryInfoList(); items += list.count(); QFileInfoList::const_iterator fi; for (fi = list.constBegin(); fi != list.constEnd(); ++fi) { if ( fi->isDir() && fi->fileName() != QLatin1String(".") && fi->fileName() != QLatin1String("..")) { items += countItemsInFolder( fi->filePath() ); } } return items; } DatabaseItem::Category CollectionScanner::category(const QFileInfo& info) { QString suffix = info.suffix().toLower(); if (d->imageFilterSet.contains(suffix)) { return DatabaseItem::Image; } else if (d->audioFilterSet.contains(suffix)) { return DatabaseItem::Audio; } else if (d->videoFilterSet.contains(suffix)) { return DatabaseItem::Video; } else { return DatabaseItem::Other; } } void CollectionScanner::markDatabaseAsScanned() { CoreDbAccess access; access.db()->setSetting(QLatin1String("Scanned"), QDateTime::currentDateTime().toString(Qt::ISODate)); } bool CollectionScanner::databaseInitialScanDone() { CoreDbAccess access; return !access.db()->getSetting(QLatin1String("Scanned")).isEmpty(); } void CollectionScanner::updateRemovedItemsTime() { // Called after a complete or partial scan finishes, to write the value // held in d->removedItemsTime to the database if (!d->removedItemsTime.isNull()) { CoreDbAccess().db()->setSetting(QLatin1String("RemovedItemsTime"), d->removedItemsTime.toString(Qt::ISODate)); d->removedItemsTime = QDateTime(); } } void CollectionScanner::incrementDeleteRemovedCompleteScanCount() { CoreDbAccess access; int count = access.db()->getSetting(QLatin1String("DeleteRemovedCompleteScanCount")).toInt(); ++count; access.db()->setSetting(QLatin1String("DeleteRemovedCompleteScanCount"), QString::number(count)); } void CollectionScanner::resetDeleteRemovedSettings() { CoreDbAccess().db()->setSetting(QLatin1String("RemovedItemsTime"), QString()); CoreDbAccess().db()->setSetting(QLatin1String("DeleteRemovedTime"), QDateTime::currentDateTime().toString(Qt::ISODate)); CoreDbAccess().db()->setSetting(QLatin1String("DeleteRemovedCompleteScanCount"), QString::number(0)); } bool CollectionScanner::checkDeleteRemoved() { // returns true if removed items shall be deleted CoreDbAccess access; // retrieve last time an item was removed (not deleted, but set to status removed) QString removedItemsTimeString = access.db()->getSetting(QLatin1String("RemovedItemsTime")); if (removedItemsTimeString.isNull()) { return false; } // retrieve last time removed items were (definitely) deleted from db QString deleteRemovedTimeString = access.db()->getSetting(QLatin1String("DeleteRemovedTime")); QDateTime removedItemsTime, deleteRemovedTime; if (!removedItemsTimeString.isNull()) { removedItemsTime = QDateTime::fromString(removedItemsTimeString, Qt::ISODate); } if (!deleteRemovedTimeString.isNull()) { deleteRemovedTime = QDateTime::fromString(deleteRemovedTimeString, Qt::ISODate); } QDateTime now = QDateTime::currentDateTime(); // retrieve number of complete collection scans since the last time that removed items were deleted int completeScans = access.db()->getSetting(QLatin1String("DeleteRemovedCompleteScanCount")).toInt(); // No removed items? No need to delete any if (!removedItemsTime.isValid()) { return false; } // give at least a week between removed item deletions if (deleteRemovedTime.isValid()) { if (deleteRemovedTime.daysTo(now) <= 7) { return false; } } // Now look at time since items were removed, and the number of complete scans // since removed items were deleted. Values arbitrarily chosen. int daysPast = removedItemsTime.daysTo(now); return (daysPast > 7 && completeScans > 2) || (daysPast > 30 && completeScans > 0) || (completeScans > 30); } bool CollectionScanner::pathContainsIgnoredDirectory(const QString& path) { QStringList ignoreDirectoryList; CoreDbAccess().db()->getIgnoreDirectoryFilterSettings(&ignoreDirectoryList); if (ignoreDirectoryList.isEmpty()) { return false; } foreach(const QString& dir, ignoreDirectoryList) { if (path.contains(dir)) { return true; } } return false; } bool CollectionScanner::ignoredDirectoryContainsFileName(const QString& fileName) { QStringList ignoreDirectoryList; CoreDbAccess().db()->getIgnoreDirectoryFilterSettings(&ignoreDirectoryList); if (ignoreDirectoryList.isEmpty()) { return false; } return ignoreDirectoryList.contains(fileName); } // ------------------------------------------------------------------------------------------ #if 0 void CollectionScanner::scanForStaleAlbums() { QStringList albumRootPaths = CollectionManager::instance()->allAvailableAlbumRootPaths(); for (QStringList::const_iterator it = albumRootPaths.constBegin(); it != albumRootPaths.constEnd(); ++it) { scanForStaleAlbums(*it); } } void CollectionScanner::scanForStaleAlbums(const QString& albumRoot) { Q_UNUSED(albumRoot); QList albumList = CoreDbAccess().db()->getAlbumShortInfos(); QList toBeDeleted; QList::const_iterator it; for (it = albumList.constBegin(); it != albumList.constEnd(); ++it) { QFileInfo fileInfo((*it).albumRoot + (*it).url); if (!fileInfo.exists() || !fileInfo.isDir()) { m_foldersToBeDeleted << (*it); } } } QStringList CollectionScanner::formattedListOfStaleAlbums() { QStringList list; QList::const_iterator it; for (it = m_foldersToBeDeleted.constBegin(); it != m_foldersToBeDeleted.constEnd(); ++it) { list << (*it).url; } return list; } void CollectionScanner::removeStaleAlbums() { CoreDbAccess access; CoreDbTransaction transaction(&access); QList::const_iterator it; for (it = m_foldersToBeDeleted.constBegin(); it != m_foldersToBeDeleted.constEnd(); ++it) { qCDebug(DIGIKAM_DATABASE_LOG) << "Removing album " << (*it).albumRoot + QLatin1Char('/') + (*it).url; access.db()->deleteAlbum((*it).id); } } QStringList CollectionScanner::formattedListOfStaleFiles() { QStringList listToBeDeleted; CoreDbAccess access; QList< QPair >::const_iterator it; for (it = m_filesToBeDeleted.constBegin(); it != m_filesToBeDeleted.constEnd(); ++it) { QString location = QLatin1String(" (") + access.db()->getAlbumPath((*it).second) + QLatin1Char(')'); listToBeDeleted.append((*it).first + location); } return listToBeDeleted; } void CollectionScanner::removeStaleFiles() { CoreDbAccess access; CoreDbTransaction transaction(&access); QList< QPair >::const_iterator it; for (it = m_filesToBeDeleted.constBegin(); it != m_filesToBeDeleted.constEnd(); ++it) { qCDebug(DIGIKAM_DATABASE_LOG) << "Removing: " << (*it).first << " in " << (*it).second; access.db()->deleteItem( (*it).second, (*it).first ); } } void CollectionScanner::scanAlbums() { QStringList albumRootPaths = CollectionManager::instance()->allAvailableAlbumRootPaths(); int count = 0; for (QStringList::const_iterator it = albumRootPaths.constBegin(); it != albumRootPaths.constEnd(); ++it) { count += countItemsInFolder(*it); } emit totalFilesToScan(count); for (QStringList::const_iterator it = albumRootPaths.constBegin(); it != albumRootPaths.constEnd(); ++it) { QDir dir(*it); QStringList fileList(dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot)); CoreDbTransaction transaction; foreach(const QString& dir, fileList) { scanAlbum(*it, QLatin1Char('/') + dir); } } } void CollectionScanner::scan(const QString& folderPath) { CollectionManager* const manager = CollectionManager::instance(); QUrl url; url.setPath(folderPath); QString albumRoot = manager->albumRootPath(url); QString album = manager->album(url); if (albumRoot.isNull()) { qCWarning(DIGIKAM_DATABASE_LOG) << "scanAlbums(QString): folder " << folderPath << " not found in collection."; return; } scan(albumRoot, album); } void CollectionScanner::scan(const QString& albumRoot, const QString& album) { // Step one: remove invalid albums scanForStaleAlbums(albumRoot); removeStaleAlbums(); emit totalFilesToScan(countItemsInFolder(albumRoot + album)); // Step two: Scan directories if (album == QLatin1String("/")) { // Don't scan files under album root, only descend into directories (?) QDir dir(albumRoot + album); QStringList fileList(dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot)); CoreDbTransaction transaction; for (QStringList::const_iterator fileIt = fileList.constBegin(); fileIt != fileList.constEnd(); ++fileIt) { scanAlbum(albumRoot, QLatin1Char('/') + (*fileIt)); } } else { CoreDbTransaction transaction; scanAlbum(albumRoot, album); } // Step three: Remove invalid files removeStaleFiles(); } void CollectionScanner::scanAlbum(const QString& filePath) { QUrl url; url.setPath(filePath); scanAlbum(CollectionManager::instance()->albumRootPath(url), CollectionManager::instance()->album(url)); } void CollectionScanner::scanAlbum(const QString& albumRoot, const QString& album) { // + Adds album if it does not yet exist in the db. // + Recursively scans subalbums of album. // + Adds files if they do not yet exist in the db. // + Adds stale files from the db to m_filesToBeDeleted // - Does not add stale albums to m_foldersToBeDeleted. QDir dir( albumRoot + album ); if ( !dir.exists() || !dir.isReadable() ) { qCWarning(DIGIKAM_DATABASE_LOG) << "Folder does not exist or is not readable: " << dir.path(); return; } emit startScanningAlbum(albumRoot, album); // get album id if album exists int albumID = CoreDbAccess().db()->getAlbumForPath(albumRoot, album, false); if (albumID == -1) { QFileInfo fi(albumRoot + album); albumID = CoreDbAccess().db()->addAlbum(albumRoot, album, QString(), fi.lastModified().date(), QString()); } QStringList filesInAlbum = CoreDbAccess().db()->getItemNamesInAlbum( albumID ); QSet filesFoundInDB; for (QStringList::const_iterator it = filesInAlbum.constBegin(); it != filesInAlbum.constEnd(); ++it) { filesFoundInDB << *it; } const QFileInfoList list = dir.entryInfoList(m_nameFilters, QDir::AllDirs | QDir::Files | QDir::NoDotAndDotDot /*not CaseSensitive*/); QFileInfoList::const_iterator fi; for (fi = list.constBegin(); fi != list.constEnd(); ++fi) { if ( fi->isFile()) { if (filesFoundInDB.contains(fi->fileName()) ) { filesFoundInDB.remove(fi->fileName()); } // ignore temp files we created ourselves else if (fi->completeSuffix() == QLatin1String("digikamtempfile.tmp")) { continue; } else { qCDebug(DIGIKAM_DATABASE_LOG) << "Adding item " << fi->fileName(); addItem(albumID, albumRoot, album, fi->fileName()); } } else if ( fi->isDir() ) { scanAlbum( albumRoot, album + QLatin1Char('/') + fi->fileName() ); } } // Removing items from the db which we did not see on disk. if (!filesFoundInDB.isEmpty()) { QSetIterator it(filesFoundInDB); while (it.hasNext()) { QPair pair(it.next(),albumID); if (m_filesToBeDeleted.indexOf(pair) == -1) { m_filesToBeDeleted << pair; } } } emit finishedScanningAlbum(albumRoot, album, list.count()); } void CollectionScanner::updateItemsWithoutDate() { QStringList urls = CoreDbAccess().db()->getAllItemURLsWithoutDate(); emit totalFilesToScan(urls.count()); QString albumRoot = CoreDbAccess::albumRoot(); { CoreDbTransaction transaction; for (QStringList::const_iterator it = urls.constBegin(); it != urls.constEnd(); ++it) { emit scanningFile(*it); QFileInfo fi(*it); QString albumURL = fi.path(); albumURL = QDir::cleanPath(albumURL.remove(albumRoot)); int albumID = CoreDbAccess().db()->getAlbumForPath(albumRoot, albumURL); if (albumID <= 0) { qCWarning(DIGIKAM_DATABASE_LOG) << "Album ID == -1: " << albumURL; } if (fi.exists()) { CollectionScanner::updateItemDate(albumID, albumRoot, albumURL, fi.fileName()); } else { QPair pair(fi.fileName(), albumID); if (m_filesToBeDeleted.indexOf(pair) == -1) { m_filesToBeDeleted << pair; } } } } } int CollectionScanner::countItemsInFolder(const QString& directory) { int items = 0; QDir dir( directory ); if ( !dir.exists() || !dir.isReadable() ) { return 0; } QFileInfoList list = dir.entryInfoList(); items += list.count(); QFileInfoList::const_iterator fi; for (fi = list.constBegin(); fi != list.constEnd(); ++fi) { if ( fi->isDir() && fi->fileName() != QLatin1String(".") && fi->fileName() != QLatin1String("..")) { items += countItemsInFolder( fi->filePath() ); } } return items; } void CollectionScanner::markDatabaseAsScanned() { CoreDbAccess access; access.db()->setSetting("Scanned", QDateTime::currentDateTime().toString(Qt::ISODate)); } // ------------------- Tools ------------------------ void CollectionScanner::addItem(int albumID, const QString& albumRoot, const QString& album, const QString& fileName) { CoreDbAccess access; addItem(access, albumID, albumRoot, album, fileName); } void CollectionScanner::addItem(Digikam::CoreDbAccess& access, int albumID, const QString& albumRoot, const QString& album, const QString& fileName) { QString filePath = albumRoot + album + QLatin1Char('/') + fileName; QString comment; QStringList keywords; QDateTime datetime; int rating; DMetadata metadata(filePath); // Try to get comments from image : // In first, from standard JPEG comments, or // In second, from EXIF comments tag, or // In third, from IPTC comments tag. comment = metadata.getImageComment(); // Try to get date and time from image : // In first, from EXIF date & time tags, or // In second, from IPTC date & time tags. datetime = metadata.getImageDateTime(); // Try to get image rating from IPTC Urgency tag // else use file system time stamp. rating = metadata.getImageRating(); if ( !datetime.isValid() ) { QFileInfo info( filePath ); datetime = info.lastModified(); } // Try to get image tags from IPTC keywords tags. metadata.getImageTagsPath(keywords); access.db()->addItem(albumID, fileName, datetime, comment, rating, keywords); } void CollectionScanner::updateItemDate(int albumID, const QString& albumRoot, const QString& album, const QString& fileName) { CoreDbAccess access; updateItemDate(access, albumID, albumRoot, album, fileName); } void CollectionScanner::updateItemDate(Digikam::CoreDbAccess& access, int albumID, const QString& albumRoot, const QString& album, const QString& fileName) { QString filePath = albumRoot + album + QLatin1Char('/') + fileName; QDateTime datetime; DMetadata metadata(filePath); // Trying to get date and time from image : // In first, from EXIF date & time tags, or // In second, from IPTC date & time tags. datetime = metadata.getImageDateTime(); if ( !datetime.isValid() ) { QFileInfo info( filePath ); datetime = info.lastModified(); } access.db()->setItemDate(albumID, fileName, datetime); } #endif } // namespace Digikam