File indexing completed on 2025-01-05 04:00:11

0001 /* ============================================================
0002  *
0003  * This file is a part of digiKam project
0004  * https://www.digikam.org
0005  *
0006  * Date        : 2020-11-14
0007  * Description : Files downloader
0008  *
0009  * SPDX-FileCopyrightText: 2020-2021 by Maik Qualmann <metzpinguin at gmail dot com>
0010  *
0011  * SPDX-License-Identifier: GPL-2.0-or-later
0012  *
0013  * ============================================================ */
0014 
0015 #include "filesdownloader.h"
0016 
0017 // Qt includes
0018 
0019 #include <QDir>
0020 #include <QLabel>
0021 #include <QTimer>
0022 #include <QPointer>
0023 #include <QCheckBox>
0024 #include <QByteArray>
0025 #include <QMessageBox>
0026 #include <QPushButton>
0027 #include <QVBoxLayout>
0028 #include <QProgressBar>
0029 #include <QApplication>
0030 #include <QStandardPaths>
0031 #include <QNetworkRequest>
0032 #include <QDialogButtonBox>
0033 #include <QCryptographicHash>
0034 #include <QNetworkAccessManager>
0035 
0036 // KDE includes
0037 
0038 #include <klocalizedstring.h>
0039 
0040 // Local includes
0041 
0042 #include "digikam_debug.h"
0043 #include "digikam_globals.h"
0044 #include "dxmlguiwindow.h"
0045 #include "systemsettings.h"
0046 #include "itempropertiestab.h"
0047 
0048 namespace Digikam
0049 {
0050 
0051 class Q_DECL_HIDDEN FilesDownloader::Private
0052 {
0053 public:
0054 
0055     explicit Private()
0056       : downloadUrls( { QLatin1String("https://files.kde.org/digikam/"),
0057                         QLatin1String("https://mirror.faigner.de/kde/files/digikam/"),
0058                         QLatin1String("https://kde-applicationdata.mirrors.omnilance.com/digikam/"),
0059                         QLatin1String("https://mirrors.ocf.berkeley.edu/kde-applicationdata/digikam/") } ),
0060         mirrorIndex     (0),
0061         redirects       (0),
0062         total           (0),
0063         count           (0),
0064         buttons         (nullptr),
0065         progress        (nullptr),
0066         facesEngineCheck(nullptr),
0067         aestheticCheck  (nullptr),
0068         autoTagsCheck   (nullptr),
0069         nameLabel       (nullptr),
0070         infoLabel       (nullptr),
0071         sizeLabel       (nullptr),
0072         reply           (nullptr),
0073         netMngr         (nullptr),
0074         system          (qApp->applicationName())
0075     {
0076     }
0077 
0078     const QStringList      downloadUrls;
0079 
0080     QString                error;
0081 
0082     QList<DownloadInfo>    files;
0083     DownloadInfo           currentInfo;
0084 
0085     int                    mirrorIndex;
0086     int                    redirects;
0087     int                    total;
0088     int                    count;
0089 
0090     QDialogButtonBox*      buttons;
0091     QProgressBar*          progress;
0092     QCheckBox*             facesEngineCheck;
0093     QCheckBox*             aestheticCheck;
0094     QCheckBox*             autoTagsCheck;
0095     QLabel*                nameLabel;
0096     QLabel*                infoLabel;
0097     QLabel*                sizeLabel;
0098 
0099     QNetworkReply*         reply;
0100     QNetworkAccessManager* netMngr;
0101 
0102     SystemSettings         system;
0103 };
0104 
0105 FilesDownloader::FilesDownloader(QWidget* const parent)
0106     : QDialog(parent),
0107       d      (new Private)
0108 {
0109     createDownloadInfo();
0110 }
0111 
0112 FilesDownloader::~FilesDownloader()
0113 {
0114     if (d->reply)
0115     {
0116         d->reply->abort();
0117         d->reply = nullptr;
0118     }
0119 
0120     delete d;
0121 }
0122 
0123 bool FilesDownloader::checkDownloadFiles() const
0124 {
0125     QString path = QStandardPaths::locate(QStandardPaths::GenericDataLocation,
0126                                           QLatin1String("digikam/facesengine"),
0127                                           QStandardPaths::LocateDirectory);
0128 
0129     if (path.isEmpty())
0130     {
0131         return false;
0132     }
0133 
0134     Q_FOREACH (const DownloadInfo& info, d->files)
0135     {
0136         QFileInfo fileInfo(path + QLatin1Char('/') + info.name);
0137 
0138         if (!fileInfo.exists() || (fileInfo.size() != info.size))
0139         {
0140             return false;
0141         }
0142     }
0143 
0144     return true;
0145 }
0146 
0147 void FilesDownloader::startDownload()
0148 {
0149     setWindowTitle(i18nc("@title:window", "Download Required Files"));
0150     setMinimumHeight(250);
0151     setMinimumWidth(600);
0152 
0153     QWidget* const mainWidget = new QWidget(this);
0154     QVBoxLayout* const vBox   = new QVBoxLayout(mainWidget);
0155 
0156     d->buttons                = new QDialogButtonBox(QDialogButtonBox::Help |
0157                                                      QDialogButtonBox::Ok |
0158                                                      QDialogButtonBox::Close,
0159                                                      mainWidget);
0160     d->buttons->button(QDialogButtonBox::Ok)->setDefault(true);
0161     d->buttons->button(QDialogButtonBox::Ok)->setText(i18n("Download"));
0162     d->buttons->button(QDialogButtonBox::Ok)->setIcon(QIcon::fromTheme(QLatin1String("edit-download")));
0163 
0164     d->infoLabel         = new QLabel(mainWidget);
0165     d->sizeLabel         = new QLabel(mainWidget);
0166 
0167     d->infoLabel->setWordWrap(true);
0168 
0169     d->facesEngineCheck  = new QCheckBox(i18n("Use Face Detection feature"), mainWidget);
0170     d->aestheticCheck    = new QCheckBox(i18n("Use Aesthetic Detection feature"), mainWidget);
0171     d->autoTagsCheck     = new QCheckBox(i18n("Use Auto Tags Assignment feature"), mainWidget);
0172 
0173     d->progress          = new QProgressBar(mainWidget);
0174     d->progress->setMinimum(0);
0175     d->progress->setMaximum(1);
0176     d->progress->setValue(0);
0177 
0178     d->nameLabel         = new QLabel(mainWidget);
0179 
0180     vBox->addWidget(d->infoLabel);
0181     vBox->addWidget(d->sizeLabel);
0182     vBox->addStretch(1);
0183     vBox->addWidget(d->facesEngineCheck);
0184     vBox->addWidget(d->aestheticCheck);
0185     vBox->addWidget(d->autoTagsCheck);
0186     vBox->addStretch(1);
0187     vBox->addWidget(d->nameLabel);
0188     vBox->addWidget(d->progress);
0189     vBox->addWidget(d->buttons);
0190 
0191     setLayout(vBox);
0192 
0193     connect(d->buttons->button(QDialogButtonBox::Ok), SIGNAL(clicked()),
0194             this, SLOT(slotDownload()));
0195 
0196     connect(d->buttons->button(QDialogButtonBox::Help), SIGNAL(clicked()),
0197             this, SLOT(slotHelp()));
0198 
0199     connect(d->buttons->button(QDialogButtonBox::Close), SIGNAL(clicked()),
0200             this, SLOT(reject()));
0201 
0202     d->facesEngineCheck->setChecked(d->system.enableFaceEngine);
0203     d->aestheticCheck->setChecked(d->system.enableAesthetic);
0204     d->autoTagsCheck->setChecked(d->system.enableAutoTags);
0205 
0206     connect(d->facesEngineCheck, SIGNAL(toggled(bool)),
0207             this, SLOT(slotUpdateDownloadInfo()));
0208 
0209     connect(d->aestheticCheck, SIGNAL(toggled(bool)),
0210             this, SLOT(slotUpdateDownloadInfo()));
0211 
0212     connect(d->autoTagsCheck, SIGNAL(toggled(bool)),
0213             this, SLOT(slotUpdateDownloadInfo()));
0214 
0215     slotUpdateDownloadInfo();
0216 
0217     (void)exec();
0218 }
0219 
0220 void FilesDownloader::slotDownload()
0221 {
0222     d->buttons->button(QDialogButtonBox::Ok)->setEnabled(false);
0223     d->facesEngineCheck->setEnabled(false);
0224     d->aestheticCheck->setEnabled(false);
0225     d->autoTagsCheck->setEnabled(false);
0226 
0227     if (d->error.isEmpty())
0228     {
0229         while (!d->files.isEmpty())
0230         {
0231             d->currentInfo = d->files.takeFirst();
0232 
0233             if (!downloadExists(d->currentInfo))
0234             {
0235                 d->count++;
0236                 download();
0237 
0238                 return;
0239             }
0240         }
0241 
0242         QMessageBox::information(this, qApp->applicationName(),
0243                                  i18n("All files were downloaded successfully."));
0244 
0245         close();
0246     }
0247     else
0248     {
0249         QPointer<QMessageBox> msgBox = new QMessageBox(QMessageBox::Critical,
0250                  i18nc("@title:window", "Download Error"),
0251                  i18n("An error occurred during the download.\n\n"
0252                       "File: %1\n\n%2\n\n"
0253                       "You can try again or continue the "
0254                       "download at the next start.",
0255                       d->currentInfo.name, d->error),
0256                  QMessageBox::Yes | QMessageBox::Cancel,
0257                  qApp->activeWindow());
0258 
0259         msgBox->button(QMessageBox::Yes)->setText(i18nc("@action:button", "Try Again"));
0260         msgBox->button(QMessageBox::Yes)->setIcon(QIcon::fromTheme(QLatin1String("edit-download")));
0261 
0262         int result = msgBox->exec();
0263         delete msgBox;
0264 
0265         if (result == QMessageBox::Yes)
0266         {
0267             d->error.clear();
0268             d->mirrorIndex++;
0269 
0270             if (d->mirrorIndex >= d->downloadUrls.size())
0271             {
0272                 d->mirrorIndex = 0;
0273             }
0274 
0275             download();
0276 
0277             return;
0278         }
0279 
0280         close();
0281     }
0282 }
0283 
0284 void FilesDownloader::download()
0285 {
0286     if (!d->netMngr)
0287     {
0288         d->netMngr = new QNetworkAccessManager(this);
0289         d->netMngr->setRedirectPolicy(QNetworkRequest::ManualRedirectPolicy);
0290 
0291         connect(d->netMngr, SIGNAL(finished(QNetworkReply*)),
0292                 this, SLOT(slotDownloaded(QNetworkReply*)));
0293     }
0294 
0295     QUrl request(d->downloadUrls.at(d->mirrorIndex) +
0296                  d->currentInfo.path + d->currentInfo.name);
0297 
0298     d->redirects = 0;
0299     createRequest(request);
0300 }
0301 
0302 void FilesDownloader::nextDownload()
0303 {
0304     QTimer::singleShot(100, this, SLOT(slotDownload()));
0305 }
0306 
0307 void FilesDownloader::createRequest(const QUrl& url)
0308 {
0309     d->progress->setMaximum(d->currentInfo.size);
0310     d->progress->setValue(0);
0311     printDownloadInfo(url);
0312 
0313     d->redirects++;
0314     d->reply = d->netMngr->get(QNetworkRequest(url));
0315 
0316     connect(d->reply, SIGNAL(downloadProgress(qint64,qint64)),
0317             this, SLOT(slotDownloadProgress(qint64,qint64)));
0318 
0319     connect(d->reply, SIGNAL(sslErrors(QList<QSslError>)),
0320             d->reply, SLOT(ignoreSslErrors()));
0321 }
0322 
0323 void FilesDownloader::printDownloadInfo(const QUrl& url)
0324 {
0325     QString text = QString::fromUtf8("%1 (%2://%3) (%4/%5)")
0326                    .arg(d->currentInfo.name)
0327                    .arg(url.scheme())
0328                    .arg(url.host())
0329                    .arg(d->count)
0330                    .arg(d->total);
0331 
0332     d->nameLabel->setText(text);
0333 }
0334 
0335 bool FilesDownloader::downloadExists(const DownloadInfo& info) const
0336 {
0337     QString path = getFacesEnginePath() +
0338                    QString::fromLatin1("/%1").arg(info.name);
0339 
0340     return (!path.isEmpty() && (QFileInfo(path).size() == info.size));
0341 }
0342 
0343 void FilesDownloader::reject()
0344 {
0345     if (d->reply)
0346     {
0347         d->reply->abort();
0348         d->reply = nullptr;
0349     }
0350 
0351     QDialog::reject();
0352 }
0353 
0354 void FilesDownloader::slotDownloaded(QNetworkReply* reply)
0355 {
0356     if (reply != d->reply)
0357     {
0358         return;
0359     }
0360 
0361     d->reply = nullptr;
0362 
0363     if ((reply->error() != QNetworkReply::NoError)             &&
0364         (reply->error() != QNetworkReply::InsecureRedirectError))
0365     {
0366         d->error = reply->errorString();
0367 
0368         reply->deleteLater();
0369 
0370         nextDownload();
0371 
0372         return;
0373     }
0374 
0375     QUrl redirectUrl = reply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl();
0376 
0377     if (redirectUrl.isValid() && (reply->url() != redirectUrl) && (d->redirects < 10))
0378     {
0379         createRequest(redirectUrl);
0380 
0381         reply->deleteLater();
0382 
0383         return;
0384     }
0385 
0386     QCryptographicHash sha256(QCryptographicHash::Sha256);
0387 
0388     QByteArray data = reply->readAll();
0389 
0390     sha256.addData(data);
0391 
0392     if (d->currentInfo.hash != QString::fromLatin1(sha256.result().toHex()))
0393     {
0394         d->error = i18n("Checksum is incorrect.");
0395 
0396         reply->deleteLater();
0397 
0398         nextDownload();
0399 
0400         return;
0401     }
0402 
0403     QString path = getFacesEnginePath();
0404 
0405     if (!QFileInfo::exists(path))
0406     {
0407         QDir().mkpath(path);
0408     }
0409 
0410     QFile file(path + QLatin1Char('/') + d->currentInfo.name);
0411 
0412     if (file.open(QIODevice::WriteOnly))
0413     {
0414         qint64 written = file.write(data);
0415 
0416         if (written != d->currentInfo.size)
0417         {
0418             d->error = i18n("File write error.");
0419         }
0420 
0421         file.close();
0422     }
0423     else
0424     {
0425         d->error = i18n("File open error.");
0426     }
0427 
0428     reply->deleteLater();
0429 
0430     nextDownload();
0431 }
0432 
0433 void FilesDownloader::slotDownloadProgress(qint64 bytesReceived, qint64 bytesTotal)
0434 {
0435     if (d->reply && (bytesReceived > d->currentInfo.size))
0436     {
0437         d->reply->abort();
0438         d->reply = nullptr;
0439 
0440         d->error = i18n("File on the server is too large.");
0441 
0442         nextDownload();
0443 
0444         return;
0445     }
0446 
0447     d->progress->setMaximum(bytesTotal);
0448     d->progress->setValue(bytesReceived);
0449 }
0450 
0451 QString FilesDownloader::getFacesEnginePath() const
0452 {
0453     QString appPath = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation);
0454     appPath        += QLatin1String("/digikam/facesengine");
0455 
0456     return appPath;
0457 }
0458 
0459 void FilesDownloader::slotHelp()
0460 {
0461     openOnlineDocumentation(QLatin1String("getting_started"), QLatin1String("quick_start"), QLatin1String("firstrun-downloads"));
0462 }
0463 
0464 void FilesDownloader::createDownloadInfo()
0465 {
0466     d->files.clear();
0467 
0468     if (d->system.enableFaceEngine)
0469     {
0470         d->files << DownloadInfo(QLatin1String("facesengine/shape-predictor/"),
0471                                  QLatin1String("shapepredictor.dat"),
0472                                  QLatin1String("6f3d2a59dc30c7c9166983224dcf5732b25de734fff1e36ff1f3047ef90ed82b"),
0473                                  67740572
0474                                 );
0475 
0476         d->files << DownloadInfo(QLatin1String("facesengine/dnnface/"),
0477                                  QLatin1String("yolov3-face.cfg"),
0478                                  QLatin1String("f6563bd6923fd6500d2c2d6025f32ebdba916a85e5c9798351d916909f62aaf5"),
0479                                  8334
0480                                 );
0481 
0482         d->files << DownloadInfo(QLatin1String("facesengine/dnnface/"),
0483                                 QLatin1String("yolov3-wider_16000.weights"),
0484                                 QLatin1String("a88f3b3882e3cce1e553a81d42beef6202cb9afc3db88e7944f9ffbcc369e7df"),
0485                                 246305388
0486                                );
0487 
0488         if (qApp->applicationName() == QLatin1String("digikam"))
0489         {
0490             d->files << DownloadInfo(QLatin1String("facesengine/dnnface/"),
0491                                      QLatin1String("openface_nn4.small2.v1.t7"),
0492                                      QLatin1String("9b72d54aeb24a64a8135dca8e792f7cc675c99a884a6940350a6cedcf7b7ba08"),
0493                                      31510785
0494                                     );
0495 
0496             d->files << DownloadInfo(QLatin1String("facesengine/dnnface/"),
0497                                      QLatin1String("deploy.prototxt"),
0498                                      QLatin1String("f62621cac923d6f37bd669298c428bb7ee72233b5f8c3389bb893e35ebbcf795"),
0499                                      28092
0500                                     );
0501 
0502             d->files << DownloadInfo(QLatin1String("facesengine/dnnface/"),
0503                                      QLatin1String("res10_300x300_ssd_iter_140000_fp16.caffemodel"),
0504                                      QLatin1String("510ffd2471bd81e3fcc88a5beb4eae4fb445ccf8333ebc54e7302b83f4158a76"),
0505                                      5351047
0506                                     );
0507         }
0508     }
0509 
0510     if (d->system.enableAesthetic)
0511     {
0512         d->files << DownloadInfo(QLatin1String("aestheticdetector/"),
0513                                  QLatin1String("weights_inceptionv3_299.pb"),
0514                                  QLatin1String("8923e3daff71c07533b9023ef32c69d8c058d1e0931d76d8b81241a201138538"),
0515                                  88007527
0516                                 );
0517     }
0518 
0519     if (d->system.enableAutoTags)
0520     {
0521         d->files << DownloadInfo(QLatin1String("autotags/"),
0522                                  QLatin1String("yolov5n_batch_16_s320.onnx"),
0523                                  QLatin1String("43cf201144e6918354892a2fda7ccb07b3df1abc136ddac3b5c968dffc847009"),
0524                                  7616008
0525                                 );
0526 
0527         d->files << DownloadInfo(QLatin1String("autotags/"),
0528                                  QLatin1String("yolov5x_batch_16_s320.onnx"),
0529                                  QLatin1String("e56b5024e6d29f8a35dbfbdd67bbdb69593fb67bfb275915081f9a463fcd355b"),
0530                                  347010839
0531                                 );
0532 
0533         d->files << DownloadInfo(QLatin1String("autotags/"),
0534                                  QLatin1String("resnet50.onnx"),
0535                                  QLatin1String("490e2761519cbf8a4433f880c2aa16b457730085cf9a8aab1e43d82bcadba4f1"),
0536                                  102146365
0537                                 );
0538 
0539         d->files << DownloadInfo(QLatin1String("autotags/"),
0540                                  QLatin1String("coco.names"),
0541                                  QLatin1String("634a1132eb33f8091d60f2c346ababe8b905ae08387037aed883953b7329af84"),
0542                                  625
0543                                 );
0544 
0545         d->files << DownloadInfo(QLatin1String("autotags/"),
0546                                  QLatin1String("classification_classes_ILSVRC2012.txt"),
0547                                  QLatin1String("4eb3da435cf544e4a6f390f62c84cb9c9bb68cf8b14e97f8a063452382e5efd2"),
0548                                  21675
0549                                 );
0550     }
0551 
0552 }
0553 
0554 void FilesDownloader::slotUpdateDownloadInfo()
0555 {
0556     QString path = QDir::toNativeSeparators(getFacesEnginePath());
0557 
0558     d->system.enableFaceEngine = d->facesEngineCheck->isChecked();
0559     d->system.enableAesthetic  = d->aestheticCheck->isChecked();
0560     d->system.enableAutoTags   = d->autoTagsCheck->isChecked();
0561     d->system.saveSettings();
0562 
0563     createDownloadInfo();
0564 
0565     qint64 size = 0;
0566     d->total    = 0;
0567 
0568     Q_FOREACH (const DownloadInfo& info, d->files)
0569     {
0570         if (!downloadExists(info))
0571         {
0572             // cppcheck-suppress useStlAlgorithm
0573             size += info.size;
0574             d->total++;
0575         }
0576     }
0577 
0578     QString sizeString = ItemPropertiesTab::humanReadableBytesCount(size);
0579 
0580     d->infoLabel->setText(i18nc("%1: folder path",
0581                                 "<p>For face detection, the red-eye removal tool, the classification "
0582                                 "of images according to aesthetics or the automatic assignment "
0583                                 "of tags to images, digiKam requires some large binary files. You "
0584                                 "can choose which feature you want to use.</p>"
0585                                 "<p>Some of these files were not found. Click “Download” to begin "
0586                                 "downloading the files you need. You can close this dialog, you "
0587                                 "will be asked again the next time you start digiKam. Without "
0588                                 "these files the selected features will not work.</p>"
0589                                 "<p>The files will be downloaded to %1.</p>"
0590                                 "<p><b>After the successful download you have to restart digiKam.</b></p>", path));
0591     if (size > 0)
0592     {
0593         d->sizeLabel->setText(i18nc("%1: file counter, %2: disk size with unit",
0594                                     "The download requires %1 files with a size of %2.",
0595                                     d->total, sizeString));
0596 
0597         d->buttons->button(QDialogButtonBox::Ok)->setEnabled(true);
0598     }
0599     else
0600     {
0601         d->sizeLabel->setText(i18n("All files of the selected features were found."));
0602 
0603         d->buttons->button(QDialogButtonBox::Ok)->setEnabled(false);
0604     }
0605 }
0606 
0607 //-----------------------------------------------------------------------------
0608 
0609 DownloadInfo::DownloadInfo()
0610     : size(0)
0611 {
0612 }
0613 
0614 DownloadInfo::DownloadInfo(const QString& _path,
0615                            const QString& _name,
0616                            const QString& _hash,
0617                            const qint64&  _size)
0618     : path(_path),
0619       name(_name),
0620       hash(_hash),
0621       size(_size)
0622 {
0623 }
0624 
0625 DownloadInfo::DownloadInfo(const DownloadInfo& other)
0626     : path(other.path),
0627       name(other.name),
0628       hash(other.hash),
0629       size(other.size)
0630 {
0631 }
0632 
0633 DownloadInfo::~DownloadInfo()
0634 {
0635 }
0636 
0637 DownloadInfo& DownloadInfo::operator=(const DownloadInfo& other)
0638 {
0639     path = other.path;
0640     name = other.name;
0641     hash = other.hash;
0642     size = other.size;
0643 
0644     return *this;
0645 }
0646 
0647 } // namespace Digikam
0648 
0649 #include "moc_filesdownloader.cpp"