QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
QGCArchiveModel.cc
Go to the documentation of this file.
1#include "QGCArchiveModel.h"
2#include "QGCCompression.h"
3#include "QGCFileHelper.h"
5
6#include <QtCore/QCollator>
7#include <QtCore/QFileInfo>
8#include <QtCore/QLocale>
9
10#include <algorithm>
11
12QGC_LOGGING_CATEGORY(QGCArchiveModelLog, "Utilities.QGCArchiveModel")
13
14QGCArchiveModel::QGCArchiveModel(QObject *parent)
15 : QAbstractListModel(parent)
16{
17}
18
19QGCArchiveModel::~QGCArchiveModel() = default;
20
21void QGCArchiveModel::setArchivePath(const QString &path)
22{
23 if (_archivePath == path) {
24 return;
25 }
26
27 _archivePath = path;
28 emit archivePathChanged();
29
30 _loadArchive();
31}
32
33void QGCArchiveModel::setArchiveUrl(const QUrl &url)
34{
35 const QString path = QGCFileHelper::toLocalPath(url);
36 if (!path.isEmpty() && !QGCFileHelper::isLocalPath(path)) {
37 qCWarning(QGCArchiveModelLog) << "Unsupported archive URL:" << url;
38 setArchivePath({});
39 return;
40 }
41
42 setArchivePath(path);
43}
44
45void QGCArchiveModel::setFilterMode(FilterMode mode)
46{
47 if (_filterMode == mode) {
48 return;
49 }
50
51 _filterMode = mode;
52 emit filterModeChanged();
53
54 _applyFilter();
55}
56
57int QGCArchiveModel::rowCount(const QModelIndex &parent) const
58{
59 if (parent.isValid()) {
60 return 0;
61 }
62 return static_cast<int>(_filteredEntries.size());
63}
64
65QVariant QGCArchiveModel::data(const QModelIndex &index, int role) const
66{
67 if (!index.isValid() || index.row() < 0 || index.row() >= _filteredEntries.size()) {
68 return {};
69 }
70
71 const QGCCompression::ArchiveEntry &entry = _filteredEntries.at(index.row());
72
73 switch (role) {
74 case Qt::DisplayRole:
75 case NameRole:
76 return entry.name;
77
78 case SizeRole:
79 return entry.size;
80
81 case ModifiedRole:
82 return entry.modified;
83
84 case IsDirectoryRole:
85 return entry.isDirectory;
86
87 case PermissionsRole:
88 return entry.permissions;
89
90 case FileNameRole: {
91 const int lastSlash = entry.name.lastIndexOf(QLatin1Char('/'));
92 if (lastSlash >= 0 && lastSlash < entry.name.size() - 1) {
93 return entry.name.mid(lastSlash + 1);
94 }
95 return entry.name;
96 }
97
98 case DirectoryRole: {
99 const int lastSlash = entry.name.lastIndexOf(QLatin1Char('/'));
100 if (lastSlash > 0) {
101 return entry.name.left(lastSlash);
102 }
103 return QString();
104 }
105
106 case FormattedSizeRole:
107 return formatSize(entry.size);
108
109 default:
110 return {};
111 }
112}
113
114QHash<int, QByteArray> QGCArchiveModel::roleNames() const
115{
116 static const QHash<int, QByteArray> roles = {
117 { NameRole, "name" },
118 { SizeRole, "size" },
119 { ModifiedRole, "modified" },
120 { IsDirectoryRole, "isDirectory" },
121 { PermissionsRole, "permissions" },
122 { FileNameRole, "fileName" },
123 { DirectoryRole, "directory" },
124 { FormattedSizeRole, "formattedSize" },
125 };
126 return roles;
127}
128
129void QGCArchiveModel::refresh()
130{
131 _loadArchive();
132}
133
134void QGCArchiveModel::clear()
135{
136 if (_filteredEntries.isEmpty() && _allEntries.isEmpty()) {
137 return;
138 }
139
140 beginResetModel();
141 _allEntries.clear();
142 _filteredEntries.clear();
143 endResetModel();
144
145 const bool hadFiles = _fileCount > 0;
146 const bool hadDirs = _directoryCount > 0;
147 const bool hadSize = _totalSize > 0;
148
149 _fileCount = 0;
150 _directoryCount = 0;
151 _totalSize = 0;
152
153 emit countChanged();
154 if (hadFiles) emit fileCountChanged();
155 if (hadDirs) emit directoryCountChanged();
156 if (hadSize) emit totalSizeChanged();
157
158 _setErrorString(QString());
159}
160
161QVariantMap QGCArchiveModel::get(int index) const
162{
163 QVariantMap result;
164
165 if (index < 0 || index >= _filteredEntries.size()) {
166 return result;
167 }
168
169 const QGCCompression::ArchiveEntry &entry = _filteredEntries.at(index);
170
171 result[QStringLiteral("name")] = entry.name;
172 result[QStringLiteral("size")] = entry.size;
173 result[QStringLiteral("modified")] = entry.modified;
174 result[QStringLiteral("isDirectory")] = entry.isDirectory;
175 result[QStringLiteral("permissions")] = entry.permissions;
176 result[QStringLiteral("formattedSize")] = formatSize(entry.size);
177
178 // Compute fileName and directory
179 const int lastSlash = entry.name.lastIndexOf(QLatin1Char('/'));
180 if (lastSlash >= 0 && lastSlash < entry.name.size() - 1) {
181 result[QStringLiteral("fileName")] = entry.name.mid(lastSlash + 1);
182 result[QStringLiteral("directory")] = entry.name.left(lastSlash);
183 } else {
184 result[QStringLiteral("fileName")] = entry.name;
185 result[QStringLiteral("directory")] = QString();
186 }
187
188 return result;
189}
190
191bool QGCArchiveModel::contains(const QString &fileName) const
192{
193 if (fileName.isEmpty()) {
194 return false;
195 }
196
197 for (const auto &entry : _allEntries) {
198 if (entry.name == fileName) {
199 return true;
200 }
201 }
202 return false;
203}
204
205QString QGCArchiveModel::formatSize(qint64 bytes)
206{
207 if (bytes < 0) {
208 return QStringLiteral("--");
209 }
210
211 static const char* units[] = { "B", "KB", "MB", "GB", "TB" };
212 constexpr int numUnits = sizeof(units) / sizeof(units[0]);
213
214 double size = static_cast<double>(bytes);
215 int unitIndex = 0;
216
217 while (size >= 1024.0 && unitIndex < numUnits - 1) {
218 size /= 1024.0;
219 unitIndex++;
220 }
221
222 if (unitIndex == 0) {
223 // Bytes - no decimals
224 return QStringLiteral("%1 %2").arg(bytes).arg(QLatin1String(units[unitIndex]));
225 }
226
227 // Use locale-aware number formatting
228 return QStringLiteral("%1 %2")
229 .arg(QLocale().toString(size, 'f', 1))
230 .arg(QLatin1String(units[unitIndex]));
231}
232
233void QGCArchiveModel::_loadArchive()
234{
235 clear();
236
237 if (_archivePath.isEmpty()) {
238 emit loadingComplete(true);
239 return;
240 }
241
242 _setLoading(true);
243 _setErrorString(QString());
244
245 qCDebug(QGCArchiveModelLog) << "Loading archive:" << _archivePath;
246
247 // Load entries using QGCCompression (already sorted with QCollator)
248 QList<QGCCompression::ArchiveEntry> entries = QGCCompression::listArchiveDetailed(_archivePath);
249
250 if (entries.isEmpty() && !_archivePath.isEmpty()) {
251 // Check if it was an error or just empty archive
252 if (!QGCCompression::validateArchive(_archivePath)) {
253 _setErrorString(QGCCompression::lastErrorString());
254 _setLoading(false);
255 emit loadingComplete(false);
256 return;
257 }
258 }
259
260 // Calculate statistics
261 int fileCount = 0;
262 int dirCount = 0;
263 qint64 totalSize = 0;
264
265 for (const auto &entry : entries) {
266 if (entry.isDirectory) {
267 dirCount++;
268 } else {
269 fileCount++;
270 totalSize += entry.size;
271 }
272 }
273
274 // Store all entries
275 _allEntries = std::move(entries);
276
277 // Update statistics
278 const bool fileCountChanged = _fileCount != fileCount;
279 const bool dirCountChanged = _directoryCount != dirCount;
280 const bool sizeChanged = _totalSize != totalSize;
281
282 _fileCount = fileCount;
283 _directoryCount = dirCount;
284 _totalSize = totalSize;
285
286 // Apply current filter (this will emit countChanged via beginResetModel/endResetModel)
287 _applyFilter();
288
289 // Emit property changes
290 if (fileCountChanged) emit this->fileCountChanged();
291 if (dirCountChanged) emit directoryCountChanged();
292 if (sizeChanged) emit totalSizeChanged();
293
294 _setLoading(false);
295
296 qCDebug(QGCArchiveModelLog) << "Loaded" << _allEntries.size() << "entries:"
297 << _fileCount << "files," << _directoryCount << "dirs,"
298 << _totalSize << "bytes";
299
300 emit loadingComplete(true);
301}
302
303void QGCArchiveModel::_applyFilter()
304{
305 beginResetModel();
306
307 switch (_filterMode) {
308 case AllEntries:
309 _filteredEntries = _allEntries;
310 break;
311
312 case FilesOnly:
313 _filteredEntries.clear();
314 for (const auto &entry : _allEntries) {
315 if (!entry.isDirectory) {
316 _filteredEntries.append(entry);
317 }
318 }
319 break;
320
321 case DirectoriesOnly:
322 _filteredEntries.clear();
323 for (const auto &entry : _allEntries) {
324 if (entry.isDirectory) {
325 _filteredEntries.append(entry);
326 }
327 }
328 break;
329 }
330
331 endResetModel();
332 emit countChanged();
333}
334
335void QGCArchiveModel::_setLoading(bool loading)
336{
337 if (_loading == loading) {
338 return;
339 }
340
341 _loading = loading;
342 emit loadingChanged();
343}
344
345void QGCArchiveModel::_setErrorString(const QString &error)
346{
347 if (_errorString == error) {
348 return;
349 }
350
351 _errorString = error;
352 emit errorStringChanged();
353}
Error error
#define QGC_LOGGING_CATEGORY(name, categoryStr)
void filterModeChanged()
void directoryCountChanged()
void archivePathChanged()
void totalSizeChanged()
void errorStringChanged()
void loadingChanged()
void loadingComplete(bool success)
Emitted when archive loading completes (success or failure)
void fileCountChanged()
QString lastErrorString()
Get a human-readable error message from the last operation (thread-local)
QList< ArchiveEntry > listArchiveDetailed(const QString &archivePath, Format format)
bool validateArchive(const QString &archivePath, Format format)
bool isLocalPath(const QString &urlOrPath)
QString toLocalPath(const QString &urlOrPath)
Metadata for a single entry in an archive.
bool isDirectory
True if entry is a directory.
qint64 size
Uncompressed size in bytes.
QString name
Path/name within archive.
quint32 permissions
Unix-style permissions.
QDateTime modified
Last modification time.