QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
QGCArchiveWatcher.cc
Go to the documentation of this file.
1#include "QGCArchiveWatcher.h"
2#include "QGCCompressionJob.h"
4
5#include <QtCore/QDir>
6#include <QtCore/QFileInfo>
7
8QGC_LOGGING_CATEGORY(QGCArchiveWatcherLog, "Utilities.QGCArchiveWatcher")
9
10// ============================================================================
11// Construction / Destruction
12// ============================================================================
13
15 : QObject(parent)
16 , _fileWatcher(new QGCFileWatcher(this))
17{
18 _fileWatcher->setDebounceDelay(500); // Longer debounce for file copy operations
19
20 connect(_fileWatcher, &QGCFileWatcher::directoryChanged,
21 this, &QGCArchiveWatcher::_onDirectoryChanged);
22}
23
25{
26 if (_extractionJob != nullptr && _extractionJob->isRunning()) {
27 _cancelPending = true;
28 _extractionJob->cancel();
29 }
30}
31
32// ============================================================================
33// Configuration
34// ============================================================================
35
37{
38 _filterMode = mode;
39}
40
42{
43 if (_autoDecompress != enable) {
44 _autoDecompress = enable;
45 emit autoDecompressChanged(_autoDecompress);
46 }
47}
48
49void QGCArchiveWatcher::setOutputDirectory(const QString &directory)
50{
51 if (_outputDirectory != directory) {
52 _outputDirectory = directory;
53 emit outputDirectoryChanged(_outputDirectory);
54 }
55}
56
58{
59 _removeAfterExtraction = remove;
60}
61
63{
64 _fileWatcher->setDebounceDelay(milliseconds);
65}
66
68{
69 return _fileWatcher->debounceDelay();
70}
71
72// ============================================================================
73// Directory Watching
74// ============================================================================
75
76bool QGCArchiveWatcher::watchDirectory(const QString &directoryPath)
77{
78 if (directoryPath.isEmpty()) {
79 qCWarning(QGCArchiveWatcherLog) << "watchDirectory: empty path";
80 return false;
81 }
82
83 const QString canonicalPath = QFileInfo(directoryPath).absoluteFilePath();
84
85 if (!QFileInfo(canonicalPath).isDir()) {
86 qCWarning(QGCArchiveWatcherLog) << "watchDirectory: not a directory:" << directoryPath;
87 return false;
88 }
89
90 // Initialize known files for this directory
91 if (!_knownFiles.contains(canonicalPath)) {
92 QDir dir(canonicalPath);
93 const QStringList entries = dir.entryList(QDir::Files);
94 QSet<QString> fileSet;
95 for (const QString &entry : entries) {
96 fileSet.insert(dir.absoluteFilePath(entry));
97 }
98 _knownFiles[canonicalPath] = fileSet;
99 qCDebug(QGCArchiveWatcherLog) << "Initialized" << fileSet.size() << "known files in" << canonicalPath;
100 }
101
102 if (_fileWatcher->watchDirectory(canonicalPath, nullptr)) {
103 qCDebug(QGCArchiveWatcherLog) << "Watching directory for archives:" << canonicalPath;
104 return true;
105 }
106
107 return false;
108}
109
110bool QGCArchiveWatcher::unwatchDirectory(const QString &directoryPath)
111{
112 const QString canonicalPath = QFileInfo(directoryPath).absoluteFilePath();
113 _knownFiles.remove(canonicalPath);
114 return _fileWatcher->unwatchDirectory(canonicalPath);
115}
116
118{
119 return _fileWatcher->watchedDirectories();
120}
121
123{
124 _fileWatcher->clear();
125 _knownFiles.clear();
126 _pendingExtractions.clear();
127 _currentArchive.clear();
128 _setExtracting(false);
129 _setProgress(0.0);
130
131 if (_extractionJob) {
132 if (_extractionJob->isRunning()) {
133 _cancelPending = true;
134 _extractionJob->cancel();
135 }
136 }
137}
138
139// ============================================================================
140// Manual Operations
141// ============================================================================
142
143QStringList QGCArchiveWatcher::scanDirectory(const QString &directoryPath) const
144{
145 QStringList archives;
146
147 QDir dir(directoryPath);
148 if (!dir.exists()) {
149 return archives;
150 }
151
152 const QStringList entries = dir.entryList(QDir::Files);
153 for (const QString &entry : entries) {
154 const QString fullPath = dir.absoluteFilePath(entry);
155 if (_isWatchedFormat(fullPath)) {
156 archives.append(fullPath);
157 }
158 }
159
160 return archives;
161}
162
164{
165 if (_extractionJob != nullptr && _extractionJob->isRunning()) {
166 qCDebug(QGCArchiveWatcherLog) << "Cancelling extraction";
167 _cancelPending = true;
168 _extractionJob->cancel();
169 }
170 _pendingExtractions.clear();
171 _currentArchive.clear();
172 _setExtracting(false);
173 _setProgress(0.0);
174}
175
176// ============================================================================
177// Private Slots
178// ============================================================================
179
180void QGCArchiveWatcher::_onDirectoryChanged(const QString &path)
181{
182 qCDebug(QGCArchiveWatcherLog) << "Directory changed:" << path;
183
184 QDir dir(path);
185 if (!dir.exists()) {
186 return;
187 }
188
189 // Get current file list
190 const QStringList currentEntries = dir.entryList(QDir::Files);
191 QSet<QString> currentFiles;
192 for (const QString &entry : currentEntries) {
193 currentFiles.insert(dir.absoluteFilePath(entry));
194 }
195
196 // Find new files
197 QSet<QString> &knownFiles = _knownFiles[path];
198 const QSet<QString> newFiles = currentFiles - knownFiles;
199
200 // Update known files
201 knownFiles = currentFiles;
202
203 // Process new files
204 for (const QString &filePath : newFiles) {
205 _processNewFile(filePath);
206 }
207}
208
209void QGCArchiveWatcher::_onExtractionProgress(qreal progress)
210{
211 _setProgress(progress);
212}
213
214void QGCArchiveWatcher::_onExtractionFinished(bool success)
215{
216 if (_cancelPending) {
217 _cancelPending = false;
218 _currentArchive.clear();
219 _setExtracting(false);
220 _setProgress(0.0);
221 return;
222 }
223
224 qCDebug(QGCArchiveWatcherLog) << "Extraction finished:" << _currentArchive
225 << "success:" << success;
226
227 QString outputPath = _extractionJob->outputPath();
228 QString errorString = success ? QString() : _extractionJob->errorString();
229
230 // Remove source archive if configured and extraction succeeded
231 if (success && _removeAfterExtraction) {
232 if (QFile::remove(_currentArchive)) {
233 qCDebug(QGCArchiveWatcherLog) << "Removed source archive:" << _currentArchive;
234 } else {
235 qCWarning(QGCArchiveWatcherLog) << "Failed to remove source archive:" << _currentArchive;
236 }
237 }
238
239 emit extractionComplete(_currentArchive, outputPath, success, errorString);
240
241 _currentArchive.clear();
242 _setExtracting(false);
243 _setProgress(0.0);
244
245 // Process next pending extraction
246 if (!_pendingExtractions.isEmpty()) {
247 const QString next = _pendingExtractions.takeFirst();
248 _startExtraction(next);
249 }
250}
251
252// ============================================================================
253// Private Methods
254// ============================================================================
255
256bool QGCArchiveWatcher::_isWatchedFormat(const QString &filePath) const
257{
259
260 switch (_filterMode) {
262 return QGCCompression::isArchiveFormat(format);
265 case FilterMode::Both:
266 return QGCCompression::isArchiveFormat(format) ||
268 }
269
270 return false;
271}
272
273void QGCArchiveWatcher::_processNewFile(const QString &filePath)
274{
275 if (!_isWatchedFormat(filePath)) {
276 return;
277 }
278
280
281 qCDebug(QGCArchiveWatcherLog) << "Detected archive:" << filePath
282 << "format:" << QGCCompression::formatName(format);
283
284 emit archiveDetected(filePath, format);
285
286 if (_autoDecompress) {
287 if (_extracting) {
288 // Queue for later
289 if (!_pendingExtractions.contains(filePath)) {
290 _pendingExtractions.append(filePath);
291 qCDebug(QGCArchiveWatcherLog) << "Queued for extraction:" << filePath;
292 }
293 } else {
294 _startExtraction(filePath);
295 }
296 }
297}
298
299void QGCArchiveWatcher::_startExtraction(const QString &archivePath)
300{
301 if (!QFileInfo::exists(archivePath)) {
302 qCWarning(QGCArchiveWatcherLog) << "Archive no longer exists:" << archivePath;
303 return;
304 }
305
306 // Determine output path
307 QString outputPath = _outputDirectory;
308 if (outputPath.isEmpty()) {
309 outputPath = QFileInfo(archivePath).absolutePath();
310 }
311
312 // For single-file compression, use decompressFile
313 // For archives, use extractArchive
315 const bool isArchive = QGCCompression::isArchiveFormat(format);
316
317 qCDebug(QGCArchiveWatcherLog) << "Starting extraction:" << archivePath
318 << "to" << outputPath
319 << (isArchive ? "(archive)" : "(compressed file)");
320
321 _currentArchive = archivePath;
322 _cancelPending = false;
323 _setExtracting(true);
324 _setProgress(0.0);
325
326 // Create extraction job if needed
327 if (_extractionJob == nullptr) {
328 _extractionJob = new QGCCompressionJob(this);
329 connect(_extractionJob, &QGCCompressionJob::progressChanged,
330 this, &QGCArchiveWatcher::_onExtractionProgress);
331 connect(_extractionJob, &QGCCompressionJob::finished,
332 this, &QGCArchiveWatcher::_onExtractionFinished);
333 }
334
335 if (isArchive) {
336 _extractionJob->extractArchive(archivePath, outputPath);
337 } else {
338 // For compressed files, output is a file, not directory
339 const QString strippedName = QFileInfo(QGCCompression::strippedPath(archivePath)).fileName();
340 const QString decompressedPath = outputPath + "/" + strippedName;
341 _extractionJob->decompressFile(archivePath, decompressedPath);
342 }
343}
344
345void QGCArchiveWatcher::_setExtracting(bool extracting)
346{
347 if (_extracting != extracting) {
348 _extracting = extracting;
349 emit extractingChanged(_extracting);
350 }
351}
352
353void QGCArchiveWatcher::_setProgress(qreal progress)
354{
355 if (!qFuzzyCompare(_progress, progress)) {
356 _progress = progress;
357 emit progressChanged(_progress);
358 }
359}
Watches directories for archive files with optional auto-decompression.
QObject wrapper for async compression operations using QtConcurrent/QPromise.
QString errorString
#define QGC_LOGGING_CATEGORY(name, categoryStr)
Watches directories for new archive files and optionally auto-decompresses them.
void setRemoveAfterExtraction(bool remove)
QStringList watchedDirectories() const
QStringList scanDirectory(const QString &directoryPath) const
qreal progress() const
Get current extraction progress.
void setOutputDirectory(const QString &directory)
void cancelExtraction()
Cancel current extraction.
void autoDecompressChanged(bool autoDecompress)
Property change signals.
void setFilterMode(FilterMode mode)
void setDebounceDelay(int milliseconds)
bool watchDirectory(const QString &directoryPath)
bool unwatchDirectory(const QString &directoryPath)
void progressChanged(qreal progress)
void setAutoDecompress(bool enable)
FilterMode
Whether auto-decompression is enabled.
@ Both
Watch for both archives and compressed files.
@ Archives
Watch for archive files (.zip, .tar, .tar.gz, .7z)
@ Compressed
Watch for single-file compressed (.gz, .xz, .zst, .bz2, .lz4)
void extractionComplete(const QString &archivePath, const QString &outputPath, bool success, const QString &errorString)
void outputDirectoryChanged(const QString &directory)
void clear()
Stop watching all directories.
~QGCArchiveWatcher() override
void extractingChanged(bool extracting)
void archiveDetected(const QString &archivePath, QGCCompression::Format format)
QObject wrapper for compression operations with progress signals.
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
void extractArchive(const QString &archivePath, const QString &outputDirectoryPath, qint64 maxBytes=0)
Callback-based file/directory watcher with debouncing support.
bool unwatchDirectory(const QString &directoryPath)
void clear()
Stop watching all files and directories.
void setDebounceDelay(int milliseconds)
void directoryChanged(const QString &path)
int debounceDelay() const
QStringList watchedDirectories() const
bool watchDirectory(const QString &directoryPath, ChangeCallback callback)
QByteArray format(const QList< LogEntry > &entries, int fmt)
bool isCompressionFormat(Format format)
Check if format is a compression format (single stream)
QString formatName(Format format)
Get human-readable name for a format.
bool isArchiveFormat(Format format)
Check if format is an archive (contains multiple files)
QString strippedPath(const QString &filePath)
Format
Archive and compression format types (for decompression)
Format detectFormat(const QString &filePath, bool useContentFallback)