QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
QGCFileDownload.cc
Go to the documentation of this file.
1#include "QGCFileDownload.h"
2#include "QGCCompression.h"
3#include "QGCCompressionJob.h"
4#include "QGCFileHelper.h"
6
7#include <QtCore/QFile>
8#include <QtCore/QFileInfo>
9#include <QtCore/QStandardPaths>
10#include <QtNetwork/QNetworkAccessManager>
11
12QGC_LOGGING_CATEGORY(QGCFileDownloadLog, "Utilities.QGCFileDownload")
13
15 : QObject(parent)
16 , _networkManager(QGCNetworkHelper::createNetworkManager(this))
17{
18 qCDebug(QGCFileDownloadLog) << "Created" << this;
19}
20
22{
23 qCDebug(QGCFileDownloadLog) << "Destroying" << this;
24 cancel();
25}
26
27// ============================================================================
28// Property Setters
29// ============================================================================
30
32{
33 if (_autoDecompress != enabled) {
34 _autoDecompress = enabled;
35 emit autoDecompressChanged(enabled);
36 }
37}
38
39void QGCFileDownload::setOutputPath(const QString &path)
40{
41 if (_outputPath != path) {
42 _outputPath = path;
43 emit outputPathChanged(path);
44 }
45}
46
47void QGCFileDownload::setExpectedHash(const QString &hash)
48{
49 if (_expectedHash != hash) {
50 _expectedHash = hash;
51 emit expectedHashChanged(hash);
52 }
53}
54
55void QGCFileDownload::setCache(QAbstractNetworkCache *cache)
56{
57 _networkManager->setCache(cache);
58}
59
60void QGCFileDownload::setTimeout(int timeoutMs)
61{
62 _timeoutMs = timeoutMs;
63}
64
65// ============================================================================
66// Public Slots
67// ============================================================================
68
69bool QGCFileDownload::start(const QString &remoteUrl)
70{
72 config.timeoutMs = _timeoutMs;
73 config.allowRedirects = true;
74 return start(remoteUrl, config);
75}
76
78{
79 if (isRunning()) {
80 qCWarning(QGCFileDownloadLog) << "Download already in progress";
81 return false;
82 }
83
84 if (remoteUrl.isEmpty()) {
85 qCWarning(QGCFileDownloadLog) << "Empty URL provided";
86 _setErrorString(tr("Empty URL"));
87 return false;
88 }
89
90 // Parse URL
91 QUrl url;
92 if (QGCFileHelper::isLocalPath(remoteUrl)) {
93 url = QUrl::fromLocalFile(QGCFileHelper::toLocalPath(remoteUrl));
94 } else if (remoteUrl.startsWith(QLatin1String("http:")) || remoteUrl.startsWith(QLatin1String("https:"))) {
95 url.setUrl(remoteUrl);
96 } else {
97 // Assume it's a local file path
98 url = QUrl::fromLocalFile(remoteUrl);
99 }
100
101 if (!url.isValid()) {
102 qCWarning(QGCFileDownloadLog) << "Invalid URL:" << remoteUrl;
103 _setErrorString(tr("Invalid URL: %1").arg(remoteUrl));
104 return false;
105 }
106
107 // Reset state
108 _cleanup();
109 _url = url;
110 emit urlChanged(_url);
111 _bytesReceived = 0;
112 emit bytesReceivedChanged(0);
113 _totalBytes = -1;
114 emit totalBytesChanged(-1);
115 _setProgress(0.0);
116 _setErrorString(QString());
117 _finishEmitted = false;
118 _lastResultFromCache = false;
119
120 // Determine output path
121 _localPath = _generateOutputPath(remoteUrl);
122 if (_localPath.isEmpty()) {
123 _setErrorString(tr("Unable to determine output path"));
124 return false;
125 }
126 emit localPathChanged(_localPath);
127
128 // Ensure parent directory exists
129 if (!QGCFileHelper::ensureParentExists(_localPath)) {
130 _setErrorString(tr("Cannot create output directory"));
131 return false;
132 }
133
134 // Open output file for streaming write
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()));
138 delete _outputFile;
139 _outputFile = nullptr;
140 return false;
141 }
142
143 // Create request with configuration
144 QNetworkRequest request = QGCNetworkHelper::createRequest(url, config);
145
146 qCDebug(QGCFileDownloadLog) << "Starting download:"
147 << url.toDisplayString(QUrl::RemoveUserInfo | QUrl::RemoveQuery | QUrl::RemoveFragment)
148 << "to" << _localPath;
149
150 // Start download
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();
156 delete _outputFile;
157 _outputFile = nullptr;
158 return false;
159 }
160
162
163 // Connect signals for streaming download
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);
172
173 _setState(State::Downloading);
174 return true;
175}
176
178{
179 const bool shouldEmitCancel = (_state != State::Idle && _state != State::Completed
180 && _state != State::Failed && _state != State::Cancelled);
181
182 if (shouldEmitCancel) {
183 _setState(State::Cancelled);
184 _setErrorString(tr("Download cancelled"));
185 _emitFinished(false, QString(), _errorString);
186 }
187
188 if (_currentReply != nullptr) {
189 qCDebug(QGCFileDownloadLog) << "Cancelling download";
190 _currentReply->abort();
191 }
192
193 if (_decompressionJob != nullptr && _decompressionJob->isRunning()) {
194 _decompressionJob->cancel();
195 }
196
197 _cleanup();
198}
199
200// ============================================================================
201// Private Slots
202// ============================================================================
203
204void QGCFileDownload::_onDownloadProgress(qint64 bytesReceived, qint64 totalBytes)
205{
206 _bytesReceived = bytesReceived;
208
209 if (totalBytes != _totalBytes) {
210 _totalBytes = totalBytes;
212 }
213
214 if (totalBytes > 0) {
215 _setProgress(static_cast<qreal>(bytesReceived) / static_cast<qreal>(totalBytes));
216 }
217
219}
220
221void QGCFileDownload::_onReadyRead()
222{
223 if (_currentReply == nullptr || _outputFile == nullptr) {
224 return;
225 }
226
227 // Stream data directly to file (memory-efficient)
228 const QByteArray data = _currentReply->readAll();
229 if (!_writeReplyData(data)) {
230 _failForWriteError(QStringLiteral("readyRead"));
231 }
232}
233
234void QGCFileDownload::_onDownloadFinished()
235{
236 QNetworkReply *reply = _currentReply;
237 _currentReply = nullptr;
238
239 if (reply == nullptr) {
240 return;
241 }
242 reply->deleteLater();
243
244 if (_state == State::Cancelled) {
245 _cleanup();
246 return;
247 }
248
249 _lastResultFromCache = reply->attribute(QNetworkRequest::SourceIsFromCacheAttribute).toBool();
250
251 // Close output file
252 if (_outputFile != nullptr) {
253 // Write any remaining data
254 const QByteArray remaining = reply->readAll();
255 if (!_writeReplyData(remaining)) {
256 _outputFile->close();
257 delete _outputFile;
258 _outputFile = nullptr;
259 _failForWriteError(QStringLiteral("finished"));
260 return;
261 }
262 _outputFile->close();
263 delete _outputFile;
264 _outputFile = nullptr;
265 }
266
267 // Check for errors
268 if (reply->error() != QNetworkReply::NoError) {
269 // Error already handled in _onDownloadError
270 if (_state == State::Downloading || _state == State::Verifying) {
271 _setState(State::Failed);
272 _emitFinished(false, QString(), QGCNetworkHelper::errorMessage(reply));
273 }
274 return;
275 }
276
277 // Check HTTP status for non-local files
278 if (!reply->url().isLocalFile()) {
279 const int statusCode = QGCNetworkHelper::httpStatusCode(reply);
280 if (!QGCNetworkHelper::isHttpSuccess(statusCode)) {
281 const QString error = tr("HTTP error %1: %2")
282 .arg(statusCode)
283 .arg(QGCNetworkHelper::httpStatusText(statusCode));
284 _setErrorString(error);
285 _setState(State::Failed);
286 _emitFinished(false, QString(), error);
287 return;
288 }
289 }
290
291 qCDebug(QGCFileDownloadLog) << "Download finished:" << _localPath
292 << "size:" << QFileInfo(_localPath).size();
293
294 // Verify hash if expected
295 if (!_expectedHash.isEmpty()) {
296 _setState(State::Verifying);
297 if (!_verifyHash()) {
298 _setState(State::Failed);
299 _emitFinished(false, QString(), _errorString);
300 return;
301 }
302 }
303
304 // Auto-decompress if enabled and file is compressed
305 if (_autoDecompress && QGCCompression::isCompressedFile(_localPath)) {
306 _startDecompression();
307 return;
308 }
309
310 // Success!
311 _setState(State::Completed);
312 _emitFinished(true, _localPath, QString());
313}
314
315void QGCFileDownload::_onDownloadError(QNetworkReply::NetworkError code)
316{
317 if (_state == State::Cancelled) {
318 return;
319 }
320
321 QString errorMsg;
322
323 switch (code) {
324 case QNetworkReply::OperationCanceledError:
325 errorMsg = tr("Download cancelled");
326 break;
327 case QNetworkReply::ContentNotFoundError:
328 errorMsg = tr("File not found (404)");
329 break;
330 case QNetworkReply::TimeoutError:
331 errorMsg = tr("Connection timed out");
332 break;
333 case QNetworkReply::HostNotFoundError:
334 errorMsg = tr("Host not found");
335 break;
336 case QNetworkReply::ConnectionRefusedError:
337 errorMsg = tr("Connection refused");
338 break;
339 case QNetworkReply::SslHandshakeFailedError:
340 errorMsg = tr("SSL handshake failed");
341 break;
342 default:
343 if (_currentReply != nullptr) {
344 errorMsg = QGCNetworkHelper::errorMessage(_currentReply);
345 } else {
346 errorMsg = tr("Network error: %1").arg(code);
347 }
348 break;
349 }
350
351 qCWarning(QGCFileDownloadLog) << "Download error:" << errorMsg << "url:"
352 << _url.toDisplayString(QUrl::RemoveUserInfo | QUrl::RemoveQuery | QUrl::RemoveFragment);
353 _setErrorString(errorMsg);
354}
355
356void QGCFileDownload::_onDecompressionFinished(bool success)
357{
358 if (_state == State::Cancelled) {
359 _compressedFilePath.clear();
360 return;
361 }
362
363 if (success) {
364 const QString decompressedPath = _decompressionJob->outputPath();
365 qCDebug(QGCFileDownloadLog) << "Decompression completed:" << decompressedPath;
366
367 // Remove compressed file if different from output
368 if (_compressedFilePath != decompressedPath && QFile::exists(_compressedFilePath)) {
369 QFile::remove(_compressedFilePath);
370 }
371
372 _localPath = decompressedPath;
373 emit localPathChanged(_localPath);
374
375 _setState(State::Completed);
376 _emitFinished(true, _localPath, QString());
377 } else {
378 const QString error = tr("Decompression failed: %1").arg(_decompressionJob->errorString());
379 qCWarning(QGCFileDownloadLog) << error;
380 _setErrorString(error);
381 _setState(State::Failed);
382
383 // Return compressed file path on decompression failure
384 _emitFinished(false, _compressedFilePath, error);
385 }
386
387 _compressedFilePath.clear();
388}
389
390// ============================================================================
391// Private Methods
392// ============================================================================
393
394void QGCFileDownload::_setState(State newState)
395{
396 if (_state != newState) {
397 const bool wasRunning = isRunning();
398 _state = newState;
399 emit stateChanged(newState);
400
401 if (wasRunning != isRunning()) {
403 }
404 }
405}
406
407void QGCFileDownload::_setProgress(qreal progress)
408{
409 if (!qFuzzyCompare(_progress, progress)) {
410 _progress = progress;
412 }
413}
414
415void QGCFileDownload::_setErrorString(const QString &error)
416{
417 if (_errorString != error) {
418 _errorString = error;
420 }
421}
422
423void QGCFileDownload::_cleanup()
424{
425 if (_currentReply != nullptr) {
426 _currentReply->disconnect(this);
427 _currentReply->deleteLater();
428 _currentReply = nullptr;
429 }
430
431 if (_outputFile != nullptr) {
432 if (_outputFile->isOpen()) {
433 _outputFile->close();
434 }
435 delete _outputFile;
436 _outputFile = nullptr;
437 }
438}
439
440void QGCFileDownload::_emitFinished(bool success, const QString &localPath, const QString &errorMessage)
441{
442 if (_finishEmitted) {
443 return;
444 }
445 _finishEmitted = true;
446 emit finished(success, localPath, errorMessage);
447}
448
449bool QGCFileDownload::_writeReplyData(const QByteArray &data)
450{
451 if (data.isEmpty()) {
452 return true;
453 }
454
455 if (_outputFile == nullptr) {
456 return false;
457 }
458
459 return _outputFile->write(data) == data.size();
460}
461
462bool QGCFileDownload::_failForWriteError(const QString &context)
463{
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);
468 _setState(State::Failed);
469
470 if (_currentReply != nullptr) {
471 _currentReply->disconnect(this);
472 _currentReply->abort();
473 }
474
475 _cleanup();
476 _emitFinished(false, QString(), error);
477 return false;
478}
479
480QString QGCFileDownload::_generateOutputPath(const QString &remoteUrl) const
481{
482 // Use custom output path if set
483 if (!_outputPath.isEmpty()) {
484 return _outputPath;
485 }
486
487 // Extract filename from URL
488 QString fileName = QUrl(remoteUrl).fileName();
489 if (fileName.isEmpty()) {
490 fileName = QStringLiteral("DownloadedFile");
491 }
492
493 // Strip query parameters
494 const int queryIndex = fileName.indexOf(QLatin1Char('?'));
495 if (queryIndex != -1) {
496 fileName = fileName.left(queryIndex);
497 }
498
499 // Find writable directory
500 QString downloadDir = QStandardPaths::writableLocation(QStandardPaths::TempLocation);
501 if (downloadDir.isEmpty()) {
502 downloadDir = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation);
503 }
504
505 if (downloadDir.isEmpty()) {
506 qCWarning(QGCFileDownloadLog) << "No writable download location found";
507 return QString();
508 }
509
510 return QGCFileHelper::joinPath(downloadDir, fileName);
511}
512
513bool QGCFileDownload::_verifyHash()
514{
515 qCDebug(QGCFileDownloadLog) << "Verifying hash for:" << _localPath;
516
517 const QString actualHash = QGCFileHelper::computeFileHash(_localPath);
518 if (actualHash.isEmpty()) {
519 _setErrorString(tr("Failed to compute file hash"));
520 return false;
521 }
522
523 if (actualHash.compare(_expectedHash, Qt::CaseInsensitive) != 0) {
524 _setErrorString(tr("Hash verification failed. Expected: %1, Got: %2")
525 .arg(_expectedHash, actualHash));
526 return false;
527 }
528
529 qCDebug(QGCFileDownloadLog) << "Hash verified successfully";
530 return true;
531}
532
533void QGCFileDownload::_startDecompression()
534{
535 _compressedFilePath = _localPath;
536 const QString decompressedPath = QGCCompression::strippedPath(_localPath);
537
538 qCDebug(QGCFileDownloadLog) << "Starting decompression:" << _localPath << "->" << decompressedPath;
539
540 _setState(State::Decompressing);
541
542 if (_decompressionJob == nullptr) {
543 _decompressionJob = new QGCCompressionJob(this);
544 connect(_decompressionJob, &QGCCompressionJob::progressChanged,
546 connect(_decompressionJob, &QGCCompressionJob::finished,
547 this, &QGCFileDownload::_onDecompressionFinished);
548 }
549
550 _decompressionJob->decompressFile(_localPath, decompressedPath);
551}
Config config
QObject wrapper for async compression operations using QtConcurrent/QPromise.
Error error
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.
@ Failed
Download failed.
@ 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.
bool isRunning() const
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)
qreal progress() const
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.