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