QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
QGCFileHelper.cc
Go to the documentation of this file.
1#include "QGCFileHelper.h"
2
3#include "QGCCompression.h"
5
6#include <QtCore/QCryptographicHash>
7#include <QtCore/QDir>
8#include <QtCore/QFile>
9#include <QtCore/QFileInfo>
10#include <QtCore/QLoggingCategory>
11#include <QtCore/QSaveFile>
12#include <QtCore/QStandardPaths>
13#include <QtCore/QStorageInfo>
14#include <QtCore/QTemporaryFile>
15#include <QtCore/QUrl>
16
17Q_DECLARE_LOGGING_CATEGORY(QGCFileHelperLog)
18Q_LOGGING_CATEGORY(QGCFileHelperLog, "Utilities.QGCFileHelper")
19
20namespace QGCFileHelper {
21
22QByteArray readFile(const QString &filePath, QString *errorString, qint64 maxBytes)
23{
24 if (filePath.isEmpty()) {
25 if (errorString != nullptr) {
26 *errorString = QObject::tr("File path is empty");
27 }
28 return {};
29 }
30
32 QGCDecompressDevice decompressor(filePath);
33 if (!decompressor.open(QIODevice::ReadOnly)) {
34 if (errorString != nullptr) {
35 *errorString = QObject::tr("Failed to open compressed file: %1").arg(filePath);
36 }
37 return {};
38 }
39
40 QByteArray data;
41 if (maxBytes > 0) {
42 data = decompressor.read(maxBytes);
43 } else {
44 data = decompressor.readAll();
45 }
46 decompressor.close();
47 return data;
48 }
49
50 QFile file(filePath);
51 if (!file.open(QIODevice::ReadOnly)) {
52 if (errorString != nullptr) {
53 *errorString = QObject::tr("Failed to open file: %1 - %2").arg(filePath, file.errorString());
54 }
55 return {};
56 }
57
58 QByteArray data;
59 if (maxBytes > 0) {
60 data = file.read(maxBytes);
61 } else {
62 data = file.readAll();
63 }
64 file.close();
65 return data;
66}
67
68size_t optimalBufferSize(const QString &path)
69{
70 // Cache the result - only compute once per process
71 static size_t cachedSize = 0;
72 if (cachedSize != 0) {
73 return cachedSize;
74 }
75
76 qint64 blockSize = 0;
77
78 // Try to get block size from specified path
79 if (!path.isEmpty()) {
80 QStorageInfo storage(path);
81 if (storage.isValid()) {
82 blockSize = storage.blockSize();
83 }
84 }
85
86 // Fallback: use root filesystem
87 if (blockSize <= 0) {
88 blockSize = QStorageInfo::root().blockSize();
89 }
90
91 if (blockSize <= 0) {
92 cachedSize = kBufferSizeDefault;
93 } else {
94 // Use 16 blocks, clamped to min/max bounds
95 // Typical: 4KB block × 16 = 64KB
96 cachedSize = static_cast<size_t>(
97 qBound(static_cast<qint64>(kBufferSizeMin),
98 blockSize * 16,
99 static_cast<qint64>(kBufferSizeMax))
100 );
101 }
102
103 return cachedSize;
104}
105
106bool exists(const QString &path)
107{
108 return isQtResource(path) || QFile::exists(path);
109}
110
111QString joinPath(const QString &dir, const QString &name)
112{
113 if (dir.isEmpty()) {
114 return name;
115 }
116 if (dir.endsWith(QLatin1Char('/'))) {
117 return dir + name;
118 }
119 return dir + QLatin1Char('/') + name;
120}
121
122bool ensureDirectoryExists(const QString &path)
123{
124 QDir dir(path);
125 if (dir.exists()) {
126 return true;
127 }
128 return dir.mkpath(path);
129}
130
131bool ensureParentExists(const QString &filePath)
132{
133 return ensureDirectoryExists(QFileInfo(filePath).absolutePath());
134}
135
136bool copyDirectoryRecursively(const QString &sourcePath, const QString &destPath)
137{
138 QDir sourceDir(sourcePath);
139 if (!sourceDir.exists()) {
140 qCWarning(QGCFileHelperLog) << "Source directory doesn't exist:" << sourcePath;
141 return false;
142 }
143
144 if (!ensureDirectoryExists(destPath)) {
145 qCWarning(QGCFileHelperLog) << "Failed to create destination directory:" << destPath;
146 return false;
147 }
148
149 // Copy files
150 const QStringList files = sourceDir.entryList(QDir::Files | QDir::NoDotAndDotDot);
151 for (const QString &fileName : files) {
152 const QString srcFilePath = joinPath(sourcePath, fileName);
153 const QString dstFilePath = joinPath(destPath, fileName);
154 if (!QFile::copy(srcFilePath, dstFilePath)) {
155 qCWarning(QGCFileHelperLog) << "Failed to copy file:" << srcFilePath << "to" << dstFilePath;
156 return false;
157 }
158 }
159
160 // Recursively copy subdirectories
161 const QStringList dirs = sourceDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
162 for (const QString &dirName : dirs) {
163 const QString srcSubDir = joinPath(sourcePath, dirName);
164 const QString dstSubDir = joinPath(destPath, dirName);
165 if (!copyDirectoryRecursively(srcSubDir, dstSubDir)) {
166 return false;
167 }
168 }
169
170 return true;
171}
172
173bool moveFileOrCopy(const QString &sourcePath, const QString &destPath)
174{
175 if (sourcePath.isEmpty() || destPath.isEmpty()) {
176 qCWarning(QGCFileHelperLog) << "moveFileOrCopy: empty path";
177 return false;
178 }
179
180 // Try atomic rename first (only works on same filesystem)
181 if (QFile::rename(sourcePath, destPath)) {
182 return true;
183 }
184
185 // Rename failed - fall back to copy + delete
186 qCDebug(QGCFileHelperLog) << "moveFileOrCopy: rename failed, using copy for:" << sourcePath;
187
188 const bool isDir = QFileInfo(sourcePath).isDir();
189 bool copySuccess = false;
190
191 if (isDir) {
192 copySuccess = copyDirectoryRecursively(sourcePath, destPath);
193 if (copySuccess) {
194 QDir(sourcePath).removeRecursively();
195 }
196 } else {
197 copySuccess = QFile::copy(sourcePath, destPath);
198 if (copySuccess) {
199 QFile::remove(sourcePath);
200 }
201 }
202
203 if (!copySuccess) {
204 qCWarning(QGCFileHelperLog) << "moveFileOrCopy: failed to move:" << sourcePath << "to" << destPath;
205 }
206
207 return copySuccess;
208}
209
210bool atomicWrite(const QString &filePath, const QByteArray &data)
211{
212 if (filePath.isEmpty()) {
213 qCWarning(QGCFileHelperLog) << "atomicWrite: file path is empty";
214 return false;
215 }
216
217 // Ensure parent directory exists
218 if (!ensureParentExists(filePath)) {
219 qCWarning(QGCFileHelperLog) << "atomicWrite: failed to create parent directory for:" << filePath;
220 return false;
221 }
222
223 // QSaveFile writes to temp file, then atomically renames on commit()
224 QSaveFile file(filePath);
225 if (!file.open(QIODevice::WriteOnly)) {
226 qCWarning(QGCFileHelperLog) << "atomicWrite: failed to open:" << filePath
227 << "-" << file.errorString();
228 return false;
229 }
230
231 if (file.write(data) != data.size()) {
232 qCWarning(QGCFileHelperLog) << "atomicWrite: write failed:" << file.errorString();
233 file.cancelWriting();
234 return false;
235 }
236
237 if (!file.commit()) {
238 qCWarning(QGCFileHelperLog) << "atomicWrite: commit failed:" << file.errorString();
239 return false;
240 }
241
242 return true;
243}
244
245qint64 availableDiskSpace(const QString &path)
246{
247 if (path.isEmpty()) {
248 return -1;
249 }
250
251 // Get storage info for the path
252 QStorageInfo storage(path);
253 if (!storage.isValid()) {
254 // Try parent directory if path doesn't exist yet
255 storage = QStorageInfo(QFileInfo(path).absolutePath());
256 }
257
258 if (!storage.isValid()) {
259 qCDebug(QGCFileHelperLog) << "availableDiskSpace: cannot determine storage for:" << path;
260 return -1;
261 }
262
263 return storage.bytesAvailable();
264}
265
266bool hasSufficientDiskSpace(const QString &path, qint64 requiredBytes, double margin)
267{
268 if (requiredBytes <= 0) {
269 return true; // Nothing to check or unknown size
270 }
271
272 const qint64 bytesAvailable = availableDiskSpace(path);
273 if (bytesAvailable < 0) {
274 qCDebug(QGCFileHelperLog) << "hasSufficientDiskSpace: cannot determine disk space, proceeding anyway";
275 return true; // Proceed if we can't determine space
276 }
277
278 const qint64 bytesRequired = static_cast<qint64>(static_cast<double>(requiredBytes) * margin);
279
280 if (bytesAvailable < bytesRequired) {
281 const int marginPercent = static_cast<int>((margin - 1.0) * 100);
282 qCWarning(QGCFileHelperLog) << "Insufficient disk space:"
283 << "required" << bytesRequired << "bytes"
284 << "(" << requiredBytes << "+" << marginPercent << "% margin)"
285 << "available" << bytesAvailable << "bytes";
286 return false;
287 }
288
289 return true;
290}
291
292// ============================================================================
293// URL/Path Utilities
294// ============================================================================
295
296QString toLocalPath(const QString &urlOrPath)
297{
298 if (urlOrPath.isEmpty()) {
299 return urlOrPath;
300 }
301
302 // Already a Qt resource path in :/ format (qrc:// URLs need conversion below)
303 if (urlOrPath.startsWith(QLatin1String(":/"))) {
304 return urlOrPath;
305 }
306
307 // Check if it's a URL (handles qrc:// -> :/ conversion)
308 QUrl url(urlOrPath);
309 if (url.isValid() && !url.scheme().isEmpty()) {
310 return toLocalPath(url);
311 }
312
313 // Plain path - return as-is
314 return urlOrPath;
315}
316
317QString toLocalPath(const QUrl &url)
318{
319 if (!url.isValid()) {
320 // QUrl(":/resource") is invalid but still carries the usable path.
321 const QString path = url.path();
322 if (!path.isEmpty()) {
323 return path;
324 }
325 return {};
326 }
327
328 const QString scheme = url.scheme().toLower();
329
330 // file:// URL
331 if (scheme == QLatin1String("file")) {
332 return url.toLocalFile();
333 }
334
335 // qrc:// URL -> :/ resource path
336 if (scheme == QLatin1String("qrc")) {
337 QString path = url.path();
338 if (!path.startsWith(QLatin1Char('/'))) {
339 path.prepend(QLatin1Char('/'));
340 }
341 return QLatin1String(":/") + path.mid(1); // qrc:/foo -> :/foo
342 }
343
344 // No scheme or unknown scheme - might be a path
345 if (scheme.isEmpty()) {
346 return url.path();
347 }
348
349 // Network or other URL - return the URL string
350 qCDebug(QGCFileHelperLog) << "toLocalPath: URL scheme not supported for local access:" << scheme;
351 return url.toString();
352}
353
354bool isLocalPath(const QString &urlOrPath)
355{
356 if (urlOrPath.isEmpty()) {
357 return false;
358 }
359
360 // Qt resource paths are "local" (accessible via QFile)
361 if (isQtResource(urlOrPath)) {
362 return true;
363 }
364
365 // Check if it looks like a URL
366 QUrl url(urlOrPath);
367 if (url.isValid() && !url.scheme().isEmpty()) {
368 const QString scheme = url.scheme().toLower();
369 return scheme == QLatin1String("file") ||
370 scheme == QLatin1String("qrc");
371 }
372
373 // Plain filesystem path
374 return true;
375}
376
377bool isQtResource(const QString &path)
378{
379 return path.startsWith(QLatin1String(":/")) ||
380 path.startsWith(QLatin1String("qrc:/"), Qt::CaseInsensitive);
381}
382
383// ============================================================================
384// Checksum Utilities
385// ============================================================================
386
387QString computeFileHash(const QString &filePath, QCryptographicHash::Algorithm algorithm)
388{
389 if (filePath.isEmpty()) {
390 qCWarning(QGCFileHelperLog) << "computeFileHash: empty file path";
391 return QString();
392 }
393
394 QFile file(filePath);
395 if (!file.open(QIODevice::ReadOnly)) {
396 qCWarning(QGCFileHelperLog) << "computeFileHash: failed to open file:" << filePath
397 << "-" << file.errorString();
398 return QString();
399 }
400
401 QCryptographicHash hash(algorithm);
402 if (!hash.addData(&file)) {
403 qCWarning(QGCFileHelperLog) << "computeFileHash: failed to read file:" << filePath;
404 return QString();
405 }
406
407 return QString::fromLatin1(hash.result().toHex());
408}
409
410QString computeDecompressedFileHash(const QString &filePath, QCryptographicHash::Algorithm algorithm)
411{
412 if (filePath.isEmpty()) {
413 qCWarning(QGCFileHelperLog) << "computeDecompressedFileHash: empty file path";
414 return {};
415 }
416
417 if (!QGCCompression::isCompressedFile(filePath)) {
418 return computeFileHash(filePath, algorithm);
419 }
420
421 QGCDecompressDevice decompressor(filePath);
422 if (!decompressor.open(QIODevice::ReadOnly)) {
423 qCWarning(QGCFileHelperLog) << "computeDecompressedFileHash: failed to open:" << filePath;
424 return {};
425 }
426
427 QCryptographicHash hash(algorithm);
428 constexpr qint64 chunkSize = 65536;
429 QByteArray buffer;
430
431 while (true) {
432 buffer = decompressor.read(chunkSize);
433 if (buffer.isEmpty()) {
434 if (decompressor.atEnd()) {
435 break;
436 }
437
438 qCWarning(QGCFileHelperLog) << "computeDecompressedFileHash: read error";
439 decompressor.close();
440 return {};
441 }
442
443 hash.addData(buffer);
444 }
445
446 decompressor.close();
447 return QString::fromLatin1(hash.result().toHex());
448}
449
450QString computeHash(const QByteArray &data, QCryptographicHash::Algorithm algorithm)
451{
452 return QString::fromLatin1(QCryptographicHash::hash(data, algorithm).toHex());
453}
454
455bool verifyFileHash(const QString &filePath, const QString &expectedHash,
456 QCryptographicHash::Algorithm algorithm)
457{
458 if (expectedHash.isEmpty()) {
459 qCWarning(QGCFileHelperLog) << "verifyFileHash: empty expected hash";
460 return false;
461 }
462
463 const QString actualHash = computeFileHash(filePath, algorithm);
464 if (actualHash.isEmpty()) {
465 return false; // Error already logged
466 }
467
468 const bool match = (actualHash.compare(expectedHash, Qt::CaseInsensitive) == 0);
469 if (!match) {
470 qCWarning(QGCFileHelperLog) << "verifyFileHash: hash mismatch for" << filePath
471 << "- expected:" << expectedHash.left(16) << "..."
472 << "actual:" << actualHash.left(16) << "...";
473 }
474 return match;
475}
476
477QString hashAlgorithmName(QCryptographicHash::Algorithm algorithm)
478{
479 switch (algorithm) {
480 case QCryptographicHash::Md4: return QStringLiteral("MD4");
481 case QCryptographicHash::Md5: return QStringLiteral("MD5");
482 case QCryptographicHash::Sha1: return QStringLiteral("SHA-1");
483 case QCryptographicHash::Sha224: return QStringLiteral("SHA-224");
484 case QCryptographicHash::Sha256: return QStringLiteral("SHA-256");
485 case QCryptographicHash::Sha384: return QStringLiteral("SHA-384");
486 case QCryptographicHash::Sha512: return QStringLiteral("SHA-512");
487 case QCryptographicHash::Sha3_224: return QStringLiteral("SHA3-224");
488 case QCryptographicHash::Sha3_256: return QStringLiteral("SHA3-256");
489 case QCryptographicHash::Sha3_384: return QStringLiteral("SHA3-384");
490 case QCryptographicHash::Sha3_512: return QStringLiteral("SHA3-512");
491 case QCryptographicHash::Blake2b_160: return QStringLiteral("BLAKE2b-160");
492 case QCryptographicHash::Blake2b_256: return QStringLiteral("BLAKE2b-256");
493 case QCryptographicHash::Blake2b_384: return QStringLiteral("BLAKE2b-384");
494 case QCryptographicHash::Blake2b_512: return QStringLiteral("BLAKE2b-512");
495 case QCryptographicHash::Blake2s_128: return QStringLiteral("BLAKE2s-128");
496 case QCryptographicHash::Blake2s_160: return QStringLiteral("BLAKE2s-160");
497 case QCryptographicHash::Blake2s_224: return QStringLiteral("BLAKE2s-224");
498 case QCryptographicHash::Blake2s_256: return QStringLiteral("BLAKE2s-256");
499 default: return QStringLiteral("Unknown");
500 }
501}
502
503// ============================================================================
504// Temporary File Utilities
505// ============================================================================
506
507namespace {
508
511QString normalizeTemplateName(const QString &templateName)
512{
513 QString name = templateName.isEmpty() ? QStringLiteral("qgc_XXXXXX") : templateName;
514
515 if (!name.contains(QLatin1String("XXXXXX"))) {
516 const qsizetype dotPos = name.lastIndexOf(QLatin1Char('.'));
517 if (dotPos > 0) {
518 name.insert(dotPos, QLatin1String("_XXXXXX"));
519 } else {
520 name.append(QLatin1String("_XXXXXX"));
521 }
522 }
523
524 return name;
525}
526
527} // namespace
528
530{
531 return QStandardPaths::writableLocation(QStandardPaths::TempLocation);
532}
533
534QString uniqueTempPath(const QString &templateName)
535{
536 const QString name = normalizeTemplateName(templateName);
537
538 QTemporaryFile temp(tempDirectory() + QLatin1Char('/') + name);
539 if (!temp.open()) {
540 qCWarning(QGCFileHelperLog) << "uniqueTempPath: failed to create temp file";
541 return {};
542 }
543
544 // Get the path before QTemporaryFile closes and removes it
545 const QString path = temp.fileName();
546 temp.close();
547 QFile::remove(path); // Remove so caller can create it themselves
548
549 return path;
550}
551
552std::unique_ptr<QTemporaryFile> createTempFile(const QByteArray &data, const QString &templateName)
553{
554 const QString name = normalizeTemplateName(templateName);
555
556 auto temp = std::make_unique<QTemporaryFile>(tempDirectory() + QLatin1Char('/') + name);
557 if (!temp->open()) {
558 qCWarning(QGCFileHelperLog) << "createTempFile: failed to create temp file";
559 return nullptr;
560 }
561
562 if (temp->write(data) != data.size()) {
563 qCWarning(QGCFileHelperLog) << "createTempFile: failed to write data";
564 return nullptr;
565 }
566
567 // Seek to beginning for reading
568 temp->seek(0);
569
570 qCDebug(QGCFileHelperLog) << "createTempFile: created" << temp->fileName()
571 << "with" << data.size() << "bytes";
572 return temp;
573}
574
575std::unique_ptr<QTemporaryFile> createTempCopy(const QString &sourcePath, const QString &templateName)
576{
577 if (sourcePath.isEmpty()) {
578 qCWarning(QGCFileHelperLog) << "createTempCopy: source path is empty";
579 return nullptr;
580 }
581
582 QFile source(sourcePath);
583 if (!source.open(QIODevice::ReadOnly)) {
584 qCWarning(QGCFileHelperLog) << "createTempCopy: failed to open source:" << sourcePath
585 << "-" << source.errorString();
586 return nullptr;
587 }
588
589 QString name = templateName;
590 if (name.isEmpty()) {
591 // Use source filename as base
592 name = QFileInfo(sourcePath).fileName() + QLatin1String("_XXXXXX");
593 }
594
595 auto temp = createTempFile(source.readAll(), name);
596 if (temp) {
597 qCDebug(QGCFileHelperLog) << "createTempCopy: copied" << sourcePath << "to" << temp->fileName();
598 }
599 return temp;
600}
601
602bool replaceFileFromTemp(QTemporaryFile *tempFile, const QString &targetPath, const QString &backupPath)
603{
604 if (!tempFile || !tempFile->isOpen()) {
605 qCWarning(QGCFileHelperLog) << "replaceFileFromTemp: temp file is null or not open";
606 return false;
607 }
608
609 if (targetPath.isEmpty()) {
610 qCWarning(QGCFileHelperLog) << "replaceFileFromTemp: target path is empty";
611 return false;
612 }
613
614 // Ensure parent directory exists
615 if (!ensureParentExists(targetPath)) {
616 qCWarning(QGCFileHelperLog) << "replaceFileFromTemp: failed to create parent directory";
617 return false;
618 }
619
620 // Flush and close temp file
621 tempFile->flush();
622
623 // Create backup of existing file if requested
624 if (!backupPath.isEmpty() && QFile::exists(targetPath)) {
625 QFile::remove(backupPath);
626 if (!QFile::copy(targetPath, backupPath)) {
627 qCWarning(QGCFileHelperLog) << "replaceFileFromTemp: failed to create backup";
628 return false;
629 }
630 qCDebug(QGCFileHelperLog) << "replaceFileFromTemp: backed up to" << backupPath;
631 }
632
633 // Remove existing target
634 QFile::remove(targetPath);
635
636 // Keep temp file around (don't auto-remove) and rename it
637 tempFile->setAutoRemove(false);
638 const QString tempPath = tempFile->fileName();
639 tempFile->close();
640
641 if (!moveFileOrCopy(tempPath, targetPath)) {
642 qCWarning(QGCFileHelperLog) << "replaceFileFromTemp: failed to move temp file to target";
643 QFile::remove(tempPath); // Clean up on failure
644 return false;
645 }
646
647 qCDebug(QGCFileHelperLog) << "replaceFileFromTemp: replaced" << targetPath;
648 return true;
649}
650
651} // namespace QGCFileHelper
Q_DECLARE_LOGGING_CATEGORY(AndroidSerialLog)
Q_LOGGING_CATEGORY(QGCDeviceInfoLog, "Utilities.QGCDeviceInfo", QtWarningMsg) namespace QGCDeviceInfo
Definition DeviceInfo.cc:6
QString errorString
QIODevice wrapper for streaming decompression.
bool open(OpenMode mode) override
bool isCompressedFile(const QString &filePath)
Check if file path indicates a compressed file (.gz, .xz, .zst)
Generic file system helper utilities.
qint64 availableDiskSpace(const QString &path)
bool isLocalPath(const QString &urlOrPath)
bool isQtResource(const QString &path)
QString uniqueTempPath(const QString &templateName)
constexpr size_t kBufferSizeMax
Maximum buffer size for I/O operations.
bool ensureParentExists(const QString &filePath)
bool moveFileOrCopy(const QString &sourcePath, const QString &destPath)
QString hashAlgorithmName(QCryptographicHash::Algorithm algorithm)
QString computeHash(const QByteArray &data, QCryptographicHash::Algorithm algorithm)
bool copyDirectoryRecursively(const QString &sourcePath, const QString &destPath)
std::unique_ptr< QTemporaryFile > createTempCopy(const QString &sourcePath, const QString &templateName)
bool atomicWrite(const QString &filePath, const QByteArray &data)
QString toLocalPath(const QString &urlOrPath)
bool hasSufficientDiskSpace(const QString &path, qint64 requiredBytes, double margin)
bool ensureDirectoryExists(const QString &path)
std::unique_ptr< QTemporaryFile > createTempFile(const QByteArray &data, const QString &templateName)
QString joinPath(const QString &dir, const QString &name)
bool exists(const QString &path)
QString computeFileHash(const QString &filePath, QCryptographicHash::Algorithm algorithm)
constexpr size_t kBufferSizeMin
Minimum buffer size for I/O operations.
size_t optimalBufferSize(const QString &path)
bool replaceFileFromTemp(QTemporaryFile *tempFile, const QString &targetPath, const QString &backupPath)
bool verifyFileHash(const QString &filePath, const QString &expectedHash, QCryptographicHash::Algorithm algorithm)
QString computeDecompressedFileHash(const QString &filePath, QCryptographicHash::Algorithm algorithm)
QByteArray readFile(const QString &filePath, QString *errorString, qint64 maxBytes)
QString tempDirectory()
constexpr size_t kBufferSizeDefault
Default buffer size when detection unavailable.