| 1 | /* |
| 2 | SPDX-FileCopyrightText: 2010 Ryan Rix <ry@n.rix.si> |
| 3 | |
| 4 | SPDX-License-Identifier: LGPL-2.0-or-later |
| 5 | */ |
| 6 | |
| 7 | #include "packageloader.h" |
| 8 | #include "private/packageloader_p.h" |
| 9 | #include "private/utils.h" |
| 10 | |
| 11 | #include "kpackage_debug.h" |
| 12 | #include <QCoreApplication> |
| 13 | #include <QDateTime> |
| 14 | #include <QDirIterator> |
| 15 | #include <QList> |
| 16 | #include <QStandardPaths> |
| 17 | |
| 18 | #include <KLazyLocalizedString> |
| 19 | #include <KPluginFactory> |
| 20 | #include <KPluginMetaData> |
| 21 | #include <unordered_set> |
| 22 | |
| 23 | #include "config-package.h" |
| 24 | |
| 25 | #include "package.h" |
| 26 | #include "packagestructure.h" |
| 27 | #include "private/packagejobthread_p.h" |
| 28 | #include "private/packages_p.h" |
| 29 | |
| 30 | namespace KPackage |
| 31 | { |
| 32 | PackageLoader::PackageLoader() |
| 33 | : d(new PackageLoaderPrivate) |
| 34 | { |
| 35 | } |
| 36 | |
| 37 | PackageLoader::~PackageLoader() |
| 38 | { |
| 39 | for (auto wp : std::as_const(t&: d->structures)) { |
| 40 | delete wp.data(); |
| 41 | } |
| 42 | delete d; |
| 43 | } |
| 44 | |
| 45 | PackageLoader *PackageLoader::self() |
| 46 | { |
| 47 | static PackageLoader *s_packageTrader = new PackageLoader; |
| 48 | return s_packageTrader; |
| 49 | } |
| 50 | |
| 51 | Package PackageLoader::loadPackage(const QString &packageFormat, const QString &packagePath) |
| 52 | { |
| 53 | if (packageFormat.isEmpty()) { |
| 54 | return Package(); |
| 55 | } |
| 56 | |
| 57 | if (PackageStructure *structure = loadPackageStructure(packageFormat)) { |
| 58 | Package p(structure); |
| 59 | if (!packagePath.isEmpty()) { |
| 60 | p.setPath(packagePath); |
| 61 | } |
| 62 | return p; |
| 63 | } |
| 64 | |
| 65 | return Package(); |
| 66 | } |
| 67 | |
| 68 | QList<Package> PackageLoader::listKPackages(const QString &packageFormat, const QString &packageRoot) |
| 69 | { |
| 70 | QList<Package> lst; |
| 71 | |
| 72 | // has been a root specified? |
| 73 | QString actualRoot = packageRoot; |
| 74 | |
| 75 | PackageStructure *structure = d->structures.value(key: packageFormat).data(); |
| 76 | // try to take it from the package structure |
| 77 | if (actualRoot.isEmpty()) { |
| 78 | if (!structure) { |
| 79 | if (packageFormat == QLatin1String("KPackage/Generic" )) { |
| 80 | structure = new GenericPackage(); |
| 81 | } else if (packageFormat == QLatin1String("KPackage/GenericQML" )) { |
| 82 | structure = new GenericQMLPackage(); |
| 83 | } else { |
| 84 | structure = loadPackageStructure(packageFormat); |
| 85 | } |
| 86 | } |
| 87 | |
| 88 | if (structure) { |
| 89 | d->structures.insert(key: packageFormat, value: structure); |
| 90 | actualRoot = Package(structure).defaultPackageRoot(); |
| 91 | } |
| 92 | } |
| 93 | |
| 94 | if (actualRoot.isEmpty()) { |
| 95 | actualRoot = packageFormat; |
| 96 | } |
| 97 | |
| 98 | QStringList paths; |
| 99 | if (QDir::isAbsolutePath(path: actualRoot)) { |
| 100 | paths = QStringList(actualRoot); |
| 101 | } else { |
| 102 | const auto listPath = QStandardPaths::standardLocations(type: QStandardPaths::GenericDataLocation); |
| 103 | for (const QString &path : listPath) { |
| 104 | paths += path + QLatin1Char('/') + actualRoot; |
| 105 | } |
| 106 | } |
| 107 | |
| 108 | for (auto const &plugindir : std::as_const(t&: paths)) { |
| 109 | QDirIterator it(plugindir, QDir::Dirs | QDir::NoDotAndDotDot); |
| 110 | std::unordered_set<QString> dirs; |
| 111 | while (it.hasNext()) { |
| 112 | it.next(); |
| 113 | |
| 114 | const QString dir = it.filePath(); |
| 115 | if (!dirs.insert(x: it.fileInfo().fileName()).second) { |
| 116 | continue; |
| 117 | } |
| 118 | Package package(structure); |
| 119 | package.setPath(dir); |
| 120 | if (package.isValid()) { |
| 121 | // Ignore packages with empty metadata here |
| 122 | if (packageFormat.isEmpty() || !package.metadata().isValid() || readKPackageType(metaData: package.metadata()) == packageFormat) { |
| 123 | lst << package; |
| 124 | } else { |
| 125 | qInfo() << "KPackage in" << package.path() << readKPackageType(metaData: package.metadata()) << "does not match requested format" << packageFormat; |
| 126 | } |
| 127 | } |
| 128 | } |
| 129 | } |
| 130 | return lst; |
| 131 | } |
| 132 | QList<KPluginMetaData> PackageLoader::listPackages(const QString &packageFormat, const QString &packageRoot) |
| 133 | { |
| 134 | // Note: Use QDateTime::currentSecsSinceEpoch() once we can depend on Qt 5.8 |
| 135 | const qint64 now = qRound64(d: QDateTime::currentMSecsSinceEpoch() / 1000.0); |
| 136 | bool useRuntimeCache = true; |
| 137 | if (now - d->pluginCacheAge > d->maxCacheAge && d->pluginCacheAge != 0) { |
| 138 | // cache is old and we're not within a few seconds of startup anymore |
| 139 | useRuntimeCache = false; |
| 140 | d->pluginCache.clear(); |
| 141 | } |
| 142 | |
| 143 | const QString cacheKey = packageFormat + QLatin1Char('.') + packageRoot; |
| 144 | if (useRuntimeCache) { |
| 145 | auto it = d->pluginCache.constFind(key: cacheKey); |
| 146 | if (it != d->pluginCache.constEnd()) { |
| 147 | return *it; |
| 148 | } |
| 149 | } |
| 150 | if (d->pluginCacheAge == 0) { |
| 151 | d->pluginCacheAge = now; |
| 152 | } |
| 153 | |
| 154 | QList<KPluginMetaData> lst; |
| 155 | |
| 156 | // has been a root specified? |
| 157 | QString actualRoot = packageRoot; |
| 158 | |
| 159 | // try to take it from the package structure |
| 160 | if (actualRoot.isEmpty()) { |
| 161 | PackageStructure *structure = d->structures.value(key: packageFormat).data(); |
| 162 | if (!structure) { |
| 163 | if (packageFormat == QLatin1String("KPackage/Generic" )) { |
| 164 | structure = new GenericPackage(); |
| 165 | } else if (packageFormat == QLatin1String("KPackage/GenericQML" )) { |
| 166 | structure = new GenericQMLPackage(); |
| 167 | } else { |
| 168 | structure = loadPackageStructure(packageFormat); |
| 169 | } |
| 170 | } |
| 171 | |
| 172 | if (structure) { |
| 173 | d->structures.insert(key: packageFormat, value: structure); |
| 174 | Package p(structure); |
| 175 | actualRoot = p.defaultPackageRoot(); |
| 176 | } |
| 177 | } |
| 178 | |
| 179 | if (actualRoot.isEmpty()) { |
| 180 | actualRoot = packageFormat; |
| 181 | } |
| 182 | |
| 183 | QSet<QString> uniqueIds; |
| 184 | QStringList paths; |
| 185 | if (QDir::isAbsolutePath(path: actualRoot)) { |
| 186 | paths = QStringList(actualRoot); |
| 187 | } else { |
| 188 | const auto listPath = QStandardPaths::standardLocations(type: QStandardPaths::GenericDataLocation); |
| 189 | for (const QString &path : listPath) { |
| 190 | paths += path + QLatin1Char('/') + actualRoot; |
| 191 | } |
| 192 | } |
| 193 | |
| 194 | for (auto const &plugindir : std::as_const(t&: paths)) { |
| 195 | QDirIterator it(plugindir, QStringList{QStringLiteral("metadata.json" )}, QDir::Files, QDirIterator::Subdirectories | QDirIterator::FollowSymlinks); |
| 196 | std::unordered_set<QString> dirs; |
| 197 | while (it.hasNext()) { |
| 198 | it.next(); |
| 199 | |
| 200 | const QString dir = it.fileInfo().absoluteDir().path(); |
| 201 | if (!dirs.insert(x: dir).second) { |
| 202 | continue; |
| 203 | } |
| 204 | |
| 205 | const QString metadataPath = it.fileInfo().absoluteFilePath(); |
| 206 | KPluginMetaData info = KPluginMetaData::fromJsonFile(jsonFile: metadataPath); |
| 207 | |
| 208 | if (!info.isValid() || uniqueIds.contains(value: info.pluginId())) { |
| 209 | continue; |
| 210 | } |
| 211 | |
| 212 | if (packageFormat.isEmpty() || readKPackageType(metaData: info) == packageFormat) { |
| 213 | uniqueIds << info.pluginId(); |
| 214 | lst << info; |
| 215 | } else { |
| 216 | qInfo() << "KPackageStructure of" << info << "does not match requested format" << packageFormat; |
| 217 | } |
| 218 | } |
| 219 | } |
| 220 | |
| 221 | if (useRuntimeCache) { |
| 222 | d->pluginCache.insert(key: cacheKey, value: lst); |
| 223 | } |
| 224 | return lst; |
| 225 | } |
| 226 | |
| 227 | QList<KPluginMetaData> PackageLoader::listPackagesMetadata(const QString &packageFormat, const QString &packageRoot) |
| 228 | { |
| 229 | return listPackages(packageFormat, packageRoot); |
| 230 | } |
| 231 | |
| 232 | QList<KPluginMetaData> |
| 233 | PackageLoader::findPackages(const QString &packageFormat, const QString &packageRoot, std::function<bool(const KPluginMetaData &)> filter) |
| 234 | { |
| 235 | QList<KPluginMetaData> lst; |
| 236 | const auto lstPlugins = listPackages(packageFormat, packageRoot); |
| 237 | for (auto const &plugin : lstPlugins) { |
| 238 | if (!filter || filter(plugin)) { |
| 239 | lst << plugin; |
| 240 | } |
| 241 | } |
| 242 | return lst; |
| 243 | } |
| 244 | |
| 245 | KPackage::PackageStructure *PackageLoader::loadPackageStructure(const QString &packageFormat) |
| 246 | { |
| 247 | PackageStructure *structure = d->structures.value(key: packageFormat).data(); |
| 248 | if (!structure) { |
| 249 | if (packageFormat == QLatin1String("KPackage/Generic" )) { |
| 250 | structure = new GenericPackage(); |
| 251 | d->structures.insert(key: packageFormat, value: structure); |
| 252 | } else if (packageFormat == QLatin1String("KPackage/GenericQML" )) { |
| 253 | structure = new GenericQMLPackage(); |
| 254 | d->structures.insert(key: packageFormat, value: structure); |
| 255 | } |
| 256 | } |
| 257 | |
| 258 | if (structure) { |
| 259 | return structure; |
| 260 | } |
| 261 | |
| 262 | const KPluginMetaData metaData = structureForKPackageType(packageFormat); |
| 263 | if (!metaData.isValid()) { |
| 264 | qCWarning(KPACKAGE_LOG) << "Invalid metadata for package structure" << packageFormat; |
| 265 | return nullptr; |
| 266 | } |
| 267 | |
| 268 | auto result = KPluginFactory::instantiatePlugin<PackageStructure>(data: metaData); |
| 269 | if (!result) { |
| 270 | qCWarning(KPACKAGE_LOG).noquote() << "Could not load installer for package of type" << packageFormat << "Error reported was: " << result.errorString; |
| 271 | return nullptr; |
| 272 | } |
| 273 | |
| 274 | structure = result.plugin; |
| 275 | |
| 276 | d->structures.insert(key: packageFormat, value: structure); |
| 277 | |
| 278 | return structure; |
| 279 | } |
| 280 | |
| 281 | void PackageLoader::addKnownPackageStructure(const QString &packageFormat, KPackage::PackageStructure *structure) |
| 282 | { |
| 283 | d->structures.insert(key: packageFormat, value: structure); |
| 284 | } |
| 285 | |
| 286 | void PackageLoader::invalidateCache() |
| 287 | { |
| 288 | self()->d->maxCacheAge = -1; |
| 289 | } |
| 290 | |
| 291 | } // KPackage Namespace |
| 292 | |