2 * SPDX-FileCopyrightText: 2019 Alexander Potashev <aspotashev@gmail.com>
4 * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
10 #include <QStandardPaths>
12 #include <QDirIterator>
13 #include <QCommandLineParser>
14 #include <QMimeDatabase>
16 #include <QGuiApplication>
17 #include <KLocalizedString>
20 #include "../../../config-packagekit.h"
22 Q_GLOBAL_STATIC_WITH_ARGS(QStringList
, binaryPackages
, ({QLatin1String("application/vnd.debian.binary-package"),
23 QLatin1String("application/x-rpm"),
24 QLatin1String("application/x-xz"),
25 QLatin1String("application/zstd")}))
27 enum PackageOperation
{
32 #ifdef HAVE_PACKAGEKIT
33 #include <PackageKit/Daemon>
34 #include <PackageKit/Details>
35 #include <PackageKit/Transaction>
37 #include <QDesktopServices>
40 // @param msg Error that gets logged to CLI
41 Q_NORETURN
void fail(const QString
&str
)
44 const QStringList args
= {"--detailederror" ,i18n("Dolphin service menu installation failed"), str
};
45 QProcess::startDetached("kdialog", args
);
50 QString
getServiceMenusDir()
52 const QString dataLocation
= QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation
);
53 return QDir(dataLocation
).absoluteFilePath("kio/servicemenus");
56 #ifdef HAVE_PACKAGEKIT
57 void packageKitInstall(const QString
&fileName
)
59 PackageKit::Transaction
*transaction
= PackageKit::Daemon::installFile(fileName
, PackageKit::Transaction::TransactionFlagNone
);
61 const auto exitWithError
= [=](PackageKit::Transaction::Error
, const QString
&details
) {
65 QObject::connect(transaction
, &PackageKit::Transaction::finished
,
66 [=](PackageKit::Transaction::Exit status
, uint
) {
67 if (status
== PackageKit::Transaction::ExitSuccess
) {
70 // Fallback error handling
71 QTimer::singleShot(500, [=](){
72 fail(i18n("Failed to install \"%1\", exited with status \"%2\"",
73 fileName
, QVariant::fromValue(status
).toString()));
76 QObject::connect(transaction
, &PackageKit::Transaction::errorCode
, exitWithError
);
79 void packageKitUninstall(const QString
&fileName
)
81 const auto exitWithError
= [=](PackageKit::Transaction::Error
, const QString
&details
) {
84 const auto uninstallLambda
= [=](PackageKit::Transaction::Exit status
, uint
) {
85 if (status
== PackageKit::Transaction::ExitSuccess
) {
90 PackageKit::Transaction
*transaction
= PackageKit::Daemon::getDetailsLocal(fileName
);
91 QObject::connect(transaction
, &PackageKit::Transaction::details
,
92 [=](const PackageKit::Details
&details
) {
93 PackageKit::Transaction
*transaction
= PackageKit::Daemon::removePackage(details
.packageId());
94 QObject::connect(transaction
, &PackageKit::Transaction::finished
, uninstallLambda
);
95 QObject::connect(transaction
, &PackageKit::Transaction::errorCode
, exitWithError
);
98 QObject::connect(transaction
, &PackageKit::Transaction::errorCode
, exitWithError
);
99 // Fallback error handling
100 QObject::connect(transaction
, &PackageKit::Transaction::finished
,
101 [=](PackageKit::Transaction::Exit status
, uint
) {
102 if (status
!= PackageKit::Transaction::ExitSuccess
) {
103 QTimer::singleShot(500, [=]() {
104 fail(i18n("Failed to uninstall \"%1\", exited with status \"%2\"",
105 fileName
, QVariant::fromValue(status
).toString()));
112 Q_NORETURN
void packageKit(PackageOperation operation
, const QString
&fileName
)
114 #ifdef HAVE_PACKAGEKIT
115 QFileInfo
fileInfo(fileName
);
116 if (!fileInfo
.exists()) {
117 fail(i18n("The file does not exist!"));
119 const QString absPath
= fileInfo
.absoluteFilePath();
120 if (operation
== PackageOperation::Install
) {
121 packageKitInstall(absPath
);
123 packageKitUninstall(absPath
);
125 QGuiApplication::exec(); // For event handling, no return after signals finish
126 fail(i18n("Unknown error when installing package"));
129 QDesktopServices::openUrl(QUrl(fileName
));
134 struct UncompressCommand
141 enum ScriptExecution
{
146 void runUncompress(const QString
&inputPath
, const QString
&outputPath
)
148 QVector
<QPair
<QStringList
, UncompressCommand
>> mimeTypeToCommand
;
149 mimeTypeToCommand
.append({{"application/x-tar", "application/tar", "application/x-gtar", "multipart/x-tar"},
150 UncompressCommand({"tar", {"-xf"}, {"-C"}})});
151 mimeTypeToCommand
.append({{"application/x-gzip", "application/gzip",
152 "application/x-gzip-compressed-tar", "application/gzip-compressed-tar",
153 "application/x-gzip-compressed", "application/gzip-compressed",
154 "application/tgz", "application/x-compressed-tar",
155 "application/x-compressed-gtar", "file/tgz",
156 "multipart/x-tar-gz", "application/x-gunzip", "application/gzipped",
158 UncompressCommand({"tar", {"-zxf"}, {"-C"}})});
159 mimeTypeToCommand
.append({{"application/bzip", "application/bzip2", "application/x-bzip",
160 "application/x-bzip2", "application/bzip-compressed",
161 "application/bzip2-compressed", "application/x-bzip-compressed",
162 "application/x-bzip2-compressed", "application/bzip-compressed-tar",
163 "application/bzip2-compressed-tar", "application/x-bzip-compressed-tar",
164 "application/x-bzip2-compressed-tar", "application/x-bz2"},
165 UncompressCommand({"tar", {"-jxf"}, {"-C"}})});
166 mimeTypeToCommand
.append({{"application/zip", "application/x-zip", "application/x-zip-compressed",
168 UncompressCommand({"unzip", {}, {"-d"}})});
170 const auto mime
= QMimeDatabase().mimeTypeForFile(inputPath
).name();
172 UncompressCommand command
{};
173 for (const auto &pair
: qAsConst(mimeTypeToCommand
)) {
174 if (pair
.first
.contains(mime
)) {
175 command
= pair
.second
;
180 if (command
.command
.isEmpty()) {
181 fail(i18n("Unsupported archive type %1: %2", mime
, inputPath
));
187 QStringList() << command
.args1
<< inputPath
<< command
.args2
<< outputPath
,
189 if (!process
.waitForStarted()) {
190 fail(i18n("Failed to run uncompressor command for %1", inputPath
));
193 if (!process
.waitForFinished()) {
195 i18n("Process did not finish in reasonable time: %1 %2", process
.program(), process
.arguments().join(" ")));
198 if (process
.exitStatus() != QProcess::NormalExit
|| process
.exitCode() != 0) {
199 fail(i18n("Failed to uncompress %1", inputPath
));
203 QString
findRecursive(const QString
&dir
, const QString
&basename
)
205 QDirIterator
it(dir
, QStringList
{basename
}, QDir::Files
, QDirIterator::Subdirectories
);
206 while (it
.hasNext()) {
207 return QFileInfo(it
.next()).canonicalFilePath();
213 bool runScriptOnce(const QString
&path
, const QStringList
&args
, ScriptExecution execution
)
216 process
.setWorkingDirectory(QFileInfo(path
).absolutePath());
218 const static bool konsoleAvailable
= !QStandardPaths::findExecutable("konsole").isEmpty();
219 if (konsoleAvailable
&& execution
== ScriptExecution::Konsole
) {
220 QString bashCommand
= KShell::quoteArg(path
) + ' ';
221 if (!args
.isEmpty()) {
222 bashCommand
.append(args
.join(' '));
224 bashCommand
.append("|| $SHELL");
225 // If the install script fails a shell opens and the user can fix the problem
226 // without an error konsole closes
227 process
.start("konsole", QStringList() << "-e" << "bash" << "-c" << bashCommand
, QIODevice::NotOpen
);
229 process
.start(path
, args
, QIODevice::NotOpen
);
231 if (!process
.waitForStarted()) {
232 fail(i18n("Failed to run installer script %1", path
));
235 // Wait until installer exits, without timeout
236 if (!process
.waitForFinished(-1)) {
237 qWarning() << "Failed to wait on installer:" << process
.program() << process
.arguments().join(" ");
241 if (process
.exitStatus() != QProcess::NormalExit
|| process
.exitCode() != 0) {
242 qWarning() << "Installer script exited with error:" << process
.program() << process
.arguments().join(" ");
249 // If hasArgVariants is true, run "path".
250 // If hasArgVariants is false, run "path argVariants[i]" until successful.
251 bool runScriptVariants(const QString
&path
, bool hasArgVariants
, const QStringList
&argVariants
, QString
&errorText
)
254 if (!file
.setPermissions(QFile::ReadOwner
| QFile::WriteOwner
| QFile::ExeOwner
)) {
255 errorText
= i18n("Failed to set permissions on %1: %2", path
, file
.errorString());
259 qInfo() << "[servicemenuinstaller]: Trying to run installer/uninstaller" << path
;
260 if (hasArgVariants
) {
261 for (const auto &arg
: argVariants
) {
262 if (runScriptOnce(path
, {arg
}, ScriptExecution::Process
)) {
266 } else if (runScriptOnce(path
, {}, ScriptExecution::Konsole
)) {
271 "%2 = comma separated list of arguments",
272 "Installer script %1 failed, tried arguments \"%2\".", path
, argVariants
.join(i18nc("Separator between arguments", "\", \"")));
276 QString
generateDirPath(const QString
&archive
)
278 return QStringLiteral("%1-dir").arg(archive
);
281 bool cmdInstall(const QString
&archive
, QString
&errorText
)
283 const auto serviceDir
= getServiceMenusDir();
284 if (!QDir().mkpath(serviceDir
)) {
285 // TODO Cannot get error string because of this bug: https://bugreports.qt.io/browse/QTBUG-1483
286 errorText
= i18n("Failed to create path %1", serviceDir
);
290 if (archive
.endsWith(QLatin1String(".desktop"))) {
291 // Append basename to destination directory
292 const auto dest
= QDir(serviceDir
).absoluteFilePath(QFileInfo(archive
).fileName());
293 if (QFileInfo::exists(dest
)) {
296 qInfo() << "Single-File Service-Menu" << archive
<< dest
;
298 QFile
source(archive
);
299 if (!source
.copy(dest
)) {
300 errorText
= i18n("Failed to copy .desktop file %1 to %2: %3", archive
, dest
, source
.errorString());
303 QFile
destFile(dest
);
304 destFile
.setPermissions(destFile
.permissions() | QFile::ExeOwner
);
306 if (binaryPackages
->contains(QMimeDatabase().mimeTypeForFile(archive
).name())) {
307 packageKit(PackageOperation::Install
, archive
);
309 const QString dir
= generateDirPath(archive
);
310 if (QFile::exists(dir
)) {
311 if (!QDir(dir
).removeRecursively()) {
312 errorText
= i18n("Failed to remove directory %1", dir
);
317 if (QDir().mkdir(dir
)) {
318 errorText
= i18n("Failed to create directory %1", dir
);
321 runUncompress(archive
, dir
);
323 // Try "install-it" first
324 QString installItPath
;
325 const QStringList basenames1
= {"install-it.sh", "install-it"};
326 for (const auto &basename
: basenames1
) {
327 const auto path
= findRecursive(dir
, basename
);
328 if (!path
.isEmpty()) {
329 installItPath
= path
;
334 if (!installItPath
.isEmpty()) {
335 return runScriptVariants(installItPath
, false, QStringList
{}, errorText
);
338 // If "install-it" is missing, try "install"
339 QString installerPath
;
340 const QStringList basenames2
= {"installKDE4.sh", "installKDE4", "install.sh", "install"};
341 for (const auto &basename
: basenames2
) {
342 const auto path
= findRecursive(dir
, basename
);
343 if (!path
.isEmpty()) {
344 installerPath
= path
;
349 if (!installerPath
.isEmpty()) {
350 // Try to run script without variants first
351 if (!runScriptVariants(installerPath
, false, {}, errorText
)) {
352 return runScriptVariants(installerPath
, true, {"--local", "--local-install", "--install"}, errorText
);
357 fail(i18n("Failed to find an installation script in %1", dir
));
363 bool cmdUninstall(const QString
&archive
, QString
&errorText
)
365 const auto serviceDir
= getServiceMenusDir();
366 if (archive
.endsWith(QLatin1String(".desktop"))) {
367 // Append basename to destination directory
368 const auto dest
= QDir(serviceDir
).absoluteFilePath(QFileInfo(archive
).fileName());
370 if (!file
.remove()) {
371 errorText
= i18n("Failed to remove .desktop file %1: %2", dest
, file
.errorString());
375 if (binaryPackages
->contains(QMimeDatabase().mimeTypeForFile(archive
).name())) {
376 packageKit(PackageOperation::Uninstall
, archive
);
378 const QString dir
= generateDirPath(archive
);
380 // Try "deinstall" first
381 QString deinstallPath
;
382 const QStringList basenames1
= {"uninstall.sh", "uninstal", "deinstall.sh", "deinstall"};
383 for (const auto &basename
: basenames1
) {
384 const auto path
= findRecursive(dir
, basename
);
385 if (!path
.isEmpty()) {
386 deinstallPath
= path
;
391 if (!deinstallPath
.isEmpty()) {
392 const bool ok
= runScriptVariants(deinstallPath
, false, {}, errorText
);
397 // If "deinstall" is missing, try "install --uninstall"
398 QString installerPath
;
399 const QStringList basenames2
= {"install-it.sh", "install-it", "installKDE4.sh",
400 "installKDE4", "install.sh", "install"};
401 for (const auto &basename
: basenames2
) {
402 const auto path
= findRecursive(dir
, basename
);
403 if (!path
.isEmpty()) {
404 installerPath
= path
;
409 if (!installerPath
.isEmpty()) {
410 const bool ok
= runScriptVariants(installerPath
, true,
411 {"--remove", "--delete", "--uninstall", "--deinstall"}, errorText
);
416 fail(i18n("Failed to find an uninstallation script in %1", dir
));
421 if (!dirObject
.removeRecursively()) {
422 errorText
= i18n("Failed to remove directory %1", dir
);
430 int main(int argc
, char *argv
[])
432 QGuiApplication
app(argc
, argv
);
434 QCommandLineParser parser
;
435 parser
.addPositionalArgument(QStringLiteral("command"), i18nc("@info:shell", "Command to execute: install or uninstall."));
436 parser
.addPositionalArgument(QStringLiteral("path"), i18nc("@info:shell", "Path to archive."));
439 const QStringList args
= parser
.positionalArguments();
440 if (args
.isEmpty()) {
441 fail(i18n("Command is required."));
443 if (args
.size() == 1) {
444 fail(i18n("Path to archive is required."));
447 const QString cmd
= args
[0];
448 const QString archive
= args
[1];
451 if (cmd
== QLatin1String("install")) {
452 if (!cmdInstall(archive
, errorText
)) {
455 } else if (cmd
== QLatin1String("uninstall")) {
456 if (!cmdUninstall(archive
, errorText
)) {
460 fail(i18n("Unsupported command %1", cmd
));