]> cloud.milkyroute.net Git - dolphin.git/blob - src/settings/services/servicemenuinstaller/servicemenuinstaller.cpp
Merge branch 'release/20.04'
[dolphin.git] / src / settings / services / servicemenuinstaller / servicemenuinstaller.cpp
1 /***************************************************************************
2 * Copyright © 2019 Alexander Potashev <aspotashev@gmail.com> *
3 * *
4 * This program is free software; you can redistribute it and/or *
5 * modify it under the terms of the GNU General Public License as *
6 * published by the Free Software Foundation; either version 2 of *
7 * the License or (at your option) version 3 or any later version *
8 * accepted by the membership of KDE e.V. (or its successor approved *
9 * by the membership of KDE e.V.), which shall act as a proxy *
10 * defined in Section 14 of version 3 of the license. *
11 * *
12 * This program is distributed in the hope that it will be useful, *
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of *
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
15 * GNU General Public License for more details. *
16 * *
17 * You should have received a copy of the GNU General Public License *
18 * along with this program. If not, see <http://www.gnu.org/licenses/>. *
19 ***************************************************************************/
20
21 #include <QDebug>
22 #include <QProcess>
23 #include <QStandardPaths>
24 #include <QDir>
25 #include <QDirIterator>
26 #include <QCommandLineParser>
27 #include <QMimeDatabase>
28 #include <QUrl>
29 #include <QDesktopServices>
30 #include <QGuiApplication>
31
32 #include <KLocalizedString>
33
34 // @param msg Error that gets logged to CLI
35 Q_NORETURN void fail(const QString &str)
36 {
37 qCritical() << str;
38
39 QProcess process;
40 auto args = QStringList{"--passivepopup", i18n("Dolphin service menu installation failed"), "15"};
41 process.start("kdialog", args, QIODevice::ReadOnly);
42 if (!process.waitForStarted()) {
43 qFatal("Failed to run kdialog");
44 }
45
46 exit(1);
47 }
48
49 QString getServiceMenusDir()
50 {
51 const QString dataLocation = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation);
52 return QDir(dataLocation).absoluteFilePath("kservices5/ServiceMenus");
53 }
54
55 struct UncompressCommand
56 {
57 QString command;
58 QStringList args1;
59 QStringList args2;
60 };
61
62 void runUncompress(const QString &inputPath, const QString &outputPath) {
63 QVector<QPair<QStringList, UncompressCommand>> mimeTypeToCommand;
64 mimeTypeToCommand.append({QStringList{"application/x-tar", "application/tar", "application/x-gtar",
65 "multipart/x-tar"},
66 UncompressCommand{"tar", QStringList() << "-xf", QStringList() << "-C"}});
67 mimeTypeToCommand.append({QStringList{"application/x-gzip", "application/gzip",
68 "application/x-gzip-compressed-tar", "application/gzip-compressed-tar",
69 "application/x-gzip-compressed", "application/gzip-compressed",
70 "application/tgz", "application/x-compressed-tar",
71 "application/x-compressed-gtar", "file/tgz",
72 "multipart/x-tar-gz", "application/x-gunzip", "application/gzipped",
73 "gzip/document"},
74 UncompressCommand{"tar", QStringList{"-zxf"}, QStringList{"-C"}}});
75 mimeTypeToCommand.append({QStringList{"application/bzip", "application/bzip2", "application/x-bzip",
76 "application/x-bzip2", "application/bzip-compressed",
77 "application/bzip2-compressed", "application/x-bzip-compressed",
78 "application/x-bzip2-compressed", "application/bzip-compressed-tar",
79 "application/bzip2-compressed-tar", "application/x-bzip-compressed-tar",
80 "application/x-bzip2-compressed-tar", "application/x-bz2"},
81 UncompressCommand{"tar", QStringList{"-jxf"}, QStringList{"-C"}}});
82 mimeTypeToCommand.append({QStringList{"application/zip", "application/x-zip", "application/x-zip-compressed",
83 "multipart/x-zip"},
84 UncompressCommand{"unzip", QStringList{}, QStringList{"-d"}}});
85
86 const auto mime = QMimeDatabase().mimeTypeForFile(inputPath).name();
87
88 UncompressCommand command{};
89 for (const auto &pair : mimeTypeToCommand) {
90 if (pair.first.contains(mime)) {
91 command = pair.second;
92 break;
93 }
94 }
95
96 if (command.command.isEmpty()) {
97 fail(i18n("Unsupported archive type %1: %2", mime, inputPath));
98 }
99
100 QProcess process;
101 process.start(
102 command.command,
103 QStringList() << command.args1 << inputPath << command.args2 << outputPath,
104 QIODevice::NotOpen);
105 if (!process.waitForStarted()) {
106 fail(i18n("Failed to run uncompressor command for %1", inputPath));
107 }
108
109 if (!process.waitForFinished()) {
110 fail(
111 i18n("Process did not finish in reasonable time: %1 %2", process.program(), process.arguments().join(" ")));
112 }
113
114 if (process.exitStatus() != QProcess::NormalExit || process.exitCode() != 0) {
115 fail(i18n("Failed to uncompress %1", inputPath));
116 }
117 }
118
119 QString findRecursive(const QString &dir, const QString &basename)
120 {
121 QDirIterator it(dir, QStringList{basename}, QDir::Files, QDirIterator::Subdirectories);
122 while (it.hasNext()) {
123 return QFileInfo(it.next()).canonicalFilePath();
124 }
125
126 return QString();
127 }
128
129 bool runInstallerScriptOnce(const QString &path, const QStringList &args)
130 {
131 QProcess process;
132 process.setWorkingDirectory(QFileInfo(path).absolutePath());
133
134 process.start(path, args, QIODevice::NotOpen);
135 if (!process.waitForStarted()) {
136 fail(i18n("Failed to run installer script %1", path));
137 }
138
139 // Wait until installer exits, without timeout
140 if (!process.waitForFinished(-1)) {
141 qWarning() << "Failed to wait on installer:" << process.program() << process.arguments().join(" ");
142 return false;
143 }
144
145 if (process.exitStatus() != QProcess::NormalExit || process.exitCode() != 0) {
146 qWarning() << "Installer script exited with error:" << process.program() << process.arguments().join(" ");
147 return false;
148 }
149
150 return true;
151 }
152
153 // If hasArgVariants is true, run "path".
154 // If hasArgVariants is false, run "path argVariants[i]" until successful.
155 bool runInstallerScript(const QString &path, bool hasArgVariants, const QStringList &argVariants, QString &errorText)
156 {
157 QFile file(path);
158 if (!file.setPermissions(QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner)) {
159 errorText = i18n("Failed to set permissions on %1: %2", path, file.errorString());
160 return false;
161 }
162
163 qInfo() << "[servicemenuinstaller]: Trying to run installer/uninstaller" << path;
164 if (hasArgVariants) {
165 for (const auto &arg : argVariants) {
166 if (runInstallerScriptOnce(path, QStringList{arg})) {
167 return true;
168 }
169 }
170 } else {
171 if (runInstallerScriptOnce(path, QStringList{})) {
172 return true;
173 }
174 }
175
176 errorText = i18nc(
177 "%2 = comma separated list of arguments",
178 "Installer script %1 failed, tried arguments \"%2\".", path, argVariants.join(i18nc("Separator between arguments", "\", \"")));
179 return false;
180 }
181
182 QString generateDirPath(const QString &archive)
183 {
184 return QStringLiteral("%1-dir").arg(archive);
185 }
186
187 bool cmdInstall(const QString &archive, QString &errorText)
188 {
189 const auto serviceDir = getServiceMenusDir();
190 if (!QDir().mkpath(serviceDir)) {
191 // TODO Cannot get error string because of this bug: https://bugreports.qt.io/browse/QTBUG-1483
192 errorText = i18n("Failed to create path %1", serviceDir);
193 return false;
194 }
195
196 if (archive.endsWith(QLatin1String(".desktop"))) {
197 // Append basename to destination directory
198 const auto dest = QDir(serviceDir).absoluteFilePath(QFileInfo(archive).fileName());
199 qInfo() << "Single-File Service-Menu" << archive << dest;
200
201 QFile source(archive);
202 if (!source.copy(dest)) {
203 errorText = i18n("Failed to copy .desktop file %1 to %2: %3", archive, dest, source.errorString());
204 return false;
205 }
206 } else {
207 const QStringList binaryPackages = {"application/vnd.debian.binary-package", "application/x-rpm"};
208 if (binaryPackages.contains(QMimeDatabase().mimeTypeForFile(archive).name())) {
209 return QDesktopServices::openUrl(QUrl(archive));
210 }
211 const QString dir = generateDirPath(archive);
212 if (QFile::exists(dir)) {
213 if (!QDir(dir).removeRecursively()) {
214 errorText = i18n("Failed to remove directory %1", dir);
215 return false;
216 }
217 }
218
219 if (QDir().mkdir(dir)) {
220 errorText = i18n("Failed to create directory %1", dir);
221 }
222
223 runUncompress(archive, dir);
224
225 // Try "install-it" first
226 QString installItPath;
227 const auto basenames1 = QStringList{"install-it.sh", "install-it"};
228 for (const auto &basename : qAsConst(basenames1)) {
229 const auto path = findRecursive(dir, basename);
230 if (!path.isEmpty()) {
231 installItPath = path;
232 break;
233 }
234 }
235
236 if (!installItPath.isEmpty()) {
237 return runInstallerScript(installItPath, false, QStringList{}, errorText);
238 }
239
240 // If "install-it" is missing, try "install"
241 QString installerPath;
242 const auto basenames2 = QStringList{"installKDE4.sh", "installKDE4", "install.sh", "install"};
243 for (const auto &basename : qAsConst(basenames2)) {
244 const auto path = findRecursive(dir, basename);
245 if (!path.isEmpty()) {
246 installerPath = path;
247 break;
248 }
249 }
250
251 if (!installerPath.isEmpty()) {
252 return runInstallerScript(installerPath, true, QStringList{"--local", "--local-install", "--install"}, errorText);
253 }
254
255 fail(i18n("Failed to find an installation script in %1", dir));
256 }
257
258 return true;
259 }
260
261 bool cmdUninstall(const QString &archive, QString &errorText)
262 {
263 const auto serviceDir = getServiceMenusDir();
264 if (archive.endsWith(QLatin1String(".desktop"))) {
265 // Append basename to destination directory
266 const auto dest = QDir(serviceDir).absoluteFilePath(QFileInfo(archive).fileName());
267 QFile file(dest);
268 if (!file.remove()) {
269 errorText = i18n("Failed to remove .desktop file %1: %2", dest, file.errorString());
270 return false;
271 }
272 } else {
273 const QString dir = generateDirPath(archive);
274
275 // Try "deinstall" first
276 QString deinstallPath;
277 const auto basenames1 = QStringList{"deinstall.sh", "deinstall"};
278 for (const auto &basename : qAsConst(basenames1)) {
279 const auto path = findRecursive(dir, basename);
280 if (!path.isEmpty()) {
281 deinstallPath = path;
282 break;
283 }
284 }
285
286 if (!deinstallPath.isEmpty()) {
287 bool ok = runInstallerScript(deinstallPath, false, QStringList{}, errorText);
288 if (!ok) {
289 return ok;
290 }
291 } else {
292 // If "deinstall" is missing, try "install --uninstall"
293
294 QString installerPath;
295 const auto basenames2 = QStringList{"install-it.sh", "install-it", "installKDE4.sh",
296 "installKDE4", "install.sh", "install"};
297 for (const auto &basename : qAsConst(basenames2)) {
298 const auto path = findRecursive(dir, basename);
299 if (!path.isEmpty()) {
300 installerPath = path;
301 break;
302 }
303 }
304
305 if (!installerPath.isEmpty()) {
306 bool ok = runInstallerScript(
307 installerPath, true, QStringList{"--remove", "--delete", "--uninstall", "--deinstall"}, errorText);
308 if (!ok) {
309 return ok;
310 }
311 } else {
312 fail(i18n("Failed to find an uninstallation script in %1", dir));
313 }
314 }
315
316 QDir dirObject(dir);
317 if (!dirObject.removeRecursively()) {
318 errorText = i18n("Failed to remove directory %1", dir);
319 return false;
320 }
321 }
322
323 return true;
324 }
325
326 int main(int argc, char *argv[])
327 {
328 QGuiApplication app(argc, argv);
329
330 QCommandLineParser parser;
331 parser.addPositionalArgument(QStringLiteral("command"), i18nc("@info:shell", "Command to execute: install or uninstall."));
332 parser.addPositionalArgument(QStringLiteral("path"), i18nc("@info:shell", "Path to archive."));
333 parser.process(app);
334
335 const QStringList args = parser.positionalArguments();
336 if (args.isEmpty()) {
337 fail(i18n("Command is required."));
338 }
339 if (args.size() == 1) {
340 fail(i18n("Path to archive is required."));
341 }
342
343 const QString cmd = args[0];
344 const QString archive = args[1];
345
346 QString errorText;
347 if (cmd == "install") {
348 if (!cmdInstall(archive, errorText)) {
349 fail(errorText);
350 }
351 } else if (cmd == "uninstall") {
352 if (!cmdUninstall(archive, errorText)) {
353 fail(errorText);
354 }
355 } else {
356 fail(i18n("Unsupported command %1", cmd));
357 }
358
359 return 0;
360 }