8#include <QtCore/QFileInfo>
9#include <QtCore/QStandardPaths>
10#include <QtNetwork/QNetworkAccessManager>
18 qCDebug(QGCFileDownloadLog) <<
"Created" <<
this;
21QGCFileDownload::~QGCFileDownload()
23 qCDebug(QGCFileDownloadLog) <<
"Destroying" <<
this;
31void QGCFileDownload::setAutoDecompress(
bool enabled)
33 if (_autoDecompress != enabled) {
34 _autoDecompress = enabled;
39void QGCFileDownload::setOutputPath(
const QString &path)
41 if (_outputPath != path) {
47void QGCFileDownload::setExpectedHash(
const QString &hash)
49 if (_expectedHash != hash) {
55void QGCFileDownload::setCache(QAbstractNetworkCache *cache)
57 _networkManager->setCache(cache);
60void QGCFileDownload::setTimeout(
int timeoutMs)
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);
171 _setState(State::Downloading);
177 const bool shouldEmitCancel = (_state != State::Idle && _state != State::Completed
178 && _state != State::Failed && _state != State::Cancelled);
180 if (shouldEmitCancel) {
181 _setState(State::Cancelled);
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)
204 _bytesReceived = bytesReceived;
207 if (totalBytes != _totalBytes) {
208 _totalBytes = totalBytes;
212 if (totalBytes > 0) {
213 _setProgress(
static_cast<qreal
>(bytesReceived) /
static_cast<qreal
>(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();
242 if (_state == State::Cancelled) {
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) {
268 if (_state == State::Downloading || _state == State::Verifying) {
269 _setState(State::Failed);
276 if (!reply->url().isLocalFile()) {
279 const QString
error = tr(
"HTTP error %1: %2")
282 _setErrorString(
error);
283 _setState(State::Failed);
284 _emitFinished(
false, QString(),
error);
289 qCDebug(QGCFileDownloadLog) <<
"Download finished:" << _localPath
290 <<
"size:" << QFileInfo(_localPath).size();
293 if (!_expectedHash.isEmpty()) {
294 _setState(State::Verifying);
295 if (!_verifyHash()) {
296 _setState(State::Failed);
297 _emitFinished(
false, QString(), _errorString);
304 _startDecompression();
309 _setState(State::Completed);
310 _emitFinished(
true, _localPath, QString());
313void QGCFileDownload::_onDownloadError(QNetworkReply::NetworkError code)
315 if (_state == State::Cancelled) {
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)
355 if (_state == State::Cancelled) {
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;
372 _setState(State::Completed);
373 _emitFinished(
true, _localPath, QString());
375 const QString
error = tr(
"Decompression failed: %1").arg(_decompressionJob->errorString());
376 qCWarning(QGCFileDownloadLog) <<
error;
377 _setErrorString(
error);
378 _setState(State::Failed);
381 _emitFinished(
false, _compressedFilePath,
error);
384 _compressedFilePath.clear();
391void QGCFileDownload::_setState(State newState)
393 if (_state != newState) {
394 const bool wasRunning = isRunning();
398 if (wasRunning != isRunning()) {
404void QGCFileDownload::_setProgress(qreal progress)
406 if (!qFuzzyCompare(_progress, progress)) {
407 _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;
443 emit
finished(success, localPath, errorMessage);
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);
465 _setState(State::Failed);
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;
537 _setState(State::Decompressing);
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)
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)
void finished(bool success, const QString &localPath, const QString &errorMessage)
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.
void stateChanged(State state)
Emitted when state changes.
void autoDecompressChanged(bool autoDecompress)
Emitted when auto-decompress setting changes.
bool start(const QString &remoteUrl)
void bytesReceivedChanged(qint64 bytesReceived)
Emitted when bytes received changes.
void errorStringChanged(const QString &errorString)
Emitted when error string changes.
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.
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.