1 /* ============================================================
2 *
3 * This file is a part of digiKam project
4 * https://www.digikam.org
5 *
6 * Date : 2020-12-31
7 * Description : Online version downloader.
8 *
9 * Copyright (C) 2020-2021 by Maik Qualmann <metzpinguin at gmail dot com>
10 * Copyright (C) 2010-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 "onlineversiondwnl.h"
26
27 // Qt includes
28
29 #include <QDir>
30 #include <QSysInfo>
31 #include <QDialog>
32 #include <QFuture>
33 #include <QFutureWatcher>
34 #include <QtConcurrent> // krazy:exclude=includes
35 #include <QByteArray>
36 #include <QPointer>
37 #include <QEventLoop>
38 #include <QApplication>
39 #include <QStandardPaths>
40 #include <QNetworkRequest>
41 #include <QCryptographicHash>
42 #include <QNetworkAccessManager>
43
44 // KDE includes
45
46 #include <klocalizedstring.h>
47
48 // Local includes
49
50 #include "digikam_debug.h"
51 #include "onlineversionchecker.h"
52
53 namespace Digikam
54 {
55
56 class Q_DECL_HIDDEN OnlineVersionDwnl::Private
57 {
58 public:
59
Private()60 explicit Private()
61 : preRelease (false),
62 updateWithDebug (false),
63 redirects (0),
64 reply (nullptr),
65 manager (nullptr)
66 {
67 }
68
69 bool preRelease; ///< Flag to check pre-releases
70 bool updateWithDebug; ///< Flag to use version with debug symbols
71 int redirects; ///< Count of redirected url
72
73 QString downloadUrl; ///< Root url for current download
74 QString checksums; ///< Current download sha256 sums
75 QString currentUrl; ///< Full url of current file to download
76 QString error; ///< Error string about current download
77 QString file; ///< Info about file to download (version string, or filename)
78 QString downloaded; ///< Local file path to downloaded data
79
80 QNetworkReply* reply; ///< Current network request reply
81 QNetworkAccessManager* manager; ///< Network manager instance
82 };
83
OnlineVersionDwnl(QObject * const parent,bool checkPreRelease,bool updateWithDebug)84 OnlineVersionDwnl::OnlineVersionDwnl(QObject* const parent,
85 bool checkPreRelease,
86 bool updateWithDebug)
87 : QObject(parent),
88 d (new Private)
89 {
90 d->preRelease = checkPreRelease;
91 d->updateWithDebug = updateWithDebug;
92
93 if (d->preRelease)
94 {
95 d->downloadUrl = QLatin1String("https://files.kde.org/digikam/");
96 }
97 else
98 {
99 d->downloadUrl = QLatin1String("https://download.kde.org/stable/digikam/");
100 }
101
102 d->manager = new QNetworkAccessManager(this);
103 d->manager->setRedirectPolicy(QNetworkRequest::ManualRedirectPolicy);
104
105 connect(d->manager, SIGNAL(finished(QNetworkReply*)),
106 this, SLOT(slotDownloaded(QNetworkReply*)));
107 }
108
~OnlineVersionDwnl()109 OnlineVersionDwnl::~OnlineVersionDwnl()
110 {
111 cancelDownload();
112 delete d;
113 }
114
downloadUrl() const115 QString OnlineVersionDwnl::downloadUrl() const
116 {
117 return d->downloadUrl;
118 }
119
cancelDownload()120 void OnlineVersionDwnl::cancelDownload()
121 {
122 if (d->reply)
123 {
124 d->reply->abort();
125 d->reply = nullptr;
126 }
127 }
128
startDownload(const QString & version)129 void OnlineVersionDwnl::startDownload(const QString& version)
130 {
131 QUrl url;
132
133 if (d->preRelease)
134 {
135 if (d->updateWithDebug)
136 {
137 QString base = version.section(QLatin1Char('.'), 0, -2);
138 QString suf = version.section(QLatin1Char('.'), -1);
139 d->file = base + QLatin1String("-debug.") + suf;
140 }
141 else
142 {
143 d->file = version;
144 }
145
146 d->currentUrl = d->downloadUrl + d->file + QLatin1String(".sha256");
147 url = QUrl(d->currentUrl);
148 }
149 else
150 {
151 QString arch;
152 QString bundle;
153 QString debug = d->updateWithDebug ? QLatin1String("-debug") : QString();
154
155 if (!OnlineVersionChecker::bundleProperties(arch, bundle))
156 {
157 emit signalDownloadError(i18n("Unsupported Architecture: %1", QSysInfo::buildAbi()));
158
159 qCDebug(DIGIKAM_GENERAL_LOG) << "Unsupported architecture";
160
161 return;
162 }
163
164 QString os =
165
166 #ifdef Q_OS_MACOS
167
168 QLatin1String("MacOS-");
169
170 #else
171
172 QString();
173
174 #endif
175
176 d->file = QString::fromLatin1("digiKam-%1-%2%3%4.%5")
177 .arg(version)
178 .arg(os)
179 .arg(arch)
180 .arg(debug)
181 .arg(bundle);
182
183 d->currentUrl = d->downloadUrl + QString::fromLatin1("%1/").arg(version) + d->file + QLatin1String(".sha256");
184 url = QUrl(d->currentUrl);
185 }
186
187 d->redirects = 0;
188 download(url);
189 }
190
download(const QUrl & url)191 void OnlineVersionDwnl::download(const QUrl& url)
192 {
193 qCDebug(DIGIKAM_GENERAL_LOG) << "Downloading: " << url;
194
195 d->redirects++;
196 d->reply = d->manager->get(QNetworkRequest(url));
197
198 connect(d->reply, SIGNAL(downloadProgress(qint64,qint64)),
199 this, SIGNAL(signalDownloadProgress(qint64,qint64)));
200
201 connect(d->reply, SIGNAL(sslErrors(QList<QSslError>)),
202 d->reply, SLOT(ignoreSslErrors()));
203 }
204
slotDownloaded(QNetworkReply * reply)205 void OnlineVersionDwnl::slotDownloaded(QNetworkReply* reply)
206 {
207 if (reply != d->reply)
208 {
209 return;
210 }
211
212 // mark for deletion
213
214 reply->deleteLater();
215 d->reply = nullptr;
216
217 if ((reply->error() != QNetworkReply::NoError) &&
218 (reply->error() != QNetworkReply::InsecureRedirectError))
219 {
220 qCDebug(DIGIKAM_GENERAL_LOG) << "Error: " << reply->errorString();
221 emit signalDownloadError(reply->errorString());
222
223 return;
224 }
225
226 QUrl redirectUrl = reply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl();
227
228 if (redirectUrl.isValid() &&
229 (reply->url() != redirectUrl) &&
230 (d->redirects < 10))
231 {
232 download(redirectUrl);
233
234 return;
235 }
236
237 // Check if checksum arrive in first
238
239 if (reply->url().url().endsWith(QLatin1String(".sha256")))
240 {
241 QByteArray data = reply->readAll();
242
243 if (data.isEmpty())
244 {
245 qCDebug(DIGIKAM_GENERAL_LOG) << "Checksum file is empty";
246 emit signalDownloadError(i18n("Checksum file is empty."));
247
248 return;
249 }
250
251 QTextStream in(&data);
252 QString line = in.readLine(); // first line and section 0 constains the checksum.
253 QString sums = line.section(QLatin1Char(' '), 0, 0);
254
255 if (sums.isEmpty())
256 {
257 qCDebug(DIGIKAM_GENERAL_LOG) << "Checksum is invalid";
258 emit signalDownloadError(i18n("Checksum is invalid."));
259
260 return;
261 }
262
263 d->checksums = sums;
264 qCDebug(DIGIKAM_GENERAL_LOG) << "Checksum is" << d->checksums;
265
266 d->redirects = 0;
267 download(QUrl(d->currentUrl.remove(QLatin1String(".sha256"))));
268
269 return;
270 }
271
272 // Whole file to download is here
273
274 QByteArray data = reply->readAll();
275
276 if (data.isEmpty())
277 {
278 qCDebug(DIGIKAM_GENERAL_LOG) << "Downloaded file is empty";
279 emit signalDownloadError(i18n("Downloaded file is empty."));
280
281 return;
282 }
283
284 // Compute checksum in a separated thread
285
286 emit signalComputeChecksum();
287
288 QString hash;
289 QPointer<QEventLoop> loop = new QEventLoop(this);
290 QFutureWatcher<void> fwatcher;
291
292 connect(&fwatcher, SIGNAL(finished()),
293 loop, SLOT(quit()));
294
295 connect(static_cast<QDialog*>(parent()), SIGNAL(rejected()),
296 &fwatcher, SLOT(cancel()));
297
298 fwatcher.setFuture(QtConcurrent::run([&hash, &data]()
299 {
300 QCryptographicHash sha256(QCryptographicHash::Sha256);
301 sha256.addData(data);
302 hash = QString::fromLatin1(sha256.result().toHex());
303 }
304 ));
305
306 loop->exec();
307
308 if (d->checksums != hash)
309 {
310 qCDebug(DIGIKAM_GENERAL_LOG) << "Checksums error";
311 emit signalDownloadError(i18n("Checksums error."));
312
313 return;
314 }
315
316 // Checksum is fine, now save data to disk
317
318 QString path = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation);
319 path = QDir::toNativeSeparators(path + QLatin1String("/") + d->file);
320
321 QFile file(path);
322
323 if (file.open(QIODevice::WriteOnly))
324 {
325 file.write(data);
326 file.close();
327
328 QFile::setPermissions(path, QFile::permissions(path) | QFileDevice::ExeUser);
329 d->downloaded = path;
330
331 qCDebug(DIGIKAM_GENERAL_LOG) << "Download is complete: " << path;
332
333 emit signalDownloadError(QString()); // No error: download is complete.
334 }
335 else
336 {
337 qCDebug(DIGIKAM_GENERAL_LOG) << "Cannot open " << path;
338 emit signalDownloadError(i18n("Cannot open target file."));
339 }
340 }
341
downloadedPath() const342 QString OnlineVersionDwnl::downloadedPath() const
343 {
344 return d->downloaded;
345 }
346
347 } // namespace Digikam
348