diff --git a/CMakeLists.txt b/CMakeLists.txt --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -32,6 +32,7 @@ find_package(KF5IconThemes ${KF5_DEP_VERSION} REQUIRED) find_package(KF5KIO ${KF5_DEP_VERSION} REQUIRED) find_package(KF5ItemViews ${KF5_DEP_VERSION} REQUIRED) +find_package(KF5Package ${KF5_DEP_VERSION} REQUIRED) find_package(KF5Service ${KF5_DEP_VERSION} REQUIRED) find_package(KF5TextWidgets ${KF5_DEP_VERSION} REQUIRED) find_package(KF5WidgetsAddons ${KF5_DEP_VERSION} REQUIRED) diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -73,6 +73,7 @@ KF5::Archive # For decompressing archives KF5::I18n # For translations KF5::ConfigCore + KF5::Package Qt5::Gui # For QImage ) diff --git a/src/core/cache.h b/src/core/cache.h --- a/src/core/cache.h +++ b/src/core/cache.h @@ -57,6 +57,17 @@ void insertRequest(const KNSCore::Provider::SearchRequest &, const KNSCore::EntryInternal::List &entries); EntryInternal::List requestFromCache(const KNSCore::Provider::SearchRequest &); + /** + * This will run through all entries in the cache, and remove all entries + * where all the installed files they refer to no longer exist. + * + * This cannot be done wholesale for all caches, as some consumers will allow + * this to happen (or indeed expect it to), and so we have to do this on a + * per-type basis + * + * This will also cause the cache store to be updated + */ + void removeDeletedEntries(); public Q_SLOTS: void registerChangedEntry(const KNSCore::EntryInternal &entry); diff --git a/src/core/cache.cpp b/src/core/cache.cpp --- a/src/core/cache.cpp +++ b/src/core/cache.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -241,6 +242,7 @@ { setProperty("dirty", true); cache.insert(entry); + QTimer::singleShot(1000, this, [this](){ writeRegistry(); }); } void Cache::insertRequest(const KNSCore::Provider::SearchRequest &request, const KNSCore::EntryInternal::List &entries) @@ -261,3 +263,22 @@ return requestCache.value(request.hashForRequest()); } +void KNSCore::Cache::removeDeletedEntries() +{ + QMutableSetIterator i(cache); + while (i.hasNext()) { + const KNSCore::EntryInternal &entry = i.next(); + bool installedFileExists{false}; + for (const auto &installedFile: entry.installedFiles()) { + if (QFile::exists(installedFile)) { + installedFileExists = true; + break; + } + } + if (!installedFileExists) { + i.remove(); + setProperty("dirty", true); + } + } + writeRegistry(); +} diff --git a/src/core/engine.h b/src/core/engine.h --- a/src/core/engine.h +++ b/src/core/engine.h @@ -89,6 +89,7 @@ * * @param configfile KNewStuff2 configuration file (*.knsrc) * @return \b true if any valid configuration was found, \b false otherwise + * @see KNS3::DownloadDialog */ bool init(const QString &configfile); diff --git a/src/core/engine.cpp b/src/core/engine.cpp --- a/src/core/engine.cpp +++ b/src/core/engine.cpp @@ -180,6 +180,9 @@ qCDebug(KNEWSTUFFCORE) << "Cache is" << m_cache << "for" << configFileName; connect(this, &Engine::signalEntryChanged, m_cache.data(), &Cache::registerChangedEntry); m_cache->readRegistry(); + if (m_installation->uncompressionSetting() == Installation::UseKPackageUncompression) { + m_cache->removeDeletedEntries(); + } m_initialized = true; diff --git a/src/core/installation.h b/src/core/installation.h --- a/src/core/installation.h +++ b/src/core/installation.h @@ -64,6 +64,13 @@ ScopeUser, ScopeSystem }; + enum UncompressionOptions { + NeverUncompress, ///@< Never attempt to decompress a file, whatever format it is. Matches "never" knsrc setting + AlwaysUncompress, ///@< Assume all downloaded files are archives, and attempt to decompress them. Will cause failure if decompression fails. Matches "always" knsrc setting + UncompressIfArchive, ///@< If the file is an archive, decompress it, otherwise just pass it on. Matches "archive" knsrc setting + UncompressIntoSubdir, ///@< As Archive, except that if there is more than an item in the file, put contents in a subdirectory with the same name as the file. Matches "subdir" knsrc setting + UseKPackageUncompression ///@< Use the internal KPackage support for installing and uninstalling the package. Matches "kpackage" knsrc setting + }; bool readConfig(const KConfigGroup &group); bool isRemote() const; @@ -116,6 +123,13 @@ */ void uninstall(KNSCore::EntryInternal entry); + /** + * Returns the uncompression setting, in a computer-readable format + * + * @return The value of this setting + */ + UncompressionOptions uncompressionSetting() const; + // TODO KF6: remove, was used with deprecated Security class. #if KNEWSTUFFCORE_ENABLE_DEPRECATED_SINCE(5, 31) KNEWSTUFFCORE_DEPRECATED_VERSION(5, 31, "No longer use") diff --git a/src/core/installation.cpp b/src/core/installation.cpp --- a/src/core/installation.cpp +++ b/src/core/installation.cpp @@ -33,6 +33,11 @@ #include "krandom.h" #include "kshell.h" +#include +#include +#include +// also ensure the cache is updated (load cache, mark kpackagetool removed items as uninstalled? (check what makes best sense)) + #include #include "klocalizedstring.h" #include @@ -54,6 +59,8 @@ , customName(false) , acceptHtml(false) { + // TODO KF6 Make this a real property, when we can refactor this and add a proper dptr + setProperty("kpackageType", QLatin1String("")); } bool Installation::readConfig(const KConfigGroup &group) @@ -68,11 +75,12 @@ else if (uncompresssetting == QLatin1String("true")) { uncompresssetting = QStringLiteral("always"); } - if (uncompresssetting != QLatin1String("always") && uncompresssetting != QLatin1String("archive") && uncompresssetting != QLatin1String("never") && uncompresssetting != QLatin1String("subdir")) { - qCCritical(KNEWSTUFFCORE) << "invalid Uncompress setting chosen, must be one of: subdir, always, archive, or never"; + if (uncompresssetting != QLatin1String("always") && uncompresssetting != QLatin1String("archive") && uncompresssetting != QLatin1String("never") && uncompresssetting != QLatin1String("subdir") && uncompresssetting != QLatin1String("kpackage")) { + qCCritical(KNEWSTUFFCORE) << "invalid Uncompress setting chosen, must be one of: subdir, always, archive, never, or kpackage"; return false; } uncompression = uncompresssetting; + setProperty("kpackageType", group.readEntry("KPackageType", QString())); postInstallationCommand = group.readEntry("InstallationCommand", QString()); uninstallCommand = group.readEntry("UninstallCommand", QString()); standardResourceDirectory = group.readEntry("StandardResource", QString()); @@ -91,6 +99,21 @@ uncompression = QStringLiteral("subdir"); } + // A touch of special treatment for the various old kpackage based knsrc files, so they work + // with the new, internal stuff. The result unfortunately is that all entries marked as + // installed in knewstuff no longer will be, but since it never worked right anyway... we'll + // simply have to live with that. + if (postInstallationCommand.startsWith(QLatin1String("kpackagetool5 -t")) && + postInstallationCommand.endsWith(QLatin1String("-i %f")) && + uninstallCommand.startsWith(QLatin1String("kpackagetool5 -t")) && + uninstallCommand.endsWith(QLatin1String("-r %f"))) { + uncompression = QLatin1String("kpackage"); + postInstallationCommand = QLatin1String(""); + // Not clearing uninstallCommand, as this is used for the fallback situation + setProperty("kpackageType", uninstallCommand.mid(17, uninstallCommand.length() - 17 - 6)); + qCWarning(KNEWSTUFFCORE) << "Your configuration file uses an old version of the kpackage support, and should be converted. Please report this to the author of the software you are currently using. The package type, we assume, is" << property("kpackageType").toString(); + } + installPath = group.readEntry("InstallPath", QString()); absoluteInstallPath = group.readEntry("AbsoluteInstallPath", QString()); customName = group.readEntry("CustomName", false); @@ -297,40 +320,42 @@ QString targetPath = targetInstallationPath(); QStringList installedFiles = installDownloadedFileAndUncompress(entry, downloadedFile, targetPath); - if (installedFiles.isEmpty()) { - if (entry.status() == KNS3::Entry::Installing) { - entry.setStatus(KNS3::Entry::Downloadable); - } else if (entry.status() == KNS3::Entry::Updating) { - entry.setStatus(KNS3::Entry::Updateable); + if (uncompression != QLatin1String("kpackage")) { + if (installedFiles.isEmpty()) { + if (entry.status() == KNS3::Entry::Installing) { + entry.setStatus(KNS3::Entry::Downloadable); + } else if (entry.status() == KNS3::Entry::Updating) { + entry.setStatus(KNS3::Entry::Updateable); + } + emit signalEntryChanged(entry); + emit signalInstallationFailed(i18n("Could not install \"%1\": file not found.", entry.name())); + return; } - emit signalEntryChanged(entry); - emit signalInstallationFailed(i18n("Could not install \"%1\": file not found.", entry.name())); - return; - } - entry.setInstalledFiles(installedFiles); + entry.setInstalledFiles(installedFiles); - auto installationFinished = [this, entry]() { - EntryInternal newentry = entry; - // update version and release date to the new ones - if (newentry.status() == KNS3::Entry::Updating) { - if (!newentry.updateVersion().isEmpty()) { - newentry.setVersion(newentry.updateVersion()); - } - if (newentry.updateReleaseDate().isValid()) { - newentry.setReleaseDate(newentry.updateReleaseDate()); + auto installationFinished = [this, entry]() { + EntryInternal newentry = entry; + // update version and release date to the new ones + if (newentry.status() == KNS3::Entry::Updating) { + if (!newentry.updateVersion().isEmpty()) { + newentry.setVersion(newentry.updateVersion()); + } + if (newentry.updateReleaseDate().isValid()) { + newentry.setReleaseDate(newentry.updateReleaseDate()); + } } - } - newentry.setStatus(KNS3::Entry::Installed); - emit signalEntryChanged(newentry); - emit signalInstallationFinished(); - }; - if (!postInstallationCommand.isEmpty()) { - QProcess* p = runPostInstallationCommand(installedFiles.size() == 1 ? installedFiles.first() : targetPath); - connect(p, static_cast(&QProcess::finished), this, installationFinished); - } else { - installationFinished(); + newentry.setStatus(KNS3::Entry::Installed); + emit signalEntryChanged(newentry); + emit signalInstallationFinished(); + }; + if (!postInstallationCommand.isEmpty()) { + QProcess* p = runPostInstallationCommand(installedFiles.size() == 1 ? installedFiles.first() : targetPath); + connect(p, static_cast(&QProcess::finished), this, installationFinished); + } else { + installationFinished(); + } } } @@ -425,141 +450,241 @@ bool isarchive = true; // respect the uncompress flag in the knsrc - if (uncompression == QLatin1String("always") || uncompression == QLatin1String("archive") || uncompression == QLatin1String("subdir")) { - // this is weird but a decompression is not a single name, so take the path instead - QMimeDatabase db; - QMimeType mimeType = db.mimeTypeForFile(payloadfile); - qCDebug(KNEWSTUFFCORE) << "Postinstallation: uncompress the file"; - - // FIXME: check for overwriting, malicious archive entries (../foo) etc. - // FIXME: KArchive should provide "safe mode" for this! - QScopedPointer archive; - - if (mimeType.inherits(QStringLiteral("application/zip"))) { - archive.reset(new KZip(payloadfile)); - } else if (mimeType.inherits(QStringLiteral("application/tar")) - || mimeType.inherits(QStringLiteral("application/x-gzip")) - || mimeType.inherits(QStringLiteral("application/x-bzip")) - || mimeType.inherits(QStringLiteral("application/x-lzma")) - || mimeType.inherits(QStringLiteral("application/x-xz")) - || mimeType.inherits(QStringLiteral("application/x-bzip-compressed-tar")) - || mimeType.inherits(QStringLiteral("application/x-compressed-tar"))) { - archive.reset(new KTar(payloadfile)); - } else { - qCCritical(KNEWSTUFFCORE) << "Could not determine type of archive file '" << payloadfile << "'"; - if (uncompression == QLatin1String("always")) { - emit signalInstallationError(i18n("Could not determine the type of archive of the downloaded file %1", payloadfile)); - return QStringList(); + if (uncompression == QLatin1String("kpackage")) { + qCDebug(KNEWSTUFFCORE) << "Using KPackage for installation"; + KPackage::PackageStructure structure; + KPackage::Package package(&structure); + QString serviceType; + package.setPath(payloadfile); + auto resetEntryStatus = [this,entry](){ + KNSCore::EntryInternal changedEntry(entry); + if (changedEntry.status() == KNS3::Entry::Installing) { + changedEntry.setStatus(KNS3::Entry::Downloadable); + } else if (changedEntry.status() == KNS3::Entry::Updating) { + changedEntry.setStatus(KNS3::Entry::Updateable); + } + emit signalEntryChanged(changedEntry); + }; + if (package.isValid() && package.metadata().isValid()) { + qCDebug(KNEWSTUFFCORE) << "Package metadata is valid"; + serviceType = package.metadata().value(QStringLiteral("X-Plasma-ServiceType")); + if (serviceType.isEmpty() && !package.metadata().serviceTypes().isEmpty()) { + serviceType = package.metadata().serviceTypes().first(); + } + if (serviceType.isEmpty()) { + serviceType = property("kpackageType").toString(); + } + if (!serviceType.isEmpty()) { + qCDebug(KNEWSTUFFCORE) << "Service type discovered as" << serviceType; + KPackage::PackageStructure *structure = KPackage::PackageLoader::self()->loadPackageStructure(serviceType); + if (structure) { + KPackage::Package installer = KPackage::Package(structure); + if (installer.hasValidStructure()) { + QString packageRoot = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Char('/') + installer.defaultPackageRoot(); + qCDebug(KNEWSTUFFCORE) << "About to attempt to install" << package.metadata().pluginId() << "into" << packageRoot; + const QString expectedDir{packageRoot + package.metadata().pluginId()}; + KJob *installJob = installer.update(payloadfile, packageRoot); + // TODO KF6 Really, i would prefer to make more functions to handle this, but as this is + // an exported class, i'd rather not pollute the public namespace with internal functions, + // and we don't have a pimpl, so... we'll just have to deal with it for now + connect(installJob, &KJob::result, this, [this,entry,payloadfile,expectedDir,resetEntryStatus](KJob* job){ + if (job->error() == KJob::NoError) { + if (QFile::exists(expectedDir)) { + EntryInternal newentry = entry; + newentry.setInstalledFiles(QStringList{expectedDir}); + // update version and release date to the new ones + if (newentry.status() == KNS3::Entry::Updating) { + if (!newentry.updateVersion().isEmpty()) { + newentry.setVersion(newentry.updateVersion()); + } + if (newentry.updateReleaseDate().isValid()) { + newentry.setReleaseDate(newentry.updateReleaseDate()); + } + } + newentry.setStatus(KNS3::Entry::Installed); + emit signalEntryChanged(newentry); + emit signalInstallationFinished(); + qCDebug(KNEWSTUFFCORE) << "Install job finished with no error and we now have files" << expectedDir; + } else { + emit signalInstallationFailed(i18n("The installation of %1 failed to create the expected new directory %2").arg(payloadfile).arg(expectedDir)); + resetEntryStatus(); + qCDebug(KNEWSTUFFCORE) << "Install job finished with no error, but we do not have the expected new directory" << expectedDir; + } + } else { + if (job->error() == KPackage::Package::JobError::NewerVersionAlreadyInstalledError) { + EntryInternal newentry = entry; + newentry.setStatus(KNS3::Entry::Installed); + emit signalEntryChanged(newentry); + emit signalInstallationFinished(); + newentry.setInstalledFiles(QStringList{expectedDir}); + qCDebug(KNEWSTUFFCORE) << "Install job finished telling us this item was already installed with this version, so... let's just make a small fib and say we totally installed that, honest, and we now have files" << expectedDir; + } else { + emit signalInstallationFailed(i18n("Installation of %1 failed: %2", payloadfile, job->errorText())); + resetEntryStatus(); + qCDebug(KNEWSTUFFCORE) << "Install job finished with error state" << job->error() << "and description" << job->error(); + } + } + }); + } else { + emit signalInstallationFailed(i18n("The installation of %1 failed, as the service type %2 was not accepted by the system (did you forget to install the KPackage support plugin for this type of package?)", payloadfile, serviceType)); + resetEntryStatus(); + qCWarning(KNEWSTUFFCORE) << "Package serviceType" << serviceType << "not found"; + } + } else { + // no package structure + emit signalInstallationFailed(i18n("The installation of %1 failed, as the downloaded package does not contain a correct KPackage structure.", payloadfile)); + resetEntryStatus(); + qCWarning(KNEWSTUFFCORE) << "Could not load the package structure for KPackage service type" << serviceType; + } + } else { + // no service type + emit signalInstallationFailed(i18n("The installation of %1 failed, as the downloaded package does not list a service type.", payloadfile)); + resetEntryStatus(); + qCWarning(KNEWSTUFFCORE) << "No service type listed in" << payloadfile; } - isarchive = false; + } else { + // package or package metadata is invalid + emit signalInstallationFailed(i18n("The installation of %1 failed, as the downloaded package does not contain any useful meta information, which means it is not a valid KPackage.", payloadfile)); + resetEntryStatus(); + qCWarning(KNEWSTUFFCORE) << "No valid meta information (which suggests no valid KPackage) found in" << payloadfile; } - - if (isarchive) { - bool success = archive->open(QIODevice::ReadOnly); - if (!success) { - qCCritical(KNEWSTUFFCORE) << "Cannot open archive file '" << payloadfile << "'"; + } else { + if (uncompression == QLatin1String("always") || uncompression == QLatin1String("archive") || uncompression == QLatin1String("subdir")) { + // this is weird but a decompression is not a single name, so take the path instead + QMimeDatabase db; + QMimeType mimeType = db.mimeTypeForFile(payloadfile); + qCDebug(KNEWSTUFFCORE) << "Postinstallation: uncompress the file"; + + // FIXME: check for overwriting, malicious archive entries (../foo) etc. + // FIXME: KArchive should provide "safe mode" for this! + QScopedPointer archive; + + if (mimeType.inherits(QStringLiteral("application/zip"))) { + archive.reset(new KZip(payloadfile)); + } else if (mimeType.inherits(QStringLiteral("application/tar")) + || mimeType.inherits(QStringLiteral("application/x-gzip")) + || mimeType.inherits(QStringLiteral("application/x-bzip")) + || mimeType.inherits(QStringLiteral("application/x-lzma")) + || mimeType.inherits(QStringLiteral("application/x-xz")) + || mimeType.inherits(QStringLiteral("application/x-bzip-compressed-tar")) + || mimeType.inherits(QStringLiteral("application/x-compressed-tar"))) { + archive.reset(new KTar(payloadfile)); + } else { + qCCritical(KNEWSTUFFCORE) << "Could not determine type of archive file '" << payloadfile << "'"; if (uncompression == QLatin1String("always")) { - emit signalInstallationError(i18n("Failed to open the archive file %1. The reported error was: %2", payloadfile, archive->errorString())); + emit signalInstallationError(i18n("Could not determine the type of archive of the downloaded file %1", payloadfile)); return QStringList(); } - // otherwise, just copy the file isarchive = false; } if (isarchive) { - const KArchiveDirectory *dir = archive->directory(); - //if there is more than an item in the file, and we are requested to do so - //put contents in a subdirectory with the same name as the file - QString installpath; - if (uncompression == QLatin1String("subdir") && dir->entries().count() > 1) { - installpath = installdir + QLatin1Char('/') + QFileInfo(archive->fileName()).baseName(); - } else { - installpath = installdir; + bool success = archive->open(QIODevice::ReadOnly); + if (!success) { + qCCritical(KNEWSTUFFCORE) << "Cannot open archive file '" << payloadfile << "'"; + if (uncompression == QLatin1String("always")) { + emit signalInstallationError(i18n("Failed to open the archive file %1. The reported error was: %2", payloadfile, archive->errorString())); + return QStringList(); + } + // otherwise, just copy the file + isarchive = false; } - if (dir->copyTo(installpath)) { - installedFiles << archiveEntries(installpath, dir); - installedFiles << installpath + QLatin1Char('/'); - } else - qCWarning(KNEWSTUFFCORE) << "could not install" << entry.name() << "to" << installpath; - - archive->close(); - QFile::remove(payloadfile); + if (isarchive) { + const KArchiveDirectory *dir = archive->directory(); + //if there is more than an item in the file, and we are requested to do so + //put contents in a subdirectory with the same name as the file + QString installpath; + if (uncompression == QLatin1String("subdir") && dir->entries().count() > 1) { + installpath = installdir + QLatin1Char('/') + QFileInfo(archive->fileName()).baseName(); + } else { + installpath = installdir; + } + + if (dir->copyTo(installpath)) { + installedFiles << archiveEntries(installpath, dir); + installedFiles << installpath + QLatin1Char('/'); + } else + qCWarning(KNEWSTUFFCORE) << "could not install" << entry.name() << "to" << installpath; + + archive->close(); + QFile::remove(payloadfile); + } } } - } - qCDebug(KNEWSTUFFCORE) << "isarchive: " << isarchive; - - //some wallpapers are compressed, some aren't - if ((!isarchive && standardResourceDirectory == QLatin1String("wallpaper")) || - (uncompression == QLatin1String("never") || (uncompression == QLatin1String("archive") && !isarchive))) { - // no decompress but move to target - - /// @todo when using KIO::get the http header can be accessed and it contains a real file name. - // FIXME: make naming convention configurable through *.knsrc? e.g. for kde-look.org image names - QUrl source = QUrl(entry.payload()); - qCDebug(KNEWSTUFFCORE) << "installing non-archive from " << source.url(); - QString installfile; - QString ext = source.fileName().section(QLatin1Char('.'), -1); - if (customName) { - installfile = entry.name(); - installfile += QLatin1Char('-') + entry.version(); - if (!ext.isEmpty()) { - installfile += QLatin1Char('.') + ext; - } - } else { - // TODO HACK This is a hack, the correct way of fixing it would be doing the KIO::get - // and using the http headers if they exist to get the file name, but as discussed in - // Randa this is not going to happen anytime soon (if ever) so go with the hack - if (source.url().startsWith(QLatin1String("http://newstuff.kde.org/cgi-bin/hotstuff-access?file="))) { - installfile = QUrlQuery(source).queryItemValue(QStringLiteral("file")); - int lastSlash = installfile.lastIndexOf(QLatin1Char('/')); - if (lastSlash >= 0) { - installfile = installfile.mid(lastSlash); + qCDebug(KNEWSTUFFCORE) << "isarchive: " << isarchive; + + //some wallpapers are compressed, some aren't + if ((!isarchive && standardResourceDirectory == QLatin1String("wallpaper")) || + (uncompression == QLatin1String("never") || (uncompression == QLatin1String("archive") && !isarchive))) { + // no decompress but move to target + + /// @todo when using KIO::get the http header can be accessed and it contains a real file name. + // FIXME: make naming convention configurable through *.knsrc? e.g. for kde-look.org image names + QUrl source = QUrl(entry.payload()); + qCDebug(KNEWSTUFFCORE) << "installing non-archive from " << source.url(); + QString installfile; + QString ext = source.fileName().section(QLatin1Char('.'), -1); + if (customName) { + installfile = entry.name(); + installfile += QLatin1Char('-') + entry.version(); + if (!ext.isEmpty()) { + installfile += QLatin1Char('.') + ext; + } + } else { + // TODO HACK This is a hack, the correct way of fixing it would be doing the KIO::get + // and using the http headers if they exist to get the file name, but as discussed in + // Randa this is not going to happen anytime soon (if ever) so go with the hack + if (source.url().startsWith(QLatin1String("http://newstuff.kde.org/cgi-bin/hotstuff-access?file="))) { + installfile = QUrlQuery(source).queryItemValue(QStringLiteral("file")); + int lastSlash = installfile.lastIndexOf(QLatin1Char('/')); + if (lastSlash >= 0) { + installfile = installfile.mid(lastSlash); + } + } + if (installfile.isEmpty()) { + installfile = source.fileName(); } } - if (installfile.isEmpty()) { - installfile = source.fileName(); + QString installpath = installdir + QLatin1Char('/') + installfile; + + qCDebug(KNEWSTUFFCORE) << "Install to file " << installpath; + // FIXME: copy goes here (including overwrite checking) + // FIXME: what must be done now is to update the cache *again* + // in order to set the new payload filename (on root tag only) + // - this might or might not need to take uncompression into account + // FIXME: for updates, we might need to force an overwrite (that is, deleting before) + QFile file(payloadfile); + bool success = true; + const bool update = ((entry.status() == KNS3::Entry::Updateable) || (entry.status() == KNS3::Entry::Updating)); + + if (QFile::exists(installpath) && QDir::tempPath() != installdir) { + if (!update) { + Question question(Question::YesNoQuestion); + question.setQuestion(i18n("This file already exists on disk (possibly due to an earlier failed download attempt). Continuing means overwriting it. Do you wish to overwrite the existing file?") + QStringLiteral("\n'") + installpath + QLatin1Char('\'')); + question.setTitle(i18n("Overwrite File")); + if(question.ask() != Question::YesResponse) { + return QStringList(); + } + } + success = QFile::remove(installpath); } - } - QString installpath = installdir + QLatin1Char('/') + installfile; - - qCDebug(KNEWSTUFFCORE) << "Install to file " << installpath; - // FIXME: copy goes here (including overwrite checking) - // FIXME: what must be done now is to update the cache *again* - // in order to set the new payload filename (on root tag only) - // - this might or might not need to take uncompression into account - // FIXME: for updates, we might need to force an overwrite (that is, deleting before) - QFile file(payloadfile); - bool success = true; - const bool update = ((entry.status() == KNS3::Entry::Updateable) || (entry.status() == KNS3::Entry::Updating)); - - if (QFile::exists(installpath) && QDir::tempPath() != installdir) { - if (!update) { - Question question(Question::YesNoQuestion); - question.setQuestion(i18n("This file already exists on disk (possibly due to an earlier failed download attempt). Continuing means overwriting it. Do you wish to overwrite the existing file?") + QStringLiteral("\n'") + installpath + QLatin1Char('\'')); - question.setTitle(i18n("Overwrite File")); - if(question.ask() != Question::YesResponse) { - return QStringList(); + if (success) { + //remove in case it's already present and in a temporary directory, so we get to actually use the path again + if (installpath.startsWith(QDir::tempPath())) { + file.remove(installpath); } + success = file.rename(installpath); + qCDebug(KNEWSTUFFCORE) << "move: " << file.fileName() << " to " << installpath; } - success = QFile::remove(installpath); - } - if (success) { - //remove in case it's already present and in a temporary directory, so we get to actually use the path again - if (installpath.startsWith(QDir::tempPath())) { - file.remove(installpath); + if (!success) { + emit signalInstallationError(i18n("Unable to move the file %1 to the intended destination %2", payloadfile, installpath)); + qCCritical(KNEWSTUFFCORE) << "Cannot move file '" << payloadfile << "' to destination '" << installpath << "'"; + return QStringList(); } - success = file.rename(installpath); - qCDebug(KNEWSTUFFCORE) << "move: " << file.fileName() << " to " << installpath; - } - if (!success) { - emit signalInstallationError(i18n("Unable to move the file %1 to the intended destination %2", payloadfile, installpath)); - qCCritical(KNEWSTUFFCORE) << "Cannot move file '" << payloadfile << "' to destination '" << installpath << "'"; - return QStringList(); + installedFiles << installpath; } - installedFiles << installpath; } } return installedFiles; @@ -595,64 +720,184 @@ void Installation::uninstall(EntryInternal entry) { - entry.setStatus(KNS3::Entry::Deleted); - - if (!uninstallCommand.isEmpty()) { + if (uncompression == QLatin1String("kpackage")) { const auto lst = entry.installedFiles(); - for (const QString &file : lst) { - QFileInfo info(file); - if (info.isFile()) { - QString fileArg(KShell::quoteArg(file)); - QString command(uninstallCommand); - command.replace(QLatin1String("%f"), fileArg); - - int exitcode = QProcess::execute(command, QStringList()); - - if (exitcode) { - emit signalInstallationError(i18n("The uninstallation process failed to successfully run the command %1", command)); - qCCritical(KNEWSTUFFCORE) << "Command failed" << command; + if (lst.length() == 1) { + const QString installedFile{lst.first()}; + if (QFileInfo(installedFile).isDir()) { + KPackage::PackageStructure structure; + KPackage::Package package(&structure); + package.setPath(installedFile); + if (package.isValid() && package.metadata().isValid()) { + QString serviceType = package.metadata().value(QStringLiteral("X-Plasma-ServiceType")); + if (serviceType.isEmpty() && !package.metadata().serviceTypes().isEmpty()) { + serviceType = package.metadata().serviceTypes().first(); + } + if (serviceType.isEmpty()) { + serviceType = property("kpackageType").toString(); + } + if (!serviceType.isEmpty()) { + KPackage::PackageStructure *structure = KPackage::PackageLoader::self()->loadPackageStructure(serviceType); + if (structure) { + KPackage::Package installer = KPackage::Package(structure); + if (!installer.hasValidStructure()) { + qWarning() << "Package serviceType" << serviceType << "not found"; + } + QString packageRoot = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Char('/') + installer.defaultPackageRoot(); + QString removedFile; + KJob *installJob = installer.uninstall(installedFile, packageRoot); + bool completed{false}; + connect(installJob, &KJob::finished, [&completed](){ completed = true; }); + connect(installJob, &KJob::result, this, [this,installedFile,installer,&removedFile,&completed](KJob* job){ + if (job->error() == KJob::NoError) { + removedFile = installer.path(); + } else { + emit signalInstallationFailed(i18n("Installation of %1 failed: %2", installedFile, job->errorText())); + } + completed = true; + }); + installJob->start(); + while(!completed) { + qApp->processEvents(); + } + if (!removedFile.isEmpty()) { + entry.setStatus(KNS3::Entry::Deleted); + entry.setUnInstalledFiles(entry.installedFiles()); + entry.setInstalledFiles(QStringList()); + } + } else { + // no package structure + emit signalInstallationFailed(i18n("The removal of %1 failed, as the installed package does not contain a correct KPackage structure.", installedFile)); + } + } else { + // no service type + emit signalInstallationFailed(i18n("The removal of %1 failed, as the installed package is not a supported type (did you forget to install the KPackage support plugin for this type of package?)", installedFile)); + } } else { - qCDebug(KNEWSTUFFCORE) << "Command executed successfully: " << command; + // package or package metadata is invalid + emit signalInstallationFailed(i18n("The removal of %1 failed, as the installed package does not contain any useful meta information, which means it is not a valid KPackage.", entry.name())); + } + } else { + QMimeDatabase db; + QMimeType mimeType = db.mimeTypeForFile(installedFile); + if (mimeType.inherits(QStringLiteral("application/zip")) || + mimeType.inherits(QStringLiteral("application/x-compressed-tar")) || + mimeType.inherits(QStringLiteral("application/x-gzip")) || + mimeType.inherits(QStringLiteral("application/x-tar")) || + mimeType.inherits(QStringLiteral("application/x-bzip-compressed-tar")) || + mimeType.inherits(QStringLiteral("application/x-xz")) || + mimeType.inherits(QStringLiteral("application/x-lzma"))) { + // Then it's one of the downloaded files installed with an old version of knewstuff prior to + // the native kpackage support being added, and we need to do some inspection-and-removal work... + KPackage::PackageStructure structure; + KPackage::Package package(&structure); + const QString serviceType{property("kpackageType").toString()}; + package.setPath(installedFile); + if (package.isValid() && package.metadata().isValid()) { + // try and load the kpackage and sniff the expected location of its installation, and ask KPackage to remove that thing, if it's there + KPackage::PackageStructure *structure = KPackage::PackageLoader::self()->loadPackageStructure(serviceType); + if (structure) { + KPackage::Package installer = KPackage::Package(structure); + if (installer.hasValidStructure()) { + QString packageRoot = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Char('/') + installer.defaultPackageRoot(); + qCDebug(KNEWSTUFFCORE) << "About to attempt to uninstall" << package.metadata().pluginId() << "from" << packageRoot; + const QString supposedInstallationDir{packageRoot + package.metadata().pluginId()}; + // Frankly, we don't care whether or not this next step succeeds, and it can just fizzle if it wants + // to. This is a cleanup step, and if it fails, it's just not really important. + installer.uninstall(supposedInstallationDir, packageRoot); + } + } + } + // Also get rid of the downloaded file, and tell everything they've gone + if (QFile::remove(installedFile)) { + entry.setStatus(KNS3::Entry::Deleted); + entry.setUnInstalledFiles(entry.installedFiles()); + entry.setInstalledFiles(QStringList()); + } else { + emit signalInstallationFailed(i18n("The removal of %1 failed, as the downloaded file %2 could not be automatically removed.", entry.name(), installedFile)); + } + } else { + // Not sure what's installed, but it's not a KPackage, not a lot we can do with this... + emit signalInstallationFailed(i18n("The removal of %1 failed, due to the installed file not being a KPackage. The file in question was %2, and you can attempt to delete it yourself, if you are certain that it is not needed.", entry.name(), installedFile)); } } + } else { + emit signalInstallationFailed(i18n("The removal of %1 failed, as there seems to somehow be more than one thing installed, which is not supposed to be possible for KPackage based entries.", entry.name())); } - } - - const auto lst = entry.installedFiles(); - for (const QString &file : lst) { - if (file.endsWith(QLatin1Char('/'))) { - QDir dir; - bool worked = dir.rmdir(file); - if (!worked) { - // Maybe directory contains user created files, ignore it - continue; - } - } else if (file.endsWith(QLatin1String("/*"))) { - QDir dir(file.left(file.size()-2)); - bool worked = dir.removeRecursively(); - if (!worked) { - qCWarning(KNEWSTUFFCORE) << "Couldn't remove" << dir.path(); - continue; + } else { + entry.setStatus(KNS3::Entry::Deleted); + + if (!uninstallCommand.isEmpty()) { + const auto lst = entry.installedFiles(); + for (const QString &file : lst) { + QFileInfo info(file); + if (info.isFile()) { + QString fileArg(KShell::quoteArg(file)); + QString command(uninstallCommand); + command.replace(QLatin1String("%f"), fileArg); + + int exitcode = QProcess::execute(command, QStringList()); + + if (exitcode) { + emit signalInstallationError(i18n("The uninstallation process failed to successfully run the command %1", command)); + qCCritical(KNEWSTUFFCORE) << "Command failed" << command; + } else { + qCDebug(KNEWSTUFFCORE) << "Command executed successfully: " << command; + } + } } - } else { - QFileInfo info(file); - if (info.exists() || info.isSymLink()) { - bool worked = QFile::remove(file); + } + + const auto lst = entry.installedFiles(); + for (const QString &file : lst) { + if (file.endsWith(QLatin1Char('/'))) { + QDir dir; + bool worked = dir.rmdir(file); if (!worked) { - qWarning() << "unable to delete file " << file; - return; + // Maybe directory contains user created files, ignore it + continue; + } + } else if (file.endsWith(QLatin1String("/*"))) { + QDir dir(file.left(file.size()-2)); + bool worked = dir.removeRecursively(); + if (!worked) { + qCWarning(KNEWSTUFFCORE) << "Couldn't remove" << dir.path(); + continue; } } else { - qWarning() << "unable to delete file " << file << ". file does not exist."; + QFileInfo info(file); + if (info.exists() || info.isSymLink()) { + bool worked = QFile::remove(file); + if (!worked) { + qWarning() << "unable to delete file " << file; + return; + } + } else { + qWarning() << "unable to delete file " << file << ". file does not exist."; + } } } + entry.setUnInstalledFiles(entry.installedFiles()); + entry.setInstalledFiles(QStringList()); } - entry.setUnInstalledFiles(entry.installedFiles()); - entry.setInstalledFiles(QStringList()); emit signalEntryChanged(entry); } +Installation::UncompressionOptions Installation::uncompressionSetting() const +{ + if (uncompression == QLatin1String("always")) { + return AlwaysUncompress; + } else if (uncompression == QLatin1String("archive")) { + return UncompressIfArchive; + } else if (uncompression == QLatin1String("subdir")) { + return UncompressIntoSubdir; + } else if (uncompression == QLatin1String("kpackage")) { + return UseKPackageUncompression; + } + return NeverUncompress; +} + void Installation::slotInstallationVerification(int result) { Q_UNUSED(result) diff --git a/src/downloaddialog.h b/src/downloaddialog.h --- a/src/downloaddialog.h +++ b/src/downloaddialog.h @@ -63,6 +63,7 @@ *
  • never: never try to extract the file
  • *
  • archive: if the file is an archive, uncompress it, otherwise just pass it on
  • *
  • subdir: logic as archive, but decompress into a subdirectory named after the payload filename
  • + *
  • kpackage: require that the downloaded file is a kpackage, and use the kpackage framework for handling installation and removal (since 5.70)
  • * * * You have different options to set the target install directory: @@ -72,6 +73,24 @@ * This is what QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Char('/') + name will return. * * + * \subsection KPackage Support + * + * To make use of the KPackage option described above, in addition to the Uncompress setting above, you should also specify + * the type of archive expected by KPackage. While it is possible to deduce this from the package metadata in many situations, + * it is not a requirement of the format that this information exists, and we need to have a fallback in the case it is not + * available there. As such, you will want to add a KPackageType entry to your knsrc file. The following example shows how this + * is done for Plasma themes: + * + *
    +   ProvidersUrl=https://autoconfig.kde.org/ocs/providers.xml
    +   Categories=Plasma Theme
    +   StandardResource=tmp
    +   TagFilter=ghns_excluded!=1,plasma##version==5
    +   DownloadTagFilter=plasma##version==5
    +   Uncompress=kpackage
    +   KPackageType=Plasma/Theme
    + * 
    + * * @since 4.4 */ class KNEWSTUFF_EXPORT DownloadDialog : public QDialog