Seamly2D
Code documentation
fvupdater.cpp
Go to the documentation of this file.
1 /***************************************************************************************************
2  **
3  ** Copyright (c) 2012 Linas Valiukas and others.
4  **
5  ** Permission is hereby granted, free of charge, to any person obtaining a copy of this
6  ** software and associated documentation files (the "Software"), to deal in the Software
7  ** without restriction, including without limitation the rights to use, copy, modify,
8  ** merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
9  ** permit persons to whom the Software is furnished to do so, subject to the following conditions:
10  **
11  ** The above copyright notice and this permission notice shall be included in all copies or
12  ** substantial portions of the Software.
13  **
14  ** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
15  ** NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
16  ** NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
17  ** DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18  ** OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
19  **
20  ******************************************************************************************************/
21 
22 #include "fvupdater.h"
23 
24 #include <qsystemdetection.h>
25 #include <qxmlstream.h>
26 #include <QApplication>
27 #include <QByteArray>
28 #include <QDate>
29 #include <QDesktopServices>
30 #include <QLatin1String>
31 #include <QMessageBox>
32 #include <QMessageLogger>
33 #include <QMutex>
34 #include <QNetworkReply>
35 #include <QNetworkRequest>
36 #include <QStaticStringData>
37 #include <QStringData>
38 #include <QStringDataPtr>
39 #include <QStringList>
40 #include <QStringRef>
41 #include <QVariant>
42 #include <QtDebug>
43 #include <QJsonDocument>
44 #include <QJsonObject>
45 #include <QJsonArray>
46 #include <QRegularExpression>
47 #include <QStandardPaths>
48 #include <QDir>
49 #include <QProcess>
50 #include <QtConcurrent/QtConcurrent>
51 #include "../ifc/exception/vexception.h"
52 #include "../ifc/xml/vabstractconverter.h"
53 #include "../vmisc/projectversion.h"
54 #include "../vmisc/vabstractapplication.h"
55 #include "../vmisc/vcommonsettings.h"
56 
57 const QString defaultFeedURL = QStringLiteral("https://api.github.com/repos/FashionFreedom/Seamly2D/releases/latest");
58 
59 QPointer<FvUpdater> FvUpdater::m_Instance;
60 
61 //---------------------------------------------------------------------------------------------------------------------
63  static QMutex mutex;
64  if (m_Instance.isNull()) {
65  mutex.lock();
66  m_Instance = new FvUpdater;
67  mutex.unlock();
68  }
69 
70  return m_Instance.data();
71 }
72 
73 //---------------------------------------------------------------------------------------------------------------------
75  static QMutex mutex;
76  mutex.lock();
77  delete m_Instance;
78  mutex.unlock();
79 }
80 
81 //---------------------------------------------------------------------------------------------------------------------
83  : QObject(nullptr),
84  m_silentAsMuchAsItCouldGet(true), m_feedURL(), m_qnam(), m_reply(nullptr),
85  m_httpRequestAborted(false), m_dropOnFinish(true) {
86  // noop
87 }
88 
89 //---------------------------------------------------------------------------------------------------------------------
91  delete m_reply;
92 }
93 
94 //---------------------------------------------------------------------------------------------------------------------
95 void FvUpdater::SetFeedURL(const QUrl &feedURL) { m_feedURL = feedURL; }
96 
97 //---------------------------------------------------------------------------------------------------------------------
98 void FvUpdater::SetFeedURL(const QString &feedURL) {
99  SetFeedURL(QUrl(feedURL));
100 }
101 
102 //---------------------------------------------------------------------------------------------------------------------
103 QString FvUpdater::GetFeedURL() const { return m_feedURL.toString(); }
104 
105 //---------------------------------------------------------------------------------------------------------------------
107 
108 //---------------------------------------------------------------------------------------------------------------------
109 void FvUpdater::SetDropOnFinish(bool value) { m_dropOnFinish = value; }
110 
111 //---------------------------------------------------------------------------------------------------------------------
112 bool FvUpdater::CheckForUpdates(bool silentAsMuchAsItCouldGet) {
113  if (m_feedURL.isEmpty()) {
114  qCritical()
115  << "Please set feed URL via setFeedURL() before calling CheckForUpdates().";
116  return false;
117  }
118 
119  m_silentAsMuchAsItCouldGet = silentAsMuchAsItCouldGet;
120 
121  // Check if application's organization name and domain are set, fail
122  // otherwise (nowhere to store QSettings to)
123  if (QCoreApplication::organizationName().isEmpty()) {
124  qCritical()
125  << "QApplication::organizationName is not set. Please do that.";
126  return false;
127  }
128  if (QCoreApplication::organizationDomain().isEmpty()) {
129  qCritical()
130  << "QApplication::organizationDomain is not set. Please do that.";
131  return false;
132  }
133 
134  // Set application name / version is not set yet
135  if (QCoreApplication::applicationName().isEmpty()) {
136  qCritical()
137  << "QApplication::applicationName is not set. Please do that.";
138  return false;
139  }
140 
141  if (QCoreApplication::applicationVersion().isEmpty()) {
142  qCritical()
143  << "QApplication::applicationVersion is not set. Please do that.";
144  return false;
145  }
146 
148  m_httpRequestAborted = false;
150 
151  return true;
152 }
153 
154 //---------------------------------------------------------------------------------------------------------------------
156  if (qApp->Settings()->GetDateOfLastRemind().daysTo(QDate::currentDate())
157  >= 1) {
158  const bool success = CheckForUpdates(true);
159  if (m_dropOnFinish && not success) {
160  drop();
161  }
162  return success;
163  } else {
164  if (m_dropOnFinish) {
165  drop();
166  }
167  return true;
168  }
169 }
170 
171 //---------------------------------------------------------------------------------------------------------------------
173  const bool success = CheckForUpdates(false);
174  if (m_dropOnFinish && not success) {
175  drop();
176  }
177  return success;
178 }
179 
181  auto fileSizeHeader = m_reply->header(QNetworkRequest::ContentLengthHeader).toInt();
182  if (m_fileSize == 0 && fileSizeHeader > 1000000) {
183  m_fileSize = fileSizeHeader;
184  }
185 }
186 
187 void FvUpdater::startDownloadFile(QUrl url, QString name) {
188  QNetworkRequest request;
189  request.setHeader(QNetworkRequest::ContentTypeHeader,
190  QStringLiteral("application/text"));
191  request.setHeader(QNetworkRequest::UserAgentHeader,
192  QCoreApplication::applicationName());
193  request.setUrl(url);
194  request.setSslConfiguration(QSslConfiguration::defaultConfiguration());
195 
196  m_reply = m_qnam.get(request);
197  connect(m_reply, SIGNAL(metaDataChanged()), this, SLOT(getFileSize()));
198  QDir downloadDir = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation);
199  downloadDir.mkdir(m_releaseName);
200  downloadDir.cd(m_releaseName);
201  auto downloadedFile = new QFile(downloadDir.filePath(name), this);
202  if(downloadedFile->exists() && !downloadedFile->remove()){
203  showErrorDialog(tr("Unable to get exclusive access to file \n%1\nPossibly the file is already being downloaded.").arg(downloadDir.filePath(name)), false);
204  return;
205  }
206  bool isOpen = downloadedFile->open(QIODevice::WriteOnly | QIODevice::Truncate);
207  if (!isOpen) {
208  showErrorDialog(tr("Unable to open file\n%1\nfor writing").arg(downloadDir.filePath(name)), false);
209  return;
210  }
211  connect(m_reply.data(), &QNetworkReply::readyRead, this, [this, downloadedFile]() {
212  // this slot gets called every time the QNetworkReply has new data.
213  // We read all of its new data and write it into the file.
214  // That way we use less RAM than when reading it at the finished()
215  // signal of the QNetworkReply
216  downloadedFile->write(m_reply->readAll());
217  int progress = int(downloadedFile->size() * 100 / m_fileSize);
218  setProgress(progress);
219  });
220 
221  connect(m_reply.data(), SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(networkError(QNetworkReply::NetworkError)));
222 
223 
224  connect(m_reply.data(), &QNetworkReply::downloadProgress, this,
225  [this](qint64 bytesRead, qint64 totalBytes) {
226  Q_UNUSED(bytesRead)
227  Q_UNUSED(totalBytes)
228 
229  if (m_httpRequestAborted) {
230  return;
231  }
232  });
233  connect(m_reply.data(), &QNetworkReply::finished, this, [=]() {
234  fileDownloadFinished(downloadedFile, name);
235  });
236 }
237 void FvUpdater::fileDownloadFinished(QFile *downloadedFile, QString name) {
238  if (m_httpRequestAborted) {
239  m_reply->deleteLater();
240  return;
241  }
242 
243  const QVariant redirectionTarget =
244  m_reply->attribute(QNetworkRequest::RedirectionTargetAttribute);
245  if (m_reply->error() != QNetworkReply::NoError) {
246  // Error.
248  tr("File download failed: %1.").arg(m_reply->errorString()), false);
249  } else if (not redirectionTarget.isNull()) {
250  downloadedFile->close();
251  const QUrl newUrl = m_feedURL.resolved(redirectionTarget.toUrl());
252  m_reply->deleteLater();
253  showInformationDialog(tr("Download has started, the installer will open once it's finished downloading"), false);
254  startDownloadFile(newUrl, name);
255  return;
256  } else {
257  setProgress(100); //just in case
258  m_fileSize = 0;
259  downloadedFile->write(m_reply->readAll());
260  downloadedFile->close();
261  auto fileInfo = QFileInfo(*downloadedFile);
262 
263 #ifdef Q_OS_LINUX
264  QDesktopServices::openUrl(QUrl::fromLocalFile(fileInfo.dir().absolutePath()));
265  QProcess proc;
266  auto res = proc.startDetached(QDir::toNativeSeparators(fileInfo.absoluteFilePath()), QStringList());
267  auto err = proc.error();
268  qDebug() << res << " " << err;
269 #else
270 
271  QDesktopServices::openUrl(QUrl::fromLocalFile(fileInfo.absoluteFilePath()));
272 #endif
273 
274  downloadedFile->deleteLater();
275  }
276 }
277 //---------------------------------------------------------------------------------------------------------------------
278 void FvUpdater::startDownloadFeed(const QUrl &url) {
279  // m_xml.clear();
280 
281  QNetworkRequest request;
282  request.setHeader(QNetworkRequest::ContentTypeHeader,
283  QStringLiteral("application/text"));
284  request.setHeader(QNetworkRequest::UserAgentHeader,
285  QCoreApplication::applicationName());
286  request.setUrl(url);
287  request.setSslConfiguration(QSslConfiguration::defaultConfiguration());
288 
289  m_reply = m_qnam.get(request);
290 
291  connect(m_reply.data(), &QNetworkReply::downloadProgress, this,
292  [this](qint64 bytesRead, qint64 totalBytes) {
293  Q_UNUSED(bytesRead)
294  Q_UNUSED(totalBytes)
295 
296  if (m_httpRequestAborted) {
297  return;
298  }
299  });
300  connect(m_reply.data(), &QNetworkReply::finished, this,
302 }
303 
304 //---------------------------------------------------------------------------------------------------------------------
306  if (m_reply) {
307  m_httpRequestAborted = true;
308  m_reply->abort();
309  }
310 }
311 
312 //---------------------------------------------------------------------------------------------------------------------
314  if (m_httpRequestAborted) {
315  m_reply->deleteLater();
316  return;
317  }
318 
319  const QVariant redirectionTarget =
320  m_reply->attribute(QNetworkRequest::RedirectionTargetAttribute);
321  if (m_reply->error() != QNetworkReply::NoError) {
322  // Error.
324  tr("Feed download failed: %1.").arg(m_reply->errorString()), false);
325  } else if (not redirectionTarget.isNull()) {
326  const QUrl newUrl = m_feedURL.resolved(redirectionTarget.toUrl());
327 
328  m_feedURL = newUrl;
329  m_reply->deleteLater();
330 
332  return;
333  } else {
334  auto jsonDoc = QJsonDocument::fromJson(m_reply->readAll());
335  qDebug() << "Response is a JSON object:" << jsonDoc.isObject();
336  if (jsonDoc.isObject()) {
337  auto tag = jsonDoc.object()["tag_name"].toString();
338  qDebug() << "Found the following tag" << tag;
339 
340  if (!releaseIsNewer(tag)) {
341  showInformationDialog(tr("No new releases available."));
342  return;
343  }
344  if (showConfirmationDialog(tr("A new release %1 is available.\nDo you want to download it?").arg(tag), true))
345  getPLatformSpecificInstaller(jsonDoc.object()["assets"].toArray());
346  return;
347  }
348  }
349 
350  m_reply->deleteLater();
351 
352  if (m_dropOnFinish) {
353  drop();
354  }
355 }
356 
358  qDebug() << "current application version" << QCoreApplication::applicationVersion();
359 
360 #ifdef Q_OS_LINUX // Defined on Linux.
361  auto searchPattern = "AppImage";
362 #else
363  #ifdef Q_OS_MACOS // Defined on macOS
364  auto searchPattern = "macos";
365  #else
366  #ifdef Q_OS_WIN64 // Defined on Windows 64-bit only.
367  auto searchPattern = "windows";
368  #else // Only windows 32-bit left
369  auto searchPattern = "win32";
370  #endif
371  #endif
372 #endif
373 
374  for (const QJsonValueRef asset : assets) {
375  auto name = asset.toObject()["name"].toString();
376  qDebug() << "Checking" << searchPattern << "against" << name;
377  if (name.contains(searchPattern,
378  Qt::CaseInsensitive)) {
379  QUrl downloadableUrl =
380  asset.toObject()["browser_download_url"].toString();
381  startDownloadFile(downloadableUrl, name);
382  }
383  }
384 }
385 
386 //---------------------------------------------------------------------------------------------------------------------
387 void FvUpdater::showErrorDialog(const QString &message,
388  bool showEvenInSilentMode) {
390  if (not showEvenInSilentMode) {
391  // Don't show errors in the silent mode
392  return;
393  }
394  }
395 
396  QMessageBox dlFailedMsgBox;
397  dlFailedMsgBox.setIcon(QMessageBox::Critical);
398  dlFailedMsgBox.setText(tr("Error"));
399  dlFailedMsgBox.setInformativeText(message);
400  dlFailedMsgBox.exec();
401 }
402 
403 //---------------------------------------------------------------------------------------------------------------------
404 void FvUpdater::showInformationDialog(const QString &message,
405  bool showEvenInSilentMode) {
407  if (not showEvenInSilentMode) {
408  // Don't show information dialogs in the silent mode
409  return;
410  }
411  }
412 
413  QMessageBox dlInformationMsgBox;
414  dlInformationMsgBox.setIcon(QMessageBox::Information);
415  dlInformationMsgBox.setText(tr("Information"));
416  dlInformationMsgBox.setInformativeText(message);
417  dlInformationMsgBox.exec();
418 }
419 
420 bool FvUpdater::showConfirmationDialog(const QString &message,
421  bool showEvenInSilentMode) {
423  if (not showEvenInSilentMode) {
424  // Don't show information dialogs in the silent mode
425  return false;
426  }
427  }
428 
429  QMessageBox dlInformationMsgBox;
430  dlInformationMsgBox.setIcon(QMessageBox::Information);
431  dlInformationMsgBox.setText(tr("Information"));
432  dlInformationMsgBox.setInformativeText(message);
433  dlInformationMsgBox.setStandardButtons(QMessageBox::Yes
434  | QMessageBox::No);
435 
436  return QMessageBox::Yes == dlInformationMsgBox.exec();
437 }
438 
439 
440 bool FvUpdater::releaseIsNewer(const QString &releaseTag) {
441  const auto releaseVersion = releaseTag.mid(1).split('.');
442  const auto currentVersion = QCoreApplication::applicationVersion().split('.');
443  for (int i = 0; i < releaseVersion.length(); i++) {
444  if (releaseVersion[i].toInt() > currentVersion[i].toInt())
445  return (m_releaseName = releaseTag), true;
446  }
447  return false;
448 }
449 
450 void FvUpdater::networkError(QNetworkReply::NetworkError) {
451  setProgress(100);
452  m_fileSize = 0;
453  showErrorDialog(m_reply->errorString(), false);
454 }
@ NoError
Definition: abstracttest.h:79
bool CheckForUpdatesNotSilent()
Definition: fvupdater.cpp:172
bool showConfirmationDialog(const QString &message, bool showEvenInSilentMode=false)
Definition: fvupdater.cpp:420
bool releaseIsNewer(const QString &releaseTag)
Definition: fvupdater.cpp:440
void cancelDownloadFeed()
Definition: fvupdater.cpp:305
void SetDropOnFinish(bool value)
Definition: fvupdater.cpp:109
bool CheckForUpdates(bool silentAsMuchAsItCouldGet=true)
Definition: fvupdater.cpp:112
void SetFeedURL(const QUrl &feedURL)
Definition: fvupdater.cpp:95
virtual ~FvUpdater()
Definition: fvupdater.cpp:90
QNetworkAccessManager m_qnam
Definition: fvupdater.h:104
bool m_dropOnFinish
Definition: fvupdater.h:107
bool m_httpRequestAborted
Definition: fvupdater.h:106
void httpFeedDownloadFinished()
Definition: fvupdater.cpp:313
QPointer< QNetworkReply > m_reply
Definition: fvupdater.h:105
QUrl m_feedURL
Definition: fvupdater.h:103
void setProgress(int)
static void drop()
Definition: fvupdater.cpp:74
void startDownloadFeed(const QUrl &url)
Definition: fvupdater.cpp:278
bool CheckForUpdatesSilent()
Definition: fvupdater.cpp:155
void getPLatformSpecificInstaller(QJsonArray assets)
Definition: fvupdater.cpp:357
void getFileSize()
Definition: fvupdater.cpp:180
int m_fileSize
Definition: fvupdater.h:108
QString GetFeedURL() const
Definition: fvupdater.cpp:103
static QPointer< FvUpdater > m_Instance
Definition: fvupdater.h:93
void fileDownloadFinished(QFile *downloadedFile, QString name)
Definition: fvupdater.cpp:237
QString m_releaseName
Definition: fvupdater.h:109
static FvUpdater * sharedUpdater()
Definition: fvupdater.cpp:62
void showInformationDialog(const QString &message, bool showEvenInSilentMode=false)
Definition: fvupdater.cpp:404
bool m_silentAsMuchAsItCouldGet
Definition: fvupdater.h:98
void showErrorDialog(const QString &message, bool showEvenInSilentMode=false)
Definition: fvupdater.cpp:387
bool IsDropOnFinish() const
Definition: fvupdater.cpp:106
void networkError(QNetworkReply::NetworkError)
Definition: fvupdater.cpp:450
void startDownloadFile(QUrl url, QString name)
Definition: fvupdater.cpp:187
const QString defaultFeedURL
Definition: fvupdater.cpp:57
#define qApp
Definition: vapplication.h:67