1 /***************************************************************************
2 * Copyright © 2019 Alexander Potashev <aspotashev@gmail.com> *
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. *
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. *
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 ***************************************************************************/
23 #include <QStandardPaths>
25 #include <QDirIterator>
26 #include <QCommandLineParser>
28 #include <KLocalizedString>
30 // @param msg Error that gets logged to CLI
31 Q_NORETURN
void fail(const QString
&str
)
36 auto args
= QStringList
{"--passivepopup", i18n("Dolphin service menu installation failed"), "15"};
37 process
.start("kdialog", args
, QIODevice::ReadOnly
);
38 if (!process
.waitForStarted()) {
39 qFatal("Failed to run kdialog");
45 bool evaluateShell(const QString
&program
, const QStringList
&arguments
, QString
&output
, QString
&errorText
)
48 process
.start(program
, arguments
, QIODevice::ReadOnly
);
49 if (!process
.waitForStarted()) {
50 fail(i18n("Failed to run process: %1 %2", program
, arguments
.join(" ")));
53 if (!process
.waitForFinished()) {
54 fail(i18n("Process did not finish in reasonable time: %1 %2", program
, arguments
.join(" ")));
57 const auto stdoutResult
= QString::fromUtf8(process
.readAllStandardOutput()).trimmed();
58 const auto stderrResult
= QString::fromUtf8(process
.readAllStandardError()).trimmed();
60 if (process
.exitStatus() == QProcess::NormalExit
&& process
.exitCode() == 0) {
61 output
= stdoutResult
;
64 errorText
= stderrResult
+ stdoutResult
;
69 QString
mimeType(const QString
&path
)
73 if (evaluateShell("xdg-mime", QStringList
{"query", "filetype", path
}, result
, errorText
)) {
76 fail(i18n("Failed to run xdg-mime %1: %2", path
, errorText
));
80 QString
getServiceMenusDir()
82 const QString dataLocation
= QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation
);
83 return QDir(dataLocation
).absoluteFilePath("kservices5/ServiceMenus");
86 struct UncompressCommand
93 void runUncompress(const QString
&inputPath
, const QString
&outputPath
) {
94 QList
<QPair
<QStringList
, UncompressCommand
>> mimeTypeToCommand
;
95 mimeTypeToCommand
.append({QStringList
{"application/x-tar", "application/tar", "application/x-gtar",
97 UncompressCommand
{"tar", QStringList() << "-xf", QStringList() << "-C"}});
98 mimeTypeToCommand
.append({QStringList
{"application/x-gzip", "application/gzip",
99 "application/x-gzip-compressed-tar", "application/gzip-compressed-tar",
100 "application/x-gzip-compressed", "application/gzip-compressed",
101 "application/tgz", "application/x-compressed-tar",
102 "application/x-compressed-gtar", "file/tgz",
103 "multipart/x-tar-gz", "application/x-gunzip", "application/gzipped",
105 UncompressCommand
{"tar", QStringList
{"-zxf"}, QStringList
{"-C"}}});
106 mimeTypeToCommand
.append({QStringList
{"application/bzip", "application/bzip2", "application/x-bzip",
107 "application/x-bzip2", "application/bzip-compressed",
108 "application/bzip2-compressed", "application/x-bzip-compressed",
109 "application/x-bzip2-compressed", "application/bzip-compressed-tar",
110 "application/bzip2-compressed-tar", "application/x-bzip-compressed-tar",
111 "application/x-bzip2-compressed-tar", "application/x-bz2"},
112 UncompressCommand
{"tar", QStringList
{"-jxf"}, QStringList
{"-C"}}});
113 mimeTypeToCommand
.append({QStringList
{"application/zip", "application/x-zip", "application/x-zip-compressed",
115 UncompressCommand
{"unzip", QStringList
{}, QStringList
{"-d"}}});
117 const auto mime
= mimeType(inputPath
);
119 UncompressCommand command
{};
120 for (const auto &pair
: mimeTypeToCommand
) {
121 if (pair
.first
.contains(mime
)) {
122 command
= pair
.second
;
127 if (command
.command
.isEmpty()) {
128 fail(i18n("Unsupported archive type %1: %2", mime
, inputPath
));
134 QStringList() << command
.args1
<< inputPath
<< command
.args2
<< outputPath
,
136 if (!process
.waitForStarted()) {
137 fail(i18n("Failed to run uncompressor command for %1", inputPath
));
140 if (!process
.waitForFinished()) {
142 i18n("Process did not finish in reasonable time: %1 %2", process
.program(), process
.arguments().join(" ")));
145 if (process
.exitStatus() != QProcess::NormalExit
|| process
.exitCode() != 0) {
146 fail(i18n("Failed to uncompress %1", inputPath
));
150 QString
findRecursive(const QString
&dir
, const QString
&basename
)
152 QDirIterator
it(dir
, QStringList
{basename
}, QDir::Files
, QDirIterator::Subdirectories
);
153 while (it
.hasNext()) {
154 return QFileInfo(it
.next()).canonicalFilePath();
160 bool runInstallerScriptOnce(const QString
&path
, const QStringList
&args
)
163 process
.setWorkingDirectory(QFileInfo(path
).absolutePath());
165 process
.start(path
, args
, QIODevice::NotOpen
);
166 if (!process
.waitForStarted()) {
167 fail(i18n("Failed to run installer script %1", path
));
170 // Wait until installer exits, without timeout
171 if (!process
.waitForFinished(-1)) {
172 qWarning() << "Failed to wait on installer:" << process
.program() << process
.arguments().join(" ");
176 if (process
.exitStatus() != QProcess::NormalExit
|| process
.exitCode() != 0) {
177 qWarning() << "Installer script exited with error:" << process
.program() << process
.arguments().join(" ");
184 // If hasArgVariants is true, run "path".
185 // If hasArgVariants is false, run "path argVariants[i]" until successful.
186 bool runInstallerScript(const QString
&path
, bool hasArgVariants
, const QStringList
&argVariants
, QString
&errorText
)
189 if (!file
.setPermissions(QFile::ReadOwner
| QFile::WriteOwner
| QFile::ExeOwner
)) {
190 errorText
= i18n("Failed to set permissions on %1: %2", path
, file
.errorString());
194 qInfo() << "[servicemenuinstaller]: Trying to run installer/uninstaller" << path
;
195 if (hasArgVariants
) {
196 for (const auto &arg
: argVariants
) {
197 if (runInstallerScriptOnce(path
, QStringList
{arg
})) {
202 if (runInstallerScriptOnce(path
, QStringList
{})) {
208 "%1 = comma separated list of arguments",
209 "Installer script %1 failed, tried arguments \"%1\".", path
, argVariants
.join(i18nc("Separator between arguments", "\", \"")));
213 QString
generateDirPath(const QString
&archive
)
215 return QStringLiteral("%1-dir").arg(archive
);
218 bool cmdInstall(const QString
&archive
, QString
&errorText
)
220 const auto serviceDir
= getServiceMenusDir();
221 if (!QDir().mkpath(serviceDir
)) {
222 // TODO Cannot get error string because of this bug: https://bugreports.qt.io/browse/QTBUG-1483
223 errorText
= i18n("Failed to create path %1", serviceDir
);
227 if (archive
.endsWith(".desktop")) {
228 // Append basename to destination directory
229 const auto dest
= QDir(serviceDir
).absoluteFilePath(QFileInfo(archive
).fileName());
230 qInfo() << "Single-File Service-Menu" << archive
<< dest
;
232 QFile
source(archive
);
233 if (!source
.copy(dest
)) {
234 errorText
= i18n("Failed to copy .desktop file %1 to %2: %3", archive
, dest
, source
.errorString());
238 const QString dir
= generateDirPath(archive
);
239 if (QFile::exists(dir
)) {
240 if (!QDir(dir
).removeRecursively()) {
241 errorText
= i18n("Failed to remove directory %1", dir
);
246 if (QDir().mkdir(dir
)) {
247 errorText
= i18n("Failed to create directory %1", dir
);
250 runUncompress(archive
, dir
);
252 // Try "install-it" first
253 QString installItPath
;
254 const auto basenames1
= QStringList
{"install-it.sh", "install-it"};
255 for (const auto &basename
: qAsConst(basenames1
)) {
256 const auto path
= findRecursive(dir
, basename
);
257 if (!path
.isEmpty()) {
258 installItPath
= path
;
263 if (!installItPath
.isEmpty()) {
264 return runInstallerScript(installItPath
, false, QStringList
{}, errorText
);
267 // If "install-it" is missing, try "install"
268 QString installerPath
;
269 const auto basenames2
= QStringList
{"installKDE4.sh", "installKDE4", "install.sh", "install"};
270 for (const auto &basename
: qAsConst(basenames2
)) {
271 const auto path
= findRecursive(dir
, basename
);
272 if (!path
.isEmpty()) {
273 installerPath
= path
;
278 if (!installerPath
.isEmpty()) {
279 return runInstallerScript(installerPath
, true, QStringList
{"--local", "--local-install", "--install"}, errorText
);
282 fail(i18n("Failed to find an installation script in %1", dir
));
288 bool cmdUninstall(const QString
&archive
, QString
&errorText
)
290 const auto serviceDir
= getServiceMenusDir();
291 if (archive
.endsWith(".desktop")) {
292 // Append basename to destination directory
293 const auto dest
= QDir(serviceDir
).absoluteFilePath(QFileInfo(archive
).fileName());
295 if (!file
.remove()) {
296 errorText
= i18n("Failed to remove .desktop file %1: %2", dest
, file
.errorString());
300 const QString dir
= generateDirPath(archive
);
302 // Try "deinstall" first
303 QString deinstallPath
;
304 const auto basenames1
= QStringList
{"deinstall.sh", "deinstall"};
305 for (const auto &basename
: qAsConst(basenames1
)) {
306 const auto path
= findRecursive(dir
, basename
);
307 if (!path
.isEmpty()) {
308 deinstallPath
= path
;
313 if (!deinstallPath
.isEmpty()) {
314 bool ok
= runInstallerScript(deinstallPath
, false, QStringList
{}, errorText
);
319 // If "deinstall" is missing, try "install --uninstall"
321 QString installerPath
;
322 const auto basenames2
= QStringList
{"install-it.sh", "install-it", "installKDE4.sh",
323 "installKDE4", "install.sh", "install"};
324 for (const auto &basename
: qAsConst(basenames2
)) {
325 const auto path
= findRecursive(dir
, basename
);
326 if (!path
.isEmpty()) {
327 installerPath
= path
;
332 if (!installerPath
.isEmpty()) {
333 bool ok
= runInstallerScript(
334 installerPath
, true, QStringList
{"--remove", "--delete", "--uninstall", "--deinstall"}, errorText
);
339 fail(i18n("Failed to find an uninstallation script in %1", dir
));
344 if (!dirObject
.removeRecursively()) {
345 errorText
= i18n("Failed to remove directory %1", dir
);
353 int main(int argc
, char *argv
[])
355 QCoreApplication
app(argc
, argv
);
357 QCommandLineParser parser
;
358 parser
.addPositionalArgument(QStringLiteral("command"), i18nc("@info:shell", "Command to execute: install or uninstall."));
359 parser
.addPositionalArgument(QStringLiteral("path"), i18nc("@info:shell", "Path to archive."));
362 const QStringList args
= parser
.positionalArguments();
363 if (args
.isEmpty()) {
364 fail(i18n("Command is required."));
366 if (args
.size() == 1) {
367 fail(i18n("Path to archive is required."));
370 const QString cmd
= args
[0];
371 const QString archive
= args
[1];
374 if (cmd
== "install") {
375 if (!cmdInstall(archive
, errorText
)) {
378 } else if (cmd
== "uninstall") {
379 if (!cmdUninstall(archive
, errorText
)) {
383 fail(i18n("Unsupported command %1", cmd
));