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