summaryrefslogtreecommitdiffstats
path: root/src/libs/installer/genericdatacache.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/libs/installer/genericdatacache.cpp')
-rw-r--r--src/libs/installer/genericdatacache.cpp695
1 files changed, 695 insertions, 0 deletions
diff --git a/src/libs/installer/genericdatacache.cpp b/src/libs/installer/genericdatacache.cpp
new file mode 100644
index 000000000..a1e31ccfe
--- /dev/null
+++ b/src/libs/installer/genericdatacache.cpp
@@ -0,0 +1,695 @@
+/**************************************************************************
+**
+** Copyright (C) 2023 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the Qt Installer Framework.
+**
+** $QT_BEGIN_LICENSE:GPL-EXCEPT$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 as published by the Free Software
+** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+**************************************************************************/
+
+#include "genericdatacache.h"
+
+#include "errors.h"
+#include "fileutils.h"
+#include "globals.h"
+#include "metadata.h"
+#include "updater.h"
+
+#include <QDir>
+#include <QDirIterator>
+#include <QtConcurrent>
+
+namespace QInstaller {
+
+static const QLatin1String scManifestFile("manifest.json");
+
+/*!
+ \inmodule QtInstallerFramework
+ \class QInstaller::CacheableItem
+ \brief The CacheableItem is a pure virtual class that defines an interface for
+ a type suited for storage with the \l{GenericDataCache} class.
+*/
+
+/*!
+ \fn QInstaller::CacheableItem::path() const
+
+ Returns the path of this item. A subclass may override this method.
+*/
+
+/*!
+ \fn QInstaller::CacheableItem::setPath(const QString &path)
+
+ Sets the path of the item to \a path. A subclass may override this method.
+*/
+
+/*!
+ \fn QInstaller::CacheableItem::checksum() const
+
+ Returns the checksum of this item. A subclass must implement this method.
+*/
+
+/*!
+ \fn QInstaller::CacheableItem::isValid() const
+
+ Returns \c true if this item is valid, \c false otherwise.
+ A subclass must implement this method.
+*/
+
+/*!
+ \fn QInstaller::CacheableItem::isActive() const
+
+ Returns \c true if this item is an actively used cache item, \c false otherwise.
+ This information is used as a hint for filtering obsolete entries, an active item
+ can never be obsolete.
+
+ A subclass must implement this method.
+*/
+
+/*!
+ \fn QInstaller::CacheableItem::obsoletes(CacheableItem *other)
+
+ Returns \c true if the calling item obsoletes \a other item, \c false otherwise.
+ This method is used for filtering obsolete entries from the cache.
+
+ A subclass must implement this method.
+*/
+
+/*!
+ Virtual destructor for \c CacheableItem.
+*/
+CacheableItem::~CacheableItem()
+{
+}
+
+
+/*!
+ \inmodule QtInstallerFramework
+ \class QInstaller::GenericDataCache
+ \brief The GenericDataCache is a template class for a checksum based storage
+ of items on disk.
+
+ GenericDataCache\<T\> manages a cache storage for a set \l{path()}, which contains
+ a subdirectory for each registered item. An item of type \c T should implement
+ methods declared in the \l{CacheableItem} interface. The GenericDataCache\<T\> class can
+ still be explicitly specialized to use the derived type as a template argument, to
+ allow retrieving items as the derived type without casting.
+
+ Each cache has a manifest file in its root directory, which lists the version
+ and wrapped type of the cache, and all its items. The file is updated automatically
+ when the cache object is destructed, or it can be updated periodically by
+ calling \l{sync()}.
+*/
+
+/*!
+ \enum GenericDataCache::RegisterMode
+ This enum holds the possible values for modes of registering items to cache.
+
+ \value Copy
+ The contents of the item are copied to the cache.
+ \value Move
+ The contents of the item are move to the cache.
+*/
+
+/*!
+ \fn template <typename T> QInstaller::GenericDataCache<T>::GenericDataCache()
+
+ Constructs a new empty cache. The cache is invalid until set with a
+ path and initialized.
+*/
+template <typename T>
+GenericDataCache<T>::GenericDataCache()
+ : m_version(QLatin1String("1.0.0"))
+ , m_invalidated(true)
+{
+}
+
+/*!
+ \fn template <typename T> QInstaller::GenericDataCache<T>::GenericDataCache(const QString &path, const QString &type, const QString &version)
+
+ Constructs a cache to \a path with the given \a type and \a version.
+ The cache is initialized automatically.
+*/
+template <typename T>
+GenericDataCache<T>::GenericDataCache(const QString &path, const QString &type,
+ const QString &version)
+ : m_path(path)
+ , m_type(type)
+ , m_version(version)
+ , m_invalidated(true)
+{
+ initialize();
+}
+
+/*!
+ \fn template <typename T> QInstaller::GenericDataCache<T>::~GenericDataCache()
+
+ Deletes the cache object. Item contents on disk are kept.
+*/
+template <typename T>
+GenericDataCache<T>::~GenericDataCache()
+{
+ if (m_invalidated)
+ return;
+
+ toDisk();
+ invalidate();
+}
+
+/*!
+ \fn template <typename T> QInstaller::GenericDataCache<T>::setType(const QString &type)
+
+ Sets the name of the wrapped type to \a type. This is used for determining if an
+ existing cache holds items of the same type. Trying to load cached items with mismatching
+ type results in discarding the old items. Optional.
+*/
+template<typename T>
+void GenericDataCache<T>::setType(const QString &type)
+{
+ QMutexLocker _(&m_mutex);
+ m_type = type;
+}
+
+/*!
+ \fn template <typename T> QInstaller::GenericDataCache<T>::setVersion(const QString &version)
+
+ Sets the version of the cache to \a version. Loading from a cache with different
+ expected version discards the old items. The version property defaults to \c{1.0.0}.
+*/
+template<typename T>
+void GenericDataCache<T>::setVersion(const QString &version)
+{
+ QMutexLocker _(&m_mutex);
+ m_version = version;
+}
+
+/*!
+ \fn template <typename T> QInstaller::GenericDataCache<T>::initialize()
+
+ Initializes a cache. Creates a new directory for the path configured for
+ this cache if it does not exist, and loads any previously cached items from
+ the directory. The cache directory is locked for access by this process only.
+ Returns \c true on success, \c false otherwise.
+*/
+template <typename T>
+bool GenericDataCache<T>::initialize()
+{
+ QMutexLocker _(&m_mutex);
+ Q_ASSERT(m_items.isEmpty());
+
+ if (m_path.isEmpty()) {
+ setErrorString(QCoreApplication::translate("GenericDataCache",
+ "Cannot initialize cache with empty path."));
+ return false;
+ }
+
+ QDir directory(m_path);
+ if (!directory.exists() && !directory.mkpath(m_path)) {
+ setErrorString(QCoreApplication::translate("GenericDataCache",
+ "Cannot create directory \"%1\" for cache.").arg(m_path));
+ return false;
+ }
+
+ if (m_lock && !m_lock->unlock()) {
+ setErrorString(QCoreApplication::translate("GenericDataCache",
+ "Cannot initialize cache: %1").arg(m_lock->errorString()));
+ return false;
+ }
+
+ m_lock.reset(new KDUpdater::LockFile(m_path + QLatin1String("/cache.lock")));
+ if (!m_lock->lock()) {
+ setErrorString(QCoreApplication::translate("GenericDataCache",
+ "Cannot initialize cache: %1").arg(m_lock->errorString()));
+ return false;
+ }
+
+ if (!fromDisk())
+ return false;
+
+ m_invalidated = false;
+ return true;
+}
+
+/*!
+ \fn template <typename T> QInstaller::GenericDataCache<T>::clear()
+
+ Removes all items from the cache and deletes their contents on disk. If
+ the cache directory becomes empty, it is also deleted. The cache becomes
+ invalid after this action, even in case of error while clearing. In that
+ case already deleted items will be lost. Returns \c true on success,
+ \c false otherwise.
+*/
+template <typename T>
+bool GenericDataCache<T>::clear()
+{
+ QMutexLocker _(&m_mutex);
+ if (m_invalidated) {
+ setErrorString(QCoreApplication::translate("GenericDataCache",
+ "Cannot clear invalidated cache."));
+ return false;
+ }
+
+ QFile manifestFile(m_path + QDir::separator() + scManifestFile);
+ if (manifestFile.exists() && !manifestFile.remove()) {
+ setErrorString(QCoreApplication::translate("GenericDataCache",
+ "Cannot remove manifest file: %1").arg(manifestFile.errorString()));
+ invalidate();
+ return false;
+ }
+
+ bool success = true;
+ for (T *item : qAsConst(m_items)) {
+ try {
+ QInstaller::removeDirectory(item->path());
+ } catch (const Error &e) {
+ setErrorString(QCoreApplication::translate("GenericDataCache",
+ "Error while clearing cache: %1").arg(e.message()));
+ success = false;
+ }
+ }
+
+ invalidate();
+ QDir().rmdir(m_path);
+ return success;
+}
+
+/*!
+ \fn template <typename T> QInstaller::GenericDataCache<T>::sync()
+
+ Synchronizes the contents of the cache to its manifest file. Returns \c true
+ if the manifest file was updates successfully, \c false otherwise.
+*/
+template<typename T>
+bool GenericDataCache<T>::sync()
+{
+ QMutexLocker _(&m_mutex);
+ if (m_invalidated) {
+ setErrorString(QCoreApplication::translate("GenericDataCache",
+ "Cannot synchronize invalidated cache."));
+ return false;
+ }
+ return toDisk();
+}
+
+/*!
+ \fn template <typename T> QInstaller::GenericDataCache<T>::isValid() const
+
+ Returns \c true if the cache is valid, \c false otherwise. A cache is considered
+ valid when it is initialized to a set path.
+*/
+template<typename T>
+bool GenericDataCache<T>::isValid() const
+{
+ QMutexLocker _(&m_mutex);
+ return !m_invalidated;
+}
+
+/*!
+ \fn template <typename T> QInstaller::GenericDataCache<T>::errorString() const
+
+ Returns a string representing the last error with the cache.
+*/
+template<typename T>
+QString GenericDataCache<T>::errorString() const
+{
+ QMutexLocker _(&m_mutex);
+ return m_error;
+}
+
+/*!
+ \fn template <typename T> QInstaller::GenericDataCache<T>::path() const
+
+ Returns the path of the cache on disk.
+*/
+template <typename T>
+QString GenericDataCache<T>::path() const
+{
+ QMutexLocker _(&m_mutex);
+ return m_path;
+}
+
+/*!
+ \fn template <typename T> QInstaller::GenericDataCache<T>::setPath(const QString &path)
+
+ Sets a new \a path for the cache and invalidates current items. Saves the information
+ of the old cache to its manifest file.
+*/
+template <typename T>
+void GenericDataCache<T>::setPath(const QString &path)
+{
+ QMutexLocker _(&m_mutex);
+ if (!m_invalidated)
+ toDisk();
+
+ m_path = path;
+ invalidate();
+}
+
+/*!
+ \fn template <typename T> QInstaller::GenericDataCache<T>::items() const
+
+ Returns a list of cached items.
+*/
+template <typename T>
+QList<T *> GenericDataCache<T>::items() const
+{
+ QMutexLocker _(&m_mutex);
+ if (m_invalidated) {
+ setErrorString(QCoreApplication::translate("GenericDataCache",
+ "Cannot retrieve items from invalidated cache."));
+ return QList<T *>();
+ }
+ return m_items.values();
+}
+
+/*!
+ \fn template <typename T> QInstaller::GenericDataCache<T>::itemByChecksum(const QByteArray &checksum) const
+
+ Returns an item that matches the \a checksum or \c nullptr in case
+ no such item is cached.
+*/
+template <typename T>
+T *GenericDataCache<T>::itemByChecksum(const QByteArray &checksum) const
+{
+ QMutexLocker _(&m_mutex);
+ if (m_invalidated) {
+ setErrorString(QCoreApplication::translate("GenericDataCache",
+ "Cannot retrieve item from invalidated cache."));
+ return nullptr;
+ }
+ return m_items.value(checksum);
+}
+
+/*!
+ \fn template <typename T> QInstaller::GenericDataCache<T>::itemByPath(const QString &path) const
+
+ Returns an item from the \a path or \c nullptr in case no such item
+ is cached. Depending on the size of the cache, this can be much
+ slower than retrieving an item with \l{itemByChecksum()}.
+*/
+template <typename T>
+T *GenericDataCache<T>::itemByPath(const QString &path) const
+{
+ QMutexLocker _(&m_mutex);
+ auto it = std::find_if(m_items.constBegin(), m_items.constEnd(),
+ [&](T *item) {
+ return (QDir::fromNativeSeparators(path) == QDir::fromNativeSeparators(item->path()));
+ }
+ );
+ if (it != m_items.constEnd())
+ return it.value();
+
+ return nullptr;
+}
+
+/*!
+ \fn template <typename T> QInstaller::GenericDataCache<T>::registerItem(T *item, bool replace, RegisterMode mode)
+
+ Registers the \a item to the cache. If \a replace is set to \c true,
+ the new \a item replaces a previous item with the same checksum.
+
+ The cache takes ownership of the object pointed by \a item. The contents of the
+ item are copied or moved to the cache with a subdirectory name that matches the checksum
+ of the item. The \a mode decides how the contents of the item are registered, either by
+ copying or moving.
+
+ Returns \c true on success or \c false if the item could not be registered.
+*/
+template <typename T>
+bool GenericDataCache<T>::registerItem(T *item, bool replace, RegisterMode mode)
+{
+ QMutexLocker _(&m_mutex);
+ if (m_invalidated) {
+ setErrorString(QCoreApplication::translate("GenericDataCache",
+ "Cannot register item to invalidated cache."));
+ return false;
+ }
+ if (!item) {
+ setErrorString(QCoreApplication::translate("GenericDataCache",
+ "Cannot register null item."));
+ return false;
+ }
+ if (!item->isValid()) {
+ setErrorString(QCoreApplication::translate("GenericDataCache",
+ "Cannot register invalid item with checksum %1").arg(QLatin1String(item->checksum())));
+ return false;
+ }
+ if (m_items.contains(item->checksum())) {
+ if (replace) {// replace existing item including contents on disk
+ remove(item->checksum());
+ } else {
+ setErrorString(QCoreApplication::translate("GenericDataCache",
+ "Cannot register item with checksum %1. An item with the same checksum "
+ "already exists in cache.").arg(QLatin1String(item->checksum())));
+ return false;
+ }
+ }
+
+ const QString newPath = m_path + QDir::separator() + QString::fromLatin1(item->checksum());
+ try {
+ // A directory is in the way but it isn't registered to the current cache, remove.
+ QDir dir;
+ if (dir.exists(newPath))
+ QInstaller::removeDirectory(newPath);
+
+ switch (mode) {
+ case Copy:
+ QInstaller::copyDirectoryContents(item->path(), newPath);
+ break;
+ case Move:
+ // First, try moving the top level directory
+ if (!dir.rename(item->path(), newPath)) {
+ qCDebug(lcDeveloperBuild) << "Failed to rename directory" << item->path()
+ << "to" << newPath << ". Trying again.";
+ // If that does not work, fallback to moving the contents one by one
+ QInstaller::moveDirectoryContents(item->path(), newPath);
+ }
+ break;
+ default:
+ throw Error(QCoreApplication::translate("GenericDataCache",
+ "Unknown register mode selected!"));
+ }
+ } catch (const Error &e) {
+ setErrorString(QCoreApplication::translate("GenericDataCache",
+ "Error while copying item to path \"%1\": %2").arg(newPath, e.message()));
+ return false;
+ }
+
+ item->setPath(newPath);
+ if (item->isValid()) {
+ m_items.insert(item->checksum(), item);
+ return true;
+ }
+ return false;
+}
+
+/*!
+ \fn template <typename T> QInstaller::GenericDataCache<T>::removeItem(const QByteArray &checksum)
+
+ Removes the item specified by \a checksum from the cache and deletes the
+ contents of the item from disk. Returns \c true if the item
+ was removed successfully, \c false otherwise.
+*/
+template <typename T>
+bool GenericDataCache<T>::removeItem(const QByteArray &checksum)
+{
+ QMutexLocker _(&m_mutex);
+ return remove(checksum);
+}
+
+/*!
+ \fn template <typename T> QInstaller::GenericDataCache<T>::obsoleteItems() const
+
+ Returns items considered obsolete from the cache.
+*/
+template<typename T>
+QList<T *> GenericDataCache<T>::obsoleteItems() const
+{
+ QMutexLocker _(&m_mutex);
+ const QList<T *> obsoletes = QtConcurrent::blockingFiltered(m_items.values(),
+ [&](T *item1) {
+ if (item1->isActive()) // We can skip the iteration for active entries
+ return false;
+
+ for (T *item2 : qAsConst(m_items)) {
+ if (item2->obsoletes(item1))
+ return true;
+ }
+ return false;
+ }
+ );
+ return obsoletes;
+}
+
+/*!
+ \internal
+
+ Marks the cache invalid and clears all items. The contents
+ on disk are not deleted. Releases the lock file of the cache.
+*/
+template <typename T>
+void GenericDataCache<T>::invalidate()
+{
+ if (!m_items.isEmpty()) {
+ qDeleteAll(m_items);
+ m_items.clear();
+ }
+ if (m_lock && !m_lock->unlock()) {
+ setErrorString(QCoreApplication::translate("GenericDataCache",
+ "Error while invalidating cache: %1").arg(m_lock->errorString()));
+ }
+ m_invalidated = true;
+}
+
+/*!
+ \internal
+
+ Sets the current error string to \a error and prints it as a warning to the console.
+*/
+template <typename T>
+void GenericDataCache<T>::setErrorString(const QString &error) const
+{
+ m_error = error;
+ qCWarning(QInstaller::lcInstallerInstallLog) << error;
+}
+
+/*!
+ \internal
+
+ Reads the manifest file of the cache if one exists, and populates the internal
+ hash from the file contents. Returns \c true if the manifests was read successfully
+ or if the reading was omitted. This is the case if the file does not exist yet, or
+ the type or version of the manifest does not match the current cache object. In case
+ of mismatch the old items are not restored. Returns \c false otherwise.
+*/
+template<typename T>
+bool GenericDataCache<T>::fromDisk()
+{
+ QFile manifestFile(m_path + QDir::separator() + scManifestFile);
+ if (!manifestFile.exists()) // Not yet created
+ return true;
+
+ if (!manifestFile.open(QIODevice::ReadOnly)) {
+ setErrorString(QCoreApplication::translate("GenericDataCache",
+ "Cannot open manifest file: %1").arg(manifestFile.errorString()));
+ return false;
+ }
+
+ const QByteArray manifestData = manifestFile.readAll();
+ const QJsonDocument manifestJsonDoc(QJsonDocument::fromJson(manifestData));
+ const QJsonObject docJsonObject = manifestJsonDoc.object();
+
+ const QJsonValue type = docJsonObject.value(QLatin1String("type"));
+ if (type.toString() != m_type) {
+ qCDebug(QInstaller::lcInstallerInstallLog) << "Discarding existing items from cache of type:"
+ << type.toString() << ". New type:" << m_type;
+ return true;
+ }
+
+ const QJsonValue version = docJsonObject.value(QLatin1String("version"));
+ if (KDUpdater::compareVersion(version.toString(), m_version) != 0) {
+ qCDebug(QInstaller::lcInstallerInstallLog) << "Discarding existing items from cache with version:"
+ << version.toString() << ". New version:" << m_version;
+ return true;
+ }
+
+ const QJsonArray itemsJsonArray = docJsonObject.value(QLatin1String("items")).toArray();
+ for (const auto &itemJsonValue : itemsJsonArray) {
+ const QString checksum = itemJsonValue.toString();
+
+ std::unique_ptr<T> item(new T(m_path + QDir::separator() + checksum));
+ m_items.insert(checksum.toLatin1(), item.release());
+
+ // The cache directory may contain other entries (unrelated directories or
+ // invalid old cache items) which we don't care about, unless registering
+ // a new entry requires overwriting them.
+ }
+ return true;
+}
+
+/*!
+ \internal
+
+ Writes the manifest file with the contents of the internal item hash.
+ Returns \c true on success, \c false otherwise.
+*/
+template<typename T>
+bool GenericDataCache<T>::toDisk()
+{
+ QFile manifestFile(m_path + QDir::separator() + scManifestFile);
+ if (!manifestFile.open(QIODevice::WriteOnly)) {
+ setErrorString(QCoreApplication::translate("GenericDataCache",
+ "Cannot open manifest file: %1").arg(manifestFile.errorString()));
+ return false;
+ }
+
+ QJsonArray itemsJsonArray;
+ const QList<QByteArray> keys = m_items.keys();
+ for (const QByteArray &key : keys)
+ itemsJsonArray.append(QJsonValue(QLatin1String(key)));
+
+ QJsonObject docJsonObject;
+ docJsonObject.insert(QLatin1String("items"), itemsJsonArray);
+ docJsonObject.insert(QLatin1String("version"), m_version);
+ if (!m_type.isEmpty())
+ docJsonObject.insert(QLatin1String("type"), m_type);
+
+ QJsonDocument manifestJsonDoc;
+ manifestJsonDoc.setObject(docJsonObject);
+ if (manifestFile.write(manifestJsonDoc.toJson()) == -1) {
+ setErrorString(QCoreApplication::translate("GenericDataCache",
+ "Cannot write contents for manifest file: %1").arg(manifestFile.errorString()));
+ return false;
+ }
+ return true;
+}
+
+/*!
+ \internal
+*/
+template<typename T>
+bool GenericDataCache<T>::remove(const QByteArray &checksum)
+{
+ if (m_invalidated) {
+ setErrorString(QCoreApplication::translate("GenericDataCache",
+ "Cannot remove item from invalidated cache."));
+ return false;
+ }
+ QScopedPointer<T> item(m_items.take(checksum));
+ if (!item) {
+ setErrorString(QCoreApplication::translate("GenericDataCache",
+ "Cannot remove item specified by checksum %1: no such item exists.").arg(QLatin1String(checksum)));
+ return false;
+ }
+
+ try {
+ QInstaller::removeDirectory(item->path());
+ } catch (const Error &e) {
+ setErrorString(QCoreApplication::translate("GenericDataCache",
+ "Error while removing directory \"%1\": %2").arg(item->path(), e.message()));
+ return false;
+ }
+ return true;
+}
+
+template class GenericDataCache<Metadata>;
+
+} // namespace QInstaller