Browse Source

modify:优化

master
Tian 2 weeks ago
parent
commit
af1d3c37e8
  1. 1
      .gitignore
  2. 241
      Src/downloader.cpp
  3. 82
      Src/downloader.h
  4. 15
      Src/main.cpp
  5. 456
      Src/updaterdialog.cpp
  6. 113
      Src/updaterdialog.h

1
.gitignore

@ -7,6 +7,7 @@ users*
*Release
Makefile*
# *.rc
updater_resource.rc
ui_*.h
moc_*.cpp
*.o

241
Src/downloader.cpp

@ -1,9 +1,41 @@
#include "downloader.h"
#include "downloader.h"
#include <QCoreApplication>
#include <QDebug>
#include <QJsonDocument>
#include <QDateTime>
#include <QFileInfo>
#include <QRegularExpression>
namespace
{
/**
* @brief Content-Disposition
* @param headerValue filename
* @return
*
*
* 使
*/
QString fileNameFromContentDisposition(const QVariant &headerValue)
{
if(!headerValue.isValid())
{
return QString();
}
const QString contentDisposition =
QString::fromUtf8(QByteArray::fromPercentEncoding(headerValue.toByteArray()));
const QRegularExpression regExp(R"(filename\*?=(?:UTF-8''|")?([^";]+))",
QRegularExpression::CaseInsensitiveOption);
const QRegularExpressionMatch match = regExp.match(contentDisposition);
if(!match.hasMatch())
{
return QString();
}
return QFileInfo(match.captured(1).trimmed()).fileName();
}
}
Downloader::Downloader(QObject *parent)
: QObject{parent}
{
@ -19,89 +51,153 @@ void Downloader::setUrl(const QString &_url)
void Downloader::startDownload(const QString &_url)
{
if(m_bIsDownloading)
{
emit doShowInfo(tr("download task is already running"));
return;
m_bIsDownloading = true;
}
if(!_url.isEmpty())
m_pUrl.setUrl(_url);
{
m_pUrl = QUrl(_url);
}
if(!dir.exists())
if(!m_pUrl.isValid() || m_pUrl.isEmpty())
{
dir.mkpath(".");
emit doShowInfo(tr("download url is invalid"));
return;
}
/* Rename old downloads */
QString _path = dir.path() + "/" + m_fileName;
if(QFile::exists(_path))
if(m_fileName.trimmed().isEmpty())
{
QFile _file(_path);
_file.rename(_path + "_" + QDateTime::currentDateTime().toString("yyyy-MM-dd"));
const QString urlFileName = QFileInfo(m_pUrl.path()).fileName();
if(urlFileName.isEmpty())
{
emit doShowInfo(tr("download file name is empty"));
return;
}
m_fileName = urlFileName;
}
QFile::remove(dir.filePath(m_fileName));
QFile::remove(dir.filePath(m_fileName + ".part"));
m_bIsDownloading = true;
if(!dir.exists() && !dir.mkpath("."))
{
m_bIsDownloading = false;
emit doShowInfo(tr("create download directory failed"));
return;
}
const QString targetPath = targetFilePath();
const QString partPath = partialFilePath();
// 保留旧版本文件,便于更新失败时人工回滚;同一天多次下载时追加时间避免重名。
if(QFile::exists(targetPath))
{
QFile oldFile(targetPath);
const QString backupPath = targetPath + "_" + QDateTime::currentDateTime().toString("yyyy-MM-dd_hhmmss");
if(!oldFile.rename(backupPath))
{
emit doShowInfo(tr("backup old file failed: %1").arg(oldFile.errorString()));
}
}
QFile::remove(partPath);
file.setFileName(partPath);
if(!file.open(QIODevice::WriteOnly | QIODevice::Truncate))
{
m_bIsDownloading = false;
emit doShowInfo(tr("open download file failed: %1").arg(file.errorString()));
return;
}
QNetworkRequest request;
request.setUrl(m_pUrl);
m_pReply = m_pNetWorkAccessManager->get(request);
connect(m_pReply, &QNetworkReply::downloadProgress, this, [&](qint64 received, qint64 total)
connect(m_pReply, &QNetworkReply::readyRead, this, [this]()
{
emit doProgress(received, total);
if (total > 0)
if(!file.isOpen() || !m_pReply)
{
return;
}
const QByteArray chunk = m_pReply->readAll();
if(file.write(chunk) != chunk.size())
{
if(file.fileName() != (dir.filePath(m_fileName + ".part")))
file.setFileName(dir.filePath(m_fileName + ".part"));
if(!file.isOpen())
{
file.open(QIODevice::WriteOnly | QIODevice::Append);
}
file.write(m_pReply->readAll());
emit doShowInfo(tr("write download file failed: %1").arg(file.errorString()));
m_pReply->abort();
}
});
connect(m_pReply, &QNetworkReply::metaDataChanged, this, [&]()
connect(m_pReply, &QNetworkReply::downloadProgress, this, [this](qint64 received, qint64 total)
{
emit doProgress(received, total);
});
connect(m_pReply, &QNetworkReply::metaDataChanged, this, [this]()
{
QString filename = "";
QVariant variant = m_pReply->header( QNetworkRequest::ContentDispositionHeader );
if ( variant.isValid() )
const QString responseFileName =
fileNameFromContentDisposition(m_pReply->header(QNetworkRequest::ContentDispositionHeader));
if(!responseFileName.isEmpty() && m_fileName.isEmpty())
{
QString contentDisposition = QByteArray::fromPercentEncoding( variant.toByteArray() ).constData();
QRegularExpression regExp( "(.*)filename=\"(?<filename>.*)\"" );
QRegularExpressionMatch match = regExp.match( contentDisposition );
if ( match.hasMatch() )
{
filename = match.captured( "filename" );
}
m_fileName = filename;
auto localApplicationFilePath = QCoreApplication::applicationDirPath();
file.setFileName(localApplicationFilePath + "/" + m_fileName);
};
m_fileName = responseFileName;
}
});
// connect(m_pReply, SIGNAL(readyRead()), this, SLOT(onReadyRead()));
connect(m_pReply, &QNetworkReply::finished, this, [&]()
connect(m_pReply, &QNetworkReply::finished, this, [this]()
{
if(file.isOpen())
file.close();
if(!file.exists())
QNetworkReply *finishedReply = m_pReply;
const QNetworkReply::NetworkError errorCode = finishedReply->error();
// HTTP 状态码用于拦截 404/500 等服务器错误,避免把错误页面当作升级包落盘。
const int httpStatusCode =
finishedReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if(errorCode != QNetworkReply::NoError)
{
emit onError(errorCode);
emit doShowInfo(finishedReply->errorString());
cleanupCurrentReply(true);
return;
}
if(httpStatusCode >= 400)
{
emit doShowInfo("not exits");
emit doShowInfo(tr("http download failed, status code: %1").arg(httpStatusCode));
cleanupCurrentReply(true);
return;
}
if(file.isOpen())
{
emit doShowInfo("not close");
file.close();
}
const QString partPath = partialFilePath();
const QString targetPath = targetFilePath();
if(!QFile::exists(partPath))
{
emit doShowInfo(tr("download temporary file not exists"));
cleanupCurrentReply(true);
return;
}
if(!file.rename(dir.path() + "/" + m_fileName))
QFile::remove(targetPath);
if(!QFile::rename(partPath, targetPath))
{
emit doShowInfo("rename file failed");
emit doShowInfo(tr("rename file failed"));
cleanupCurrentReply(true);
return;
}
file.setFileName(dir.filePath(m_fileName));
file.setFileName(targetPath);
m_bIsDownloading = false;
finishedReply->deleteLater();
m_pReply = nullptr;
emit doFinished();
});
connect(m_pReply, SIGNAL(errorOccurred(QNetworkReply::NetworkError)), this, SIGNAL(onError(QNetworkReply::NetworkError)));
}
void Downloader::checkVersion()
{
QNetworkRequest request(CHECK_URL);
QNetworkRequest request{QUrl(CHECK_URL)};
m_pNetWorkAccessManager->get(request);
}
@ -122,5 +218,46 @@ const QString &Downloader::fileName() const
void Downloader::setFileName(const QString &newFileName)
{
m_fileName = newFileName;
m_fileName = QFileInfo(newFileName).fileName();
}
void Downloader::cleanupCurrentReply(bool removePartial)
{
if(file.isOpen())
{
file.close();
}
if(removePartial)
{
QFile::remove(partialFilePath());
}
if(m_pReply)
{
m_pReply->deleteLater();
m_pReply = nullptr;
}
m_bIsDownloading = false;
}
QString Downloader::partialFilePath() const
{
if(m_fileName.isEmpty())
{
return QString();
}
return dir.filePath(m_fileName + ".part");
}
QString Downloader::targetFilePath() const
{
if(m_fileName.isEmpty())
{
return QString();
}
return dir.filePath(m_fileName);
}

82
Src/downloader.h

@ -4,7 +4,6 @@
#include <QObject>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <memory>
#include <QUrl>
#include <QFile>
#include <QDir>
@ -15,43 +14,118 @@ class Downloader : public QObject
{
Q_OBJECT
public:
/**
* @brief
* @param parent Qt
*
* Downloader
* QFile 线线使
* 线 QFile QNetworkReply
*/
explicit Downloader(QObject *parent = nullptr);
/**
* @brief
* @param url URL
*/
void setUrl(const QString &);
void startDownload(const QString &_url = NULL);
/**
* @brief
* @param url 使 setUrl()
*
* doProgress/doFinished/onError/doShowInfo
* Downloader URL
* .part
*/
void startDownload(const QString &url = QString());
/**
* @brief
*
* QNetworkAccessManager::finished
*/
void checkVersion();
/**
* @brief
* @return QFile /
*/
const QFile &getFile() const;
/**
* @brief
* @return
*/
const QDir &getDir() const;
/**
* @brief
* @return
*/
const QString &fileName() const;
/**
* @brief
* @param newFileName
*/
void setFileName(const QString &newFileName);
public slots:
signals:
/** @brief 下载进度,received/total 单位为字节;total 为 -1 时表示服务器未给出总长度。 */
void doProgress(qint64, qint64);
/** @brief 网络错误通知;错误发生后 Downloader 会关闭文件并释放 reply。 */
void onError(QNetworkReply::NetworkError);
/** @brief 下载完成通知;仅在网络无错误且临时文件成功改名后发出。 */
void doFinished();
/** @brief 文本状态通知,用于把边界条件、文件错误等信息显示到界面。 */
void doShowInfo(const QString&);
private:
/**
* @brief QNetworkReply
* @param removePartial true .part /
*
* finished/error
*/
void cleanupCurrentReply(bool removePartial);
/**
* @brief
* @return .part
*/
QString partialFilePath() const;
/**
* @brief
* @return
*/
QString targetFilePath() const;
/** @brief 网络访问管理器,归 Downloader 父子对象树管理,必须在同一线程使用。 */
QNetworkAccessManager *m_pNetWorkAccessManager{nullptr};
/** @brief 当前下载响应对象,finished 后通过 deleteLater 释放,避免悬空回调。 */
QNetworkReply *m_pReply{nullptr};
bool m_bIsDownloading = false; //正在下载标识
/** @brief 正在下载标识,防止重复点击造成多个请求同时写同一个文件。 */
bool m_bIsDownloading = false;
/** @brief 当前下载 URL;startDownload 允许临时覆盖该地址。 */
QUrl m_pUrl{};
/** @brief 本地目标文件名,仅保存文件名不保存目录,避免路径注入。 */
QString m_fileName = "";
/** @brief 下载落盘文件句柄,下载中指向 .part 文件,完成后指向正式文件。 */
QFile file;
/** @brief 下载目录,默认应用程序目录;目录不存在时 startDownload 会创建。 */
QDir dir;
};

15
Src/main.cpp

@ -1,12 +1,20 @@
#include <QApplication>
#include <QApplication>
#include <QFile>
#include <QTextStream>
//#include <QDebug>
#include "updaterdialog.h"
/**
* @brief
*
*
* QFile 退 Qt
*/
void loadQss()
{
QFile f(":qdarkstyle/dark/darkstyle.qss");
// QFile f(":qdarkstyle/light/lightstyle.qss");
if (!f.exists())
if(!f.exists())
{
printf("Unable to set stylesheet, file not found\n");
}
@ -24,7 +32,8 @@ int main(int argc, char *argv[])
loadQss();
if(argc > 1)
// 参数由主程序启动更新器时传入,数量不足时进入手动输入模式,避免访问 argv 越界。
if(argc >= 5)
{
APPNAME = argv[1];
APPVERSION = argv[2];

456
Src/updaterdialog.cpp

@ -1,12 +1,15 @@
#include "updaterdialog.h"
#include "updaterdialog.h"
#include "ui_updaterdialog.h"
#include <QApplication>
#include <QDesktopServices>
#include <QFileInfo>
#include <QJsonDocument>
#include <QJsonObject>
#include <QMessageBox>
#include <QMetaEnum>
#include <cmath>
#include <QUrl>
#include <QtGlobal>
#include <private/qzipreader_p.h>
@ -18,16 +21,67 @@ QString APPNAME = "";
QString APPDATE = "0";
QString MODIFYCNT = "0";
namespace
{
/**
* @brief x.y.z
* @param left
* @param right
* @return left < right -1left == right 0left > right 1
*
* 0 0
*
*/
int compareVersionString(const QString &left, const QString &right)
{
const QStringList leftParts = left.trimmed().split(".", Qt::SkipEmptyParts);
const QStringList rightParts = right.trimmed().split(".", Qt::SkipEmptyParts);
const int partCount = qMax(leftParts.count(), rightParts.count());
for(int i = 0; i < partCount; ++i)
{
const uint leftValue = (i < leftParts.count()) ? leftParts.at(i).toUInt() : 0U;
const uint rightValue = (i < rightParts.count()) ? rightParts.at(i).toUInt() : 0U;
if(leftValue < rightValue)
{
return -1;
}
if(leftValue > rightValue)
{
return 1;
}
}
return 0;
}
/**
* @brief URL
* @param baseUrl
* @param appName
* @param fileName
* @return URL
*/
QString buildDownloadUrl(QString baseUrl, const QString &appName, const QString &fileName)
{
if(!baseUrl.endsWith('/'))
{
baseUrl.append('/');
}
return baseUrl + appName + "/" + fileName;
}
}
UpdaterDialog::UpdaterDialog(QWidget *parent) :
QDialog(parent),
ui(new Ui::UpdaterDialog)
{
ui->setupUi(this);
this->setWindowTitle("Updater");
ui->btn_update->setEnabled(0);
ui->btn_update_patch->setEnabled(0);
setUpdateButtonsEnabled(false);
m_startTime = 0;
m_totalSize = 0;
m_version = APPVERSION;
ui->versionInput->setText(m_version);
@ -36,45 +90,36 @@ UpdaterDialog::UpdaterDialog(QWidget *parent) :
ui->show->setEnabled(0);
ui->label_2->setText("");
ui->label_3->setText("");
// ui->label_4->setText(APPNAME);
// ui->versionInput->setEnabled(false);
// ui->show->appendPlainText(APPNAME);
// ui->show->appendPlainText(APPVERSION);
// ui->show->appendPlainText(APPDATE);
// ui->show->appendPlainText(MODIFYCNT);
m_pDownloader = new Downloader(this);
connect(m_pDownloader, &Downloader::doShowInfo, this, [&](const QString & s)
connect(m_pDownloader, &Downloader::doShowInfo, this, [this](const QString &s)
{
showStatus(s);
});
connect(m_pDownloader, &Downloader::onError, this, [&](QNetworkReply::NetworkError e)
connect(m_pDownloader, &Downloader::onError, this, [this](QNetworkReply::NetworkError e)
{
QMetaEnum metaEnum = QMetaEnum::fromType<QNetworkReply::NetworkError>();
QString str = QString(metaEnum.valueToKey(e));
const QMetaEnum metaEnum = QMetaEnum::fromType<QNetworkReply::NetworkError>();
const QString str = QString::fromLatin1(metaEnum.valueToKey(e));
ui->show->appendPlainText(str);
setUpdateButtonsEnabled(true);
});
connect(m_pDownloader, &Downloader::doFinished, this, [&]()
connect(m_pDownloader, &Downloader::doFinished, this, [this]()
{
uint _end_time = QDateTime::currentSecsSinceEpoch();
uint _total_time = _end_time - m_startTime;
ui->label_3->setText("total time: " + QString::number(_total_time, 'g', 3) +
"s average speed: " + QString::number((m_totalSize / 1024.0 / _total_time), 'g', 3) + "kb/s");
ui->show->appendPlainText("download finished");
onDownloadFinished();
});
connect(m_pDownloader, &Downloader::doProgress, this, [&](quint64 received, quint64 total)
connect(m_pDownloader, &Downloader::doProgress, this, [this](qint64 received, qint64 total)
{
m_totalSize = total;
if (total > 0)
m_totalSize = total > 0 ? static_cast<quint64>(total) : 0;
if(total > 0)
{
ui->progressBar->setMinimum(0);
ui->progressBar->setMaximum(100);
ui->progressBar->setValue((received * 100) / total);
ui->progressBar->setValue(static_cast<int>((received * 100) / total));
calculateSizes(received, total);
calculateTimeRemaining(received, total);
}
@ -83,319 +128,348 @@ UpdaterDialog::UpdaterDialog(QWidget *parent) :
ui->progressBar->setMinimum(0);
ui->progressBar->setMaximum(0);
ui->progressBar->setValue(-1);
ui->show->appendPlainText(tr("Downloading Updates") + "...");
ui->label_2->setText(tr("Downloading updates"));
}
});
}
UpdaterDialog::~UpdaterDialog()
{
delete m_UpdateProc;
delete ui;
}
void UpdaterDialog::showStatus(const QString &s)
{
// ui->show->clear();
ui->show->appendPlainText(s);
}
void UpdaterDialog::on_btn_check_clicked()
{
APPNAME = ui->appNameInput->text();
if(QString(APPNAME) == "")
APPNAME = ui->appNameInput->text().trimmed();
if(APPNAME.isEmpty())
{
ui->show->appendPlainText("Please input app name");
return;
}
ui->show->clear();
ui->btn_update->setEnabled(0);
ui->btn_update_patch->setEnabled(0);
//version
m_version = ui->versionInput->text();
setUpdateButtonsEnabled(false);
m_version = ui->versionInput->text().trimmed();
if(!m_pManager)
{
m_pManager = new QNetworkAccessManager(this);
connect(m_pManager, SIGNAL(finished(QNetworkReply *)), this, SLOT(onCheckReply(QNetworkReply *)));
}
QString _url = CHECK_URL.arg(APPNAME);
qDebug() << APPNAME << _url;
QNetworkRequest request(_url);
const QString url = CHECK_URL.arg(APPNAME);
QNetworkRequest request{QUrl(url)};
m_pManager->get(request);
}
bool UpdaterDialog::checkVersion(const QString &_localVersion, const QString &_remoteVersion)
{
//版本号分割,1.1.2分为1 1 2
QStringList _list_curr = _localVersion.split(".");
QStringList _list_lastest = _remoteVersion.split(".");
//检查是否需要更新
bool _isNotLatest = false;
//版本号为空
if(_list_curr.count() == 0)
// 本地版本为空或为 0 时视为首次安装/未知版本,优先下载完整包降低补丁失败风险。
if(_localVersion.trimmed().isEmpty() || _localVersion.trimmed() == "0")
{
m_bDownloadFullExe = true;
return true;
}
if(_list_curr.count() != _list_lastest.count())
_isNotLatest = true;
for(int i = 0; i < _list_curr.count(); ++i)
const int versionCompareResult = compareVersionString(_localVersion, _remoteVersion);
if(versionCompareResult > 0)
{
if(_list_curr[i].toUInt() < _list_lastest[i].toUInt())
{
_isNotLatest = true;
}
return false;
}
//版本号一致,检查日期
if(!_isNotLatest)
bool isNotLatest = versionCompareResult < 0;
if(!isNotLatest)
{
// 版本号一致时再比较构建日期和修改计数,兼容同版本热修复包。
if(m_lastChangeTime.toUInt() > APPDATE.toUInt())
{
_isNotLatest = true;
isNotLatest = true;
}
//检查修改次数
if(m_modifyCnt != "" && m_modifyCnt != " " && MODIFYCNT != "" && MODIFYCNT != " ")
if(!m_modifyCnt.trimmed().isEmpty() && !MODIFYCNT.trimmed().isEmpty())
{
if(m_modifyCnt.toUInt() > MODIFYCNT.toUInt())
{
_isNotLatest = true;
isNotLatest = true;
}
}
}
return _isNotLatest;
return isNotLatest;
}
void UpdaterDialog::onCheckReply(QNetworkReply *reply)
{
/* There was a network error */
if (reply->error() != QNetworkReply::NoError)
reply->deleteLater();
if(reply->error() != QNetworkReply::NoError)
{
showStatus("check error");
showStatus("check error: " + reply->errorString());
return;
}
/* Try to create a JSON document from downloaded data */
QJsonDocument document = QJsonDocument::fromJson(reply->readAll());
/* JSON is invalid */
if (document.isNull())
const QJsonDocument document = QJsonDocument::fromJson(reply->readAll());
if(document.isNull() || !document.isObject())
{
showStatus("reply doc is null");
return;
}
/* Get the platform information */
QJsonObject updates = document.object().value("updates").toObject();
QJsonObject platform = updates.value(PLATFORM).toObject();
const QJsonObject updates = document.object().value("updates").toObject();
const QJsonObject platform = updates.value(PLATFORM).toObject();
if(platform.isEmpty())
{
showStatus(tr("no update information for platform: %1").arg(PLATFORM));
return;
}
/* Get update information */
m_changelog = platform.value("changelog").toString();
m_patchVersion = platform.value("patch-version").toString();
m_fullVersion = platform.value("full-version").toString();
m_lastChangeTime = platform.value("modify-time").toString();
m_modifyCnt = platform.value("modifyCnt").toString();
m_bDownloadFullExe = platform.value("download-full").toBool();
if(m_version == "" || m_version == "0") //当前版本号为空
m_patchBaseUrl = platform.value("patch-url").toString();
m_fullBaseUrl = platform.value("full-url").toString();
if(m_version.isEmpty() || m_version == "0")
{
m_bDownloadFullExe = true;
}
m_fileName = APPNAME + "-patch-v" + m_patchVersion + ".zip";
m_patchUrl = platform.value("patch-url").toString() + APPNAME + "/" + APPNAME + "-patch-v" + m_patchVersion + ".zip";
m_fullUrl = platform.value("full-url").toString() + APPNAME + "/" + APPNAME + "-full-v" + m_fullVersion + ".exe";
QString _remoteVersion = m_bDownloadFullExe ? m_fullVersion : m_patchVersion;
bool _isNotLatest = checkVersion(m_version, _remoteVersion);
rebuildDownloadInfo();
const QString remoteVersion = m_bDownloadFullExe ? m_fullVersion : m_patchVersion;
const bool isNotLatest = checkVersion(m_version, remoteVersion);
if(_isNotLatest)
if(isNotLatest)
{
showStatus("======================================"
+ tr("\ncurrent version: v") + m_version
+ tr("\nmodify time: ") + APPDATE
+ tr("\nmodify cnt: ") + MODIFYCNT
+ "\n ------------------------------"
+ tr("\nlatest version: v") + _remoteVersion
+ tr("\nlatest version: v") + remoteVersion
+ tr("\nmodify time: ") + m_lastChangeTime
+ tr("\nmodify cnt: ") + m_modifyCnt
+ tr("\nchange log: ") + m_changelog
+ "\n======================================"
);
+ "\n======================================");
if(m_bDownloadFullExe)
{
showStatus("need download full exe");
m_fileName = APPNAME + "-full-v" + m_patchVersion + ".exe";
}
showStatus(tr("click update button to update"));
setUpdateButtonsEnabled(true);
}
else
{
showStatus("======================================"
+ tr("\ncurrent software is up-to-date")
+ tr("\nversion : v") + _remoteVersion
+ tr("\nversion : v") + remoteVersion
+ tr("\nmodify time: ") + m_lastChangeTime
+ tr("\nmodify cnt: ") + m_modifyCnt
+ "\n======================================"
+ tr("\nno need to update")
+ tr("\nclick cancel button to quit"));
setUpdateButtonsEnabled(false);
}
ui->btn_update->setEnabled(1);
ui->btn_update_patch->setEnabled(1);
}
void UpdaterDialog::calculateSizes(qint64 received, qint64 total)
{
QString totalSize;
QString receivedSize;
if (total < 1024)
totalSize = tr("%1 bytes").arg(total);
else if (total < 1048576)
totalSize = tr("%1 KB").arg(round(total / 1024));
else
totalSize = tr("%1 MB").arg((total / 1048576.0));
if (received < 1024)
receivedSize = tr("%1 bytes").arg(received);
else if (received < 1048576)
receivedSize = tr("%1 KB").arg(received / 1024);
else
receivedSize = tr("%1 MB").arg(received / 1048576.0);
const QString totalSize = formatBytes(total);
const QString receivedSize = formatBytes(received);
ui->label_2->setText(tr("Downloading updates") + " (" + receivedSize + " " + tr("of") + " " + totalSize
+ ")");
}
/**
* Uses two time samples (from the current time and a previous sample) to
* calculate how many bytes have been downloaded.
*
* Then, this function proceeds to calculate the appropiate units of time
* (hours, minutes or seconds) and constructs a user-friendly string, which
* is displayed in the dialog.
*/
void UpdaterDialog::calculateTimeRemaining(qint64 received, qint64 total)
{
static quint64 lastRec = 0;
static uint lastTime = 0;
uint _time = QDateTime::currentDateTime().toMSecsSinceEpoch();
double _speed = (received - lastRec) / 1024.0 / (_time - lastTime) * 1000.0;
lastTime = _time;
lastRec = received;
uint difference = QDateTime::currentDateTime().toSecsSinceEpoch() - m_startTime;
if (difference > 0)
if(total <= 0 || received <= 0)
{
QString timeString;
qreal timeRemaining = (total - received) / (received / difference);
if (timeRemaining > 7200)
{
timeRemaining /= 3600;
int hours = int(timeRemaining + 0.5);
if (hours > 1)
timeString = tr("about %1 hours").arg(hours);
else
timeString = tr("about one hour");
}
else if (timeRemaining > 60)
{
timeRemaining /= 60;
int minutes = int(timeRemaining + 0.5);
return;
}
if (minutes > 1)
timeString = tr("%1 minutes").arg(minutes);
else
timeString = tr("1 minute");
}
const qint64 currentMs = m_downloadTimer.elapsed();
const qint64 elapsedSinceLast = currentMs - m_lastProgressMs;
double speed = 0.0;
if(elapsedSinceLast > 0)
{
speed = (received - m_lastProgressBytes) / 1024.0 / elapsedSinceLast * 1000.0;
}
else if (timeRemaining <= 60)
{
int seconds = int(timeRemaining + 0.5);
m_lastProgressMs = currentMs;
m_lastProgressBytes = received;
if (seconds > 1)
timeString = tr("%1 seconds").arg(seconds);
else
timeString = tr("1 second");
}
const qint64 elapsedSeconds = qMax<qint64>(1, currentMs / 1000);
qreal timeRemaining = (total - received) / (received / static_cast<qreal>(elapsedSeconds));
ui->label_3->setText(tr("speed: %1 kb/s ").arg(_speed) + tr("Time remaining") + ": " + timeString);
QString timeString;
if(timeRemaining > 7200)
{
timeRemaining /= 3600;
const int hours = int(timeRemaining + 0.5);
timeString = hours > 1 ? tr("about %1 hours").arg(hours) : tr("about one hour");
}
else if(timeRemaining > 60)
{
timeRemaining /= 60;
const int minutes = int(timeRemaining + 0.5);
timeString = minutes > 1 ? tr("%1 minutes").arg(minutes) : tr("1 minute");
}
else
{
const int seconds = int(timeRemaining + 0.5);
timeString = seconds > 1 ? tr("%1 seconds").arg(seconds) : tr("1 second");
}
ui->label_3->setText(tr("speed: %1 kb/s ").arg(speed, 0, 'f', 1) + tr("Time remaining") + ": " + timeString);
}
void UpdaterDialog::on_btn_update_clicked()
{
rebuildDownloadInfo();
m_pDownloader->setFileName(m_fileName);
m_startTime = QDateTime::currentDateTime().toSecsSinceEpoch();
m_downloadTimer.restart();
m_lastProgressMs = 0;
m_lastProgressBytes = 0;
QString _url = m_bDownloadFullExe ? m_fullUrl : m_patchUrl;
m_pDownloader->startDownload(_url);
qDebug() << _url;
const QString url = m_bDownloadFullExe ? m_fullUrl : m_patchUrl;
m_pDownloader->startDownload(url);
}
void UpdaterDialog::on_btn_cancel_clicked()
{
if(!m_UpdateProc)
if(restartApplication())
{
m_UpdateProc = new QProcess;
QApplication::quit();
}
#if defined Q_OS_WIN
m_UpdateProc->setProgram(APPNAME + ".exe");
#elif defined Q_OS_LINUX
m_UpdateProc->setProgram("./" + APPNAME);
#endif
m_UpdateProc->start();
m_UpdateProc->waitForStarted();
QApplication::quit();
}
void UpdaterDialog::onDownloadFinished()
{
// ui->show->appendPlainText(m_pDownloader->fileName());
// ui->show->appendPlainText("m_bDownloadFullExe");
auto localInformation = QMessageBox::question(this, "update", "download finished\nstart update?");
if(localInformation == QMessageBox::Yes)
const qreal totalSeconds = qMax<qreal>(0.001, m_downloadTimer.elapsed() / 1000.0);
if(m_totalSize > 0)
{
QString _appName;
if(!m_bDownloadFullExe)
{
//解压文件
QFileInfo f(m_pDownloader->getFile());
QString _path = f.fileName();
QZipReader * zipReader = new QZipReader(_path);
zipReader->extractAll(f.path());
_appName = APPNAME + ".exe";
}
else
{
_appName = m_pDownloader->fileName();
}
ui->label_3->setText("total time: " + QString::number(totalSeconds, 'f', 1) +
"s average speed: " + QString::number((m_totalSize / 1024.0 / totalSeconds), 'g', 3) + "kb/s");
}
const auto localInformation = QMessageBox::question(this, "update", "download finished\nstart update?");
if(localInformation != QMessageBox::Yes)
{
return;
}
QFileInfo f2(QCoreApplication::applicationDirPath() + "/" + _appName);
if(f2.exists())
QString appName;
if(!m_bDownloadFullExe)
{
// 补丁包下载完成后在应用目录解压;解压失败时保留更新器,避免退出后主程序缺文件。
QFileInfo fileInfo(m_pDownloader->getFile());
QZipReader zipReader(fileInfo.absoluteFilePath());
if(!zipReader.extractAll(fileInfo.absolutePath()))
{
QDesktopServices::openUrl(f2.absoluteFilePath());
showStatus("extract patch failed");
return;
}
appName = APPNAME + ".exe";
}
else
{
appName = m_pDownloader->fileName();
}
const QFileInfo appFile(QCoreApplication::applicationDirPath() + "/" + appName);
if(appFile.exists())
{
QDesktopServices::openUrl(QUrl::fromLocalFile(appFile.absoluteFilePath()));
QApplication::quit();
}
else
{
showStatus("update target file not exists: " + appFile.absoluteFilePath());
}
}
void UpdaterDialog::on_btn_update_patch_clicked()
{
m_pDownloader->setFileName(m_fileName);
m_startTime = QDateTime::currentDateTime().toSecsSinceEpoch();
m_bDownloadFullExe = false;
QString _url = m_patchUrl;
m_pDownloader->startDownload(_url);
rebuildDownloadInfo();
m_pDownloader->setFileName(m_fileName);
m_downloadTimer.restart();
m_lastProgressMs = 0;
m_lastProgressBytes = 0;
m_pDownloader->startDownload(m_patchUrl);
}
void UpdaterDialog::setUpdateButtonsEnabled(bool enabled)
{
ui->btn_update->setEnabled(enabled);
ui->btn_update_patch->setEnabled(enabled && !m_patchUrl.isEmpty());
}
void UpdaterDialog::rebuildDownloadInfo()
{
const QString patchFileName = APPNAME + "-patch-v" + m_patchVersion + ".zip";
const QString fullFileName = APPNAME + "-full-v" + m_fullVersion + ".exe";
m_patchUrl = buildDownloadUrl(m_patchBaseUrl, APPNAME, patchFileName);
m_fullUrl = buildDownloadUrl(m_fullBaseUrl, APPNAME, fullFileName);
m_fileName = m_bDownloadFullExe ? fullFileName : patchFileName;
}
QString UpdaterDialog::formatBytes(qint64 bytes) const
{
if(bytes < 0)
{
return tr("unknown");
}
if(bytes < 1024)
{
return tr("%1 bytes").arg(bytes);
}
if(bytes < 1048576)
{
return tr("%1 KB").arg(QString::number(bytes / 1024.0, 'f', 1));
}
return tr("%1 MB").arg(QString::number(bytes / 1048576.0, 'f', 2));
}
bool UpdaterDialog::restartApplication()
{
if(APPNAME.trimmed().isEmpty())
{
showStatus("app name is empty");
return false;
}
QString program;
#if defined Q_OS_WIN
program = APPNAME + ".exe";
#elif defined Q_OS_LINUX
program = "./" + APPNAME;
#else
program = APPNAME;
#endif
// 取消更新后需要让原程序独立运行,使用 startDetached 避免更新器退出时影响子进程生命周期。
if(!QProcess::startDetached(program, QStringList()))
{
showStatus("restart app failed");
return false;
}
return true;
}

113
Src/updaterdialog.h

@ -2,6 +2,7 @@
#define UPDATERDIALOG_H
#include <QDialog>
#include <QElapsedTimer>
#include <QProcess>
#include "downloader.h"
@ -22,56 +23,166 @@ class UpdaterDialog : public QDialog
Q_OBJECT
public:
/**
* @brief
* @param parent Qt Downloader/QNetworkAccessManager
*
*
* Downloader UI
*/
explicit UpdaterDialog(QWidget *parent = nullptr);
/**
* @brief
*
* Qt Downloader QObject new parent
*/
~UpdaterDialog();
/**
* @brief
* @param message
*/
void showStatus(const QString &);
/**
* @brief
* @param received
* @param total 0
*/
void calculateSizes(qint64 received, qint64 total);
/**
* @brief
* @param received
* @param total
*/
void calculateTimeRemaining(qint64 received, qint64 total);
/**
* @brief
* @param _localVersion x.y.z
* @param _remoteVersion x.y.z
* @return true false
*
* 0 访
*/
bool checkVersion(const QString & _localVersion, const QString & _remoteVersion);
private slots:
/** @brief “check” 按钮槽函数,发起远程 updates.json 检测请求。 */
void on_btn_check_clicked();
/** @brief 版本检测响应处理槽函数,解析 JSON 并刷新可更新状态。 */
void onCheckReply(QNetworkReply *);
/** @brief “update-full” 按钮槽函数,下载完整安装包或当前推荐包。 */
void on_btn_update_clicked();
/** @brief “cancel” 按钮槽函数,重新启动原程序并退出更新器。 */
void on_btn_cancel_clicked();
/** @brief 下载完成槽函数,按用户确认执行补丁解压或启动完整包。 */
void onDownloadFinished();
/** @brief “update-patch” 按钮槽函数,强制下载增量补丁包。 */
void on_btn_update_patch_clicked();
private:
/**
* @brief
* @param enabled true
*/
void setUpdateButtonsEnabled(bool enabled);
/**
* @brief URL
*
* updates.json APPNAME/
*/
void rebuildDownloadInfo();
/**
* @brief
* @param bytes
* @return B/KB/MB
*/
QString formatBytes(qint64 bytes) const;
/**
* @brief
*
* Windows APPNAME.exeLinux ./APPNAME
*/
bool restartApplication();
/** @brief UI 指针,由 setupUi 创建,析构时手动 delete。 */
Ui::UpdaterDialog *ui;
/** @brief 文件下载器,父对象为本窗口,信号在 UI 线程中更新界面。 */
Downloader * m_pDownloader = nullptr;
/** @brief 本地版本号,用于和远程补丁/全量版本比较。 */
QString m_version;
/** @brief 版本检测网络管理器,懒加载创建,避免未检测时占用网络资源。 */
QNetworkAccessManager *m_pManager{nullptr};
/** @brief 平台标识,当前保留字段;实际解析使用全局 PLATFORM。 */
QString m_platform;
/** @brief 远程更新日志,用于展示给用户判断更新内容。 */
QString m_changelog;
/** @brief 模块名保留字段,用于未来拆分多模块更新。 */
QString m_moduleName;
/** @brief 增量补丁下载 URL。 */
QString m_patchUrl;
/** @brief 增量补丁基础 URL,来自 updates.json,重复点击下载时用于重新生成完整地址。 */
QString m_patchBaseUrl;
/** @brief 增量补丁版本号。 */
QString m_patchVersion;
/** @brief 模块版本保留字段,用于未来按模块比较版本。 */
QString m_moduleVersion;
/** @brief 完整安装包下载 URL。 */
QString m_fullUrl;
/** @brief 完整安装包基础 URL,来自 updates.json,重复点击下载时用于重新生成完整地址。 */
QString m_fullBaseUrl;
/** @brief 完整安装包版本号。 */
QString m_fullVersion;
/** @brief 远程最后修改日期,通常为 yyyyMMdd 字符串。 */
QString m_lastChangeTime;
/** @brief 远程当天修改计数,同版本同日期时用于判断是否仍需更新。 */
QString m_modifyCnt;
/** @brief 当前下载目标文件名,不包含目录。 */
QString m_fileName = "";
/** @brief true 表示本次推荐下载完整包,false 表示推荐下载补丁包。 */
bool m_bDownloadFullExe = false;
uint m_startTime;
/** @brief 下载计时器,用于计算平均速度和剩余时间,避免系统时间变化造成负值。 */
QElapsedTimer m_downloadTimer;
/** @brief 最近一次进度采样的计时毫秒,用于计算瞬时速度。 */
qint64 m_lastProgressMs = 0;
/** @brief 最近一次进度采样的已收字节数,用于计算瞬时速度。 */
qint64 m_lastProgressBytes = 0;
/** @brief 当前下载总字节数,未知时为 0。 */
quint64 m_totalSize;
/** @brief 用于取消时重启原程序;无父对象时析构函数负责释放,避免进程对象泄漏。 */
QProcess *m_UpdateProc{nullptr};
};

Loading…
Cancel
Save