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;
73 config.allowRedirects =
true;
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:"
147 <<
url.toDisplayString(QUrl::RemoveUserInfo | QUrl::RemoveQuery | QUrl::RemoveFragment)
148 <<
"to" << _localPath;
151 _currentReply = _networkManager->get(request);
152 if (_currentReply ==
nullptr) {
153 qCWarning(QGCFileDownloadLog) <<
"QNetworkAccessManager::get failed";
154 _setErrorString(tr(
"Failed to start download"));
155 _outputFile->close();
157 _outputFile =
nullptr;
164 connect(_currentReply, &QNetworkReply::downloadProgress,
165 this, &QGCFileDownload::_onDownloadProgress);
166 connect(_currentReply, &QNetworkReply::readyRead,
167 this, &QGCFileDownload::_onReadyRead);
168 connect(_currentReply, &QNetworkReply::finished,
169 this, &QGCFileDownload::_onDownloadFinished);
170 connect(_currentReply, &QNetworkReply::errorOccurred,
171 this, &QGCFileDownload::_onDownloadError);
182 if (shouldEmitCancel) {
184 _setErrorString(tr(
"Download cancelled"));
185 _emitFinished(
false, QString(), _errorString);
188 if (_currentReply !=
nullptr) {
189 qCDebug(QGCFileDownloadLog) <<
"Cancelling download";
190 _currentReply->abort();
193 if (_decompressionJob !=
nullptr && _decompressionJob->
isRunning()) {
194 _decompressionJob->
cancel();
204void QGCFileDownload::_onDownloadProgress(qint64 bytesReceived, qint64 totalBytes)
221void QGCFileDownload::_onReadyRead()
223 if (_currentReply ==
nullptr || _outputFile ==
nullptr) {
228 const QByteArray data = _currentReply->readAll();
229 if (!_writeReplyData(data)) {
230 _failForWriteError(QStringLiteral(
"readyRead"));
234void QGCFileDownload::_onDownloadFinished()
236 QNetworkReply *reply = _currentReply;
237 _currentReply =
nullptr;
239 if (reply ==
nullptr) {
242 reply->deleteLater();
249 _lastResultFromCache = reply->attribute(QNetworkRequest::SourceIsFromCacheAttribute).toBool();
252 if (_outputFile !=
nullptr) {
254 const QByteArray
remaining = reply->readAll();
255 if (!_writeReplyData(remaining)) {
256 _outputFile->close();
258 _outputFile =
nullptr;
259 _failForWriteError(QStringLiteral(
"finished"));
262 _outputFile->close();
264 _outputFile =
nullptr;
268 if (reply->error() != QNetworkReply::NoError) {
278 if (!reply->url().isLocalFile()) {
281 const QString
error = tr(
"HTTP error %1: %2")
284 _setErrorString(
error);
286 _emitFinished(
false, QString(),
error);
291 qCDebug(QGCFileDownloadLog) <<
"Download finished:" << _localPath
292 <<
"size:" << QFileInfo(_localPath).size();
295 if (!_expectedHash.isEmpty()) {
297 if (!_verifyHash()) {
299 _emitFinished(
false, QString(), _errorString);
306 _startDecompression();
312 _emitFinished(
true, _localPath, QString());
315void QGCFileDownload::_onDownloadError(QNetworkReply::NetworkError code)
324 case QNetworkReply::OperationCanceledError:
325 errorMsg = tr(
"Download cancelled");
327 case QNetworkReply::ContentNotFoundError:
328 errorMsg = tr(
"File not found (404)");
330 case QNetworkReply::TimeoutError:
331 errorMsg = tr(
"Connection timed out");
333 case QNetworkReply::HostNotFoundError:
334 errorMsg = tr(
"Host not found");
336 case QNetworkReply::ConnectionRefusedError:
337 errorMsg = tr(
"Connection refused");
339 case QNetworkReply::SslHandshakeFailedError:
340 errorMsg = tr(
"SSL handshake failed");
343 if (_currentReply !=
nullptr) {
346 errorMsg = tr(
"Network error: %1").arg(code);
351 qCWarning(QGCFileDownloadLog) <<
"Download error:" << errorMsg <<
"url:"
352 << _url.toDisplayString(QUrl::RemoveUserInfo | QUrl::RemoveQuery | QUrl::RemoveFragment);
353 _setErrorString(errorMsg);
356void QGCFileDownload::_onDecompressionFinished(
bool success)
359 _compressedFilePath.clear();
364 const QString decompressedPath = _decompressionJob->
outputPath();
365 qCDebug(QGCFileDownloadLog) <<
"Decompression completed:" << decompressedPath;
368 if (_compressedFilePath != decompressedPath && QFile::exists(_compressedFilePath)) {
369 QFile::remove(_compressedFilePath);
372 _localPath = decompressedPath;
376 _emitFinished(
true, _localPath, QString());
378 const QString
error = tr(
"Decompression failed: %1").arg(_decompressionJob->
errorString());
379 qCWarning(QGCFileDownloadLog) <<
error;
380 _setErrorString(
error);
384 _emitFinished(
false, _compressedFilePath,
error);
387 _compressedFilePath.clear();
394void QGCFileDownload::_setState(State newState)
396 if (_state != newState) {
407void QGCFileDownload::_setProgress(qreal progress)
409 if (!qFuzzyCompare(_progress,
progress)) {
415void QGCFileDownload::_setErrorString(
const QString &
error)
417 if (_errorString !=
error) {
418 _errorString =
error;
423void QGCFileDownload::_cleanup()
425 if (_currentReply !=
nullptr) {
426 _currentReply->disconnect(
this);
427 _currentReply->deleteLater();
428 _currentReply =
nullptr;
431 if (_outputFile !=
nullptr) {
432 if (_outputFile->isOpen()) {
433 _outputFile->close();
436 _outputFile =
nullptr;
440void QGCFileDownload::_emitFinished(
bool success,
const QString &localPath,
const QString &errorMessage)
442 if (_finishEmitted) {
445 _finishEmitted =
true;
449bool QGCFileDownload::_writeReplyData(
const QByteArray &data)
451 if (data.isEmpty()) {
455 if (_outputFile ==
nullptr) {
459 return _outputFile->write(data) == data.size();
462bool QGCFileDownload::_failForWriteError(
const QString &context)
464 const QString
error = tr(
"Failed to write downloaded file (%1): %2")
465 .arg(context, _outputFile !=
nullptr ? _outputFile->errorString() : QString());
466 qCWarning(QGCFileDownloadLog) <<
error;
467 _setErrorString(
error);
470 if (_currentReply !=
nullptr) {
471 _currentReply->disconnect(
this);
472 _currentReply->abort();
476 _emitFinished(
false, QString(),
error);
480QString QGCFileDownload::_generateOutputPath(
const QString &remoteUrl)
const
483 if (!_outputPath.isEmpty()) {
488 QString fileName = QUrl(remoteUrl).fileName();
489 if (fileName.isEmpty()) {
490 fileName = QStringLiteral(
"DownloadedFile");
494 const int queryIndex = fileName.indexOf(QLatin1Char(
'?'));
495 if (queryIndex != -1) {
496 fileName = fileName.left(queryIndex);
500 QString downloadDir = QStandardPaths::writableLocation(QStandardPaths::TempLocation);
501 if (downloadDir.isEmpty()) {
502 downloadDir = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation);
505 if (downloadDir.isEmpty()) {
506 qCWarning(QGCFileDownloadLog) <<
"No writable download location found";
513bool QGCFileDownload::_verifyHash()
515 qCDebug(QGCFileDownloadLog) <<
"Verifying hash for:" << _localPath;
518 if (actualHash.isEmpty()) {
519 _setErrorString(tr(
"Failed to compute file hash"));
523 if (actualHash.compare(_expectedHash, Qt::CaseInsensitive) != 0) {
524 _setErrorString(tr(
"Hash verification failed. Expected: %1, Got: %2")
525 .arg(_expectedHash, actualHash));
529 qCDebug(QGCFileDownloadLog) <<
"Hash verified successfully";
533void QGCFileDownload::_startDecompression()
535 _compressedFilePath = _localPath;
538 qCDebug(QGCFileDownloadLog) <<
"Starting decompression:" << _localPath <<
"->" << decompressedPath;
542 if (_decompressionJob ==
nullptr) {
547 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)
constexpr std::chrono::microseconds remaining(uint64_t timestampUs, uint64_t nowUs, std::chrono::microseconds lifetime)
Time left before a receipt reaches lifetime; zero for a missing, future or expired one.
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)
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.