1 /*
2 SPDX-FileCopyrightText: 2007 Henrique Pinto <henrique.pinto@kdemail.net>
3 SPDX-FileCopyrightText: 2008-2009 Harald Hvaal <haraldhv@stud.ntnu.no>
4 SPDX-FileCopyrightText: 2010-2012 Raphael Kubo da Costa <rakuco@FreeBSD.org>
5 SPDX-FileCopyrightText: 2016 Vladyslav Batyrenko <mvlabat@gmail.com>
6
7 SPDX-License-Identifier: GPL-2.0-or-later
8 */
9
10 #include "archivemodel.h"
11 #include "ark_debug.h"
12 #include "jobs.h"
13 #include "util.h"
14 #include "qstringtokenizer.h"
15
16 #include <KIO/Global>
17 #include <KLocalizedString>
18
19 #include <QApplication>
20 #include <QDBusConnection>
21 #include <QMimeData>
22 #include <QRegularExpression>
23 #include <QStyle>
24 #include <QUrl>
25
26 using namespace Kerfuffle;
27
28 // Used to speed up the loading of large archives.
29 static Archive::Entry *s_previousMatch = nullptr;
Q_GLOBAL_STATIC(QString,s_previousPath)30 Q_GLOBAL_STATIC(QString, s_previousPath)
31
32 ArchiveModel::ArchiveModel(const QString &dbusPathName, QObject *parent)
33 : QAbstractItemModel(parent)
34 , m_dbusPathName(dbusPathName)
35 , m_numberOfFiles(0)
36 , m_numberOfFolders(0)
37 , m_fileEntryListed(false)
38 {
39 initRootEntry();
40
41 // Mappings between column indexes and entry properties.
42 m_propertiesMap = {
43 { FullPath, "fullPath" },
44 { Size, "size" },
45 { CompressedSize, "compressedSize" },
46 { Permissions, "permissions" },
47 { Owner, "owner" },
48 { Group, "group" },
49 { Ratio, "ratio" },
50 { CRC, "CRC" },
51 { BLAKE2, "BLAKE2" },
52 { Method, "method" },
53 { Version, "version" },
54 { Timestamp, "timestamp" },
55 };
56 }
57
~ArchiveModel()58 ArchiveModel::~ArchiveModel()
59 {
60 }
61
data(const QModelIndex & index,int role) const62 QVariant ArchiveModel::data(const QModelIndex &index, int role) const
63 {
64 if (index.isValid()) {
65 Archive::Entry *entry = static_cast<Archive::Entry*>(index.internalPointer());
66 switch (role) {
67 case Qt::DisplayRole: {
68 // TODO: complete the columns.
69 int column = m_showColumns.at(index.column());
70 switch (column) {
71 case FullPath:
72 return entry->name();
73 case Size:
74 if (entry->isDir()) {
75 uint dirs;
76 uint files;
77 entry->countChildren(dirs, files);
78 return KIO::itemsSummaryString(dirs + files, files, dirs, 0, false);
79 } else if (!entry->property("link").toString().isEmpty()) {
80 return QVariant();
81 } else {
82 return KIO::convertSize(entry->property("size").toULongLong());
83 }
84 case CompressedSize:
85 if (entry->isDir() || !entry->property("link").toString().isEmpty()) {
86 return QVariant();
87 } else {
88 qulonglong compressedSize = entry->property("compressedSize").toULongLong();
89 if (compressedSize != 0) {
90 return KIO::convertSize(compressedSize);
91 } else {
92 return QVariant();
93 }
94 }
95 case Ratio: // TODO: Use entry->metaData()[Ratio] when available.
96 if (entry->isDir() || !entry->property("link").toString().isEmpty()) {
97 return QVariant();
98 } else {
99 qulonglong compressedSize = entry->property("compressedSize").toULongLong();
100 qulonglong size = entry->property("size").toULongLong();
101 if (compressedSize == 0 || size == 0) {
102 return QVariant();
103 } else {
104 int ratio = int(100 * ((double)size - compressedSize) / size);
105 return QString(QString::number(ratio) + QStringLiteral(" %"));
106 }
107 }
108
109 case Timestamp: {
110 const QDateTime timeStamp = entry->property("timestamp").toDateTime();
111 return QLocale().toString(timeStamp, QLocale::ShortFormat);
112 }
113
114 default:
115 return entry->property(m_propertiesMap[column].constData());
116 }
117 }
118 case Qt::DecorationRole:
119 if (index.column() == 0) {
120 Archive::Entry *e = static_cast<Archive::Entry*>(index.internalPointer());
121 QIcon::Mode mode = (filesToMove.contains(e->fullPath())) ? QIcon::Disabled : QIcon::Normal;
122 return e->icon().pixmap(QApplication::style()->pixelMetric(QStyle::PM_SmallIconSize), mode);
123 }
124 return QVariant();
125 case Qt::FontRole: {
126 QFont f;
127 f.setItalic(entry->property("isPasswordProtected").toBool());
128 return f;
129 }
130 default:
131 return QVariant();
132 }
133 }
134 return QVariant();
135 }
136
flags(const QModelIndex & index) const137 Qt::ItemFlags ArchiveModel::flags(const QModelIndex &index) const
138 {
139 Qt::ItemFlags defaultFlags = QAbstractItemModel::flags(index);
140
141 if (index.isValid()) {
142 return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsDragEnabled | defaultFlags;
143 }
144
145 return Qt::NoItemFlags;
146 }
147
headerData(int section,Qt::Orientation,int role) const148 QVariant ArchiveModel::headerData(int section, Qt::Orientation, int role) const
149 {
150 if (role == Qt::DisplayRole) {
151 if (section >= m_showColumns.size()) {
152 qCDebug(ARK) << "WEIRD: showColumns.size = " << m_showColumns.size()
153 << " and section = " << section;
154 return QVariant();
155 }
156
157 int columnId = m_showColumns.at(section);
158
159 switch (columnId) {
160 case FullPath:
161 return i18nc("Name of a file inside an archive", "Name");
162 case Size:
163 return i18nc("Uncompressed size of a file inside an archive", "Size");
164 case CompressedSize:
165 return i18nc("Compressed size of a file inside an archive", "Compressed");
166 case Ratio:
167 return i18nc("Compression rate of file", "Rate");
168 case Owner:
169 return i18nc("File's owner username", "Owner");
170 case Group:
171 return i18nc("File's group", "Group");
172 case Permissions:
173 return i18nc("File permissions", "Mode");
174 case CRC:
175 return i18nc("CRC hash code", "CRC checksum");
176 case BLAKE2:
177 return i18nc("BLAKE2 hash code", "BLAKE2 checksum");
178 case Method:
179 return i18nc("Compression method", "Method");
180 case Version:
181 // TODO: what exactly is a file version?
182 return i18nc("File version", "Version");
183 case Timestamp:
184 return i18nc("Timestamp", "Date");
185 default:
186 return i18nc("Unnamed column", "??");
187 }
188 }
189 return QVariant();
190 }
191
index(int row,int column,const QModelIndex & parent) const192 QModelIndex ArchiveModel::index(int row, int column, const QModelIndex &parent) const
193 {
194 if (hasIndex(row, column, parent)) {
195 const Archive::Entry *parentEntry = parent.isValid()
196 ? static_cast<Archive::Entry*>(parent.internalPointer())
197 : m_rootEntry.data();
198
199 Q_ASSERT(parentEntry->isDir());
200
201 const Archive::Entry *item = parentEntry->entries().value(row, nullptr);
202 if (item != nullptr) {
203 return createIndex(row, column, const_cast<Archive::Entry*>(item));
204 }
205 }
206
207 return QModelIndex();
208 }
209
parent(const QModelIndex & index) const210 QModelIndex ArchiveModel::parent(const QModelIndex &index) const
211 {
212 if (index.isValid()) {
213 Archive::Entry *item = static_cast<Archive::Entry*>(index.internalPointer());
214 Q_ASSERT(item);
215 if (item->getParent() && (item->getParent() != m_rootEntry.data())) {
216 return createIndex(item->getParent()->row(), 0, item->getParent());
217 }
218 }
219 return QModelIndex();
220 }
221
entryForIndex(const QModelIndex & index)222 Archive::Entry *ArchiveModel::entryForIndex(const QModelIndex &index)
223 {
224 if (index.isValid()) {
225 Archive::Entry *item = static_cast<Archive::Entry*>(index.internalPointer());
226 Q_ASSERT(item);
227 return item;
228 }
229 return nullptr;
230 }
231
rowCount(const QModelIndex & parent) const232 int ArchiveModel::rowCount(const QModelIndex &parent) const
233 {
234 if (parent.column() <= 0) {
235 const Archive::Entry *parentEntry = parent.isValid()
236 ? static_cast<Archive::Entry*>(parent.internalPointer())
237 : m_rootEntry.data();
238
239 if (parentEntry && parentEntry->isDir()) {
240 return parentEntry->entries().count();
241 }
242 }
243 return 0;
244 }
245
columnCount(const QModelIndex & parent) const246 int ArchiveModel::columnCount(const QModelIndex &parent) const
247 {
248 Q_UNUSED(parent)
249 return m_showColumns.size();
250 }
251
supportedDropActions() const252 Qt::DropActions ArchiveModel::supportedDropActions() const
253 {
254 return Qt::CopyAction | Qt::MoveAction;
255 }
256
mimeTypes() const257 QStringList ArchiveModel::mimeTypes() const
258 {
259 QStringList types;
260
261 // MIME types we accept for dragging (eg. Dolphin -> Ark).
262 types << QStringLiteral("text/uri-list")
263 << QStringLiteral("text/plain")
264 << QStringLiteral("text/x-moz-url");
265
266 // MIME types we accept for dropping (eg. Ark -> Dolphin).
267 types << QStringLiteral("application/x-kde-ark-dndextract-service")
268 << QStringLiteral("application/x-kde-ark-dndextract-path");
269
270 return types;
271 }
272
mimeData(const QModelIndexList & indexes) const273 QMimeData *ArchiveModel::mimeData(const QModelIndexList &indexes) const
274 {
275 Q_UNUSED(indexes)
276
277 QMimeData *mimeData = new QMimeData;
278 mimeData->setData(QStringLiteral("application/x-kde-ark-dndextract-service"),
279 QDBusConnection::sessionBus().baseService().toUtf8());
280 mimeData->setData(QStringLiteral("application/x-kde-ark-dndextract-path"),
281 m_dbusPathName.toUtf8());
282
283 return mimeData;
284 }
285
dropMimeData(const QMimeData * data,Qt::DropAction action,int row,int column,const QModelIndex & parent)286 bool ArchiveModel::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent)
287 {
288 Q_UNUSED(action)
289
290 if (!data->hasUrls()) {
291 return false;
292 }
293
294 if (archive()->isReadOnly() ||
295 (archive()->encryptionType() != Archive::Unencrypted &&
296 archive()->password().isEmpty())) {
297 Q_EMIT messageWidget(KMessageWidget::Error, i18n("Adding files is not supported for this archive."));
298 return false;
299 }
300
301 QStringList paths;
302 const auto urls = data->urls();
303 for (const QUrl &url : urls) {
304 if (!url.isLocalFile()) {
305 Q_EMIT messageWidget(KMessageWidget::Error, i18n("You can only add local files to an archive."));
306 return false;
307 }
308 paths << url.toLocalFile();
309 }
310
311 const Archive::Entry *entry = nullptr;
312 QModelIndex droppedOnto = index(row, column, parent);
313 if (droppedOnto.isValid()) {
314 entry = entryForIndex(droppedOnto);
315 if (!entry->isDir()) {
316 entry = entry->getParent();
317 }
318 }
319
320 Q_EMIT droppedFiles(paths, entry);
321
322 return true;
323 }
324
325 // For a rationale, see bugs #194241, #241967 and #355839
cleanFileName(const QString & fileName)326 QString ArchiveModel::cleanFileName(const QString& fileName)
327 {
328 // Skip entries with filename "/" or "//" or "."
329 // "." is present in ISO files.
330 static QRegularExpression pattern(QStringLiteral("/+|\\."));
331 QRegularExpressionMatch match;
332 if (fileName.contains(pattern, &match) && match.captured() == fileName) {
333 qCDebug(ARK) << "Skipping entry with filename" << fileName;
334 return QString();
335 } else if (fileName.startsWith(QLatin1String("./"))) {
336 return fileName.mid(2);
337 }
338
339 return fileName;
340 }
341
initRootEntry()342 void ArchiveModel::initRootEntry()
343 {
344 m_rootEntry.reset(new Archive::Entry());
345 m_rootEntry->setProperty("isDirectory", true);
346 }
347
parentFor(const Archive::Entry * entry,InsertBehaviour behaviour)348 Archive::Entry *ArchiveModel::parentFor(const Archive::Entry *entry, InsertBehaviour behaviour)
349 {
350 QString fullPath = entry->fullPath();
351
352 if (fullPath.endsWith(QLatin1Char('/'))) {
353 fullPath = fullPath.chopped(1);
354 }
355
356 // Used to speed up loading of large archives.
357 const int index = fullPath.lastIndexOf(QLatin1Char('/'));
358 const QString folderPath = index != -1 ? fullPath.left(index) : QString();
359
360 if (s_previousMatch && *s_previousPath == folderPath) {
361 return s_previousMatch;
362 }
363
364 Archive::Entry *parent = m_rootEntry.data();
365
366 const auto pieces = QStringTokenizer{folderPath, QLatin1Char('/'), Qt::SkipEmptyParts};
367
368 for (const auto piece : pieces) {
369 Archive::Entry *entry = parent->find(piece);
370 if (!entry) {
371 // Directory entry will be traversed later (that happens for some archive formats, 7z for instance).
372 // We have to create one before, in order to construct tree from its children,
373 // and then delete the existing one (see ArchiveModel::newEntry).
374 entry = new Archive::Entry(parent);
375
376 entry->setProperty("fullPath", (parent == m_rootEntry.data())
377 ? QString(piece + QLatin1Char('/'))
378 : QString(parent->fullPath(WithTrailingSlash) + piece + QLatin1Char('/')));
379 entry->setProperty("isDirectory", true);
380 insertEntry(entry, behaviour);
381 }
382 if (!entry->isDir()) {
383 Archive::Entry *e = new Archive::Entry(parent);
384 e->copyMetaData(entry);
385 // Maybe we have both a file and a directory of the same name.
386 // We avoid removing previous entries unless necessary.
387 insertEntry(e, behaviour);
388 }
389 parent = entry;
390 }
391
392 s_previousMatch = parent;
393 *s_previousPath = folderPath;
394
395 return parent;
396 }
397
indexForEntry(Archive::Entry * entry)398 QModelIndex ArchiveModel::indexForEntry(Archive::Entry *entry)
399 {
400 Q_ASSERT(entry);
401 if (entry != m_rootEntry.data()) {
402 Q_ASSERT(entry->getParent());
403 Q_ASSERT(entry->getParent()->isDir());
404 return createIndex(entry->row(), 0, entry);
405 }
406 return QModelIndex();
407 }
408
slotEntryRemoved(const QString & path)409 void ArchiveModel::slotEntryRemoved(const QString & path)
410 {
411 const QString entryFileName(cleanFileName(path));
412 if (entryFileName.isEmpty()) {
413 return;
414 }
415
416 Archive::Entry *entry = m_rootEntry->findByPath(entryFileName.split(QLatin1Char('/'), Qt::SkipEmptyParts));
417 if (entry) {
418 Archive::Entry *parent = entry->getParent();
419 QModelIndex index = indexForEntry(entry);
420 Q_UNUSED(index);
421
422 beginRemoveRows(indexForEntry(parent), entry->row(), entry->row());
423 parent->removeEntryAt(entry->row());
424 endRemoveRows();
425 }
426 }
427
slotUserQuery(Kerfuffle::Query * query)428 void ArchiveModel::slotUserQuery(Kerfuffle::Query *query)
429 {
430 query->execute();
431 }
432
slotNewEntry(Archive::Entry * entry)433 void ArchiveModel::slotNewEntry(Archive::Entry *entry)
434 {
435 newEntry(entry, NotifyViews);
436 }
437
slotListEntry(Archive::Entry * entry)438 void ArchiveModel::slotListEntry(Archive::Entry *entry)
439 {
440 newEntry(entry, DoNotNotifyViews);
441 }
442
newEntry(Archive::Entry * receivedEntry,InsertBehaviour behaviour)443 void ArchiveModel::newEntry(Archive::Entry *receivedEntry, InsertBehaviour behaviour)
444 {
445 if (receivedEntry->fullPath().isEmpty()) {
446 qCDebug(ARK) << "Weird, received empty entry (no filename) - skipping";
447 return;
448 }
449
450 // If there are no columns registered, then populate columns from entry. If the first entry
451 // is a directory we check again for the first file entry to ensure all relevant columms are shown.
452 if (m_showColumns.isEmpty() || !m_fileEntryListed) {
453 QList<int> toInsert;
454
455 const auto size = receivedEntry->property("size").toULongLong();
456 const auto compressedSize = receivedEntry->property("compressedSize").toULongLong();
457 for (auto i = m_propertiesMap.begin(); i != m_propertiesMap.end(); ++i) {
458 // Singlefile plugin doesn't report the uncompressed size.
459 if (i.key() == Size && size == 0 && compressedSize > 0) {
460 continue;
461 }
462 if (!receivedEntry->property(i.value().constData()).toString().isEmpty()) {
463 if (i.key() != CompressedSize || receivedEntry->compressedSizeIsSet) {
464 if (!m_showColumns.contains(i.key())) {
465 toInsert << i.key();
466 }
467 }
468 }
469 }
470 if (behaviour == NotifyViews) {
471 beginInsertColumns(QModelIndex(), 0, toInsert.size() - 1);
472 }
473 m_showColumns << toInsert;
474 if (behaviour == NotifyViews) {
475 endInsertColumns();
476 }
477
478 m_fileEntryListed = !receivedEntry->isDir();
479 }
480
481 // #194241: Filenames such as "./file" should be displayed as "file"
482 // #241967: Entries called "/" should be ignored
483 // #355839: Entries called "//" should be ignored
484 QString entryFileName = cleanFileName(receivedEntry->fullPath());
485 if (entryFileName.isEmpty()) { // The entry contains only "." or "./"
486 return;
487 }
488 receivedEntry->setProperty("fullPath", entryFileName);
489
490 // For some archive formats (e.g. AppImage and RPM) paths of folders do not
491 // contain a trailing slash, so we append it.
492 if (receivedEntry->property("isDirectory").toBool() &&
493 !receivedEntry->property("fullPath").toString().endsWith(QLatin1Char('/'))) {
494 receivedEntry->setProperty("fullPath", QString(receivedEntry->property("fullPath").toString() + QLatin1Char('/')));
495 qCDebug(ARK) << "Trailing slash appended to entry:" << receivedEntry->property("fullPath");
496 }
497
498 // Skip already created entries.
499 Archive::Entry *existing = m_rootEntry->findByPath(entryFileName.split(QLatin1Char('/')));
500 if (existing) {
501 existing->setProperty("fullPath", entryFileName);
502 // Multi-volume files are repeated at least in RAR archives.
503 // In that case, we need to sum the compressed size for each volume
504 qulonglong currentCompressedSize = existing->property("compressedSize").toULongLong();
505 existing->setProperty("compressedSize", currentCompressedSize + receivedEntry->property("compressedSize").toULongLong());
506 return;
507 }
508
509 // Find parent entry, creating missing directory Archive::Entry's in the process.
510 Archive::Entry *parent = parentFor(receivedEntry, behaviour);
511
512 // Create an Archive::Entry.
513 Archive::Entry *entry = parent->find(Kerfuffle::Util::lastPathSegment(entryFileName));
514 if (entry) {
515 entry->copyMetaData(receivedEntry);
516 entry->setProperty("fullPath", entryFileName);
517 } else {
518 receivedEntry->setParent(parent);
519 insertEntry(receivedEntry, behaviour);
520 }
521 }
522
slotLoadingFinished(KJob * job)523 void ArchiveModel::slotLoadingFinished(KJob *job)
524 {
525 std::sort(m_showColumns.begin(), m_showColumns.end());
526
527 if (!job->error()) {
528
529 qCDebug(ARK) << "Showing columns: " << m_showColumns;
530
531 m_archive.reset(qobject_cast<LoadJob*>(job)->archive());
532
533 beginResetModel();
534 endResetModel();
535 }
536
537 Q_EMIT loadingFinished(job);
538 }
539
insertEntry(Archive::Entry * entry,InsertBehaviour behaviour)540 void ArchiveModel::insertEntry(Archive::Entry *entry, InsertBehaviour behaviour)
541 {
542 Q_ASSERT(entry);
543 Archive::Entry *parent = entry->getParent();
544 Q_ASSERT(parent);
545 if (behaviour == NotifyViews) {
546 beginInsertRows(indexForEntry(parent), parent->entries().count(), parent->entries().count());
547 }
548 parent->appendEntry(entry);
549 if (behaviour == NotifyViews) {
550 endInsertRows();
551 }
552 }
553
archive() const554 Kerfuffle::Archive* ArchiveModel::archive() const
555 {
556 return m_archive.data();
557 }
558
reset()559 void ArchiveModel::reset()
560 {
561 m_archive.reset(nullptr);
562 s_previousMatch = nullptr;
563 s_previousPath->clear();
564 initRootEntry();
565
566 // TODO: make sure if it's ok to not have calls to beginRemoveColumns here
567 m_showColumns.clear();
568 beginResetModel();
569 endResetModel();
570 }
571
createEmptyArchive(const QString & path,const QString & mimeType,QObject * parent)572 void ArchiveModel::createEmptyArchive(const QString &path, const QString &mimeType, QObject *parent)
573 {
574 reset();
575 m_archive.reset(Archive::createEmpty(path, mimeType, parent));
576 }
577
loadArchive(const QString & path,const QString & mimeType,QObject * parent)578 KJob *ArchiveModel::loadArchive(const QString &path, const QString &mimeType, QObject *parent)
579 {
580 reset();
581
582 auto loadJob = Archive::load(path, mimeType, parent);
583 connect(loadJob, &KJob::result, this, &ArchiveModel::slotLoadingFinished);
584 connect(loadJob, &Job::newEntry, this, &ArchiveModel::slotListEntry);
585 connect(loadJob, &Job::userQuery, this, &ArchiveModel::slotUserQuery);
586
587 Q_EMIT loadingStarted();
588
589 return loadJob;
590 }
591
extractFile(Archive::Entry * file,const QString & destinationDir,Kerfuffle::ExtractionOptions options) const592 ExtractJob* ArchiveModel::extractFile(Archive::Entry *file, const QString& destinationDir, Kerfuffle::ExtractionOptions options) const
593 {
594 QVector<Archive::Entry*> files({file});
595 return extractFiles(files, destinationDir, options);
596 }
597
extractFiles(const QVector<Archive::Entry * > & files,const QString & destinationDir,Kerfuffle::ExtractionOptions options) const598 ExtractJob* ArchiveModel::extractFiles(const QVector<Archive::Entry*>& files, const QString& destinationDir, Kerfuffle::ExtractionOptions options) const
599 {
600 Q_ASSERT(m_archive);
601 ExtractJob *newJob = m_archive->extractFiles(files, destinationDir, options);
602 connect(newJob, &ExtractJob::userQuery, this, &ArchiveModel::slotUserQuery);
603 return newJob;
604 }
605
preview(Archive::Entry * file) const606 Kerfuffle::PreviewJob *ArchiveModel::preview(Archive::Entry *file) const
607 {
608 Q_ASSERT(m_archive);
609 PreviewJob *job = m_archive->preview(file);
610 connect(job, &Job::userQuery, this, &ArchiveModel::slotUserQuery);
611 return job;
612 }
613
open(Archive::Entry * file) const614 OpenJob *ArchiveModel::open(Archive::Entry *file) const
615 {
616 Q_ASSERT(m_archive);
617 OpenJob *job = m_archive->open(file);
618 connect(job, &Job::userQuery, this, &ArchiveModel::slotUserQuery);
619 return job;
620 }
621
openWith(Archive::Entry * file) const622 OpenWithJob *ArchiveModel::openWith(Archive::Entry *file) const
623 {
624 Q_ASSERT(m_archive);
625 OpenWithJob *job = m_archive->openWith(file);
626 connect(job, &Job::userQuery, this, &ArchiveModel::slotUserQuery);
627 return job;
628 }
629
addFiles(QVector<Archive::Entry * > & entries,const Archive::Entry * destination,const CompressionOptions & options)630 AddJob* ArchiveModel::addFiles(QVector<Archive::Entry*> &entries, const Archive::Entry *destination, const CompressionOptions& options)
631 {
632 if (!m_archive) {
633 return nullptr;
634 }
635
636 if (!m_archive->isReadOnly()) {
637 AddJob *job = m_archive->addFiles(entries, destination, options);
638 connect(job, &AddJob::newEntry, this, &ArchiveModel::slotNewEntry);
639 connect(job, &AddJob::userQuery, this, &ArchiveModel::slotUserQuery);
640
641
642 return job;
643 }
644 return nullptr;
645 }
646
moveFiles(QVector<Archive::Entry * > & entries,Archive::Entry * destination,const CompressionOptions & options)647 Kerfuffle::MoveJob *ArchiveModel::moveFiles(QVector<Archive::Entry*> &entries, Archive::Entry *destination, const CompressionOptions &options)
648 {
649 if (!m_archive) {
650 return nullptr;
651 }
652
653 if (!m_archive->isReadOnly()) {
654 MoveJob *job = m_archive->moveFiles(entries, destination, options);
655 connect(job, &MoveJob::newEntry, this, &ArchiveModel::slotNewEntry);
656 connect(job, &MoveJob::userQuery, this, &ArchiveModel::slotUserQuery);
657 connect(job, &MoveJob::entryRemoved, this, &ArchiveModel::slotEntryRemoved);
658 connect(job, &MoveJob::finished, this, &ArchiveModel::slotCleanupEmptyDirs);
659
660
661 return job;
662 }
663 return nullptr;
664 }
copyFiles(QVector<Archive::Entry * > & entries,Archive::Entry * destination,const CompressionOptions & options)665 Kerfuffle::CopyJob *ArchiveModel::copyFiles(QVector<Archive::Entry*> &entries, Archive::Entry *destination, const CompressionOptions &options)
666 {
667 if (!m_archive) {
668 return nullptr;
669 }
670
671 if (!m_archive->isReadOnly()) {
672 CopyJob *job = m_archive->copyFiles(entries, destination, options);
673 connect(job, &CopyJob::newEntry, this, &ArchiveModel::slotNewEntry);
674 connect(job, &CopyJob::userQuery, this, &ArchiveModel::slotUserQuery);
675
676
677 return job;
678 }
679 return nullptr;
680 }
681
deleteFiles(QVector<Archive::Entry * > entries)682 DeleteJob* ArchiveModel::deleteFiles(QVector<Archive::Entry*> entries)
683 {
684 Q_ASSERT(m_archive);
685 if (!m_archive->isReadOnly()) {
686 DeleteJob *job = m_archive->deleteFiles(entries);
687 connect(job, &DeleteJob::entryRemoved, this, &ArchiveModel::slotEntryRemoved);
688
689 connect(job, &DeleteJob::finished, this, &ArchiveModel::slotCleanupEmptyDirs);
690
691 connect(job, &DeleteJob::userQuery, this, &ArchiveModel::slotUserQuery);
692 return job;
693 }
694 return nullptr;
695 }
696
encryptArchive(const QString & password,bool encryptHeader)697 void ArchiveModel::encryptArchive(const QString &password, bool encryptHeader)
698 {
699 if (!m_archive) {
700 return;
701 }
702
703 m_archive->encrypt(password, encryptHeader);
704 }
705
conflictingEntries(QList<const Archive::Entry * > & conflictingEntries,const QStringList & entries,bool allowMerging) const706 bool ArchiveModel::conflictingEntries(QList<const Archive::Entry*> &conflictingEntries, const QStringList &entries, bool allowMerging) const
707 {
708 bool error = false;
709
710 // We can't accept destination as an argument, because it can be a new entry path for renaming.
711 const Archive::Entry *destination;
712 {
713 QStringList destinationParts = entries.first().split(QLatin1Char('/'), Qt::SkipEmptyParts);
714 destinationParts.removeLast();
715 if (destinationParts.count() > 0) {
716 destination = m_rootEntry->findByPath(destinationParts);
717 } else {
718 destination = m_rootEntry.data();
719 }
720 }
721 const Archive::Entry *lastDirEntry = destination;
722 QString skippedDirPath;
723
724 for (const QString &entry : entries) {
725 if (skippedDirPath.count() > 0 && entry.startsWith(skippedDirPath)) {
726 continue;
727 } else {
728 skippedDirPath.clear();
729 }
730
731 while (!entry.startsWith(lastDirEntry->fullPath())) {
732 lastDirEntry = lastDirEntry->getParent();
733 }
734
735 bool isDir = entry.right(1) == QLatin1String("/");
736 const Archive::Entry *archiveEntry = lastDirEntry->find(entry.split(QLatin1Char('/'), Qt::SkipEmptyParts).last());
737
738 if (archiveEntry != nullptr) {
739 if (archiveEntry->isDir() != isDir || !allowMerging) {
740 if (isDir) {
741 skippedDirPath = lastDirEntry->fullPath();
742 }
743
744 if (!error) {
745 conflictingEntries.clear();
746 error = true;
747 }
748 conflictingEntries << archiveEntry;
749 } else {
750 if (isDir) {
751 lastDirEntry = archiveEntry;
752 }
753 else if (!error) {
754 conflictingEntries << archiveEntry;
755 }
756 }
757 } else if (isDir) {
758 skippedDirPath = entry;
759 }
760 }
761
762 return error;
763 }
764
hasDuplicatedEntries(const QStringList & entries)765 bool ArchiveModel::hasDuplicatedEntries(const QStringList &entries)
766 {
767 QStringList tempList;
768 for (const QString &entry : entries) {
769 if (tempList.contains(entry)) {
770 return true;
771 }
772 tempList << entry;
773 }
774 return false;
775 }
776
entryMap(const QVector<Archive::Entry * > & entries)777 QMap<QString, Archive::Entry*> ArchiveModel::entryMap(const QVector<Archive::Entry*> &entries)
778 {
779 QMap<QString, Archive::Entry*> map;
780 for (Archive::Entry *entry : entries) {
781 map.insert(entry->fullPath(), entry);
782 }
783 return map;
784 }
785
slotCleanupEmptyDirs()786 void ArchiveModel::slotCleanupEmptyDirs()
787 {
788 QList<QPersistentModelIndex> queue;
789 QList<QPersistentModelIndex> nodesToDelete;
790
791 // Add root nodes.
792 for (int i = 0; i < rowCount(); ++i) {
793 queue.append(QPersistentModelIndex(index(i, 0)));
794 }
795
796 // Breadth-first traverse.
797 while (!queue.isEmpty()) {
798 QPersistentModelIndex node = queue.takeFirst();
799 Archive::Entry *entry = entryForIndex(node);
800
801 if (!hasChildren(node)) {
802 if (entry->fullPath().isEmpty()) {
803 nodesToDelete << node;
804 }
805 } else {
806 for (int i = 0; i < rowCount(node); ++i) {
807 queue.append(QPersistentModelIndex(index(i, 0, node)));
808 }
809 }
810 }
811
812 for (const QPersistentModelIndex& node : std::as_const(nodesToDelete)) {
813 Archive::Entry *rawEntry = static_cast<Archive::Entry*>(node.internalPointer());
814 qCDebug(ARK) << "Delete with parent entries " << rawEntry->getParent()->entries() << " and row " << rawEntry->row();
815 beginRemoveRows(parent(node), rawEntry->row(), rawEntry->row());
816 rawEntry->getParent()->removeEntryAt(rawEntry->row());
817 endRemoveRows();
818 }
819 }
820
countEntriesAndSize()821 void ArchiveModel::countEntriesAndSize()
822 {
823 // This function is used to count the number of folders/files and
824 // the total compressed size. This is needed for PropertiesDialog
825 // to update the corresponding values after adding/deleting files.
826
827 // When ArchiveModel has been properly fixed, this code can likely
828 // be removed.
829
830 m_numberOfFiles = 0;
831 m_numberOfFolders = 0;
832 m_uncompressedSize = 0;
833
834 QElapsedTimer timer;
835 timer.start();
836
837 traverseAndCountDirNode(m_rootEntry.data());
838
839 qCDebug(ARK) << "Time to count entries and size:" << timer.elapsed() << "ms";
840 }
841
traverseAndCountDirNode(Archive::Entry * dir)842 void ArchiveModel::traverseAndCountDirNode(Archive::Entry *dir)
843 {
844 const auto entries = dir->entries();
845 for (Archive::Entry *entry : entries) {
846 if (entry->isDir()) {
847 traverseAndCountDirNode(entry);
848 m_numberOfFolders++;
849 } else {
850 m_numberOfFiles++;
851 m_uncompressedSize += entry->property("size").toULongLong();
852 }
853 }
854 }
855
numberOfFiles() const856 qulonglong ArchiveModel::numberOfFiles() const
857 {
858 return m_numberOfFiles;
859 }
860
numberOfFolders() const861 qulonglong ArchiveModel::numberOfFolders() const
862 {
863 return m_numberOfFolders;
864 }
865
uncompressedSize() const866 qulonglong ArchiveModel::uncompressedSize() const
867 {
868 return m_uncompressedSize;
869 }
870
shownColumns() const871 QList<int> ArchiveModel::shownColumns() const
872 {
873 return m_showColumns;
874 }
875
propertiesMap() const876 QMap<int, QByteArray> ArchiveModel::propertiesMap() const
877 {
878 return m_propertiesMap;
879 }
880