#include "downloader.h" #include #include #include #include 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); }