ScreenTranslator/src/service/updates.cpp
2020-04-20 20:46:29 +03:00

1175 lines
29 KiB
C++

#include "updates.h"
#include "debug.h"
#include <QAbstractItemView>
#include <QApplication>
#include <QComboBox>
#include <QDir>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QMenu>
#include <QNetworkReply>
#include <QScopeGuard>
#include <QSortFilterProxyModel>
#include <QStandardPaths>
#include <QTimer>
#include <QTreeView>
#include <random>
#define MINIZ_NO_ZLIB_APIS
#define MINIZ_NO_ZLIB_COMPATIBLE_NAMES
#define MINIZ_NO_MALLOC
#define MINIZ_NO_STDIO
#define MINIZ_NO_ARCHIVE_WRITING_APIS
#include <miniz/miniz.h>
static QByteArray unpack(const QByteArray &data)
{
if (data.size() <= 4 || data.left(2) != "PK") {
LTRACE() << "Incorrect data to unpack" << LARG(data.size())
<< data.left(10);
return {};
}
mz_zip_archive zip;
memset(&zip, 0, sizeof(zip));
if (!mz_zip_reader_init_mem(&zip, data.data(), data.size(), 0)) {
LTRACE() << "Failed to init zip reader for " << data.left(10)
<< mz_zip_get_error_string(zip.m_last_error);
return {};
}
const auto guard = qScopeGuard([&zip] { mz_zip_reader_end(&zip); });
const auto fileCount = mz_zip_reader_get_num_files(&zip);
if (fileCount < 1) {
LTRACE() << "No files in zip archive";
return {};
}
for (auto i = 0u; i < fileCount; ++i) {
mz_zip_archive_file_stat file_stat;
if (!mz_zip_reader_file_stat(&zip, i, &file_stat)) {
LTRACE() << "Failed to get file info" << LARG(i)
<< mz_zip_get_error_string(zip.m_last_error);
return {};
}
if (file_stat.m_is_directory)
continue;
QByteArray result(file_stat.m_uncomp_size, 0);
mz_zip_reader_extract_to_mem(&zip, 0, result.data(), result.size(), 0);
return result;
}
return {};
}
namespace update
{
namespace
{
const auto versionKey = "version";
const auto filesKey = "files";
QString sizeString(qint64 bytes, int precision)
{
if (bytes < 1)
return {};
const auto kb = 1024.0;
const auto mb = 1024 * kb;
const auto gb = 1024 * mb;
const auto tb = 1024 * gb;
if (bytes >= tb) {
return QString::number(bytes / tb, 'f', precision) + ' ' +
QApplication::translate("Updates", "Tb");
}
if (bytes >= gb) {
return QString::number(bytes / gb, 'f', precision) + ' ' +
QApplication::translate("Updates", "Gb");
}
if (bytes >= mb) {
return QString::number(bytes / mb, 'f', precision) + ' ' +
QApplication::translate("Updates", "Mb");
}
if (bytes >= kb) {
return QString::number(bytes / kb, 'f', precision) + ' ' +
QApplication::translate("Updates", "Kb");
}
return QString::number(bytes) + ' ' +
QApplication::translate("Updates", "bytes");
}
QString toString(State state)
{
const QMap<State, QString> names{
{State::NotAvailable, {}},
{State::NotInstalled,
QApplication::translate("Updates", "Not installed")},
{State::UpdateAvailable,
QApplication::translate("Updates", "Update available")},
{State::Actual, QApplication::translate("Updates", "Up to date")},
};
return names.value(state);
}
QString toString(Action action)
{
const QMap<Action, QString> names{
{Action::NoAction, {}},
{Action::Remove, QApplication::translate("Updates", "Remove")},
{Action::Install, QApplication::translate("Updates", "Install/Update")},
};
return names.value(action);
}
QStringList toList(const QJsonValue &value)
{
if (value.isString())
return {value.toString()};
if (!value.isArray())
return {};
const auto array = value.toArray();
QStringList result;
for (const auto &i : array) {
if (i.isString())
result.append(i.toString());
}
return result;
}
} // namespace
Loader::Loader(const update::Loader::Urls &updateUrls, QObject *parent)
: QObject(parent)
, network_(new QNetworkAccessManager(this))
, model_(new Model(this))
, updateUrls_(updateUrls)
, downloadPath_(
QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) +
"/updates")
{
std::random_device device;
std::mt19937 generator(device());
std::shuffle(updateUrls_.begin(), updateUrls_.end(), generator);
connect(network_, &QNetworkAccessManager::finished, //
this, &Loader::handleReply);
}
void Loader::handleReply(QNetworkReply *reply)
{
if (updateUrls_.contains(reply->url())) {
handleUpdateReply(reply);
} else {
handleComponentReply(reply);
}
}
void Loader::checkForUpdates()
{
startDownloadUpdates({});
}
void Loader::startDownloadUpdates(const QUrl &previous)
{
SOFT_ASSERT(!updateUrls_.isEmpty(), return );
QUrl url;
if (previous.isEmpty())
url = updateUrls_.first();
else {
const auto index = updateUrls_.indexOf(previous);
SOFT_ASSERT(index != -1, return );
if (index == updateUrls_.size() - 1)
return;
url = updateUrls_[index + 1];
}
if (url.isEmpty()) {
dumpErrors();
return;
}
auto reply = network_->get(QNetworkRequest(url));
if (reply->error() != QNetworkReply::NoError)
handleUpdateReply(reply);
}
void Loader::handleUpdateReply(QNetworkReply *reply)
{
reply->deleteLater();
const auto url = reply->url();
if (reply->error() != QNetworkReply::NoError) {
addError(toError(*reply));
startDownloadUpdates(url);
return;
}
const auto replyData = reply->readAll();
if (replyData.isEmpty()) {
addError(tr("Empty updates info from\n%1").arg(url.toString()));
startDownloadUpdates(url);
return;
}
const auto unpacked =
url.toString().endsWith(".zip") ? unpack(replyData) : replyData;
if (unpacked.isEmpty()) {
addError(tr("Empty updates info unpacked from\n%1").arg(url.toString()));
startDownloadUpdates(url);
return;
}
SOFT_ASSERT(model_, return );
const auto parseError = model_->parse(unpacked);
if (!parseError.isEmpty()) {
addError(tr("Failed to parse updates from\n%1 (%2)")
.arg(url.toString(), parseError));
startDownloadUpdates(url);
return;
}
errors_.clear();
if (model_->hasUpdates())
emit updatesAvailable();
}
QString Loader::toError(QNetworkReply &reply) const
{
return tr("Failed to download file\n%1. Error %2")
.arg(reply.url().toString(), reply.errorString());
}
void Loader::applyUserActions()
{
SOFT_ASSERT(model_, return );
if (!currentActions_.empty() || !downloads_.empty()) {
emit error(tr("Already updating"));
return;
}
currentActions_ = model_->userActions();
if (currentActions_.empty()) {
emit error(tr("No actions selected"));
return;
}
for (auto &action : currentActions_) {
if (action.first != Action::Install)
continue;
auto &file = action.second;
file.downloadPath = downloadPath_ + '/' + file.rawPath;
if (!startDownload(file)) {
finishUpdate();
return;
}
}
if (downloads_.empty()) // no downloads
commitUpdate();
}
bool Loader::startDownload(File &file)
{
if (file.urls.empty())
return false;
auto reply = network_->get(QNetworkRequest(file.urls.takeFirst()));
downloads_.emplace(reply, &file);
connect(reply, &QNetworkReply::downloadProgress, //
this, &Loader::updateProgress);
if (reply->error() == QNetworkReply::NoError)
return true;
return handleComponentReply(reply);
}
bool Loader::handleComponentReply(QNetworkReply *reply)
{
reply->deleteLater();
if (currentActions_.empty()) { // aborted?
finishUpdate();
return false;
}
SOFT_ASSERT(downloads_.count(reply) == 1, return false);
auto *file = downloads_[reply];
SOFT_ASSERT(file, return false);
downloads_.erase(reply);
if (reply->error() != QNetworkReply::NoError) {
addError(toError(*reply));
if (!startDownload(*file))
finishUpdate();
return false;
}
const auto &fileName = file->downloadPath;
auto dir = QFileInfo(fileName).absoluteDir();
if (!dir.exists() && !dir.mkpath(".")) {
finishUpdate(tr("Failed to create temp path\n%1").arg(dir.absolutePath()));
return false;
}
const auto url = reply->url();
const auto replyData = reply->readAll();
if (replyData.isEmpty()) {
addError(tr("Empty data downloaded from\n%1").arg(url.toString()));
if (!startDownload(*file))
finishUpdate();
return false;
}
const auto mustUnpack =
url.toString().endsWith(".zip") && !fileName.endsWith(".zip");
const auto unpacked = mustUnpack ? unpack(replyData) : replyData;
if (unpacked.isEmpty()) {
addError(tr("Empty data unpacked from\n%1").arg(url.toString()));
if (!startDownload(*file))
finishUpdate();
return false;
}
QFile f(fileName);
if (!f.open(QFile::WriteOnly)) {
const auto error = tr("Failed to save downloaded file\n%1\nto %2\nError %3")
.arg(url.toString(), f.fileName(), f.errorString());
finishUpdate(error);
return false;
}
f.write(unpacked);
f.close();
if (downloads_.empty())
commitUpdate();
return true;
}
void Loader::finishUpdate(const QString &error)
{
currentActions_.clear();
for (const auto &i : downloads_) i.first->deleteLater();
downloads_.clear();
if (!error.isEmpty())
addError(error);
dumpErrors();
SOFT_ASSERT(model_, return );
model_->updateStates();
}
void Loader::commitUpdate()
{
SOFT_ASSERT(!currentActions_.empty(), return );
Installer installer(currentActions_);
if (installer.commit()) {
model_->resetProgress();
errors_.clear();
emit updated();
} else {
addError(tr("Update failed: %1").arg(installer.errorString()));
}
finishUpdate();
}
void Loader::updateProgress(qint64 bytesSent, qint64 bytesTotal)
{
if (bytesTotal < 1)
return;
const auto reply = qobject_cast<QNetworkReply *>(sender());
SOFT_ASSERT(reply, return );
const auto progress = int(100.0 * bytesSent / bytesTotal);
model_->updateProgress(reply->url(), progress);
}
Model *Loader::model() const
{
return model_;
}
void Loader::addError(const QString &text)
{
LTRACE() << text;
errors_.append(text);
}
void Loader::dumpErrors()
{
if (errors_.isEmpty())
return;
const auto summary = errors_.join('\n');
emit error(summary);
errors_.clear();
}
Model::Model(QObject *parent)
: QAbstractItemModel(parent)
{
}
void Model::initView(QTreeView *view)
{
view->setSelectionMode(QAbstractItemView::ExtendedSelection);
auto proxy = new QSortFilterProxyModel(view);
proxy->setSourceModel(this);
view->setModel(proxy);
view->setItemDelegate(new update::UpdateDelegate(view));
#ifndef DEVELOP
view->hideColumn(int(update::Model::Column::Files));
#endif
view->setContextMenuPolicy(Qt::CustomContextMenu);
connect(view, &QAbstractItemView::customContextMenuRequested, //
this, [this, view, proxy] {
QMenu menu;
using A = Action;
QMap<QAction *, Action> actions;
for (auto i : QVector<A>{A::Install, A::Remove, A::NoAction})
actions[menu.addAction(toString(i))] = i;
menu.addSeparator();
auto updateAll = menu.addAction(tr("Select all updates"));
auto reset = menu.addAction(tr("Reset actions"));
const auto menuItem = menu.exec(QCursor::pos());
if (!menuItem)
return;
if (menuItem == updateAll) {
selectAllUpdates();
return;
}
if (menuItem == reset) {
resetActions();
return;
}
const auto selection = view->selectionModel();
SOFT_ASSERT(selection, return );
const auto indexes = selection->selectedRows(int(Column::Action));
if (indexes.isEmpty())
return;
const auto action = actions[menuItem];
for (const auto &proxyIndex : indexes) {
auto modelIndex = proxy->mapToSource(proxyIndex);
if (!modelIndex.isValid() || rowCount(modelIndex) > 0)
continue;
setData(modelIndex, int(action), Qt::EditRole);
}
});
}
QString Model::parse(const QByteArray &data)
{
QJsonParseError error;
const auto doc = QJsonDocument::fromJson(data, &error);
if (doc.isNull()) {
return tr("Failed to parse: %1 at %2")
.arg(error.errorString())
.arg(error.offset);
}
const auto json = doc.object();
const auto version = json[versionKey].toInt();
if (version != 1) {
return tr("Wrong updates version: %1").arg(version);
}
beginResetModel();
root_ = parse(json);
if (root_)
updateState(*root_);
endResetModel();
if (!root_)
return tr("No data parsed");
return {};
}
std::unique_ptr<Model::Component> Model::parse(const QJsonObject &json) const
{
auto result = std::make_unique<Component>();
if (json[filesKey].isArray()) { // concrete component
const auto host = json["host"].toString().toLower();
if (!host.isEmpty()) {
#if defined(Q_OS_LINUX) && defined(Q_PROCESSOR_X86_64)
if (host != "linux")
return {};
#elif defined(Q_OS_WINDOWS) && defined(Q_PROCESSOR_X86_64)
if (host != "win64")
return {};
#elif defined(Q_OS_WINDOWS) && defined(Q_PROCESSOR_X86)
if (host != "win32")
return {};
#elif defined(Q_OS_MACOS)
if (host != "macos")
return {};
#else
return {};
#endif
}
result->version = json["version"].toString();
const auto files = json[filesKey].toArray();
result->files.reserve(files.size());
for (const auto &fileInfo : files) {
const auto object = fileInfo.toObject();
File file;
for (const auto &s : toList(object["url"])) {
const auto url = QUrl(s);
if (url.isValid())
file.urls.append(url);
}
if (file.urls.isEmpty()) {
result->checkOnly = true;
} else if (file.urls.size() > 1) {
std::random_device device;
std::mt19937 generator(device());
std::shuffle(file.urls.begin(), file.urls.end(), generator);
}
file.rawPath = object["path"].toString();
file.md5 = object["md5"].toString();
file.versionDate =
QDateTime::fromString(object["date"].toString(), Qt::ISODate);
const auto size = object["size"].toInt();
result->size += size;
result->files.push_back(file);
}
return result;
}
result->children.reserve(json.size());
auto index = -1;
for (const auto &name : json.keys()) {
if (name == versionKey)
continue;
auto child = parse(json[name].toObject());
if (!child)
continue;
child->name = name;
child->index = ++index;
child->parent = result.get();
result->children.push_back(std::move(child));
}
return result;
}
void Model::updateProgress(Model::Component &component, const QUrl &url,
int progress)
{
if (!component.files.empty()) {
for (auto &file : component.files) {
if (!url.isEmpty() && !file.urls.contains(url))
continue;
file.progress = progress;
component.progress = progress;
for (const auto &file : component.files)
component.progress = std::max(file.progress, component.progress);
const auto index = toIndex(component, int(Column::Progress));
emit dataChanged(index, index, {Qt::DisplayRole});
if (!url.isEmpty())
break;
}
return;
}
if (!component.children.empty()) {
for (auto &child : component.children)
updateProgress(*child, url, progress);
return;
}
}
void Model::setExpansions(const std::map<QString, QString> &expansions)
{
expansions_ = expansions;
updateStates();
}
UserActions Model::userActions() const
{
if (!root_)
return {};
UserActions actions;
const auto visitor = [&actions](const Component &component, auto v) -> void {
if (!component.files.empty()) {
if (component.action == Action::NoAction)
return;
for (auto &file : component.files)
actions.emplace(component.action, file);
return;
}
if (!component.children.empty()) {
for (auto &child : component.children) v(*child, v);
return;
}
};
visitor(*root_, visitor);
return actions;
}
void Model::updateStates()
{
if (!root_)
return;
updateState(*root_);
const auto visitor = [this](const QModelIndex &parent, auto v) -> void {
const auto count = rowCount(parent);
if (count == 0)
return;
emit dataChanged(index(0, int(Column::State), parent),
index(count - 1, int(Column::Action), parent),
{Qt::DisplayRole, Qt::EditRole});
for (auto i = 0; i < count; ++i) v(index(0, 0, parent), v);
};
visitor(QModelIndex(), visitor);
}
bool Model::hasUpdates() const
{
if (!root_)
return false;
const auto visitor = [](const Component &component, auto v) -> bool {
for (const auto &i : component.children) {
if (i->state == State::UpdateAvailable || v(*i, v))
return true;
}
return false;
};
return visitor(*root_, visitor);
}
void Model::updateProgress(const QUrl &url, int progress)
{
if (!root_)
return;
updateProgress(*root_, url, progress);
}
void Model::resetProgress()
{
if (!root_)
return;
updateProgress(*root_, {}, 0);
}
void Model::selectAllUpdates()
{
if (!root_)
return;
const auto visitor = [this](Component &component, auto v) -> void {
if (component.state == State::UpdateAvailable) {
component.action = Action::Install;
const auto index = toIndex(component, int(Column::Action));
emit dataChanged(index, index, {Qt::DisplayRole, Qt::EditRole});
}
if (!component.children.empty()) {
for (auto &child : component.children) v(*child, v);
}
};
visitor(*root_, visitor);
}
void Model::resetActions()
{
if (!root_)
return;
const auto visitor = [this](Component &component, auto v) -> void {
if (component.action != Action::NoAction) {
component.action = Action::NoAction;
const auto index = toIndex(component, int(Column::Action));
emit dataChanged(index, index, {Qt::DisplayRole, Qt::EditRole});
}
if (!component.children.empty()) {
for (auto &child : component.children) v(*child, v);
}
};
visitor(*root_, visitor);
}
void Model::updateState(Model::Component &component)
{
if (!component.files.empty()) {
std::vector<State> states;
states.reserve(component.files.size());
for (auto &file : component.files) {
file.expandedPath = expanded(file.rawPath);
states.push_back(currentState(file));
}
component.state = *std::min_element(states.cbegin(), states.cend());
component.action = Action::NoAction;
return;
}
if (!component.children.empty()) {
for (auto &child : component.children) updateState(*child);
return;
}
}
State Model::currentState(const File &file) const
{
if (file.expandedPath.isEmpty() ||
(file.md5.isEmpty() && !file.versionDate.isValid()))
return State::NotAvailable;
if (!QFile::exists(file.expandedPath))
return State::NotInstalled;
if (file.versionDate.isValid()) {
QFileInfo info(file.expandedPath);
const auto date = info.fileTime(QFile::FileModificationTime);
if (!date.isValid())
return State::NotInstalled;
return date >= file.versionDate ? State::Actual : State::UpdateAvailable;
}
QFile f(file.expandedPath);
if (!f.open(QFile::ReadOnly))
return State::NotInstalled;
const auto data = f.readAll();
const auto md5 =
QCryptographicHash::hash(data, QCryptographicHash::Md5).toHex();
if (md5 != file.md5)
return State::UpdateAvailable;
return State::Actual;
}
QString Model::expanded(const QString &source) const
{
auto result = source;
for (const auto &expansion : expansions_) {
if (!result.contains(expansion.first))
continue;
result.replace(expansion.first, expansion.second);
}
return result;
}
Model::Component *Model::toComponent(const QModelIndex &index) const
{
return static_cast<Component *>(index.internalPointer());
}
QModelIndex Model::toIndex(const Model::Component &component, int column) const
{
return createIndex(component.index, column,
const_cast<Model::Component *>(&component));
}
QModelIndex Model::index(int row, int column, const QModelIndex &parent) const
{
if (!root_)
return {};
if (auto ptr = toComponent(parent)) {
SOFT_ASSERT(row >= 0 && row < int(ptr->children.size()), return {});
return toIndex(*ptr->children[row], column);
}
if (row < 0 && row >= int(root_->children.size()))
return {};
return toIndex(*root_->children[row], column);
}
QModelIndex Model::parent(const QModelIndex &child) const
{
auto ptr = toComponent(child);
if (auto parent = ptr->parent)
return createIndex(parent->index, 0, parent);
return {};
}
int Model::rowCount(const QModelIndex &parent) const
{
if (auto ptr = toComponent(parent)) {
return int(ptr->children.size());
}
return root_ ? int(root_->children.size()) : 0;
}
int Model::columnCount(const QModelIndex & /*parent*/) const
{
return int(Column::Count);
}
QVariant Model::headerData(int section, Qt::Orientation orientation,
int role) const
{
if (role != Qt::DisplayRole)
return {};
if (orientation == Qt::Vertical)
return section + 1;
const QMap<Column, QString> names{
{Column::Name, tr("Name")}, {Column::State, tr("State")},
{Column::Action, tr("Action")}, {Column::Size, tr("Size")},
{Column::Version, tr("Version")}, {Column::Progress, tr("Progress")},
{Column::Files, tr("Files")},
};
return names.value(Column(section));
}
QVariant Model::data(const QModelIndex &index, int role) const
{
if ((role != Qt::DisplayRole && role != Qt::EditRole) || !index.isValid())
return {};
auto ptr = toComponent(index);
SOFT_ASSERT(ptr, return {});
switch (index.column()) {
case int(Column::Name): return QObject::tr(qPrintable(ptr->name));
case int(Column::State): return toString(ptr->state);
case int(Column::Action): return toString(ptr->action);
case int(Column::Size): return sizeString(ptr->size, 1);
case int(Column::Version): return ptr->version;
case int(Column::Progress):
return ptr->progress > 0 ? ptr->progress : QVariant();
case int(Column::Files): {
QStringList files;
files.reserve(int(ptr->files.size()));
for (const auto &f : ptr->files) files.append(f.expandedPath);
return files.join(',');
}
}
return {};
}
bool Model::setData(const QModelIndex &index, const QVariant &value, int role)
{
if (!index.isValid() || role != Qt::EditRole)
return false;
auto ptr = toComponent(index);
SOFT_ASSERT(ptr, return false);
if (index.column() != int(Column::Action))
return false;
const auto newAction = Action(
std::clamp(value.toInt(), int(Action::NoAction), int(Action::Install)));
if (ptr->action == newAction)
return false;
if (newAction != Action::NoAction) {
const QMap<State, QVector<Action>> supported{
{State::NotAvailable, {}},
{State::Actual, {Action::Remove}},
{State::NotInstalled, {Action::Install}},
{State::UpdateAvailable, {Action::Remove, Action::Install}},
};
if (!supported[ptr->state].contains(newAction))
return false;
}
ptr->action = newAction;
emit dataChanged(index, index, {Qt::DisplayRole, Qt::EditRole});
return true;
}
Qt::ItemFlags Model::flags(const QModelIndex &index) const
{
auto ptr = toComponent(index);
SOFT_ASSERT(ptr, return {});
auto result = Qt::NoItemFlags | Qt::ItemIsSelectable;
if (ptr->checkOnly)
return result;
result |= Qt::ItemIsEnabled;
return result;
}
UpdateDelegate::UpdateDelegate(QObject *parent)
: QStyledItemDelegate(parent)
{
}
void UpdateDelegate::paint(QPainter *painter,
const QStyleOptionViewItem &option,
const QModelIndex &index) const
{
if (index.column() == int(Model::Column::Progress) &&
!index.data().isNull()) {
QStyleOptionProgressBar progressBarOption;
progressBarOption.rect = option.rect;
progressBarOption.minimum = 0;
progressBarOption.maximum = 100;
const auto progress = index.data().toInt();
progressBarOption.progress = progress;
progressBarOption.text = QString::number(progress) + "%";
progressBarOption.textVisible = true;
QApplication::style()->drawControl(QStyle::CE_ProgressBar,
&progressBarOption, painter);
return;
}
QStyledItemDelegate::paint(painter, option, index);
}
Installer::Installer(const UserActions &actions)
: actions_(actions)
{
}
bool Installer::commit()
{
if (!checkIsPossible())
return false;
for (const auto &action : actions_) {
const auto &file = action.second;
if (action.first == Action::Remove)
remove(file);
else if (action.first == Action::Install)
install(file);
}
return errors_.isEmpty();
}
bool Installer::checkIsPossible()
{
errors_.clear();
for (const auto &action : actions_) {
const auto &file = action.second;
if (action.first == Action::Remove)
checkRemove(file);
else if (action.first == Action::Install)
checkInstall(file);
}
errors_.removeDuplicates();
return errors_.isEmpty();
}
void Installer::checkRemove(const File &file)
{
QFileInfo installDir(QFileInfo(file.expandedPath).absolutePath());
if (!QFile::exists(file.expandedPath))
return;
if (installDir.exists() && !installDir.isWritable()) {
errors_.append(
QApplication::translate("Updates", "Directory is not writable\n%1")
.arg(installDir.absolutePath()));
}
}
void Installer::checkInstall(const File &file)
{
if (!QFileInfo::exists(file.downloadPath)) {
errors_.append(
QApplication::translate("Updates", "Downloaded file not exists\n%1")
.arg(file.downloadPath));
// no return
}
QFileInfo installDir(QFileInfo(file.expandedPath).absolutePath());
if (installDir.exists() && !installDir.isWritable()) {
errors_.append(
QApplication::translate("Updates", "Directory is not writable\n%1")
.arg(installDir.absolutePath()));
}
}
void Installer::remove(const File &file)
{
QFile f(file.expandedPath);
if (!f.exists())
return;
if (!f.remove()) {
errors_.append(QApplication::translate(
"Updates", "Failed to remove file\n%1\nError %2")
.arg(f.fileName(), f.errorString()));
}
}
void Installer::install(const File &file)
{
auto installDir = QFileInfo(file.expandedPath).absoluteDir();
if (!installDir.exists() && !installDir.mkpath(".")) {
errors_.append(
QApplication::translate("Updates", "Failed to create path\n%1")
.arg(installDir.absolutePath()));
return;
}
QFile existing(file.expandedPath);
if (existing.exists() && !existing.remove()) {
errors_.append(QApplication::translate(
"Updates", "Failed to remove file\n%1\nError %2")
.arg(existing.fileName(), existing.errorString()));
return;
}
QFile f(file.downloadPath);
if (!f.rename(file.expandedPath)) {
errors_.append(QApplication::translate(
"Updates", "Failed to move file\n%1\nto %2\nError %3")
.arg(f.fileName(), file.expandedPath, f.errorString()));
return;
}
if (!file.versionDate.isValid())
return;
if (!f.open(QFile::WriteOnly | QFile::Append) ||
!f.setFileTime(file.versionDate, QFile::FileTime::FileModificationTime)) {
errors_.append(QApplication::translate("Updates",
"Failed to set modification time of "
"file\n%1\nto %2. Error %3")
.arg(f.fileName(),
file.versionDate.toString(Qt::ISODate),
f.errorString()));
}
}
QString Installer::errorString() const
{
return errors_.join('\n');
}
AutoChecker::AutoChecker(Loader &loader, QObject *parent)
: QObject(parent)
, loader_(loader)
{
SOFT_ASSERT(loader.model(), return );
connect(loader.model(), &Model::modelReset, //
this, &AutoChecker::handleModelReset);
}
AutoChecker::~AutoChecker() = default;
bool AutoChecker::isLastCheckDateChanged() const
{
return isLastCheckDateChanged_;
}
QDateTime AutoChecker::lastCheckDate() const
{
return lastCheckDate_;
}
void AutoChecker::setCheckIntervalDays(int days)
{
checkIntervalDays_ = days;
scheduleNextCheck();
}
void AutoChecker::setLastCheckDate(const QDateTime &dt)
{
isLastCheckDateChanged_ = false;
lastCheckDate_ = dt;
if (!lastCheckDate_.isValid())
lastCheckDate_ = QDateTime::currentDateTime();
scheduleNextCheck();
}
void AutoChecker::scheduleNextCheck()
{
if (timer_)
timer_->stop();
if (checkIntervalDays_ < 1 || !lastCheckDate_.isValid())
return;
if (!timer_) {
timer_ = std::make_unique<QTimer>();
timer_->setSingleShot(true);
connect(timer_.get(), &QTimer::timeout, //
&loader_, &Loader::checkForUpdates);
}
auto nextTime = lastCheckDate_.addDays(checkIntervalDays_);
const auto now = QDateTime::currentDateTime();
if (nextTime < now)
nextTime = now.addSecs(5);
timer_->start(now.msecsTo(nextTime));
}
void AutoChecker::handleModelReset()
{
lastCheckDate_ = QDateTime::currentDateTime();
isLastCheckDateChanged_ = true;
scheduleNextCheck();
}
} // namespace update