File indexing completed on 2024-04-28 05:47:43

0001 /*
0002     SPDX-FileCopyrightText: 2007 Henrique Pinto <henrique.pinto@kdemail.net>
0003     SPDX-FileCopyrightText: 2008-2009 Harald Hvaal <haraldhv@stud.ntnu.no>
0004     SPDX-FileCopyrightText: 2010-2012 Raphael Kubo da Costa <rakuco@FreeBSD.org>
0005     SPDX-FileCopyrightText: 2016 Vladyslav Batyrenko <mvlabat@gmail.com>
0006 
0007     SPDX-License-Identifier: GPL-2.0-or-later
0008 */
0009 
0010 #include "archivemodel.h"
0011 #include "ark_debug.h"
0012 #include "jobs.h"
0013 #include "qstringtokenizer.h"
0014 #include "util.h"
0015 
0016 #include <KIO/Global>
0017 #include <KLocalizedString>
0018 
0019 #include <QApplication>
0020 #include <QDBusConnection>
0021 #include <QMimeData>
0022 #include <QRegularExpression>
0023 #include <QStyle>
0024 #include <QUrl>
0025 
0026 using namespace Kerfuffle;
0027 
0028 // Used to speed up the loading of large archives.
0029 static Archive::Entry *s_previousMatch = nullptr;
0030 Q_GLOBAL_STATIC(QString, s_previousPath)
0031 
0032 ArchiveModel::ArchiveModel(const QString &dbusPathName, QObject *parent)
0033     : QAbstractItemModel(parent)
0034     , m_dbusPathName(dbusPathName)
0035     , m_numberOfFiles(0)
0036     , m_numberOfFolders(0)
0037     , m_fileEntryListed(false)
0038 {
0039     initRootEntry();
0040 
0041     // Mappings between column indexes and entry properties.
0042     m_propertiesMap = {
0043         {DisplayName, "displayName"},
0044         {Size, "size"},
0045         {CompressedSize, "compressedSize"},
0046         {Permissions, "permissions"},
0047         {Owner, "owner"},
0048         {Group, "group"},
0049         {Ratio, "ratio"},
0050         {CRC, "CRC"},
0051         {BLAKE2, "BLAKE2"},
0052         {Method, "method"},
0053         {Version, "version"},
0054         {Timestamp, "timestamp"},
0055     };
0056 }
0057 
0058 ArchiveModel::~ArchiveModel()
0059 {
0060 }
0061 
0062 QVariant ArchiveModel::data(const QModelIndex &index, int role) const
0063 {
0064     if (index.isValid()) {
0065         Archive::Entry *entry = static_cast<Archive::Entry *>(index.internalPointer());
0066         switch (role) {
0067         case Qt::DisplayRole: {
0068             // TODO: complete the columns.
0069             int column = m_showColumns.at(index.column());
0070             switch (column) {
0071             case DisplayName:
0072                 return entry->displayName();
0073             case Size:
0074                 if (!entry->property("link").toString().isEmpty()) {
0075                     return QVariant();
0076                 } else {
0077                     return KIO::convertSize(entry->property("size").toULongLong());
0078                 }
0079             case CompressedSize:
0080                 if (entry->isDir() || !entry->property("link").toString().isEmpty()) {
0081                     return QVariant();
0082                 } else {
0083                     qulonglong compressedSize = entry->property("compressedSize").toULongLong();
0084                     if (compressedSize != 0) {
0085                         return KIO::convertSize(compressedSize);
0086                     } else {
0087                         return QVariant();
0088                     }
0089                 }
0090             case Ratio: // TODO: Use entry->metaData()[Ratio] when available.
0091                 if (entry->isDir() || !entry->property("link").toString().isEmpty()) {
0092                     return QVariant();
0093                 } else {
0094                     qulonglong compressedSize = entry->property("compressedSize").toULongLong();
0095                     qulonglong size = entry->property("size").toULongLong();
0096                     if (compressedSize == 0 || size == 0) {
0097                         return QVariant();
0098                     } else {
0099                         int ratio = int(100 * ((double)size - compressedSize) / size);
0100                         return QString(QString::number(ratio) + QStringLiteral(" %"));
0101                     }
0102                 }
0103 
0104             case Timestamp: {
0105                 const QDateTime timeStamp = entry->property("timestamp").toDateTime();
0106                 return QLocale().toString(timeStamp, QLocale::ShortFormat);
0107             }
0108 
0109             default:
0110                 return entry->property(m_propertiesMap[column].constData());
0111             }
0112         }
0113         case Qt::DecorationRole:
0114             if (index.column() == 0) {
0115                 Archive::Entry *e = static_cast<Archive::Entry *>(index.internalPointer());
0116                 QIcon::Mode mode = (filesToMove.contains(e->fullPath())) ? QIcon::Disabled : QIcon::Normal;
0117                 return e->icon().pixmap(QApplication::style()->pixelMetric(QStyle::PM_SmallIconSize), mode);
0118             }
0119             return QVariant();
0120         case Qt::FontRole: {
0121             QFont f;
0122             f.setItalic(entry->property("isPasswordProtected").toBool());
0123             return f;
0124         }
0125         default:
0126             return QVariant();
0127         }
0128     }
0129     return QVariant();
0130 }
0131 
0132 Qt::ItemFlags ArchiveModel::flags(const QModelIndex &index) const
0133 {
0134     if (index.isValid()) {
0135         const auto itemFlags = Qt::ItemIsEnabled | Qt::ItemIsSelectable | QAbstractItemModel::flags(index);
0136         return index.column() ? itemFlags : itemFlags | Qt::ItemIsDragEnabled;
0137     }
0138 
0139     return Qt::NoItemFlags;
0140 }
0141 
0142 QVariant ArchiveModel::headerData(int section, Qt::Orientation, int role) const
0143 {
0144     if (role == Qt::DisplayRole) {
0145         if (section >= m_showColumns.size()) {
0146             qCDebug(ARK) << "WEIRD: showColumns.size = " << m_showColumns.size() << " and section = " << section;
0147             return QVariant();
0148         }
0149 
0150         int columnId = m_showColumns.at(section);
0151 
0152         switch (columnId) {
0153         case DisplayName:
0154             return i18nc("Name of a file inside an archive", "Name");
0155         case Size:
0156             return i18nc("Uncompressed size of a file inside an archive", "Original Size");
0157         case CompressedSize:
0158             return i18nc("Compressed size of a file inside an archive", "Compressed Size");
0159         case Ratio:
0160             return i18nc("Compression rate of file", "Rate");
0161         case Owner:
0162             return i18nc("File's owner username", "Owner");
0163         case Group:
0164             return i18nc("File's group", "Group");
0165         case Permissions:
0166             return i18nc("File permissions", "Mode");
0167         case CRC:
0168             return i18nc("CRC hash code", "CRC checksum");
0169         case BLAKE2:
0170             return i18nc("BLAKE2 hash code", "BLAKE2 checksum");
0171         case Method:
0172             return i18nc("Compression method", "Method");
0173         case Version:
0174             // TODO: what exactly is a file version?
0175             return i18nc("File version", "Version");
0176         case Timestamp:
0177             return i18nc("Timestamp", "Date");
0178         default:
0179             return i18nc("Unnamed column", "??");
0180         }
0181     }
0182     return QVariant();
0183 }
0184 
0185 QModelIndex ArchiveModel::index(int row, int column, const QModelIndex &parent) const
0186 {
0187     if (hasIndex(row, column, parent)) {
0188         const Archive::Entry *parentEntry = parent.isValid() ? static_cast<Archive::Entry *>(parent.internalPointer()) : m_rootEntry.data();
0189 
0190         Q_ASSERT(parentEntry->isDir());
0191 
0192         const Archive::Entry *item = parentEntry->entries().value(row, nullptr);
0193         if (item != nullptr) {
0194             return createIndex(row, column, const_cast<Archive::Entry *>(item));
0195         }
0196     }
0197 
0198     return QModelIndex();
0199 }
0200 
0201 QModelIndex ArchiveModel::parent(const QModelIndex &index) const
0202 {
0203     if (index.isValid()) {
0204         Archive::Entry *item = static_cast<Archive::Entry *>(index.internalPointer());
0205         Q_ASSERT(item);
0206         if (item->getParent() && (item->getParent() != m_rootEntry.data())) {
0207             return createIndex(item->getParent()->row(), 0, item->getParent());
0208         }
0209     }
0210     return QModelIndex();
0211 }
0212 
0213 Archive::Entry *ArchiveModel::entryForIndex(const QModelIndex &index)
0214 {
0215     if (index.isValid()) {
0216         Archive::Entry *item = static_cast<Archive::Entry *>(index.internalPointer());
0217         Q_ASSERT(item);
0218         return item;
0219     }
0220     return nullptr;
0221 }
0222 
0223 int ArchiveModel::rowCount(const QModelIndex &parent) const
0224 {
0225     if (parent.column() <= 0) {
0226         const Archive::Entry *parentEntry = parent.isValid() ? static_cast<Archive::Entry *>(parent.internalPointer()) : m_rootEntry.data();
0227 
0228         if (parentEntry && parentEntry->isDir()) {
0229             return parentEntry->entries().count();
0230         }
0231     }
0232     return 0;
0233 }
0234 
0235 int ArchiveModel::columnCount(const QModelIndex &parent) const
0236 {
0237     Q_UNUSED(parent)
0238     return m_showColumns.size();
0239 }
0240 
0241 Qt::DropActions ArchiveModel::supportedDropActions() const
0242 {
0243     return Qt::CopyAction | Qt::MoveAction;
0244 }
0245 
0246 QStringList ArchiveModel::mimeTypes() const
0247 {
0248     QStringList types;
0249 
0250     // MIME types we accept for dragging (eg. Dolphin -> Ark).
0251     types << QStringLiteral("text/uri-list") << QStringLiteral("text/plain") << QStringLiteral("text/x-moz-url");
0252 
0253     // MIME types we accept for dropping (eg. Ark -> Dolphin).
0254     types << QStringLiteral("application/x-kde-ark-dndextract-service") << QStringLiteral("application/x-kde-ark-dndextract-path");
0255 
0256     return types;
0257 }
0258 
0259 QMimeData *ArchiveModel::mimeData(const QModelIndexList &indexes) const
0260 {
0261     Q_UNUSED(indexes)
0262 
0263     QMimeData *mimeData = new QMimeData;
0264     mimeData->setData(QStringLiteral("application/x-kde-ark-dndextract-service"), QDBusConnection::sessionBus().baseService().toUtf8());
0265     mimeData->setData(QStringLiteral("application/x-kde-ark-dndextract-path"), m_dbusPathName.toUtf8());
0266 
0267     return mimeData;
0268 }
0269 
0270 bool ArchiveModel::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent)
0271 {
0272     Q_UNUSED(action)
0273 
0274     if (!data->hasUrls()) {
0275         return false;
0276     }
0277 
0278     if (archive()->isReadOnly() || (archive()->encryptionType() != Archive::Unencrypted && archive()->password().isEmpty())) {
0279         Q_EMIT messageWidget(KMessageWidget::Error, i18n("Adding files is not supported for this archive."));
0280         return false;
0281     }
0282 
0283     QStringList paths;
0284     const auto urls = data->urls();
0285     for (const QUrl &url : urls) {
0286         if (!url.isLocalFile()) {
0287             Q_EMIT messageWidget(KMessageWidget::Error, i18n("You can only add local files to an archive."));
0288             return false;
0289         }
0290         paths << url.toLocalFile();
0291     }
0292 
0293     const Archive::Entry *entry = nullptr;
0294     QModelIndex droppedOnto = index(row, column, parent);
0295     if (droppedOnto.isValid()) {
0296         entry = entryForIndex(droppedOnto);
0297         if (!entry->isDir()) {
0298             entry = entry->getParent();
0299         }
0300     }
0301 
0302     Q_EMIT droppedFiles(paths, entry);
0303 
0304     return true;
0305 }
0306 
0307 // For a rationale, see bugs #194241, #241967 and #355839
0308 QString ArchiveModel::cleanFileName(const QString &fileName)
0309 {
0310     // Skip entries with filename "/" or "//" or "."
0311     // "." is present in ISO files.
0312     static QRegularExpression pattern(QStringLiteral("/+|\\."));
0313     QRegularExpressionMatch match;
0314     if (fileName.contains(pattern, &match) && match.captured() == fileName) {
0315         qCDebug(ARK) << "Skipping entry with filename" << fileName;
0316         return QString();
0317     } else if (fileName.startsWith(QLatin1String("./"))) {
0318         return fileName.mid(2);
0319     }
0320 
0321     return fileName;
0322 }
0323 
0324 void ArchiveModel::initRootEntry()
0325 {
0326     m_rootEntry.reset(new Archive::Entry());
0327     m_rootEntry->setProperty("isDirectory", true);
0328 }
0329 
0330 Archive::Entry *ArchiveModel::parentFor(const Archive::Entry *entry, InsertBehaviour behaviour)
0331 {
0332     QString fullPath = entry->fullPath();
0333 
0334     if (fullPath.endsWith(QLatin1Char('/'))) {
0335         fullPath = fullPath.chopped(1);
0336     }
0337 
0338     // Used to speed up loading of large archives.
0339     const int index = fullPath.lastIndexOf(QLatin1Char('/'));
0340     const QString folderPath = index != -1 ? fullPath.left(index) : QString();
0341 
0342     if (s_previousMatch && *s_previousPath == folderPath) {
0343         return s_previousMatch;
0344     }
0345 
0346     Archive::Entry *parent = m_rootEntry.data();
0347 
0348     const auto pieces = QStringTokenizer{folderPath, QLatin1Char('/'), Qt::SkipEmptyParts};
0349 
0350     for (const auto piece : pieces) {
0351         Archive::Entry *entry = parent->find(piece);
0352         if (!entry) {
0353             // Directory entry will be traversed later (that happens for some archive formats, 7z for instance).
0354             // We have to create one before, in order to construct tree from its children,
0355             // and then delete the existing one (see ArchiveModel::newEntry).
0356             entry = new Archive::Entry(parent);
0357 
0358             entry->setProperty("fullPath",
0359                                (parent == m_rootEntry.data()) ? QString(piece + QLatin1Char('/'))
0360                                                               : QString(parent->fullPath(WithTrailingSlash) + piece + QLatin1Char('/')));
0361             entry->setProperty("isDirectory", true);
0362             insertEntry(entry, behaviour);
0363         }
0364         if (!entry->isDir()) {
0365             Archive::Entry *e = new Archive::Entry(parent);
0366             e->copyMetaData(entry);
0367             // Maybe we have both a file and a directory of the same name.
0368             // We avoid removing previous entries unless necessary.
0369             insertEntry(e, behaviour);
0370         }
0371         parent = entry;
0372     }
0373 
0374     s_previousMatch = parent;
0375     *s_previousPath = folderPath;
0376 
0377     return parent;
0378 }
0379 
0380 QModelIndex ArchiveModel::indexForEntry(Archive::Entry *entry)
0381 {
0382     Q_ASSERT(entry);
0383     if (entry != m_rootEntry.data()) {
0384         Q_ASSERT(entry->getParent());
0385         Q_ASSERT(entry->getParent()->isDir());
0386         return createIndex(entry->row(), 0, entry);
0387     }
0388     return QModelIndex();
0389 }
0390 
0391 void ArchiveModel::slotEntryRemoved(const QString &path)
0392 {
0393     const QString entryFileName(cleanFileName(path));
0394     if (entryFileName.isEmpty()) {
0395         return;
0396     }
0397 
0398     Archive::Entry *entry = m_rootEntry->findByPath(entryFileName.split(QLatin1Char('/'), Qt::SkipEmptyParts));
0399     if (entry) {
0400         Archive::Entry *parent = entry->getParent();
0401         QModelIndex index = indexForEntry(entry);
0402         Q_UNUSED(index);
0403 
0404         beginRemoveRows(indexForEntry(parent), entry->row(), entry->row());
0405         parent->removeEntryAt(entry->row());
0406         endRemoveRows();
0407     }
0408 }
0409 
0410 void ArchiveModel::slotUserQuery(Kerfuffle::Query *query)
0411 {
0412     query->execute();
0413 }
0414 
0415 void ArchiveModel::slotNewEntry(Archive::Entry *entry)
0416 {
0417     newEntry(entry, NotifyViews);
0418 }
0419 
0420 void ArchiveModel::slotListEntry(Archive::Entry *entry)
0421 {
0422     newEntry(entry, DoNotNotifyViews);
0423 }
0424 
0425 void ArchiveModel::newEntry(Archive::Entry *receivedEntry, InsertBehaviour behaviour)
0426 {
0427     if (receivedEntry->fullPath().isEmpty()) {
0428         qCDebug(ARK) << "Weird, received empty entry (no filename) - skipping";
0429         return;
0430     }
0431 
0432     // If there are no columns registered, then populate columns from entry. If the first entry
0433     // is a directory we check again for the first file entry to ensure all relevant columms are shown.
0434     if (m_showColumns.isEmpty() || !m_fileEntryListed) {
0435         QList<int> toInsert;
0436 
0437         const auto size = receivedEntry->property("size").toULongLong();
0438         const auto compressedSize = receivedEntry->property("compressedSize").toULongLong();
0439         for (auto i = m_propertiesMap.begin(); i != m_propertiesMap.end(); ++i) {
0440             // libarchive plugin doesn't report the uncompressed size for "single-file" archives.
0441             if (i.key() == Size && size == 0 && compressedSize > 0) {
0442                 continue;
0443             }
0444             if (!receivedEntry->property(i.value().constData()).toString().isEmpty()) {
0445                 if (i.key() != CompressedSize || receivedEntry->compressedSizeIsSet) {
0446                     if (!m_showColumns.contains(i.key())) {
0447                         toInsert << i.key();
0448                     }
0449                 }
0450             }
0451         }
0452         if (behaviour == NotifyViews) {
0453             beginInsertColumns(QModelIndex(), 0, toInsert.size() - 1);
0454         }
0455         m_showColumns << toInsert;
0456         if (behaviour == NotifyViews) {
0457             endInsertColumns();
0458         }
0459 
0460         m_fileEntryListed = !receivedEntry->isDir();
0461     }
0462 
0463     // #194241: Filenames such as "./file" should be displayed as "file"
0464     // #241967: Entries called "/" should be ignored
0465     // #355839: Entries called "//" should be ignored
0466     QString entryFileName = cleanFileName(receivedEntry->fullPath());
0467     if (entryFileName.isEmpty()) { // The entry contains only "." or "./"
0468         return;
0469     }
0470     receivedEntry->setProperty("fullPath", entryFileName);
0471 
0472     // For some archive formats (e.g. AppImage and RPM) paths of folders do not
0473     // contain a trailing slash, so we append it.
0474     if (receivedEntry->property("isDirectory").toBool() && !receivedEntry->property("fullPath").toString().endsWith(QLatin1Char('/'))) {
0475         receivedEntry->setProperty("fullPath", QString(receivedEntry->property("fullPath").toString() + QLatin1Char('/')));
0476         qCDebug(ARK) << "Trailing slash appended to entry:" << receivedEntry->property("fullPath");
0477     }
0478 
0479     // Skip already created entries.
0480     Archive::Entry *existing = m_rootEntry->findByPath(entryFileName.split(QLatin1Char('/')));
0481     if (existing) {
0482         existing->setProperty("fullPath", entryFileName);
0483         // Multi-volume files are repeated at least in RAR archives.
0484         // In that case, we need to sum the compressed size for each volume
0485         qulonglong currentCompressedSize = existing->property("compressedSize").toULongLong();
0486         existing->setProperty("compressedSize", currentCompressedSize + receivedEntry->property("compressedSize").toULongLong());
0487         return;
0488     }
0489 
0490     // Find parent entry, creating missing directory Archive::Entry's in the process.
0491     Archive::Entry *parent = parentFor(receivedEntry, behaviour);
0492 
0493     // Create an Archive::Entry.
0494     Archive::Entry *entry = parent->find(Kerfuffle::Util::lastPathSegment(entryFileName));
0495     if (entry) {
0496         entry->copyMetaData(receivedEntry);
0497         entry->setProperty("fullPath", entryFileName);
0498     } else {
0499         receivedEntry->setParent(parent);
0500         insertEntry(receivedEntry, behaviour);
0501     }
0502 }
0503 
0504 void ArchiveModel::slotLoadingFinished(KJob *job)
0505 {
0506     std::sort(m_showColumns.begin(), m_showColumns.end());
0507 
0508     if (!job->error()) {
0509         qCDebug(ARK) << "Showing columns: " << m_showColumns;
0510 
0511         m_archive.reset(qobject_cast<LoadJob *>(job)->archive());
0512 
0513         beginResetModel();
0514         endResetModel();
0515     }
0516 
0517     Q_EMIT loadingFinished(job);
0518 }
0519 
0520 void ArchiveModel::insertEntry(Archive::Entry *entry, InsertBehaviour behaviour)
0521 {
0522     Q_ASSERT(entry);
0523     Archive::Entry *parent = entry->getParent();
0524     Q_ASSERT(parent);
0525     if (behaviour == NotifyViews) {
0526         beginInsertRows(indexForEntry(parent), parent->entries().count(), parent->entries().count());
0527     }
0528     parent->appendEntry(entry);
0529     if (behaviour == NotifyViews) {
0530         endInsertRows();
0531     }
0532 }
0533 
0534 Kerfuffle::Archive *ArchiveModel::archive() const
0535 {
0536     return m_archive.data();
0537 }
0538 
0539 void ArchiveModel::reset()
0540 {
0541     m_archive.reset(nullptr);
0542     s_previousMatch = nullptr;
0543     s_previousPath->clear();
0544     initRootEntry();
0545 
0546     // TODO: make sure if it's ok to not have calls to beginRemoveColumns here
0547     m_showColumns.clear();
0548     beginResetModel();
0549     endResetModel();
0550 }
0551 
0552 void ArchiveModel::createEmptyArchive(const QString &path, const QString &mimeType, QObject *parent)
0553 {
0554     reset();
0555     m_archive.reset(Archive::createEmpty(path, mimeType, parent));
0556 }
0557 
0558 Kerfuffle::LoadJob *ArchiveModel::loadArchive(const QString &path, const QString &mimeType, QObject *parent)
0559 {
0560     reset();
0561 
0562     auto loadJob = Archive::load(path, mimeType, parent);
0563     connect(loadJob, &KJob::result, this, &ArchiveModel::slotLoadingFinished);
0564     connect(loadJob, &Job::newEntry, this, &ArchiveModel::slotListEntry);
0565     connect(loadJob, &Job::userQuery, this, &ArchiveModel::slotUserQuery);
0566 
0567     Q_EMIT loadingStarted();
0568 
0569     return loadJob;
0570 }
0571 
0572 ExtractJob *ArchiveModel::extractFile(Archive::Entry *file, const QString &destinationDir, Kerfuffle::ExtractionOptions options) const
0573 {
0574     QVector<Archive::Entry *> files({file});
0575     return extractFiles(files, destinationDir, options);
0576 }
0577 
0578 ExtractJob *ArchiveModel::extractFiles(const QVector<Archive::Entry *> &files, const QString &destinationDir, Kerfuffle::ExtractionOptions options) const
0579 {
0580     Q_ASSERT(m_archive);
0581     ExtractJob *newJob = m_archive->extractFiles(files, destinationDir, options);
0582     connect(newJob, &ExtractJob::userQuery, this, &ArchiveModel::slotUserQuery);
0583     return newJob;
0584 }
0585 
0586 Kerfuffle::PreviewJob *ArchiveModel::preview(Archive::Entry *file) const
0587 {
0588     Q_ASSERT(m_archive);
0589     PreviewJob *job = m_archive->preview(file);
0590     connect(job, &Job::userQuery, this, &ArchiveModel::slotUserQuery);
0591     return job;
0592 }
0593 
0594 OpenJob *ArchiveModel::open(Archive::Entry *file) const
0595 {
0596     Q_ASSERT(m_archive);
0597     OpenJob *job = m_archive->open(file);
0598     connect(job, &Job::userQuery, this, &ArchiveModel::slotUserQuery);
0599     return job;
0600 }
0601 
0602 OpenWithJob *ArchiveModel::openWith(Archive::Entry *file) const
0603 {
0604     Q_ASSERT(m_archive);
0605     OpenWithJob *job = m_archive->openWith(file);
0606     connect(job, &Job::userQuery, this, &ArchiveModel::slotUserQuery);
0607     return job;
0608 }
0609 
0610 AddJob *ArchiveModel::addFiles(QVector<Archive::Entry *> &entries, const Archive::Entry *destination, const CompressionOptions &options)
0611 {
0612     if (!m_archive) {
0613         return nullptr;
0614     }
0615 
0616     if (!m_archive->isReadOnly()) {
0617         AddJob *job = m_archive->addFiles(entries, destination, options);
0618         connect(job, &AddJob::newEntry, this, &ArchiveModel::slotNewEntry);
0619         connect(job, &AddJob::userQuery, this, &ArchiveModel::slotUserQuery);
0620 
0621         return job;
0622     }
0623     return nullptr;
0624 }
0625 
0626 Kerfuffle::MoveJob *ArchiveModel::moveFiles(QVector<Archive::Entry *> &entries, Archive::Entry *destination, const CompressionOptions &options)
0627 {
0628     if (!m_archive) {
0629         return nullptr;
0630     }
0631 
0632     if (!m_archive->isReadOnly()) {
0633         MoveJob *job = m_archive->moveFiles(entries, destination, options);
0634         connect(job, &MoveJob::newEntry, this, &ArchiveModel::slotNewEntry);
0635         connect(job, &MoveJob::userQuery, this, &ArchiveModel::slotUserQuery);
0636         connect(job, &MoveJob::entryRemoved, this, &ArchiveModel::slotEntryRemoved);
0637         connect(job, &MoveJob::finished, this, &ArchiveModel::slotCleanupEmptyDirs);
0638 
0639         return job;
0640     }
0641     return nullptr;
0642 }
0643 Kerfuffle::CopyJob *ArchiveModel::copyFiles(QVector<Archive::Entry *> &entries, Archive::Entry *destination, const CompressionOptions &options)
0644 {
0645     if (!m_archive) {
0646         return nullptr;
0647     }
0648 
0649     if (!m_archive->isReadOnly()) {
0650         CopyJob *job = m_archive->copyFiles(entries, destination, options);
0651         connect(job, &CopyJob::newEntry, this, &ArchiveModel::slotNewEntry);
0652         connect(job, &CopyJob::userQuery, this, &ArchiveModel::slotUserQuery);
0653 
0654         return job;
0655     }
0656     return nullptr;
0657 }
0658 
0659 DeleteJob *ArchiveModel::deleteFiles(QVector<Archive::Entry *> entries)
0660 {
0661     Q_ASSERT(m_archive);
0662     if (!m_archive->isReadOnly()) {
0663         DeleteJob *job = m_archive->deleteFiles(entries);
0664         connect(job, &DeleteJob::entryRemoved, this, &ArchiveModel::slotEntryRemoved);
0665 
0666         connect(job, &DeleteJob::finished, this, &ArchiveModel::slotCleanupEmptyDirs);
0667 
0668         connect(job, &DeleteJob::userQuery, this, &ArchiveModel::slotUserQuery);
0669         return job;
0670     }
0671     return nullptr;
0672 }
0673 
0674 void ArchiveModel::encryptArchive(const QString &password, bool encryptHeader)
0675 {
0676     if (!m_archive) {
0677         return;
0678     }
0679 
0680     m_archive->encrypt(password, encryptHeader);
0681 }
0682 
0683 bool ArchiveModel::conflictingEntries(QList<const Archive::Entry *> &conflictingEntries, const QStringList &entries, bool allowMerging) const
0684 {
0685     bool error = false;
0686 
0687     // We can't accept destination as an argument, because it can be a new entry path for renaming.
0688     const Archive::Entry *destination;
0689     {
0690         QStringList destinationParts = entries.first().split(QLatin1Char('/'), Qt::SkipEmptyParts);
0691         destinationParts.removeLast();
0692         if (destinationParts.count() > 0) {
0693             destination = m_rootEntry->findByPath(destinationParts);
0694         } else {
0695             destination = m_rootEntry.data();
0696         }
0697     }
0698     const Archive::Entry *lastDirEntry = destination;
0699     QString skippedDirPath;
0700 
0701     for (const QString &entry : entries) {
0702         if (!skippedDirPath.isEmpty() && entry.startsWith(skippedDirPath)) {
0703             continue;
0704         } else {
0705             skippedDirPath.clear();
0706         }
0707 
0708         while (!entry.startsWith(lastDirEntry->fullPath())) {
0709             lastDirEntry = lastDirEntry->getParent();
0710         }
0711 
0712         bool isDir = entry.right(1) == QLatin1String("/");
0713         const Archive::Entry *archiveEntry = lastDirEntry->find(entry.split(QLatin1Char('/'), Qt::SkipEmptyParts).last());
0714 
0715         if (archiveEntry != nullptr) {
0716             if (archiveEntry->isDir() != isDir || !allowMerging) {
0717                 if (isDir) {
0718                     skippedDirPath = lastDirEntry->fullPath();
0719                 }
0720 
0721                 if (!error) {
0722                     conflictingEntries.clear();
0723                     error = true;
0724                 }
0725                 conflictingEntries << archiveEntry;
0726             } else {
0727                 if (isDir) {
0728                     lastDirEntry = archiveEntry;
0729                 } else if (!error) {
0730                     conflictingEntries << archiveEntry;
0731                 }
0732             }
0733         } else if (isDir) {
0734             skippedDirPath = entry;
0735         }
0736     }
0737 
0738     return error;
0739 }
0740 
0741 bool ArchiveModel::hasDuplicatedEntries(const QStringList &entries)
0742 {
0743     QStringList tempList;
0744     for (const QString &entry : entries) {
0745         if (tempList.contains(entry)) {
0746             return true;
0747         }
0748         tempList << entry;
0749     }
0750     return false;
0751 }
0752 
0753 QMap<QString, Archive::Entry *> ArchiveModel::entryMap(const QVector<Archive::Entry *> &entries)
0754 {
0755     QMap<QString, Archive::Entry *> map;
0756     for (Archive::Entry *entry : entries) {
0757         map.insert(entry->fullPath(), entry);
0758     }
0759     return map;
0760 }
0761 
0762 void ArchiveModel::slotCleanupEmptyDirs()
0763 {
0764     QList<QPersistentModelIndex> queue;
0765     QList<QPersistentModelIndex> nodesToDelete;
0766 
0767     // Add root nodes.
0768     for (int i = 0; i < rowCount(); ++i) {
0769         queue.append(QPersistentModelIndex(index(i, 0)));
0770     }
0771 
0772     // Breadth-first traverse.
0773     while (!queue.isEmpty()) {
0774         QPersistentModelIndex node = queue.takeFirst();
0775         Archive::Entry *entry = entryForIndex(node);
0776 
0777         if (!hasChildren(node)) {
0778             if (entry->fullPath().isEmpty()) {
0779                 nodesToDelete << node;
0780             }
0781         } else {
0782             for (int i = 0; i < rowCount(node); ++i) {
0783                 queue.append(QPersistentModelIndex(index(i, 0, node)));
0784             }
0785         }
0786     }
0787 
0788     for (const QPersistentModelIndex &node : std::as_const(nodesToDelete)) {
0789         Archive::Entry *rawEntry = static_cast<Archive::Entry *>(node.internalPointer());
0790         qCDebug(ARK) << "Delete with parent entries " << rawEntry->getParent()->entries() << " and row " << rawEntry->row();
0791         beginRemoveRows(parent(node), rawEntry->row(), rawEntry->row());
0792         rawEntry->getParent()->removeEntryAt(rawEntry->row());
0793         endRemoveRows();
0794     }
0795 }
0796 
0797 void ArchiveModel::countEntriesAndSize()
0798 {
0799     // This function is used to count the number of folders/files and
0800     // the total compressed size. This is needed for PropertiesDialog
0801     // to update the corresponding values after adding/deleting files.
0802 
0803     // When ArchiveModel has been properly fixed, this code can likely
0804     // be removed.
0805 
0806     m_numberOfFiles = 0;
0807     m_numberOfFolders = 0;
0808 
0809     QElapsedTimer timer;
0810     timer.start();
0811 
0812     traverseAndComputeDirSizes(m_rootEntry.data());
0813 
0814     qCDebug(ARK) << "Time to count entries and size:" << timer.elapsed() << "ms";
0815 }
0816 
0817 qulonglong ArchiveModel::traverseAndComputeDirSizes(Archive::Entry *dir)
0818 {
0819     const auto entries = dir->entries();
0820     qulonglong uncompressedSize = 0;
0821     for (Archive::Entry *entry : entries) {
0822         if (entry->isDir()) {
0823             m_numberOfFolders++;
0824             uncompressedSize += traverseAndComputeDirSizes(entry);
0825         } else {
0826             m_numberOfFiles++;
0827             uncompressedSize += entry->property("size").toULongLong();
0828         }
0829     }
0830     dir->setProperty("size", uncompressedSize);
0831     return uncompressedSize;
0832 }
0833 
0834 qulonglong ArchiveModel::numberOfFiles() const
0835 {
0836     return m_numberOfFiles;
0837 }
0838 
0839 qulonglong ArchiveModel::numberOfFolders() const
0840 {
0841     return m_numberOfFolders;
0842 }
0843 
0844 qulonglong ArchiveModel::uncompressedSize() const
0845 {
0846     return m_rootEntry.data()->property("size").toULongLong();
0847 }
0848 
0849 QList<int> ArchiveModel::shownColumns() const
0850 {
0851     return m_showColumns;
0852 }
0853 
0854 QMap<int, QByteArray> ArchiveModel::propertiesMap() const
0855 {
0856     return m_propertiesMap;
0857 }
0858 
0859 #include "moc_archivemodel.cpp"