From 9892a9f29bd5e2aa11cc10607e8c7b614b4dc596 Mon Sep 17 00:00:00 2001 From: Gres Date: Sun, 18 Oct 2015 12:19:20 +0300 Subject: [PATCH] Added Updater class. --- Recources.qrc | 1 + ScreenTranslator.pro | 7 +- Updater.cpp | 263 +++++++++++++++++++++++++++++++++++++++++++ Updater.h | 65 +++++++++++ version.json | 30 +++++ 5 files changed, 364 insertions(+), 2 deletions(-) create mode 100644 Updater.cpp create mode 100644 Updater.h create mode 100644 version.json diff --git a/Recources.qrc b/Recources.qrc index 67a7a5f..e636270 100644 --- a/Recources.qrc +++ b/Recources.qrc @@ -6,5 +6,6 @@ images/STIconGreen.png images/STIconOrange.png images/STIconRed.png + version.json diff --git a/ScreenTranslator.pro b/ScreenTranslator.pro index 1376116..09445db 100644 --- a/ScreenTranslator.pro +++ b/ScreenTranslator.pro @@ -41,7 +41,8 @@ SOURCES += main.cpp\ WebTranslatorProxy.cpp \ TranslatorHelper.cpp \ RecognizerHelper.cpp \ - Utils.cpp + Utils.cpp \ + Updater.cpp HEADERS += \ Manager.h \ @@ -59,7 +60,8 @@ HEADERS += \ StAssert.h \ TranslatorHelper.h \ RecognizerHelper.h \ - Utils.h + Utils.h \ + Updater.h FORMS += \ SettingsEditor.ui \ @@ -76,6 +78,7 @@ TRANSLATIONS += \ OTHER_FILES += \ app.rc \ images/icon.ico \ + version.json \ README.md \ uncrustify.cfg\ translators/google.js \ diff --git a/Updater.cpp b/Updater.cpp new file mode 100644 index 0000000..ff74312 --- /dev/null +++ b/Updater.cpp @@ -0,0 +1,263 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "Updater.h" +#include "StAssert.h" + +namespace { +#define FIELD(NAME) const QString _ ## NAME = #NAME + FIELD (Application); + + FIELD (name); + FIELD (version); + FIELD (compatibleVersion); + FIELD (built_in); + FIELD (versionString); + FIELD (permissions); + FIELD (url); + FIELD (path); +#undef FIELD + +#if defined(Q_OS_WIN) + const QString _platform = "_win"; +#elif defined(Q_OS_LINUX) + const QString _platform = "_linux"; +#endif + + QString versionField (const QJsonObject &component, const QString &field) { + return component.contains (field + _platform) + ? component[field + _platform].toString () + : component[field].toString (); + } + + QFileInfo fileDir (const QString &fileName) { + return QFileInfo (fileName).absolutePath (); + } +} + +Updater::Updater (QObject *parent) + : QObject (parent), + network_ (new QNetworkAccessManager (this)), + componentsUpdating_ (0) { + updatesFileName_ = "updates.json"; + backupSuffix_ = "_backup"; + connect (network_, SIGNAL (finished (QNetworkReply *)), + SLOT (replyFinished (QNetworkReply *))); + + getCurrentVersion (); + updateCurrentVersion (); +} + +QDateTime Updater::nextCheckTime (const QDateTime &lastCheckTime, int updateType) const { + QDateTime nextTime; + switch (updateType) { + case UpdateTypeDaily: + nextTime = lastCheckTime.addDays (1); + break; + case UpdateTypeWeekly: + nextTime = lastCheckTime.addDays (7); + break; + case UpdateTypeMonthly: + nextTime = lastCheckTime.addDays (30); + break; + case UpdateTypeNever: + default: + return QDateTime (); + } + if (nextTime < QDateTime::currentDateTime ()) { + return QDateTime::currentDateTime ().addSecs (5); + } + return nextTime; +} + +void Updater::getCurrentVersion () { + QFile f (":/version.json"); + if (f.open (QFile::ReadOnly)) { + currentVersion_ = QJsonDocument::fromJson (f.readAll ()).object (); + f.close (); + } + else { + emit error (tr ("Ошибка определения текущей версии. Обновление недоступно.")); + } +} + +void Updater::updateCurrentVersion () { + QFile f (updatesFileName_); + if (!f.open (QFile::ReadOnly)) { + return; + } + QJsonObject updated = QJsonDocument::fromJson (f.readAll ()).object (); + f.close (); + foreach (const QString &component, updated.keys ()) { + QJsonObject current = currentVersion_[component].toObject (); + int updatedVersion = updated[component].toInt (); + if (current[_built_in].toBool () || current[_version].toInt () >= updatedVersion) { + continue; + } + current[_version] = updatedVersion; + currentVersion_[component] = current; + } +} + +QString Updater::currentAppVersion () const { + return currentVersion_[_Application].toObject ()[_versionString].toString (); +} + +void Updater::checkForUpdates () { + getAvailableVersion (); +} + +void Updater::getAvailableVersion () { + QNetworkRequest request (versionField (currentVersion_, _url)); + request.setAttribute (QNetworkRequest::User, _version); + network_->get (request); +} + +void Updater::replyFinished (QNetworkReply *reply) { + if (reply->error () != QNetworkReply::NoError) { + emit tr ("Ошибка загрузки информации для обновления."); + return; + } + QByteArray content = reply->readAll (); + QString component = reply->request ().attribute (QNetworkRequest::User).toString (); + if (component == _version) { + availableVersion_ = QJsonDocument::fromJson (content).object (); + parseAvailableVersion (); + } + else if (availableVersion_.contains (component) && !content.isEmpty ()) { + installComponent (component, content); + } + reply->deleteLater (); +} + +void Updater::parseAvailableVersion () { + QStringList inaccessible, incompatible; + QStringList updateList; + QDir currentDir; + foreach (const QString &component, availableVersion_.keys ()) { + QJsonObject available = availableVersion_[component].toObject (); + QJsonObject current = currentVersion_[component].toObject (); + QString path = versionField (available, _path); + if (path.isEmpty ()) { + continue; + } + + QFileInfo installDir = fileDir (path); + if (!installDir.exists ()) { + currentDir.mkpath (installDir.absoluteFilePath ()); + } + if (!installDir.isWritable ()) { // check dir because install = remove + make new + inaccessible << installDir.absoluteFilePath (); + } + if (current[_version].toInt () < available[_compatibleVersion].toInt ()) { + incompatible << component; + } + if (!QFile::exists (path) || current[_version].toInt () < available[_version].toInt ()) { + updateList << component; + } + } + if (updateList.isEmpty ()) { + return; + } + + QFileInfo updateFileDir = fileDir (updatesFileName_); + if (!updateFileDir.isWritable ()) { + inaccessible << updateFileDir.absoluteFilePath (); + } + inaccessible.removeDuplicates (); + + QString message = tr ("Доступно обновлений: %1.\n").arg (updateList.size ()); + QMessageBox::StandardButtons buttons = QMessageBox::Ok; + if (!incompatible.isEmpty ()) { + message += tr ("Несовместимых обновлений: %1.\nВыполните обновление вручную.") + .arg (incompatible.size ()); + } + else if (!inaccessible.isEmpty ()) { + message += tr ("Недоступных для записи директорий: %1.\n%2\nИзмените права доступа и " + "повторите попытку или выполните обновление вручную.") + .arg (inaccessible.size ()).arg (inaccessible.join ("\n")); + } + else { + message += tr ("Обновить?"); + buttons = QMessageBox::Yes | QMessageBox::No; + } + int result = QMessageBox::question (NULL, tr ("Обновление"), message, buttons); + if (result == QMessageBox::Yes) { + componentsUpdating_ = updateList.size (); + foreach (const QString &component, updateList) { + getComponent (component); + } + } +} + +void Updater::getComponent (const QString &component) { + QJsonObject available = availableVersion_[component].toObject (); + QString path = versionField (available, _path); + if (path.isEmpty ()) { + --componentsUpdating_; + return; + } + + QString url = versionField (available, _url); + if (url.isEmpty ()) { // just remove component + installComponent (component, QByteArray ()); + } + else { + QNetworkRequest request (url); + request.setAttribute (QNetworkRequest::User, component); + network_->get (request); + } +} + +void Updater::installComponent (const QString &component, const QByteArray &newContent) { + --componentsUpdating_; + ST_ASSERT (availableVersion_.contains (component)); + QJsonObject available = availableVersion_[component].toObject (); + QString path = versionField (available, _path); + ST_ASSERT (!path.isEmpty ()); + + QString backup = path + backupSuffix_; + QFile::remove (backup); + QFile::rename (path, backup); + + if (!newContent.isEmpty ()) { + QFile f (path); + if (!f.open (QFile::WriteOnly)) { + emit error (tr ("Ошибка обновления файла (%1).").arg (path)); + return; + } + f.write (newContent); + f.close (); + bool ok; + QFileDevice::Permissions perm (available[_permissions].toString ().toUInt (&ok, 16)); + if (ok) { + f.setPermissions (perm); + } + } + updateVersionInfo (component, available[_version].toInt ()); + + if (componentsUpdating_ == 0) { + emit updated (); + QString message = tr ("Обновление завершено. Для активации некоторых компонентов " + "может потребоваться перезапуск."); + QMessageBox::information (NULL, tr ("Обновление"), message, QMessageBox::Ok); + } +} + +void Updater::updateVersionInfo (const QString &component, int version) { + QFile f (updatesFileName_); + if (!f.open (QFile::ReadWrite)) { + emit error (tr ("Ошибка обновления файла с текущей версией.")); + return; + } + QJsonObject updated = QJsonDocument::fromJson (f.readAll ()).object (); + updated[component] = version; + f.seek (0); + f.write (QJsonDocument (updated).toJson ()); + f.close (); +} diff --git a/Updater.h b/Updater.h new file mode 100644 index 0000000..8b5d17a --- /dev/null +++ b/Updater.h @@ -0,0 +1,65 @@ +#ifndef UPDATER_H +#define UPDATER_H + +#include +#include +#include + +/*! + * \brief The Updater class. + * + * Allows to download and copy files from remote source to local machine. + */ +class Updater : public QObject { + Q_OBJECT + + public: + enum UpdateType { + UpdateTypeNever, UpdateTypeDaily, UpdateTypeWeekly, UpdateTypeMonthly + }; + + explicit Updater (QObject *parent = 0); + + QString currentAppVersion () const; + + //! Initiate updates check. + void checkForUpdates (); + + //! Get nearest update check time based on given settings. + QDateTime nextCheckTime (const QDateTime &lastCheckTime, int updateType) const; + + signals: + void error (const QString &message); + //! Emited after all components updated. + void updated (); + + private slots: + //! Handle remote downloads finish. + void replyFinished (QNetworkReply *reply); + + private: + //! Load current version info (built-in). + void getCurrentVersion (); + //! Update current version info with information about preformed updates. + void updateCurrentVersion (); + //! Load latest available version info from remote source. + void getAvailableVersion (); + //! Check is updates available, prompt user and start update. + void parseAvailableVersion (); + //! Start update of given component. + void getComponent (const QString &component); + //! Finalize update of given component with given new content. + void installComponent (const QString &component, const QByteArray &newContent); + //! Save information about component update on disk (for updateCurrentVersion()). + void updateVersionInfo (const QString &component, int version); + + private: + QNetworkAccessManager *network_; + QJsonObject availableVersion_; + QJsonObject currentVersion_; + QString updatesFileName_; + int componentsUpdating_; + QString backupSuffix_; +}; + +#endif // UPDATER_H diff --git a/version.json b/version.json new file mode 100644 index 0000000..8d78aa1 --- /dev/null +++ b/version.json @@ -0,0 +1,30 @@ +{ + "version": 1, + "url": "https://cdn.rawgit.com/OneMoreGres/ScreenTranslator/master/version.json", + "Application": { + "version": 1, + "compatibleVersion": 1, + "built_in": true, + "versionString": "2.0.0", + "permissions": "0x7755", + "url_win": "https://raw.githubusercontent.com/OneMoreGres/ScreenTranslator/releases/2.0.0/ScreenTranslator.exe", + "path_win": "ScreenTranslator.exe", + "url_linux": "https://raw.githubusercontent.com/OneMoreGres/ScreenTranslator/releases/2.0.0/ScreenTranslator", + "path_linux": "ScreenTranslator" + }, + "Bing translator": { + "version": 1, + "url": "https://raw.githubusercontent.com/OneMoreGres/ScreenTranslator/master/translators/bing.js", + "path": "translators/bing.js" + }, + "Google translator": { + "version": 1, + "url": "https://raw.githubusercontent.com/OneMoreGres/ScreenTranslator/master/translators/google.js", + "path": "translators/google.js" + }, + "Yandex translator": { + "version": 1, + "url": "https://raw.githubusercontent.com/OneMoreGres/ScreenTranslator/master/translators/yandex.js", + "path": "translators/yandex.js" + } +}