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
24QGCArchiveWatcher::~QGCArchiveWatcher()
25{
26 if (_extractionJob != nullptr && _extractionJob->isRunning()) {
27 _cancelPending = true;
28 _extractionJob->cancel();
29 }
30}
31
32// ============================================================================
33// Configuration
34// ============================================================================
35
36void QGCArchiveWatcher::setFilterMode(FilterMode mode)
37{
38 _filterMode = mode;
39}
40
41void QGCArchiveWatcher::setAutoDecompress(bool enable)
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
57void QGCArchiveWatcher::setRemoveAfterExtraction(bool remove)
58{
59 _removeAfterExtraction = remove;
60}
61
62void QGCArchiveWatcher::setDebounceDelay(int milliseconds)
63{
64 _fileWatcher->setDebounceDelay(milliseconds);
65}
66
67int QGCArchiveWatcher::debounceDelay() const
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
117QStringList QGCArchiveWatcher::watchedDirectories() const
118{
119 return _fileWatcher->watchedDirectories();
120}
121
122void QGCArchiveWatcher::clear()
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
163void QGCArchiveWatcher::cancelExtraction()
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) {
261 case FilterMode::Archives:
262 return QGCCompression::isArchiveFormat(format);
263 case FilterMode::Compressed:
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
314 const QGCCompression::Format format = QGCCompression::detectFormat(archivePath);
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)
void autoDecompressChanged(bool autoDecompress)
Property change signals.
void progressChanged(qreal progress)
void extractionComplete(const QString &archivePath, const QString &outputPath, bool success, const QString &errorString)
void outputDirectoryChanged(const QString &directory)
void extractingChanged(bool extracting)
void archiveDetected(const QString &archivePath, QGCCompression::Format format)
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)
void extractArchive(const QString &archivePath, const QString &outputDirectoryPath, qint64 maxBytes=0)
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)
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)