1 // SPDX-FileCopyrightText: 2003-2020 The KPhotoAlbum Development Team
2 // SPDX-FileCopyrightText: 2021 Johannes Zarl-Zierl <johannes@zarl-zierl.at>
3 //
4 // SPDX-License-Identifier: GPL-2.0-or-later
5 
6 #include "Database.h"
7 
8 #include "DatabaseElement.h"
9 
10 #include <kpabase/Logging.h>
11 #include <kpabase/UIDelegate.h>
12 
13 #include <KLocalizedString>
14 #include <QCoreApplication>
15 #include <QDir>
16 #include <QFile>
17 #include <QSqlDatabase>
18 #include <QSqlDriver>
19 #include <QSqlError>
20 #include <QSqlQuery>
21 #include <exiv2/exif.hpp>
22 #include <exiv2/image.hpp>
23 
24 using namespace Exif;
25 
26 namespace
27 {
28 // schema version; bump it up whenever the database schema changes
29 constexpr int DB_VERSION = 3;
elements(int since=0)30 const Database::ElementList elements(int since = 0)
31 {
32     static Database::ElementList elms;
33     static int sinceDBVersion[DB_VERSION] {};
34 
35     if (elms.count() == 0) {
36         elms.append(new RationalExifElement("Exif.Photo.FocalLength"));
37         elms.append(new RationalExifElement("Exif.Photo.ExposureTime"));
38 
39         elms.append(new RationalExifElement("Exif.Photo.ApertureValue"));
40         elms.append(new RationalExifElement("Exif.Photo.FNumber"));
41         //elms.append( new RationalExifElement( "Exif.Photo.FlashEnergy" ) );
42 
43         elms.append(new IntExifElement("Exif.Photo.Flash"));
44         elms.append(new IntExifElement("Exif.Photo.Contrast"));
45         elms.append(new IntExifElement("Exif.Photo.Sharpness"));
46         elms.append(new IntExifElement("Exif.Photo.Saturation"));
47         elms.append(new IntExifElement("Exif.Image.Orientation"));
48         elms.append(new IntExifElement("Exif.Photo.MeteringMode"));
49         elms.append(new IntExifElement("Exif.Photo.ISOSpeedRatings"));
50         elms.append(new IntExifElement("Exif.Photo.ExposureProgram"));
51 
52         elms.append(new StringExifElement("Exif.Image.Make"));
53         elms.append(new StringExifElement("Exif.Image.Model"));
54         // gps info has been added in database schema version 2:
55         sinceDBVersion[1] = elms.size();
56         elms.append(new IntExifElement("Exif.GPSInfo.GPSVersionID")); // actually a byte value
57         elms.append(new RationalExifElement("Exif.GPSInfo.GPSAltitude"));
58         elms.append(new IntExifElement("Exif.GPSInfo.GPSAltitudeRef")); // actually a byte value
59         elms.append(new StringExifElement("Exif.GPSInfo.GPSMeasureMode"));
60         elms.append(new RationalExifElement("Exif.GPSInfo.GPSDOP"));
61         elms.append(new RationalExifElement("Exif.GPSInfo.GPSImgDirection"));
62         elms.append(new RationalExifElement("Exif.GPSInfo.GPSLatitude"));
63         elms.append(new StringExifElement("Exif.GPSInfo.GPSLatitudeRef"));
64         elms.append(new RationalExifElement("Exif.GPSInfo.GPSLongitude"));
65         elms.append(new StringExifElement("Exif.GPSInfo.GPSLongitudeRef"));
66         elms.append(new RationalExifElement("Exif.GPSInfo.GPSTimeStamp"));
67         // lens info has been added in database schema version 3:
68         sinceDBVersion[2] = elms.size();
69         elms.append(new LensExifElement());
70     }
71 
72     // query only for the newly added stuff:
73     if (since > 0)
74         return elms.mid(sinceDBVersion[since]);
75 
76     return elms;
77 }
78 
isSQLiteDriverAvailable()79 bool isSQLiteDriverAvailable()
80 {
81 #ifdef QT_NO_SQL
82     return false;
83 #else
84     return QSqlDatabase::isDriverAvailable(QString::fromLatin1("QSQLITE"));
85 #endif
86 }
87 }
88 
89 class Database::DatabasePrivate
90 {
91 public:
92     DatabasePrivate(Database *q, const QString &exifDBFile, DB::UIDelegate &uiDelegate);
93     ~DatabasePrivate();
94 
95     bool isOpen() const;
96     bool isUsable() const;
97     int DBFileVersion() const;
98 
99     QString getFileName() const;
100 
101 protected:
102     Database *q_ptr;
103     Q_DECLARE_PUBLIC(Database)
104 
105     enum DBSchemaChangeType { SchemaChanged,
106                               SchemaAndDataChanged };
107     void openDatabase();
108     void populateDatabase();
109     void updateDatabase();
110     void createMetadataTable(DBSchemaChangeType change);
111     static QString connectionName();
112     bool insert(const DB::FileName &filename, Exiv2::ExifData);
113     bool insert(const QList<DBExifInfo> &);
114 
115 private:
116     mutable bool m_isFailed = false;
117     DB::UIDelegate &m_ui;
118     QSqlDatabase m_db;
119     const QString m_fileName;
120     bool m_isOpen = false;
121     bool m_doUTF8Conversion = false;
122     QSqlQuery *m_insertTransaction = nullptr;
123     QString m_queryString;
124 
125     void showErrorAndFail(QSqlQuery &query) const;
126     void showErrorAndFail(const QString &errorMessage, const QString &technicalInfo) const;
127     void init();
128     QSqlQuery *getInsertQuery();
129     void concludeInsertQuery(QSqlQuery *);
130 };
131 
DatabasePrivate(Database * q,const QString & exifDBFile,DB::UIDelegate & uiDelegate)132 Database::DatabasePrivate::DatabasePrivate(Database *q, const QString &exifDBFile, DB::UIDelegate &uiDelegate)
133     : q_ptr(q)
134     , m_ui(uiDelegate)
135     , m_db(QSqlDatabase::addDatabase(QString::fromLatin1("QSQLITE"), QString::fromLatin1("exif")))
136     , m_fileName(exifDBFile)
137 {
138     init();
139 }
140 
init()141 void Exif::Database::DatabasePrivate::init()
142 {
143     Q_Q(Database);
144     if (!q->isAvailable())
145         return;
146 
147     m_isFailed = false;
148     m_insertTransaction = nullptr;
149     const bool dbExists = QFile::exists(m_fileName);
150 
151     openDatabase();
152 
153     if (!isOpen())
154         return;
155 
156     if (!dbExists)
157         populateDatabase();
158     else
159         updateDatabase();
160 }
161 
162 /**
163  * @brief show and error message for the failed \p query and disable the Exif database.
164  * The database is closed because at this point we can not trust the data inside.
165  * @param query
166  */
showErrorAndFail(QSqlQuery & query) const167 void Database::DatabasePrivate::showErrorAndFail(QSqlQuery &query) const
168 {
169     const QString txt = i18n("<p>There was an error while accessing the Exif search database. "
170                              "The error is likely due to a broken database file.</p>"
171                              "<p>To fix this problem run Maintenance->Recreate Exif Search database.</p>"
172                              "<hr/>"
173                              "<p>For debugging: the command that was attempted to be executed was:<br/>%1</p>"
174                              "<p>The error message obtained was:<br/>%2</p>",
175                              query.lastQuery(), query.lastError().text());
176 
177     const QString technicalInfo = QString::fromUtf8("Error running query: %1\n Error was: %2")
178                                       .arg(query.lastQuery(), query.lastError().text());
179     showErrorAndFail(txt, technicalInfo);
180 }
181 
showErrorAndFail(const QString & errorMessage,const QString & technicalInfo) const182 void Database::DatabasePrivate::showErrorAndFail(const QString &errorMessage, const QString &technicalInfo) const
183 {
184     m_ui.information(DB::LogMessage { ExifLog(), technicalInfo }, errorMessage, i18n("Error in Exif database"), QString::fromLatin1("sql_error_in_exif_DB"));
185     // disable exif db for now:
186     m_isFailed = true;
187 }
188 
Database(const QString & sqliteFileName,DB::UIDelegate & uiDelegate)189 Exif::Database::Database(const QString &sqliteFileName, DB::UIDelegate &uiDelegate)
190     : d_ptr(new DatabasePrivate(this, sqliteFileName, uiDelegate))
191 {
192 }
193 
~Database()194 Database::~Database()
195 {
196     delete d_ptr;
197 }
198 
openDatabase()199 void Exif::Database::DatabasePrivate::openDatabase()
200 {
201     m_db.setDatabaseName(m_fileName);
202 
203     m_isOpen = m_db.open();
204     if (!m_isOpen) {
205         const QString txt = i18n("<p>There was an error while opening the Exif search database.</p> "
206                                  "<p>To fix this problem run Maintenance->Recreate Exif Search database.</p>"
207                                  "<hr/>"
208                                  "<p>The error message obtained was:<br/>%1</p>",
209                                  m_db.lastError().text());
210         const QString logMsg = QString::fromUtf8("Could not open Exif search database! "
211                                                  "Error was: %1")
212                                    .arg(m_db.lastError().text());
213         showErrorAndFail(txt, logMsg);
214         return;
215     }
216     // If SQLite in Qt has Unicode feature, it will convert queries to
217     // UTF-8 automatically. Otherwise we should do the conversion to
218     // be able to store any Unicode character.
219     m_doUTF8Conversion = !m_db.driver()->hasFeature(QSqlDriver::Unicode);
220 }
221 
~DatabasePrivate()222 Exif::Database::DatabasePrivate::~DatabasePrivate()
223 {
224     // We have to close the database before destroying the QSqlDatabase object,
225     // otherwise Qt screams and kittens might die (see QSqlDatabase's
226     // documentation)
227     if (m_db.isOpen())
228         m_db.close();
229 }
230 
isOpen() const231 bool Exif::Database::DatabasePrivate::isOpen() const
232 {
233     return m_isOpen && !m_isFailed;
234 }
235 
populateDatabase()236 void Exif::Database::DatabasePrivate::populateDatabase()
237 {
238     createMetadataTable(SchemaAndDataChanged);
239     QStringList attributes;
240     const auto allElements = elements();
241     for (const DatabaseElement *element : allElements) {
242         attributes.append(element->createString());
243     }
244 
245     QSqlQuery query(QString::fromLatin1("create table if not exists exif (filename string PRIMARY KEY, %1 )")
246                         .arg(attributes.join(QString::fromLatin1(", "))),
247                     m_db);
248     if (!query.exec())
249         showErrorAndFail(query);
250 }
251 
updateDatabase()252 void Exif::Database::DatabasePrivate::updateDatabase()
253 {
254     if (m_db.tables().isEmpty()) {
255         const QString txt = i18n("<p>The Exif search database is corrupted and has no data.</p> "
256                                  "<p>To fix this problem run Maintenance->Recreate Exif Search database.</p>");
257         const QString logMsg = QString::fromUtf8("Database open but empty!");
258         showErrorAndFail(txt, logMsg);
259         return;
260     }
261 
262     const int version = DBFileVersion();
263     if (m_isFailed)
264         return;
265     if (version < DBVersion()) {
266         // on the next update, we can just query the DB Version
267         createMetadataTable(SchemaChanged);
268     }
269     // update schema
270     if (version < DBVersion()) {
271         QSqlQuery query(m_db);
272         for (const DatabaseElement *e : elements(version)) {
273             query.prepare(QString::fromLatin1("alter table exif add column %1")
274                               .arg(e->createString()));
275             if (!query.exec())
276                 showErrorAndFail(query);
277         }
278     }
279 }
280 
createMetadataTable(DBSchemaChangeType change)281 void Exif::Database::DatabasePrivate::createMetadataTable(DBSchemaChangeType change)
282 {
283     QSqlQuery query(m_db);
284     query.prepare(QString::fromLatin1("create table if not exists settings (keyword TEXT PRIMARY KEY, value TEXT) without rowid"));
285     if (!query.exec()) {
286         showErrorAndFail(query);
287         return;
288     }
289 
290     query.prepare(QString::fromLatin1("insert or replace into settings (keyword, value) values('DBVersion','%1')").arg(Database::DBVersion()));
291     if (!query.exec()) {
292         showErrorAndFail(query);
293         return;
294     }
295 
296     if (change == SchemaAndDataChanged) {
297         query.prepare(QString::fromLatin1("insert or replace into settings (keyword, value) values('GuaranteedDataVersion','%1')").arg(Database::DBVersion()));
298         if (!query.exec())
299             showErrorAndFail(query);
300     }
301 }
302 
add(const DB::FileName & filename,Exiv2::ExifData data)303 bool Database::add(const DB::FileName &filename, Exiv2::ExifData data)
304 {
305     if (!isUsable())
306         return false;
307 
308     Q_D(Database);
309     // we might as well rename insert() to add()
310     return d->insert(filename, data);
311 }
312 
add(const DB::FileName & fileName)313 bool Exif::Database::add(const DB::FileName &fileName)
314 {
315     if (!isUsable())
316         return false;
317 
318     try {
319         Exiv2::Image::AutoPtr image = Exiv2::ImageFactory::open(fileName.absolute().toLocal8Bit().data());
320         Q_ASSERT(image.get() != nullptr);
321         image->readMetadata();
322         Exiv2::ExifData &exifData = image->exifData();
323         Q_D(Database);
324         return d->insert(fileName, exifData);
325     } catch (...) {
326         qCWarning(ExifLog, "Error while reading exif information from %s", qPrintable(fileName.absolute()));
327         return false;
328     }
329 }
330 
add(const DB::FileNameList & list)331 bool Exif::Database::add(const DB::FileNameList &list)
332 {
333     if (!isUsable())
334         return false;
335 
336     QList<DBExifInfo> map;
337 
338     for (const DB::FileName &fileName : list) {
339         try {
340             Exiv2::Image::AutoPtr image = Exiv2::ImageFactory::open(fileName.absolute().toLocal8Bit().data());
341             Q_ASSERT(image.get() != nullptr);
342             image->readMetadata();
343             map << DBExifInfo(fileName, image->exifData());
344         } catch (...) {
345             qCWarning(ExifLog, "Error while reading exif information from %s", qPrintable(fileName.absolute()));
346         }
347     }
348     Q_D(Database);
349     d->insert(map);
350     return true;
351 }
352 
remove(const DB::FileName & fileName)353 void Exif::Database::remove(const DB::FileName &fileName)
354 {
355     if (!isUsable())
356         return;
357 
358     Q_D(Database);
359     QSqlQuery query(d->m_db);
360     query.prepare(QString::fromLatin1("DELETE FROM exif WHERE fileName=?"));
361     query.bindValue(0, fileName.absolute());
362     if (!query.exec())
363         d->showErrorAndFail(query);
364 }
365 
remove(const DB::FileNameList & list)366 void Exif::Database::remove(const DB::FileNameList &list)
367 {
368     if (!isUsable())
369         return;
370 
371     Q_D(Database);
372     d->m_db.transaction();
373     QSqlQuery query(d->m_db);
374     query.prepare(QString::fromLatin1("DELETE FROM exif WHERE fileName=?"));
375     for (const DB::FileName &fileName : list) {
376         query.bindValue(0, fileName.absolute());
377         if (!query.exec()) {
378             d->m_db.rollback();
379             d->showErrorAndFail(query);
380             return;
381         }
382     }
383     d->m_db.commit();
384 }
385 
getInsertQuery()386 QSqlQuery *Exif::Database::DatabasePrivate::getInsertQuery()
387 {
388     if (!isUsable())
389         return nullptr;
390     if (m_insertTransaction)
391         return m_insertTransaction;
392     if (m_queryString.isEmpty()) {
393         QStringList formalList;
394         const Database::ElementList elms = elements();
395         for (const DatabaseElement *e : elms) {
396             formalList.append(e->queryString());
397         }
398         m_queryString = QString::fromLatin1("INSERT OR REPLACE into exif values (?, %1) ").arg(formalList.join(QString::fromLatin1(", ")));
399     }
400     QSqlQuery *query = new QSqlQuery(m_db);
401     if (query)
402         query->prepare(m_queryString);
403     return query;
404 }
405 
concludeInsertQuery(QSqlQuery * query)406 void Exif::Database::DatabasePrivate::concludeInsertQuery(QSqlQuery *query)
407 {
408     if (m_insertTransaction)
409         return;
410     m_db.commit();
411     delete query;
412 }
413 
startInsertTransaction()414 bool Exif::Database::startInsertTransaction()
415 {
416     if (!isUsable())
417         return false;
418 
419     Q_D(Database);
420     Q_ASSERT(d->m_insertTransaction == nullptr);
421     d->m_insertTransaction = d->getInsertQuery();
422     d->m_db.transaction();
423     return (d->m_insertTransaction != nullptr);
424 }
425 
commitInsertTransaction()426 bool Exif::Database::commitInsertTransaction()
427 {
428     if (!isUsable())
429         return false;
430 
431     Q_D(Database);
432     if (d->m_insertTransaction) {
433         d->m_db.commit();
434         delete d->m_insertTransaction;
435         d->m_insertTransaction = nullptr;
436     } else
437         qCWarning(ExifLog, "Trying to commit transaction, but no transaction is active!");
438     return true;
439 }
440 
abortInsertTransaction()441 bool Exif::Database::abortInsertTransaction()
442 {
443     if (!isUsable())
444         return false;
445 
446     Q_D(Database);
447     if (d->m_insertTransaction) {
448         d->m_db.rollback();
449         delete d->m_insertTransaction;
450         d->m_insertTransaction = nullptr;
451     } else
452         qCWarning(ExifLog, "Trying to abort transaction, but no transaction is active!");
453     return true;
454 }
455 
insert(const DB::FileName & filename,Exiv2::ExifData data)456 bool Exif::Database::DatabasePrivate::insert(const DB::FileName &filename, Exiv2::ExifData data)
457 {
458     if (!isUsable())
459         return false;
460 
461     QSqlQuery *query = getInsertQuery();
462     query->bindValue(0, filename.absolute());
463     int i = 1;
464     for (const DatabaseElement *e : elements()) {
465         query->bindValue(i++, e->valueFromExif(data));
466     }
467 
468     bool status = query->exec();
469     if (!status)
470         showErrorAndFail(*query);
471     concludeInsertQuery(query);
472     return status;
473 }
474 
insert(const QList<DBExifInfo> & map)475 bool Exif::Database::DatabasePrivate::insert(const QList<DBExifInfo> &map)
476 {
477     if (!isUsable())
478         return false;
479 
480     QSqlQuery *query = getInsertQuery();
481     // not a const reference because DatabaseElement::valueFromExif uses operator[] on the exif datum
482     for (DBExifInfo elt : map) {
483         query->bindValue(0, elt.first.absolute());
484         int i = 1;
485         for (const DatabaseElement *e : elements()) {
486             query->bindValue(i++, e->valueFromExif(elt.second));
487         }
488 
489         if (!query->exec()) {
490             showErrorAndFail(*query);
491         }
492     }
493     concludeInsertQuery(query);
494     return true;
495 }
496 
getFileName() const497 QString Exif::Database::DatabasePrivate::getFileName() const
498 {
499     return m_fileName;
500 }
501 
isAvailable()502 bool Exif::Database::isAvailable()
503 {
504     return isSQLiteDriverAvailable();
505 }
506 
DBFileVersion() const507 int Exif::Database::DatabasePrivate::DBFileVersion() const
508 {
509     // previous to KPA 4.6, there was no metadata table:
510     if (!m_db.tables().contains(QString::fromLatin1("settings")))
511         return 1;
512 
513     QSqlQuery query(QString::fromLatin1("SELECT value FROM settings WHERE keyword = 'DBVersion'"), m_db);
514     if (!query.exec())
515         showErrorAndFail(query);
516 
517     if (query.first()) {
518         return query.value(0).toInt();
519     }
520     return 0;
521 }
522 
DBFileVersion() const523 int Exif::Database::DBFileVersion() const
524 {
525     Q_D(const Database);
526     return d->DBFileVersion();
527 }
528 
DBFileVersionGuaranteed() const529 int Exif::Database::DBFileVersionGuaranteed() const
530 {
531     Q_D(const Database);
532 
533     // previous to KPA 4.6, there was no metadata table:
534     if (!d->m_db.tables().contains(QString::fromLatin1("settings")))
535         return 0;
536 
537     QSqlQuery query(QString::fromLatin1("SELECT value FROM settings WHERE keyword = 'GuaranteedDataVersion'"), d->m_db);
538     if (!query.exec())
539         d->showErrorAndFail(query);
540 
541     if (query.first()) {
542         return query.value(0).toInt();
543     }
544     return 0;
545 }
546 
DBVersion()547 int Exif::Database::DBVersion()
548 {
549     return DB_VERSION;
550 }
551 
isUsable() const552 bool Exif::Database::DatabasePrivate::isUsable() const
553 {
554     return isSQLiteDriverAvailable() && isOpen();
555 }
isUsable() const556 bool Exif::Database::isUsable() const
557 {
558     Q_D(const Database);
559     return d->isUsable();
560 }
561 
readFields(const DB::FileName & fileName,ElementList & fields) const562 bool Exif::Database::readFields(const DB::FileName &fileName, ElementList &fields) const
563 {
564     if (!isUsable())
565         return false;
566 
567     bool foundIt = false;
568     QStringList fieldList;
569     for (const DatabaseElement *e : fields) {
570         fieldList.append(e->columnName());
571     }
572 
573     Q_D(const Database);
574     QSqlQuery query(d->m_db);
575     // the query returns a single value, so we don't need the overhead for random access:
576     query.setForwardOnly(true);
577 
578     query.prepare(QString::fromLatin1("select %1 from exif where filename=?")
579                       .arg(fieldList.join(QString::fromLatin1(", "))));
580     query.bindValue(0, fileName.absolute());
581 
582     if (!query.exec()) {
583         d->showErrorAndFail(query);
584     }
585     if (query.next()) {
586         // file in exif db -> write back results
587         int i = 0;
588         for (DatabaseElement *e : fields) {
589             e->setValue(query.value(i++));
590         }
591         foundIt = true;
592     }
593     return foundIt;
594 }
595 
filesMatchingQuery(const QString & queryStr) const596 DB::FileNameSet Exif::Database::filesMatchingQuery(const QString &queryStr) const
597 {
598     if (!isUsable())
599         return DB::FileNameSet();
600 
601     DB::FileNameSet result;
602     Q_D(const Database);
603     QSqlQuery query(queryStr, d->m_db);
604 
605     if (!query.exec())
606         d->showErrorAndFail(query);
607 
608     else {
609         if (d->m_doUTF8Conversion)
610             while (query.next())
611                 result.insert(DB::FileName::fromAbsolutePath(QString::fromUtf8(query.value(0).toByteArray())));
612         else
613             while (query.next())
614                 result.insert(DB::FileName::fromAbsolutePath(query.value(0).toString()));
615     }
616 
617     return result;
618 }
619 
cameras() const620 QList<QPair<QString, QString>> Exif::Database::cameras() const
621 {
622     QList<QPair<QString, QString>> result;
623 
624     if (!isUsable())
625         return result;
626 
627     Q_D(const Database);
628     QSqlQuery query(QString::fromLatin1("SELECT DISTINCT Exif_Image_Make, Exif_Image_Model FROM exif"), d->m_db);
629     if (!query.exec()) {
630         d->showErrorAndFail(query);
631     } else {
632         while (query.next()) {
633             QString make = query.value(0).toString();
634             QString model = query.value(1).toString();
635             if (!make.isEmpty() && !model.isEmpty())
636                 result.append(qMakePair(make, model));
637         }
638     }
639 
640     return result;
641 }
642 
lenses() const643 QList<QString> Exif::Database::lenses() const
644 {
645     QList<QString> result;
646 
647     if (!isUsable())
648         return result;
649 
650     Q_D(const Database);
651     QSqlQuery query(QString::fromLatin1("SELECT DISTINCT Exif_Photo_LensModel FROM exif"), d->m_db);
652     if (!query.exec()) {
653         d->showErrorAndFail(query);
654     } else {
655         while (query.next()) {
656             QString lens = query.value(0).toString();
657             if (!lens.isEmpty())
658                 result.append(lens);
659         }
660     }
661 
662     return result;
663 }
664 
size() const665 int Database::size() const
666 {
667     if (!isUsable())
668         return 0;
669 
670     Q_D(const Database);
671     QSqlQuery query(QLatin1String("SELECT count(*) FROM exif"), d->m_db);
672     int result = 0;
673     if (!query.exec()) {
674         d->showErrorAndFail(query);
675     } else {
676         if (query.first()) {
677             result = query.value(0).toInt();
678         }
679     }
680     return result;
681 }
682 
recreate(const DB::FileNameList & allImageFiles,DB::AbstractProgressIndicator & progressIndicator)683 void Exif::Database::recreate(const DB::FileNameList &allImageFiles, DB::AbstractProgressIndicator &progressIndicator)
684 {
685     progressIndicator.setMinimum(0);
686     progressIndicator.setMaximum(allImageFiles.size());
687 
688     Q_D(Database);
689     // We create a backup of the current database in case
690     // the user presse 'cancel' or there is any error. In that case
691     // we want to go back to the original DB.
692 
693     const QString origBackup = d->getFileName() + QLatin1String(".bak");
694     d->m_db.close();
695 
696     QDir().remove(origBackup);
697     QDir().rename(d->getFileName(), origBackup);
698     d->init();
699 
700     // using a transaction here removes a *huge* overhead on the insert statements
701     startInsertTransaction();
702     int i = 0;
703     for (const auto &fileName : allImageFiles) {
704         progressIndicator.setValue(i++);
705         add(fileName);
706         if (i % 10) {
707             auto app = QCoreApplication::instance();
708             if (app)
709                 app->processEvents();
710         }
711         if (progressIndicator.wasCanceled())
712             break;
713     }
714 
715     // PENDING(blackie) We should count the amount of files that did not succeeded and warn the user.
716     if (progressIndicator.wasCanceled()) {
717         abortInsertTransaction();
718         d->m_db.close();
719         QDir().remove(d->getFileName());
720         QDir().rename(origBackup, d->getFileName());
721         d->init();
722     } else {
723         commitInsertTransaction();
724         QDir().remove(origBackup);
725     }
726 }
727 
728 // vi:expandtab:tabstop=4 shiftwidth=4:
729