8#include <QtCore/QFileInfo>
9#include <QtCore/QStandardPaths>
10#include <QtNetwork/QNetworkAccessManager>
18 qCDebug(QGCFileDownloadLog) <<
"Created" <<
this;
23 qCDebug(QGCFileDownloadLog) <<
"Destroying" <<
this;
33 if (_autoDecompress != enabled) {
34 _autoDecompress = enabled;
41 if (_outputPath != path) {
49 if (_expectedHash != hash) {
57 _networkManager->setCache(cache);
62 _timeoutMs = timeoutMs;
74 return start(remoteUrl, config);
80 qCWarning(QGCFileDownloadLog) <<
"Download already in progress";
84 if (remoteUrl.isEmpty()) {
85 qCWarning(QGCFileDownloadLog) <<
"Empty URL provided";
86 _setErrorString(tr(
"Empty URL"));
94 }
else if (remoteUrl.startsWith(QLatin1String(
"http:")) || remoteUrl.startsWith(QLatin1String(
"https:"))) {
95 url.setUrl(remoteUrl);
98 url = QUrl::fromLocalFile(remoteUrl);
101 if (!
url.isValid()) {
102 qCWarning(QGCFileDownloadLog) <<
"Invalid URL:" << remoteUrl;
103 _setErrorString(tr(
"Invalid URL: %1").arg(remoteUrl));
116 _setErrorString(QString());
117 _finishEmitted =
false;
118 _lastResultFromCache =
false;
121 _localPath = _generateOutputPath(remoteUrl);
122 if (_localPath.isEmpty()) {
123 _setErrorString(tr(
"Unable to determine output path"));
130 _setErrorString(tr(
"Cannot create output directory"));
135 _outputFile =
new QFile(_localPath,
this);
136 if (!_outputFile->open(QIODevice::WriteOnly | QIODevice::Truncate)) {
137 _setErrorString(tr(
"Cannot open output file: %1").arg(_outputFile->errorString()));
139 _outputFile =
nullptr;
146 qCDebug(QGCFileDownloadLog) <<
"Starting download:" <<
url.toString() <<
"to" << _localPath;
149 _currentReply = _networkManager->get(request);
150 if (_currentReply ==
nullptr) {
151 qCWarning(QGCFileDownloadLog) <<
"QNetworkAccessManager::get failed";
152 _setErrorString(tr(
"Failed to start download"));
153 _outputFile->close();
155 _outputFile =
nullptr;
162 connect(_currentReply, &QNetworkReply::downloadProgress,
163 this, &QGCFileDownload::_onDownloadProgress);
164 connect(_currentReply, &QNetworkReply::readyRead,
165 this, &QGCFileDownload::_onReadyRead);
166 connect(_currentReply, &QNetworkReply::finished,
167 this, &QGCFileDownload::_onDownloadFinished);
168 connect(_currentReply, &QNetworkReply::errorOccurred,
169 this, &QGCFileDownload::_onDownloadError);
180 if (shouldEmitCancel) {
182 _setErrorString(tr(
"Download cancelled"));
183 _emitFinished(
false, QString(), _errorString);
186 if (_currentReply !=
nullptr) {
187 qCDebug(QGCFileDownloadLog) <<
"Cancelling download";
188 _currentReply->abort();
191 if (_decompressionJob !=
nullptr && _decompressionJob->
isRunning()) {
192 _decompressionJob->
cancel();
202void QGCFileDownload::_onDownloadProgress(qint64 bytesReceived, qint64 totalBytes)
219void QGCFileDownload::_onReadyRead()
221 if (_currentReply ==
nullptr || _outputFile ==
nullptr) {
226 const QByteArray data = _currentReply->readAll();
227 if (!_writeReplyData(data)) {
228 _failForWriteError(QStringLiteral(
"readyRead"));
232void QGCFileDownload::_onDownloadFinished()
234 QNetworkReply *reply = _currentReply;
235 _currentReply =
nullptr;
237 if (reply ==
nullptr) {
240 reply->deleteLater();
247 _lastResultFromCache = reply->attribute(QNetworkRequest::SourceIsFromCacheAttribute).toBool();
250 if (_outputFile !=
nullptr) {
252 const QByteArray remaining = reply->readAll();
253 if (!_writeReplyData(remaining)) {
254 _outputFile->close();
256 _outputFile =
nullptr;
257 _failForWriteError(QStringLiteral(
"finished"));
260 _outputFile->close();
262 _outputFile =
nullptr;
266 if (reply->error() != QNetworkReply::NoError) {
276 if (!reply->url().isLocalFile()) {
279 const QString
error = tr(
"HTTP error %1: %2")
282 _setErrorString(
error);
284 _emitFinished(
false, QString(),
error);
289 qCDebug(QGCFileDownloadLog) <<
"Download finished:" << _localPath
290 <<
"size:" << QFileInfo(_localPath).size();
293 if (!_expectedHash.isEmpty()) {
295 if (!_verifyHash()) {
297 _emitFinished(
false, QString(), _errorString);
304 _startDecompression();
310 _emitFinished(
true, _localPath, QString());
313void QGCFileDownload::_onDownloadError(QNetworkReply::NetworkError code)
322 case QNetworkReply::OperationCanceledError:
323 errorMsg = tr(
"Download cancelled");
325 case QNetworkReply::ContentNotFoundError:
326 errorMsg = tr(
"File not found (404)");
328 case QNetworkReply::TimeoutError:
329 errorMsg = tr(
"Connection timed out");
331 case QNetworkReply::HostNotFoundError:
332 errorMsg = tr(
"Host not found");
334 case QNetworkReply::ConnectionRefusedError:
335 errorMsg = tr(
"Connection refused");
337 case QNetworkReply::SslHandshakeFailedError:
338 errorMsg = tr(
"SSL handshake failed");
341 if (_currentReply !=
nullptr) {
344 errorMsg = tr(
"Network error: %1").arg(code);
349 qCWarning(QGCFileDownloadLog) <<
"Download error:" << errorMsg;
350 _setErrorString(errorMsg);
353void QGCFileDownload::_onDecompressionFinished(
bool success)
356 _compressedFilePath.clear();
361 const QString decompressedPath = _decompressionJob->
outputPath();
362 qCDebug(QGCFileDownloadLog) <<
"Decompression completed:" << decompressedPath;
365 if (_compressedFilePath != decompressedPath && QFile::exists(_compressedFilePath)) {
366 QFile::remove(_compressedFilePath);
369 _localPath = decompressedPath;
373 _emitFinished(
true, _localPath, QString());
375 const QString
error = tr(
"Decompression failed: %1").arg(_decompressionJob->
errorString());
376 qCWarning(QGCFileDownloadLog) <<
error;
377 _setErrorString(
error);
381 _emitFinished(
false, _compressedFilePath,
error);
384 _compressedFilePath.clear();
391void QGCFileDownload::_setState(State newState)
393 if (_state != newState) {
404void QGCFileDownload::_setProgress(qreal progress)
406 if (!qFuzzyCompare(_progress,
progress)) {
412void QGCFileDownload::_setErrorString(
const QString &
error)
414 if (_errorString !=
error) {
415 _errorString =
error;
420void QGCFileDownload::_cleanup()
422 if (_currentReply !=
nullptr) {
423 _currentReply->disconnect(
this);
424 _currentReply->deleteLater();
425 _currentReply =
nullptr;
428 if (_outputFile !=
nullptr) {
429 if (_outputFile->isOpen()) {
430 _outputFile->close();
433 _outputFile =
nullptr;
437void QGCFileDownload::_emitFinished(
bool success,
const QString &localPath,
const QString &errorMessage)
439 if (_finishEmitted) {
442 _finishEmitted =
true;
446bool QGCFileDownload::_writeReplyData(
const QByteArray &data)
448 if (data.isEmpty()) {
452 if (_outputFile ==
nullptr) {
456 return _outputFile->write(data) == data.size();
459bool QGCFileDownload::_failForWriteError(
const QString &context)
461 const QString
error = tr(
"Failed to write downloaded file (%1): %2")
462 .arg(context, _outputFile !=
nullptr ? _outputFile->errorString() : QString());
463 qCWarning(QGCFileDownloadLog) <<
error;
464 _setErrorString(
error);
467 if (_currentReply !=
nullptr) {
468 _currentReply->disconnect(
this);
469 _currentReply->abort();
473 _emitFinished(
false, QString(),
error);
477QString QGCFileDownload::_generateOutputPath(
const QString &remoteUrl)
const
480 if (!_outputPath.isEmpty()) {
486 if (fileName.isEmpty()) {
487 fileName = QStringLiteral(
"DownloadedFile");
491 const int queryIndex = fileName.indexOf(QLatin1Char(
'?'));
492 if (queryIndex != -1) {
493 fileName = fileName.left(queryIndex);
497 QString downloadDir = QStandardPaths::writableLocation(QStandardPaths::TempLocation);
498 if (downloadDir.isEmpty()) {
499 downloadDir = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation);
502 if (downloadDir.isEmpty()) {
503 qCWarning(QGCFileDownloadLog) <<
"No writable download location found";
510bool QGCFileDownload::_verifyHash()
512 qCDebug(QGCFileDownloadLog) <<
"Verifying hash for:" << _localPath;
515 if (actualHash.isEmpty()) {
516 _setErrorString(tr(
"Failed to compute file hash"));
520 if (actualHash.compare(_expectedHash, Qt::CaseInsensitive) != 0) {
521 _setErrorString(tr(
"Hash verification failed. Expected: %1, Got: %2")
522 .arg(_expectedHash, actualHash));
526 qCDebug(QGCFileDownloadLog) <<
"Hash verified successfully";
530void QGCFileDownload::_startDecompression()
532 _compressedFilePath = _localPath;
535 qCDebug(QGCFileDownloadLog) <<
"Starting decompression:" << _localPath <<
"->" << decompressedPath;
539 if (_decompressionJob ==
nullptr) {
544 this, &QGCFileDownload::_onDecompressionFinished);
QObject wrapper for async compression operations using QtConcurrent/QPromise.
Unified file download utility with decompression, verification, and QML support.
#define QGC_LOGGING_CATEGORY(name, categoryStr)
QObject wrapper for compression operations with progress signals.
QString errorString() const
void progressChanged(qreal progress)
Emitted when progress changes (0.0 to 1.0)
void cancel()
Cancel current operation.
void finished(bool success)
void decompressFile(const QString &inputPath, const QString &outputPath=QString(), qint64 maxBytes=0)
QString outputPath() const
File download with progress, decompression, and hash verification.
@ Completed
Download completed successfully.
@ Verifying
Verifying file hash.
@ Cancelled
Download was cancelled.
@ Decompressing
Decompressing downloaded file.
@ Downloading
Actively downloading.
@ Idle
No download in progress.
void setTimeout(int timeoutMs)
Set request timeout in milliseconds.
void finished(bool success, const QString &localPath, const QString &errorMessage)
void setExpectedHash(const QString &hash)
void localPathChanged(const QString &localPath)
Emitted when local path changes.
void runningChanged(bool running)
Emitted when running state changes.
void totalBytesChanged(qint64 totalBytes)
Emitted when total bytes changes.
qint64 totalBytes() const
void stateChanged(State state)
Emitted when state changes.
void autoDecompressChanged(bool autoDecompress)
Emitted when auto-decompress setting changes.
QString localPath() const
qint64 bytesReceived() const
bool start(const QString &remoteUrl)
void setCache(QAbstractNetworkCache *cache)
Set network cache for downloads.
void bytesReceivedChanged(qint64 bytesReceived)
Emitted when bytes received changes.
void errorStringChanged(const QString &errorString)
Emitted when error string changes.
void setOutputPath(const QString &path)
void setAutoDecompress(bool enabled)
void cancel()
Cancel current download.
void progressChanged(qreal progress)
Emitted when download progress changes.
void expectedHashChanged(const QString &expectedHash)
Emitted when expected hash setting changes.
void downloadProgress(qint64 bytesReceived, qint64 totalBytes)
Emitted during download with byte counts.
void outputPathChanged(const QString &outputPath)
Emitted when output path setting changes.
~QGCFileDownload() override
void urlChanged(const QUrl &url)
Emitted when URL changes.
void decompressionProgress(qreal progress)
Emitted during decompression (0.0 to 1.0)
QString strippedPath(const QString &filePath)
bool isCompressedFile(const QString &filePath)
Check if file path indicates a compressed file (.gz, .xz, .zst)
bool isLocalPath(const QString &urlOrPath)
bool ensureParentExists(const QString &filePath)
QString toLocalPath(const QString &urlOrPath)
QString joinPath(const QString &dir, const QString &name)
QString computeFileHash(const QString &filePath, QCryptographicHash::Algorithm algorithm)
QNetworkRequest createRequest(const QUrl &url, const RequestConfig &config)
QString urlFileName(const QUrl &url)
Extract filename from URL path (last path segment)
void ignoreSslErrorsIfNeeded(QNetworkReply *reply)
int httpStatusCode(const QNetworkReply *reply)
QString httpStatusText(HttpStatusCode statusCode)
bool isHttpSuccess(int statusCode)
Check if HTTP status indicates success (2xx)
QString errorMessage(const QNetworkReply *reply)
Common request configuration options.