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"