QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
QGCCachedFileDownload.cc
Go to the documentation of this file.
2#include "QGCFileDownload.h"
3#include "QGCFileHelper.h"
5
6#include <QtCore/QDateTime>
7#include <QtCore/QDir>
8#include <QtCore/QFile>
9#include <QtNetwork/QNetworkDiskCache>
10#include <QtNetwork/QNetworkRequest>
11
12QGC_LOGGING_CATEGORY(QGCCachedFileDownloadLog, "Utilities.QGCCachedFileDownload")
13
14// ============================================================================
15// Construction / Destruction
16// ============================================================================
17
18QGCCachedFileDownload::QGCCachedFileDownload(const QString &cacheDirectory, QObject *parent)
19 : QObject(parent)
20 , _fileDownload(new QGCFileDownload(this))
21 , _diskCache(new QNetworkDiskCache(this))
22{
23 qCDebug(QGCCachedFileDownloadLog) << "Created with cache dir:" << cacheDirectory;
24
25 _initializeCache(cacheDirectory);
26
27 connect(_fileDownload, &QGCFileDownload::finished,
28 this, &QGCCachedFileDownload::_onDownloadFinished);
29 connect(_fileDownload, &QGCFileDownload::downloadProgress,
30 this, &QGCCachedFileDownload::_onDownloadProgress);
31}
32
33QGCCachedFileDownload::QGCCachedFileDownload(QObject *parent)
34 : QObject(parent)
35 , _fileDownload(new QGCFileDownload(this))
36 , _diskCache(new QNetworkDiskCache(this))
37{
38 qCDebug(QGCCachedFileDownloadLog) << "Created without cache dir";
39
40 connect(_fileDownload, &QGCFileDownload::finished,
41 this, &QGCCachedFileDownload::_onDownloadFinished);
42 connect(_fileDownload, &QGCFileDownload::downloadProgress,
43 this, &QGCCachedFileDownload::_onDownloadProgress);
44}
45
46QGCCachedFileDownload::~QGCCachedFileDownload()
47{
48 qCDebug(QGCCachedFileDownloadLog) << "Destroying";
49}
50
51// ============================================================================
52// Property Getters/Setters
53// ============================================================================
54
55QString QGCCachedFileDownload::cacheDirectory() const
56{
57 return _diskCache->cacheDirectory();
58}
59
60void QGCCachedFileDownload::setCacheDirectory(const QString &directory)
61{
62 if (cacheDirectory() != directory) {
63 _initializeCache(directory);
64 emit cacheDirectoryChanged(directory);
65 }
66}
67
68qint64 QGCCachedFileDownload::maxCacheSize() const
69{
70 return _diskCache->maximumCacheSize();
71}
72
73void QGCCachedFileDownload::setMaxCacheSize(qint64 bytes)
74{
75 if (maxCacheSize() != bytes) {
76 _diskCache->setMaximumCacheSize(bytes);
77 emit maxCacheSizeChanged(bytes);
78 }
79}
80
81qint64 QGCCachedFileDownload::cacheSize() const
82{
83 return _diskCache->cacheSize();
84}
85
86// ============================================================================
87// Cache Query Methods
88// ============================================================================
89
90bool QGCCachedFileDownload::isCached(const QString &url, int maxAgeSec) const
91{
92 const QNetworkCacheMetaData metadata = _diskCache->metaData(QUrl::fromUserInput(url));
93 if (!metadata.isValid()) {
94 return false;
95 }
96
97 if (maxAgeSec <= 0) {
98 return true; // Any age is valid
99 }
100
101 const QDateTime timestamp = _getCacheTimestamp(url);
102 if (!timestamp.isValid()) {
103 return false; // Age-limited checks require a valid timestamp
104 }
105
106 return timestamp.addSecs(maxAgeSec) >= QDateTime::currentDateTime();
107}
108
109QString QGCCachedFileDownload::cachedPath(const QString &url) const
110{
111 QIODevice *device = _diskCache->data(QUrl::fromUserInput(url));
112 if (device == nullptr) {
113 return {};
114 }
115
116 const QByteArray data = device->readAll();
117 device->close();
118 delete device;
119
120 const QString tempPath = QGCFileHelper::uniqueTempPath(QStringLiteral("qgc_cache_XXXXXX"));
121 if (tempPath.isEmpty()) {
122 return {};
123 }
124
125 if (!QGCFileHelper::atomicWrite(tempPath, data)) {
126 QFile::remove(tempPath);
127 return {};
128 }
129
130 return tempPath;
131}
132
133int QGCCachedFileDownload::cacheAge(const QString &url) const
134{
135 const QDateTime timestamp = _getCacheTimestamp(url);
136 if (!timestamp.isValid()) {
137 return -1;
138 }
139
140 return static_cast<int>(timestamp.secsTo(QDateTime::currentDateTime()));
141}
142
143// ============================================================================
144// Download Methods
145// ============================================================================
146
147bool QGCCachedFileDownload::download(const QString &url, int maxCacheAgeSec)
148{
149 if (_running) {
150 qCWarning(QGCCachedFileDownloadLog) << "Download already in progress";
151 return false;
152 }
153
154 if (url.isEmpty()) {
155 qCWarning(QGCCachedFileDownloadLog) << "Empty URL";
156 _setErrorString(tr("Empty URL"));
157 return false;
158 }
159
160 if (cacheDirectory().isEmpty()) {
161 qCWarning(QGCCachedFileDownloadLog) << "Cache directory not set";
162 _setErrorString(tr("Cache directory not configured"));
163 return false;
164 }
165
166 _pendingUrl = url;
167 _maxCacheAgeSec = maxCacheAgeSec;
168 _networkAttemptFailed = false;
169 _forceNetwork = false;
170 _cancelRequested = false;
171
172 _url = QUrl::fromUserInput(url);
173 emit urlChanged(_url);
174
175 // Check if we have a valid cached version
176 const QNetworkCacheMetaData metadata = _diskCache->metaData(_url);
177 if (metadata.isValid()) {
178 const QDateTime cacheTime = _getCacheTimestamp(url);
179 bool expired = false;
180 if (maxCacheAgeSec > 0) {
181 if (!cacheTime.isValid()) {
182 expired = true;
183 } else {
184 expired = cacheTime.addSecs(maxCacheAgeSec) < QDateTime::currentDateTime();
185 }
186 }
187
188 if (!expired) {
189 qCDebug(QGCCachedFileDownloadLog) << "Using cached version for:" << url;
190 return _startDownload(url, false, true); // Prefer cache
191 }
192
193 // Cache expired - try network first, will fallback to cache on failure
194 qCDebug(QGCCachedFileDownloadLog) << "Cache expired, trying network:" << url;
195 _forceNetwork = true;
196 return _startDownload(url, true, false);
197 }
198
199 // No cached version - download from network
200 qCDebug(QGCCachedFileDownloadLog) << "No cache, downloading:" << url;
201 return _startDownload(url, false, false);
202}
203
205{
206 if (_running) {
207 qCWarning(QGCCachedFileDownloadLog) << "Download already in progress";
208 return false;
209 }
210
211 if (url.isEmpty()) {
212 qCWarning(QGCCachedFileDownloadLog) << "Empty URL";
213 _setErrorString(tr("Empty URL"));
214 return false;
215 }
216
217 if (cacheDirectory().isEmpty()) {
218 qCWarning(QGCCachedFileDownloadLog) << "Cache directory not set";
219 _setErrorString(tr("Cache directory not configured"));
220 return false;
221 }
222
223 _pendingUrl = url;
224 _maxCacheAgeSec = 0;
225 _networkAttemptFailed = false;
226 _forceNetwork = false;
227 _cancelRequested = false;
228
229 _url = QUrl::fromUserInput(url);
230 emit urlChanged(_url);
231
232 return _startDownload(url, false, true);
233}
234
236{
237 if (_running) {
238 qCWarning(QGCCachedFileDownloadLog) << "Download already in progress";
239 return false;
240 }
241
242 if (url.isEmpty()) {
243 qCWarning(QGCCachedFileDownloadLog) << "Empty URL";
244 _setErrorString(tr("Empty URL"));
245 return false;
246 }
247
248 _pendingUrl = url;
249 _maxCacheAgeSec = 0;
250 _networkAttemptFailed = false;
251 _forceNetwork = true;
252 _cancelRequested = false;
253
254 _url = QUrl::fromUserInput(url);
255 emit urlChanged(_url);
256
257 return _startDownload(url, true, false);
258}
259
261{
262 if (!_running) {
263 return;
264 }
265
266 _cancelRequested = true;
267
268 if (_fileDownload != nullptr) {
269 _fileDownload->cancel();
270 }
271
272 _setErrorString(tr("Download cancelled"));
273 _setProgress(0.0);
274 _setRunning(false);
275 _pendingUrl.clear();
276 _emitFinished(false, {}, _errorString);
277}
278
279// ============================================================================
280// Cache Management
281// ============================================================================
282
284{
285 qCDebug(QGCCachedFileDownloadLog) << "Clearing cache";
286 _diskCache->clear();
287 emit cacheSizeChanged(0);
288}
289
291{
292 const bool removed = _diskCache->remove(QUrl::fromUserInput(url));
293 if (removed) {
294 emit cacheSizeChanged(cacheSize());
295 }
296 return removed;
297}
298
299// ============================================================================
300// Private Slots
301// ============================================================================
302
303void QGCCachedFileDownload::_onDownloadFinished(bool success,
304 const QString &localPath,
305 const QString &errorMessage)
306{
307 if (_cancelRequested) {
308 _cancelRequested = false;
309 return;
310 }
311
312 qCDebug(QGCCachedFileDownloadLog) << "Download finished - success:" << success
313 << "path:" << localPath
314 << "error:" << errorMessage;
315
316 if (success) {
317 // Update cache timestamp
318 _updateCacheTimestamp(_pendingUrl);
319
320 _localPath = localPath;
321 emit localPathChanged(_localPath);
322 _setFromCache(_fileDownload->lastResultFromCache());
323 _setErrorString({});
324 _setRunning(false);
325 _pendingUrl.clear();
326 _emitFinished(true, localPath, {});
327
328 } else if (_forceNetwork && !_networkAttemptFailed) {
329 // Network attempt failed, try cache fallback
330 _networkAttemptFailed = true;
331 qCDebug(QGCCachedFileDownloadLog) << "Network failed, trying cache fallback";
332
333 if (!_startDownload(_pendingUrl, false, true)) {
334 // Cache fallback also failed
335 _setErrorString(errorMessage);
336 _setRunning(false);
337 _pendingUrl.clear();
338 _emitFinished(false, {}, errorMessage);
339 }
340 return;
341
342 } else {
343 _setErrorString(errorMessage);
344 _setRunning(false);
345 _pendingUrl.clear();
346 _emitFinished(false, {}, errorMessage);
347 }
348}
349
350void QGCCachedFileDownload::_onDownloadProgress(qint64 bytesReceived, qint64 totalBytes)
351{
352 if (totalBytes > 0) {
353 _setProgress(static_cast<qreal>(bytesReceived) / static_cast<qreal>(totalBytes));
354 }
355 emit downloadProgress(bytesReceived, totalBytes);
356}
357
358// ============================================================================
359// Private Methods
360// ============================================================================
361
362void QGCCachedFileDownload::_initializeCache(const QString &directory)
363{
364 if (!directory.isEmpty()) {
365 QDir().mkpath(directory);
366 _diskCache->setCacheDirectory(directory);
367 _fileDownload->setCache(_diskCache);
368 qCDebug(QGCCachedFileDownloadLog) << "Cache initialized:" << directory;
369 }
370}
371
372void QGCCachedFileDownload::_setRunning(bool running)
373{
374 if (_running != running) {
375 _running = running;
376 emit runningChanged(running);
377 }
378}
379
380void QGCCachedFileDownload::_setProgress(qreal progress)
381{
382 if (!qFuzzyCompare(_progress, progress)) {
383 _progress = progress;
384 emit progressChanged(progress);
385 }
386}
387
388void QGCCachedFileDownload::_setErrorString(const QString &error)
389{
390 if (_errorString != error) {
391 _errorString = error;
393 }
394}
395
396void QGCCachedFileDownload::_setFromCache(bool fromCache)
397{
398 if (_fromCache != fromCache) {
399 _fromCache = fromCache;
400 emit fromCacheChanged(fromCache);
401 }
402}
403
404void QGCCachedFileDownload::_updateCacheTimestamp(const QString &url)
405{
406 QNetworkCacheMetaData metadata = _diskCache->metaData(QUrl::fromUserInput(url));
407 if (metadata.isValid()) {
408 QNetworkCacheMetaData::AttributesMap attributes = metadata.attributes();
409 attributes.insert(QNetworkRequest::Attribute::User, QDateTime::currentDateTime());
410 metadata.setAttributes(attributes);
411 _diskCache->updateMetaData(metadata);
412 }
413}
414
415QDateTime QGCCachedFileDownload::_getCacheTimestamp(const QString &url) const
416{
417 const QNetworkCacheMetaData metadata = _diskCache->metaData(QUrl::fromUserInput(url));
418 if (!metadata.isValid()) {
419 return {};
420 }
421
422 const auto &attributes = metadata.attributes();
423 const auto it = attributes.find(QNetworkRequest::Attribute::User);
424 if (it != attributes.end()) {
425 return it->toDateTime();
426 }
427
428 return {};
429}
430
431bool QGCCachedFileDownload::_startDownload(const QString &url, bool forceNetwork, bool preferCache)
432{
433 _setRunning(true);
434 _setProgress(0.0);
435 _setFromCache(false);
436
438
439 if (forceNetwork) {
440 config.requestAttributes.append({
441 QNetworkRequest::CacheLoadControlAttribute,
442 QVariant{QNetworkRequest::AlwaysNetwork},
443 });
444 } else if (preferCache) {
445 config.requestAttributes.append({
446 QNetworkRequest::CacheLoadControlAttribute,
447 QVariant{QNetworkRequest::PreferCache},
448 });
449 }
450
451 if (!_fileDownload->start(url, config)) {
452 _setRunning(false);
453 _setFromCache(false);
454 return false;
455 }
456
457 return true;
458}
459
460void QGCCachedFileDownload::_emitFinished(bool success,
461 const QString &path,
462 const QString &error)
463{
464 emit finished(success, path, error, _fromCache);
465}
Cached file download with time-based expiration and fallback support.
Error error
Unified file download utility with decompression, verification, and QML support.
#define QGC_LOGGING_CATEGORY(name, categoryStr)
void maxCacheSizeChanged(qint64 bytes)
Emitted when max cache size changes.
bool downloadPreferCache(const QString &url)
void urlChanged(const QUrl &url)
Emitted when URL changes.
void cancel()
Cancel current download.
bool removeFromCache(const QString &url)
void downloadProgress(qint64 bytesReceived, qint64 totalBytes)
Emitted during download with byte counts.
void localPathChanged(const QString &localPath)
Emitted when local path changes.
void cacheDirectoryChanged(const QString &directory)
Emitted when cache directory changes.
void cacheSizeChanged(qint64 bytes)
Emitted when cache size changes.
void progressChanged(qreal progress)
Emitted when download progress changes.
void clearCache()
Clear all cached files.
void errorStringChanged(const QString &errorString)
Emitted when error string changes.
bool downloadNoCache(const QString &url)
bool download(const QString &url, int maxCacheAgeSec)
void runningChanged(bool running)
Emitted when running state changes.
void finished(bool success, const QString &localPath, const QString &errorMessage, bool fromCache)
void fromCacheChanged(bool fromCache)
Emitted when fromCache state changes.
void finished(bool success, const QString &localPath, const QString &errorMessage)
bool start(const QString &remoteUrl)
void cancel()
Cancel current download.
void downloadProgress(qint64 bytesReceived, qint64 totalBytes)
Emitted during download with byte counts.
QString uniqueTempPath(const QString &templateName)
bool atomicWrite(const QString &filePath, const QByteArray &data)
QString errorMessage(const QNetworkReply *reply)
Common request configuration options.
QList< QPair< QNetworkRequest::Attribute, QVariant > > requestAttributes