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
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);
|
|
}
|
|
|