1 /* ============================================================
2  *
3  * This file is a part of digiKam project
4  * https://www.digikam.org
5  *
6  * Date        : 2011-11-07
7  * Description : Directory watch interface
8  *
9  * Copyright (C) 2011 by Marcel Wiesweg <marcel dot wiesweg at gmx dot de>
10  * Copyright (C) 2015-2021 by Gilles Caulier <caulier dot gilles at gmail dot com>
11  *
12  * This program is free software; you can redistribute it
13  * and/or modify it under the terms of the GNU General
14  * Public License as published by the Free Software Foundation;
15  * either version 2, or (at your option)
16  * any later version.
17  *
18  * This program is distributed in the hope that it will be useful,
19  * but WITHOUT ANY WARRANTY; without even the implied warranty of
20  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
21  * GNU General Public License for more details.
22  *
23  * ============================================================ */
24 
25 #include "albumwatch.h"
26 
27 // Qt includes
28 
29 #include <QFileSystemWatcher>
30 #include <QDateTime>
31 #include <QFileInfo>
32 #include <QDir>
33 
34 // Local includes
35 
36 #include "digikam_debug.h"
37 #include "album.h"
38 #include "albummanager.h"
39 #include "collectionlocation.h"
40 #include "collectionmanager.h"
41 #include "dbengineparameters.h"
42 #include "applicationsettings.h"
43 #include "scancontroller.h"
44 #include "dio.h"
45 
46 namespace Digikam
47 {
48 
49 class Q_DECL_HIDDEN AlbumWatch::Private
50 {
51 public:
52 
Private()53     explicit Private()
54       : dirWatch(nullptr)
55     {
56     }
57 
58     bool             inBlackList(const QString& path) const;
59     bool             inDirWatchParametersBlackList(const QFileInfo& info, const QString& path);
60     QList<QDateTime> buildDirectoryModList(const QFileInfo& dbFile) const;
61 
62 public:
63 
64     QFileSystemWatcher* dirWatch;
65 
66     DbEngineParameters  params;
67     QStringList         fileNameBlackList;
68     QList<QDateTime>    dbPathModificationDateList;
69 };
70 
inBlackList(const QString & path) const71 bool AlbumWatch::Private::inBlackList(const QString& path) const
72 {
73     // Filter out dirty signals triggered by changes on the database file
74 
75     foreach (const QString& bannedFile, fileNameBlackList)
76     {
77         if (path.endsWith(bannedFile))
78         {
79             return true;
80         }
81     }
82 
83     return false;
84 }
85 
inDirWatchParametersBlackList(const QFileInfo & info,const QString & path)86 bool AlbumWatch::Private::inDirWatchParametersBlackList(const QFileInfo& info, const QString& path)
87 {
88     if (params.isSQLite())
89     {
90         QDir dir;
91 
92         if (info.isDir())
93         {
94             dir = QDir(path);
95         }
96         else
97         {
98             dir = info.dir();
99         }
100 
101         QFileInfo dbFile(params.SQLiteDatabaseFile());
102 
103         // is the signal for the directory containing the database file?
104 
105         if (dbFile.dir() == dir)
106         {
107             // retrieve modification dates
108 
109             QList<QDateTime> modList = buildDirectoryModList(dbFile);
110 
111             // check for equality
112 
113             if (modList == dbPathModificationDateList)
114             {
115                 //qCDebug(DIGIKAM_GENERAL_LOG) << "Filtering out db-file-triggered dir watch signal";
116 
117                 // we can skip the signal
118 
119                 return true;
120             }
121 
122             // set new list
123 
124             dbPathModificationDateList = modList;
125         }
126     }
127 
128     return false;
129 }
130 
buildDirectoryModList(const QFileInfo & dbFile) const131 QList<QDateTime> AlbumWatch::Private::buildDirectoryModList(const QFileInfo& dbFile) const
132 {
133     // Retrieve modification dates
134 
135     QList<QDateTime> modList;
136     QFileInfoList    fileInfoList = dbFile.dir().entryInfoList(QDir::Dirs    |
137                                                                QDir::Files   |
138                                                                QDir::NoDotAndDotDot);
139 
140     // Build the list
141 
142     foreach (const QFileInfo& info, fileInfoList)
143     {
144         // Ignore digikam4.db and journal and other temporary files
145 
146         if (!fileNameBlackList.contains(info.fileName()))
147         {
148             modList << info.lastModified();
149         }
150     }
151 
152     return modList;
153 }
154 
155 // -------------------------------------------------------------------------------------
156 
AlbumWatch(AlbumManager * const parent)157 AlbumWatch::AlbumWatch(AlbumManager* const parent)
158     : QObject(parent),
159       d(new Private)
160 {
161     d->dirWatch = new QFileSystemWatcher(this);
162 
163     if (ApplicationSettings::instance()->getAlbumMonitoring())
164     {
165         qCDebug(DIGIKAM_GENERAL_LOG) << "AlbumWatch use QFileSystemWatcher";
166 
167         connect(d->dirWatch, SIGNAL(directoryChanged(QString)),
168                 this, SLOT(slotQFSWatcherDirty(QString)));
169 
170         connect(d->dirWatch, SIGNAL(fileChanged(QString)),
171                 this, SLOT(slotQFSWatcherDirty(QString)));
172 
173         connect(parent, SIGNAL(signalAlbumAdded(Album*)),
174                 this, SLOT(slotAlbumAdded(Album*)));
175 
176         connect(parent, SIGNAL(signalAlbumRenamed(Album*)),
177                 this, SLOT(slotAlbumAdded(Album*)));
178 
179         connect(parent, SIGNAL(signalAlbumNewPath(Album*)),
180                 this, SLOT(slotAlbumAdded(Album*)));
181 
182         connect(parent, SIGNAL(signalAlbumAboutToBeDeleted(Album*)),
183                 this, SLOT(slotAlbumAboutToBeDeleted(Album*)));
184     }
185     else
186     {
187         qCDebug(DIGIKAM_GENERAL_LOG) << "AlbumWatch is disabled";
188     }
189 }
190 
~AlbumWatch()191 AlbumWatch::~AlbumWatch()
192 {
193     delete d;
194 }
195 
clear()196 void AlbumWatch::clear()
197 {
198     if (d->dirWatch && !d->dirWatch->directories().isEmpty())
199     {
200         d->dirWatch->removePaths(d->dirWatch->directories());
201     }
202 }
203 
removeWatchedPAlbums(const PAlbum * const album)204 void AlbumWatch::removeWatchedPAlbums(const PAlbum* const album)
205 {
206     if (!album || d->dirWatch->directories().isEmpty())
207     {
208         return;
209     }
210 
211     foreach (const QString& dir, d->dirWatch->directories())
212     {
213         if (dir.startsWith(album->folderPath()))
214         {
215             d->dirWatch->removePath(dir);
216         }
217     }
218 }
219 
setDbEngineParameters(const DbEngineParameters & params)220 void AlbumWatch::setDbEngineParameters(const DbEngineParameters& params)
221 {
222     d->params = params;
223 
224     d->fileNameBlackList.clear();
225 
226     // filter out notifications caused by database operations
227 
228     if (params.isSQLite())
229     {
230         d->fileNameBlackList << QLatin1String("thumbnails-digikam.db")
231                              << QLatin1String("thumbnails-digikam.db-journal");
232         d->fileNameBlackList << QLatin1String("recognition.db")
233                              << QLatin1String("recognition.db-journal");
234 
235         QFileInfo dbFile(params.SQLiteDatabaseFile());
236         d->fileNameBlackList << dbFile.fileName()
237                              << dbFile.fileName() + QLatin1String("-journal");
238 
239         // ensure this is done after setting up the black list
240 
241         d->dbPathModificationDateList = d->buildDirectoryModList(dbFile);
242     }
243 }
244 
slotAlbumAdded(Album * a)245 void AlbumWatch::slotAlbumAdded(Album* a)
246 {
247     if (a->isRoot() || a->isTrashAlbum() || (a->type() != Album::PHYSICAL))
248     {
249         return;
250     }
251 
252     PAlbum* const album         = static_cast<PAlbum*>(a);
253     CollectionLocation location = CollectionManager::instance()->locationForAlbumRootId(album->albumRootId());
254 
255     if (!location.isAvailable())
256     {
257         return;
258     }
259 
260     QString dir = album->folderPath();
261 
262     if (dir.isEmpty())
263     {
264         return;
265     }
266 
267     d->dirWatch->addPath(dir);
268 }
269 
slotAlbumAboutToBeDeleted(Album * a)270 void AlbumWatch::slotAlbumAboutToBeDeleted(Album* a)
271 {
272     if (a->isRoot() || a->isTrashAlbum() || (a->type() != Album::PHYSICAL))
273     {
274         return;
275     }
276 
277     PAlbum* const album = static_cast<PAlbum*>(a);
278     QString dir         = album->folderPath();
279 
280     if (dir.isEmpty())
281     {
282         return;
283     }
284 
285     d->dirWatch->removePath(dir);
286 }
287 
rescanDirectory(const QString & dir)288 void AlbumWatch::rescanDirectory(const QString& dir)
289 {
290     if (DIO::itemsUnderProcessing())
291     {
292         return;
293     }
294 
295     qCDebug(DIGIKAM_GENERAL_LOG) << "Detected change, triggering rescan of" << dir;
296 
297     ScanController::instance()->scheduleCollectionScanExternal(dir);
298 }
299 
slotQFSWatcherDirty(const QString & path)300 void AlbumWatch::slotQFSWatcherDirty(const QString& path)
301 {
302     if (d->inBlackList(path))
303     {
304         return;
305     }
306 
307     QFileInfo info(path);
308 
309     if (d->inDirWatchParametersBlackList(info, path))
310     {
311         return;
312     }
313 
314     if (info.isDir())
315     {
316         rescanDirectory(path);
317     }
318     else
319     {
320         rescanDirectory(info.path());
321     }
322 }
323 
324 } // namespace Digikam
325