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); |
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 | |