更新模块
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

263 lines
6.8 KiB

#include "downloader.h"
#include <QCoreApplication>
#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}
{
m_pNetWorkAccessManager = new QNetworkAccessManager(this);
dir.setPath(QCoreApplication::applicationDirPath());
}
void Downloader::setUrl(const QString &_url)
{
m_pUrl = QUrl(_url);
}
void Downloader::startDownload(const QString &_url)
{
if(m_bIsDownloading)
{
emit doShowInfo(tr("download task is already running"));
return;
}
if(!_url.isEmpty())
{
m_pUrl = QUrl(_url);
}
if(!m_pUrl.isValid() || m_pUrl.isEmpty())
{
emit doShowInfo(tr("download url is invalid"));
return;
}
if(m_fileName.trimmed().isEmpty())
{
const QString urlFileName = QFileInfo(m_pUrl.path()).fileName();
if(urlFileName.isEmpty())
{
emit doShowInfo(tr("download file name is empty"));
return;
}
m_fileName = urlFileName;
}
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::readyRead, this, [this]()
{
if(!file.isOpen() || !m_pReply)
{
return;
}
const QByteArray chunk = m_pReply->readAll();
if(file.write(chunk) != chunk.size())
{
emit doShowInfo(tr("write download file failed: %1").arg(file.errorString()));
m_pReply->abort();
}
});
connect(m_pReply, &QNetworkReply::downloadProgress, this, [this](qint64 received, qint64 total)
{
emit doProgress(received, total);
});
connect(m_pReply, &QNetworkReply::metaDataChanged, this, [this]()
{
const QString responseFileName =
fileNameFromContentDisposition(m_pReply->header(QNetworkRequest::ContentDispositionHeader));
if(!responseFileName.isEmpty() && m_fileName.isEmpty())
{
m_fileName = responseFileName;
}
});
connect(m_pReply, &QNetworkReply::finished, this, [this]()
{
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(tr("http download failed, status code: %1").arg(httpStatusCode));
cleanupCurrentReply(true);
return;
}
if(file.isOpen())
{
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;
}
QFile::remove(targetPath);
if(!QFile::rename(partPath, targetPath))
{
emit doShowInfo(tr("rename file failed"));
cleanupCurrentReply(true);
return;
}
file.setFileName(targetPath);
m_bIsDownloading = false;
finishedReply->deleteLater();
m_pReply = nullptr;
emit doFinished();
});
}
void Downloader::checkVersion()
{
QNetworkRequest request{QUrl(CHECK_URL)};
m_pNetWorkAccessManager->get(request);
}
const QFile &Downloader::getFile() const
{
return file;
}
const QDir &Downloader::getDir() const
{
return dir;
}
const QString &Downloader::fileName() const
{
return m_fileName;
}
void Downloader::setFileName(const QString &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);
}