Flow.Launcher/Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs

506 lines
21 KiB
C#
Raw Permalink Normal View History

2022-08-10 03:18:37 +00:00
using Flow.Launcher.Core.ExternalPlugins;
2021-02-13 10:03:26 +00:00
using Flow.Launcher.Core.Plugin;
2020-12-07 10:21:14 +00:00
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.Http;
2020-12-07 10:21:14 +00:00
using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin.SharedCommands;
2020-12-06 08:58:27 +00:00
using System;
using System.Collections.Generic;
2020-12-07 10:21:14 +00:00
using System.IO;
2020-12-06 20:40:42 +00:00
using System.Linq;
using System.Net.Http;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
2020-12-07 10:21:14 +00:00
using System.Windows;
2020-12-06 08:58:27 +00:00
namespace Flow.Launcher.Plugin.PluginsManager
{
internal class PluginsManager
{
const string zip = "zip";
2020-12-10 10:28:01 +00:00
private PluginInitContext Context { get; set; }
2020-12-06 20:42:23 +00:00
private Settings Settings { get; set; }
2020-12-17 10:07:47 +00:00
private bool shouldHideWindow = true;
private bool ShouldHideWindow
2020-12-17 10:07:47 +00:00
{
set { shouldHideWindow = value; }
get
{
var setValue = shouldHideWindow;
// Default value for hide main window is true. Revert after get call.
// This ensures when set by another method to false, it is only used once.
shouldHideWindow = true;
return setValue;
}
}
internal readonly string icoPath = "Images\\pluginsmanager.png";
2020-12-06 20:42:23 +00:00
internal PluginsManager(PluginInitContext context, Settings settings)
2020-12-06 20:40:42 +00:00
{
2020-12-10 10:28:01 +00:00
Context = context;
Settings = settings;
2020-12-06 20:40:42 +00:00
}
2021-02-03 02:25:29 +00:00
private Task _downloadManifestTask = Task.CompletedTask;
internal Task UpdateManifestAsync(CancellationToken token = default, bool silent = false)
{
2021-02-03 02:23:38 +00:00
if (_downloadManifestTask.Status == TaskStatus.Running)
{
return _downloadManifestTask;
}
else
{
_downloadManifestTask = PluginsManifest.UpdateManifestAsync(token);
if (!silent)
_downloadManifestTask.ContinueWith(_ =>
Context.API.ShowMsg(Context.API.GetTranslation("plugin_pluginsmanager_update_failed_title"),
Context.API.GetTranslation("plugin_pluginsmanager_update_failed_subtitle"), icoPath, false),
TaskContinuationOptions.OnlyOnFaulted);
2021-07-04 10:35:00 +00:00
return _downloadManifestTask;
2021-02-03 02:23:38 +00:00
}
}
internal List<Result> GetDefaultHotKeys()
{
return new List<Result>()
{
new Result()
{
Title = Settings.InstallCommand,
IcoPath = icoPath,
AutoCompleteText = $"{Context.CurrentPluginMetadata.ActionKeyword} {Settings.InstallCommand} ",
Action = _ =>
{
Context.API.ChangeQuery($"{Context.CurrentPluginMetadata.ActionKeyword} {Settings.InstallCommand} ");
return false;
}
},
new Result()
{
Title = Settings.UninstallCommand,
IcoPath = icoPath,
AutoCompleteText = $"{Context.CurrentPluginMetadata.ActionKeyword} {Settings.UninstallCommand} ",
Action = _ =>
{
Context.API.ChangeQuery($"{Context.CurrentPluginMetadata.ActionKeyword} {Settings.UninstallCommand} ");
return false;
}
},
new Result()
{
Title = Settings.UpdateCommand,
IcoPath = icoPath,
AutoCompleteText = $"{Context.CurrentPluginMetadata.ActionKeyword} {Settings.UpdateCommand} ",
Action = _ =>
{
Context.API.ChangeQuery($"{Context.CurrentPluginMetadata.ActionKeyword} {Settings.UpdateCommand} ");
return false;
}
}
};
}
2022-08-10 03:18:37 +00:00
internal async Task InstallOrUpdateAsync(UserPlugin plugin)
2020-12-06 08:58:27 +00:00
{
if (PluginExists(plugin.ID))
2020-12-06 08:58:27 +00:00
{
if (Context.API.GetAllPlugins()
.Any(x => x.Metadata.ID == plugin.ID && x.Metadata.Version.CompareTo(plugin.Version) < 0))
{
if (MessageBox.Show(Context.API.GetTranslation("plugin_pluginsmanager_update_exists"),
Context.API.GetTranslation("plugin_pluginsmanager_update_title"),
MessageBoxButton.YesNo) == MessageBoxResult.Yes)
Context
.API
.ChangeQuery(
$"{Context.CurrentPluginMetadata.ActionKeywords.FirstOrDefault()} {Settings.UpdateCommand} {plugin.Name}");
var mainWindow = Application.Current.MainWindow;
mainWindow.Show();
mainWindow.Focus();
shouldHideWindow = false;
return;
}
Context.API.ShowMsg(Context.API.GetTranslation("plugin_pluginsmanager_update_alreadyexists"));
2020-12-10 20:45:01 +00:00
return;
2020-12-06 08:58:27 +00:00
}
var message = string.Format(Context.API.GetTranslation("plugin_pluginsmanager_install_prompt"),
plugin.Name, plugin.Author,
Environment.NewLine, Environment.NewLine);
2020-12-10 11:58:18 +00:00
if (MessageBox.Show(message, Context.API.GetTranslation("plugin_pluginsmanager_install_title"),
MessageBoxButton.YesNo) == MessageBoxResult.No)
2020-12-10 11:58:18 +00:00
return;
// at minimum should provide a name, but handle plugin that is not downloaded from plugins manifest and is a url download
var downloadFilename = string.IsNullOrEmpty(plugin.Version)
? $"{plugin.Name}-{Guid.NewGuid()}.zip"
: $"{plugin.Name}-{plugin.Version}.zip";
var filePath = Path.Combine(DataLocation.PluginsDirectory, downloadFilename);
2020-12-07 10:21:14 +00:00
try
2020-12-06 08:58:27 +00:00
{
await Http.DownloadAsync(plugin.UrlDownload, filePath).ConfigureAwait(false);
2020-12-07 10:21:14 +00:00
2020-12-10 10:28:01 +00:00
Context.API.ShowMsg(Context.API.GetTranslation("plugin_pluginsmanager_downloading_plugin"),
Context.API.GetTranslation("plugin_pluginsmanager_download_success"));
2021-01-01 08:13:14 +00:00
Install(plugin, filePath);
2020-12-07 10:21:14 +00:00
}
catch (Exception e)
2020-12-07 10:21:14 +00:00
{
if (e is HttpRequestException)
MessageBox.Show(Context.API.GetTranslation("plugin_pluginsmanager_download_error"),
Context.API.GetTranslation("plugin_pluginsmanager_downloading_plugin"));
Context.API.ShowMsgError(Context.API.GetTranslation("plugin_pluginsmanager_install_error_title"),
string.Format(Context.API.GetTranslation("plugin_pluginsmanager_install_error_subtitle"),
plugin.Name));
2020-12-07 10:21:14 +00:00
2021-01-01 08:13:14 +00:00
Log.Exception("PluginsManager", "An error occured while downloading plugin", e, "InstallOrUpdate");
2021-01-01 10:54:34 +00:00
return;
2020-12-06 08:58:27 +00:00
}
Context.API.ShowMsg(Context.API.GetTranslation("plugin_pluginsmanager_installing_plugin"),
Context.API.GetTranslation("plugin_pluginsmanager_install_success_restart"));
2021-11-20 07:47:10 +00:00
2020-12-29 10:48:00 +00:00
Context.API.RestartApp();
2020-12-06 08:58:27 +00:00
}
2022-08-10 03:18:37 +00:00
internal async ValueTask<List<Result>> RequestUpdateAsync(string search, CancellationToken token)
2020-12-06 08:58:27 +00:00
{
await UpdateManifestAsync(token);
var resultsForUpdate =
from existingPlugin in Context.API.GetAllPlugins()
join pluginFromManifest in PluginsManifest.UserPlugins
on existingPlugin.Metadata.ID equals pluginFromManifest.ID
where existingPlugin.Metadata.Version.CompareTo(pluginFromManifest.Version) <
0 // if current version precedes manifest version
select
new
{
pluginFromManifest.Name,
pluginFromManifest.Author,
CurrentVersion = existingPlugin.Metadata.Version,
NewVersion = pluginFromManifest.Version,
existingPlugin.Metadata.IcoPath,
PluginExistingMetadata = existingPlugin.Metadata,
PluginNewUserPlugin = pluginFromManifest
};
2020-12-17 09:37:01 +00:00
if (!resultsForUpdate.Any())
return new List<Result>
{
new Result
{
Title = Context.API.GetTranslation("plugin_pluginsmanager_update_noresult_title"),
SubTitle = Context.API.GetTranslation("plugin_pluginsmanager_update_noresult_subtitle"),
IcoPath = icoPath
}
};
2020-12-17 09:37:01 +00:00
var results = resultsForUpdate
.Select(x =>
new Result
{
Title = $"{x.Name} by {x.Author}",
SubTitle = $"Update from version {x.CurrentVersion} to {x.NewVersion}",
IcoPath = x.IcoPath,
Action = e =>
{
string message = string.Format(
Context.API.GetTranslation("plugin_pluginsmanager_update_prompt"),
x.Name, x.Author,
Environment.NewLine, Environment.NewLine);
if (MessageBox.Show(message,
Context.API.GetTranslation("plugin_pluginsmanager_update_title"),
MessageBoxButton.YesNo) == MessageBoxResult.Yes)
{
2021-02-18 21:05:03 +00:00
Uninstall(x.PluginExistingMetadata, false);
var downloadToFilePath = Path.Combine(DataLocation.PluginsDirectory,
$"{x.Name}-{x.NewVersion}.zip");
2020-12-29 10:48:00 +00:00
Task.Run(async delegate
{
await Http.DownloadAsync(x.PluginNewUserPlugin.UrlDownload, downloadToFilePath)
.ConfigureAwait(false);
2021-01-03 12:40:18 +00:00
Context.API.ShowMsg(
Context.API.GetTranslation("plugin_pluginsmanager_downloading_plugin"),
Context.API.GetTranslation("plugin_pluginsmanager_download_success"));
2021-01-03 12:40:18 +00:00
2020-12-29 10:48:00 +00:00
Install(x.PluginNewUserPlugin, downloadToFilePath);
Context.API.RestartApp();
2021-01-03 12:49:14 +00:00
}).ContinueWith(t =>
{
Log.Exception("PluginsManager", $"Update failed for {x.Name}",
t.Exception.InnerException, "RequestUpdate");
Context.API.ShowMsg(
Context.API.GetTranslation("plugin_pluginsmanager_install_error_title"),
string.Format(
Context.API.GetTranslation("plugin_pluginsmanager_install_error_subtitle"),
x.Name));
2021-01-03 12:49:14 +00:00
}, TaskContinuationOptions.OnlyOnFaulted);
return true;
}
return false;
},
ContextData =
new UserPlugin
{
Website = x.PluginNewUserPlugin.Website, UrlSourceCode = x.PluginNewUserPlugin.UrlSourceCode
}
});
2020-12-17 09:37:01 +00:00
return Search(results, search);
2020-12-06 08:58:27 +00:00
}
2020-12-10 11:58:18 +00:00
internal bool PluginExists(string id)
2020-12-06 08:58:27 +00:00
{
2020-12-10 11:58:18 +00:00
return Context.API.GetAllPlugins().Any(x => x.Metadata.ID == id);
2020-12-06 08:58:27 +00:00
}
2020-12-20 09:08:52 +00:00
internal List<Result> Search(IEnumerable<Result> results, string searchName)
2020-12-10 10:28:01 +00:00
{
if (string.IsNullOrEmpty(searchName))
2020-12-20 09:08:52 +00:00
return results.ToList();
2020-12-10 10:28:01 +00:00
return results
.Where(x =>
{
var matchResult = StringMatcher.FuzzySearch(searchName, x.Title);
if (matchResult.IsSearchPrecisionScoreMet())
x.Score = matchResult.Score;
2020-12-14 08:14:50 +00:00
return matchResult.IsSearchPrecisionScoreMet();
})
.ToList();
2020-12-10 10:28:01 +00:00
}
2021-11-18 15:35:45 +00:00
internal List<Result> InstallFromWeb(string url)
{
var filename = url.Split("/").Last();
var name = filename.Split(string.Format(".{0}", zip)).First();
2021-11-18 15:35:45 +00:00
var plugin = new UserPlugin
{
ID = "",
Name = name,
Version = string.Empty,
Author = Context.API.GetTranslation("plugin_pluginsmanager_unknown_author"),
2021-11-18 15:35:45 +00:00
UrlDownload = url
};
2021-11-18 15:35:45 +00:00
var result = new Result
{
Title = string.Format(Context.API.GetTranslation("plugin_pluginsmanager_install_from_web"), filename),
SubTitle = plugin.UrlDownload,
2021-11-18 16:40:06 +00:00
IcoPath = icoPath,
2021-11-18 15:35:45 +00:00
Action = e =>
{
if (e.SpecialKeyState.CtrlPressed)
{
SearchWeb.OpenInBrowserTab(plugin.UrlDownload);
2021-11-18 15:35:45 +00:00
return ShouldHideWindow;
}
if (Settings.WarnFromUnknownSource)
{
if (!InstallSourceKnown(plugin.UrlDownload)
&& MessageBox.Show(string.Format(Context.API.GetTranslation("plugin_pluginsmanager_install_unknown_source_warning"),
Environment.NewLine),
Context.API.GetTranslation("plugin_pluginsmanager_install_unknown_source_warning_title"),
MessageBoxButton.YesNo) == MessageBoxResult.No)
return false;
}
2021-11-18 15:35:45 +00:00
Application.Current.MainWindow.Hide();
2022-08-10 03:18:37 +00:00
_ = InstallOrUpdateAsync(plugin);
2021-11-18 15:35:45 +00:00
return ShouldHideWindow;
}
};
return new List<Result>
{
result
};
2021-11-18 15:35:45 +00:00
}
private bool InstallSourceKnown(string url)
{
var author = url.Split('/')[3];
var acceptedSource = "https://github.com";
var contructedUrlPart = string.Format("{0}/{1}/", acceptedSource, author);
return url.StartsWith(acceptedSource) && Context.API.GetAllPlugins().Any(x => x.Metadata.Website.StartsWith(contructedUrlPart));
}
internal async ValueTask<List<Result>> RequestInstallOrUpdate(string search, CancellationToken token)
2020-12-06 20:40:42 +00:00
{
await UpdateManifestAsync(token);
2021-01-17 10:46:08 +00:00
if (Uri.IsWellFormedUriString(search, UriKind.Absolute)
&& search.Split('.').Last() == zip)
return InstallFromWeb(search);
var results =
PluginsManifest
.UserPlugins
.Select(x =>
new Result
2020-12-06 20:40:42 +00:00
{
Title = $"{x.Name} by {x.Author}",
SubTitle = x.Description,
IcoPath = icoPath,
Action = e =>
{
if (e.SpecialKeyState.CtrlPressed)
{
SearchWeb.OpenInBrowserTab(x.Website);
return ShouldHideWindow;
}
Application.Current.MainWindow.Hide();
2022-08-10 03:18:37 +00:00
_ = InstallOrUpdateAsync(x); // No need to wait
return ShouldHideWindow;
},
ContextData = x
});
2020-12-06 20:40:42 +00:00
return Search(results, search);
2020-12-06 20:40:42 +00:00
}
private void Install(UserPlugin plugin, string downloadedFilePath)
{
2020-12-10 11:58:18 +00:00
if (!File.Exists(downloadedFilePath))
return;
2020-12-10 11:58:18 +00:00
var tempFolderPath = Path.Combine(Path.GetTempPath(), "flowlauncher");
var tempFolderPluginPath = Path.Combine(tempFolderPath, "plugin");
2020-12-10 11:58:18 +00:00
if (Directory.Exists(tempFolderPath))
Directory.Delete(tempFolderPath, true);
2020-12-10 11:58:18 +00:00
Directory.CreateDirectory(tempFolderPath);
2020-12-10 11:58:18 +00:00
var zipFilePath = Path.Combine(tempFolderPath, Path.GetFileName(downloadedFilePath));
File.Copy(downloadedFilePath, zipFilePath);
File.Delete(downloadedFilePath);
2020-12-10 11:58:18 +00:00
Utilities.UnZip(zipFilePath, tempFolderPluginPath, true);
2020-12-10 11:58:18 +00:00
var pluginFolderPath = Utilities.GetContainingFolderPathAfterUnzip(tempFolderPluginPath);
2020-12-10 11:58:18 +00:00
var metadataJsonFilePath = string.Empty;
if (File.Exists(Path.Combine(pluginFolderPath, Constant.PluginMetadataFileName)))
metadataJsonFilePath = Path.Combine(pluginFolderPath, Constant.PluginMetadataFileName);
2020-12-10 11:58:18 +00:00
if (string.IsNullOrEmpty(metadataJsonFilePath) || string.IsNullOrEmpty(pluginFolderPath))
{
MessageBox.Show(Context.API.GetTranslation("plugin_pluginsmanager_install_errormetadatafile"),
Context.API.GetTranslation("plugin_pluginsmanager_install_error_title"));
throw new FileNotFoundException(
string.Format("Unable to find plugin.json from the extracted zip file, or this path {0} does not exist", pluginFolderPath));
2020-12-10 11:58:18 +00:00
}
if (SameOrLesserPluginVersionExists(metadataJsonFilePath))
{
MessageBox.Show(string.Format(Context.API.GetTranslation("plugin_pluginsmanager_install_error_duplicate"), plugin.Name),
Context.API.GetTranslation("plugin_pluginsmanager_install_error_title"));
throw new InvalidOperationException(
string.Format("A plugin with the same ID and version already exists, " +
"or the version is greater than this downloaded plugin {0}",
plugin.Name));
}
var directory = string.IsNullOrEmpty(plugin.Version) ? $"{plugin.Name}-{Guid.NewGuid()}" : $"{plugin.Name}-{plugin.Version}";
var newPluginPath = Path.Combine(DataLocation.PluginsDirectory, directory);
FilesFolders.CopyAll(pluginFolderPath, newPluginPath);
Directory.Delete(pluginFolderPath, true);
}
2020-12-10 10:28:01 +00:00
internal List<Result> RequestUninstall(string search)
{
2020-12-20 09:08:52 +00:00
var results = Context.API
.GetAllPlugins()
.Select(x =>
new Result
{
Title = $"{x.Metadata.Name} by {x.Metadata.Author}",
SubTitle = x.Metadata.Description,
IcoPath = x.Metadata.IcoPath,
Action = e =>
{
string message = string.Format(
Context.API.GetTranslation("plugin_pluginsmanager_uninstall_prompt"),
x.Metadata.Name, x.Metadata.Author,
Environment.NewLine, Environment.NewLine);
if (MessageBox.Show(message,
Context.API.GetTranslation("plugin_pluginsmanager_uninstall_title"),
MessageBoxButton.YesNo) == MessageBoxResult.Yes)
{
Application.Current.MainWindow.Hide();
Uninstall(x.Metadata);
Context.API.RestartApp();
return true;
}
return false;
}
});
2020-12-10 10:28:01 +00:00
return Search(results, search);
2020-12-10 10:28:01 +00:00
}
2021-02-18 21:05:03 +00:00
private void Uninstall(PluginMetadata plugin, bool removedSetting = true)
2020-12-10 10:28:01 +00:00
{
2021-02-18 21:05:03 +00:00
if (removedSetting)
{
PluginManager.Settings.Plugins.Remove(plugin.ID);
PluginManager.AllPlugins.RemoveAll(p => p.Metadata.ID == plugin.ID);
}
2020-12-17 09:37:01 +00:00
// Marked for deletion. Will be deleted on next start up
using var _ = File.CreateText(Path.Combine(plugin.PluginDirectory, "NeedDelete.txt"));
2020-12-10 10:28:01 +00:00
}
2020-12-17 07:54:30 +00:00
private bool SameOrLesserPluginVersionExists(string metadataPath)
{
var newMetadata = JsonSerializer.Deserialize<PluginMetadata>(File.ReadAllText(metadataPath));
return Context.API.GetAllPlugins()
.Any(x => x.Metadata.ID == newMetadata.ID
&& newMetadata.Version.CompareTo(x.Metadata.Version) <= 0);
}
2020-12-06 08:58:27 +00:00
}
2022-08-10 03:18:37 +00:00
}