6#include <QtCore/QCryptographicHash>
9#include <QtCore/QFileInfo>
10#include <QtCore/QLoggingCategory>
11#include <QtCore/QSaveFile>
12#include <QtCore/QStandardPaths>
13#include <QtCore/QStorageInfo>
14#include <QtCore/QTemporaryFile>
24 if (filePath.isEmpty()) {
33 if (!decompressor.
open(QIODevice::ReadOnly)) {
35 *
errorString = QObject::tr(
"Failed to open compressed file: %1").arg(filePath);
42 data = decompressor.read(maxBytes);
44 data = decompressor.readAll();
51 if (!file.open(QIODevice::ReadOnly)) {
53 *
errorString = QObject::tr(
"Failed to open file: %1 - %2").arg(filePath, file.errorString());
60 data = file.read(maxBytes);
62 data = file.readAll();
71 static size_t cachedSize = 0;
72 if (cachedSize != 0) {
79 if (!path.isEmpty()) {
80 QStorageInfo storage(path);
81 if (storage.isValid()) {
82 blockSize = storage.blockSize();
88 blockSize = QStorageInfo::root().blockSize();
96 cachedSize =
static_cast<size_t>(
111QString
joinPath(
const QString &dir,
const QString &name)
116 if (dir.endsWith(QLatin1Char(
'/'))) {
119 return dir + QLatin1Char(
'/') + name;
128 return dir.mkpath(path);
138 QDir sourceDir(sourcePath);
139 if (!sourceDir.exists()) {
140 qCWarning(QGCFileHelperLog) <<
"Source directory doesn't exist:" << sourcePath;
145 qCWarning(QGCFileHelperLog) <<
"Failed to create destination directory:" << destPath;
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;
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);
175 if (sourcePath.isEmpty() || destPath.isEmpty()) {
176 qCWarning(QGCFileHelperLog) <<
"moveFileOrCopy: empty path";
181 if (QFile::rename(sourcePath, destPath)) {
186 qCDebug(QGCFileHelperLog) <<
"moveFileOrCopy: rename failed, using copy for:" << sourcePath;
188 const bool isDir = QFileInfo(sourcePath).isDir();
189 bool copySuccess =
false;
194 QDir(sourcePath).removeRecursively();
197 copySuccess = QFile::copy(sourcePath, destPath);
199 QFile::remove(sourcePath);
204 qCWarning(QGCFileHelperLog) <<
"moveFileOrCopy: failed to move:" << sourcePath <<
"to" << destPath;
212 if (filePath.isEmpty()) {
213 qCWarning(QGCFileHelperLog) <<
"atomicWrite: file path is empty";
219 qCWarning(QGCFileHelperLog) <<
"atomicWrite: failed to create parent directory for:" << filePath;
224 QSaveFile file(filePath);
225 if (!file.open(QIODevice::WriteOnly)) {
226 qCWarning(QGCFileHelperLog) <<
"atomicWrite: failed to open:" << filePath
227 <<
"-" << file.errorString();
231 if (file.write(data) != data.size()) {
232 qCWarning(QGCFileHelperLog) <<
"atomicWrite: write failed:" << file.errorString();
233 file.cancelWriting();
237 if (!file.commit()) {
238 qCWarning(QGCFileHelperLog) <<
"atomicWrite: commit failed:" << file.errorString();
247 if (path.isEmpty()) {
252 QStorageInfo storage(path);
253 if (!storage.isValid()) {
255 storage = QStorageInfo(QFileInfo(path).absolutePath());
258 if (!storage.isValid()) {
259 qCDebug(QGCFileHelperLog) <<
"availableDiskSpace: cannot determine storage for:" << path;
263 return storage.bytesAvailable();
268 if (requiredBytes <= 0) {
273 if (bytesAvailable < 0) {
274 qCDebug(QGCFileHelperLog) <<
"hasSufficientDiskSpace: cannot determine disk space, proceeding anyway";
278 const qint64 bytesRequired =
static_cast<qint64
>(
static_cast<double>(requiredBytes) * margin);
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";
298 if (urlOrPath.isEmpty()) {
303 if (urlOrPath.startsWith(QLatin1String(
":/"))) {
309 if (url.isValid() && !url.scheme().isEmpty()) {
319 if (!url.isValid()) {
321 const QString path = url.path();
322 if (!path.isEmpty()) {
328 const QString scheme = url.scheme().toLower();
331 if (scheme == QLatin1String(
"file")) {
332 return url.toLocalFile();
336 if (scheme == QLatin1String(
"qrc")) {
337 QString path = url.path();
338 if (!path.startsWith(QLatin1Char(
'/'))) {
339 path.prepend(QLatin1Char(
'/'));
341 return QLatin1String(
":/") + path.mid(1);
345 if (scheme.isEmpty()) {
350 qCDebug(QGCFileHelperLog) <<
"toLocalPath: URL scheme not supported for local access:" << scheme;
351 return url.toString();
356 if (urlOrPath.isEmpty()) {
367 if (url.isValid() && !url.scheme().isEmpty()) {
368 const QString scheme = url.scheme().toLower();
369 return scheme == QLatin1String(
"file") ||
370 scheme == QLatin1String(
"qrc");
379 return path.startsWith(QLatin1String(
":/")) ||
380 path.startsWith(QLatin1String(
"qrc:/"), Qt::CaseInsensitive);
387QString
computeFileHash(
const QString &filePath, QCryptographicHash::Algorithm algorithm)
389 if (filePath.isEmpty()) {
390 qCWarning(QGCFileHelperLog) <<
"computeFileHash: empty file path";
394 QFile file(filePath);
395 if (!file.open(QIODevice::ReadOnly)) {
396 qCWarning(QGCFileHelperLog) <<
"computeFileHash: failed to open file:" << filePath
397 <<
"-" << file.errorString();
401 QCryptographicHash hash(algorithm);
402 if (!hash.addData(&file)) {
403 qCWarning(QGCFileHelperLog) <<
"computeFileHash: failed to read file:" << filePath;
407 return QString::fromLatin1(hash.result().toHex());
412 if (filePath.isEmpty()) {
413 qCWarning(QGCFileHelperLog) <<
"computeDecompressedFileHash: empty file path";
422 if (!decompressor.
open(QIODevice::ReadOnly)) {
423 qCWarning(QGCFileHelperLog) <<
"computeDecompressedFileHash: failed to open:" << filePath;
427 QCryptographicHash hash(algorithm);
428 constexpr qint64 chunkSize = 65536;
432 buffer = decompressor.read(chunkSize);
433 if (buffer.isEmpty()) {
434 if (decompressor.atEnd()) {
438 qCWarning(QGCFileHelperLog) <<
"computeDecompressedFileHash: read error";
439 decompressor.
close();
443 hash.addData(buffer);
446 decompressor.
close();
447 return QString::fromLatin1(hash.result().toHex());
450QString
computeHash(
const QByteArray &data, QCryptographicHash::Algorithm algorithm)
452 return QString::fromLatin1(QCryptographicHash::hash(data, algorithm).toHex());
456 QCryptographicHash::Algorithm algorithm)
458 if (expectedHash.isEmpty()) {
459 qCWarning(QGCFileHelperLog) <<
"verifyFileHash: empty expected hash";
464 if (actualHash.isEmpty()) {
468 const bool match = (actualHash.compare(expectedHash, Qt::CaseInsensitive) == 0);
470 qCWarning(QGCFileHelperLog) <<
"verifyFileHash: hash mismatch for" << filePath
471 <<
"- expected:" << expectedHash.left(16) <<
"..."
472 <<
"actual:" << actualHash.left(16) <<
"...";
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");
511QString normalizeTemplateName(
const QString &templateName)
513 QString name = templateName.isEmpty() ? QStringLiteral(
"qgc_XXXXXX") : templateName;
515 if (!name.contains(QLatin1String(
"XXXXXX"))) {
516 const qsizetype dotPos = name.lastIndexOf(QLatin1Char(
'.'));
518 name.insert(dotPos, QLatin1String(
"_XXXXXX"));
520 name.append(QLatin1String(
"_XXXXXX"));
531 return QStandardPaths::writableLocation(QStandardPaths::TempLocation);
536 const QString name = normalizeTemplateName(templateName);
538 QTemporaryFile temp(
tempDirectory() + QLatin1Char(
'/') + name);
540 qCWarning(QGCFileHelperLog) <<
"uniqueTempPath: failed to create temp file";
545 const QString path = temp.fileName();
552std::unique_ptr<QTemporaryFile>
createTempFile(
const QByteArray &data,
const QString &templateName)
554 const QString name = normalizeTemplateName(templateName);
556 auto temp = std::make_unique<QTemporaryFile>(
tempDirectory() + QLatin1Char(
'/') + name);
558 qCWarning(QGCFileHelperLog) <<
"createTempFile: failed to create temp file";
562 if (temp->write(data) != data.size()) {
563 qCWarning(QGCFileHelperLog) <<
"createTempFile: failed to write data";
570 qCDebug(QGCFileHelperLog) <<
"createTempFile: created" << temp->fileName()
571 <<
"with" << data.size() <<
"bytes";
575std::unique_ptr<QTemporaryFile>
createTempCopy(
const QString &sourcePath,
const QString &templateName)
577 if (sourcePath.isEmpty()) {
578 qCWarning(QGCFileHelperLog) <<
"createTempCopy: source path is empty";
582 QFile source(sourcePath);
583 if (!source.open(QIODevice::ReadOnly)) {
584 qCWarning(QGCFileHelperLog) <<
"createTempCopy: failed to open source:" << sourcePath
585 <<
"-" << source.errorString();
589 QString name = templateName;
590 if (name.isEmpty()) {
592 name = QFileInfo(sourcePath).fileName() + QLatin1String(
"_XXXXXX");
597 qCDebug(QGCFileHelperLog) <<
"createTempCopy: copied" << sourcePath <<
"to" << temp->fileName();
604 if (!tempFile || !tempFile->isOpen()) {
605 qCWarning(QGCFileHelperLog) <<
"replaceFileFromTemp: temp file is null or not open";
609 if (targetPath.isEmpty()) {
610 qCWarning(QGCFileHelperLog) <<
"replaceFileFromTemp: target path is empty";
616 qCWarning(QGCFileHelperLog) <<
"replaceFileFromTemp: failed to create parent directory";
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";
630 qCDebug(QGCFileHelperLog) <<
"replaceFileFromTemp: backed up to" << backupPath;
634 QFile::remove(targetPath);
637 tempFile->setAutoRemove(
false);
638 const QString tempPath = tempFile->fileName();
642 qCWarning(QGCFileHelperLog) <<
"replaceFileFromTemp: failed to move temp file to target";
643 QFile::remove(tempPath);
647 qCDebug(QGCFileHelperLog) <<
"replaceFileFromTemp: replaced" << targetPath;
Q_DECLARE_LOGGING_CATEGORY(AndroidSerialLog)
Q_LOGGING_CATEGORY(QGCDeviceInfoLog, "Utilities.QGCDeviceInfo", QtWarningMsg) namespace QGCDeviceInfo
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)
constexpr size_t kBufferSizeDefault
Default buffer size when detection unavailable.