Merge pull request #2520 from flooxo/local_install

Feature: Add support for local plugin installation
This commit is contained in:
Jeremy Wu 2024-05-20 08:44:47 +10:00 committed by GitHub
commit 1bbe0302fa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 156 additions and 35 deletions

View file

@ -1,4 +1,4 @@
using System; using System;
namespace Flow.Launcher.Core.ExternalPlugins namespace Flow.Launcher.Core.ExternalPlugins
{ {
@ -13,9 +13,11 @@ namespace Flow.Launcher.Core.ExternalPlugins
public string Website { get; set; } public string Website { get; set; }
public string UrlDownload { get; set; } public string UrlDownload { get; set; }
public string UrlSourceCode { get; set; } public string UrlSourceCode { get; set; }
public string LocalInstallPath { get; set; }
public string IcoPath { get; set; } public string IcoPath { get; set; }
public DateTime? LatestReleaseDate { get; set; } public DateTime? LatestReleaseDate { get; set; }
public DateTime? DateAdded { get; set; } public DateTime? DateAdded { get; set; }
public bool IsFromLocalInstallPath => !string.IsNullOrEmpty(LocalInstallPath);
} }
} }

View file

@ -380,7 +380,8 @@ namespace Flow.Launcher.Core.Plugin
/// <summary> /// <summary>
/// Update a plugin to new version, from a zip file. Will Delete zip after updating. /// Update a plugin to new version, from a zip file. By default will remove the zip file if update is via url,
/// unless it's a local path installation
/// </summary> /// </summary>
public static void UpdatePlugin(PluginMetadata existingVersion, UserPlugin newVersion, string zipFilePath) public static void UpdatePlugin(PluginMetadata existingVersion, UserPlugin newVersion, string zipFilePath)
{ {
@ -390,11 +391,11 @@ namespace Flow.Launcher.Core.Plugin
} }
/// <summary> /// <summary>
/// Install a plugin. Will Delete zip after updating. /// Install a plugin. By default will remove the zip file if installation is from url, unless it's a local path installation
/// </summary> /// </summary>
public static void InstallPlugin(UserPlugin plugin, string zipFilePath) public static void InstallPlugin(UserPlugin plugin, string zipFilePath)
{ {
InstallPlugin(plugin, zipFilePath, true); InstallPlugin(plugin, zipFilePath, checkModified: true);
} }
/// <summary> /// <summary>
@ -420,7 +421,9 @@ namespace Flow.Launcher.Core.Plugin
// Unzip plugin files to temp folder // Unzip plugin files to temp folder
var tempFolderPluginPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); var tempFolderPluginPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
System.IO.Compression.ZipFile.ExtractToDirectory(zipFilePath, tempFolderPluginPath); System.IO.Compression.ZipFile.ExtractToDirectory(zipFilePath, tempFolderPluginPath);
File.Delete(zipFilePath);
if(!plugin.IsFromLocalInstallPath)
File.Delete(zipFilePath);
var pluginFolderPath = GetContainingFolderPathAfterUnzip(tempFolderPluginPath); var pluginFolderPath = GetContainingFolderPathAfterUnzip(tempFolderPluginPath);

View file

@ -1,6 +1,7 @@
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq;
#pragma warning disable IDE0005 #pragma warning disable IDE0005
using System.Windows; using System.Windows;
#pragma warning restore IDE0005 #pragma warning restore IDE0005
@ -200,6 +201,24 @@ namespace Flow.Launcher.Plugin.SharedCommands
} }
} }
///<summary>
/// This checks whether a given string is a zip file path.
/// By default does not check if the zip file actually exist on disk, can do so by
/// setting checkFileExists = true.
///</summary>
public static bool IsZipFilePath(string querySearchString, bool checkFileExists = false)
{
if (IsLocationPathString(querySearchString) && querySearchString.Split('.').Last() == "zip")
{
if (checkFileExists)
return FileExists(querySearchString);
return true;
}
return false;
}
///<summary> ///<summary>
/// This checks whether a given string is a directory path or network location string. /// This checks whether a given string is a directory path or network location string.
/// It does not check if location actually exists. /// It does not check if location actually exists.

View file

@ -191,11 +191,15 @@ namespace Flow.Launcher.Test.Plugins
[TestCase(@"\c:\", false)] [TestCase(@"\c:\", false)]
[TestCase(@"cc:\", false)] [TestCase(@"cc:\", false)]
[TestCase(@"\\\SomeNetworkLocation\", false)] [TestCase(@"\\\SomeNetworkLocation\", false)]
[TestCase(@"\\SomeNetworkLocation\", true)]
[TestCase("RandomFile", false)] [TestCase("RandomFile", false)]
[TestCase(@"c:\>*", true)] [TestCase(@"c:\>*", true)]
[TestCase(@"c:\>", true)] [TestCase(@"c:\>", true)]
[TestCase(@"c:\SomeLocation\SomeOtherLocation\>", true)] [TestCase(@"c:\SomeLocation\SomeOtherLocation\>", true)]
[TestCase(@"c:\SomeLocation\SomeOtherLocation", true)] [TestCase(@"c:\SomeLocation\SomeOtherLocation", true)]
[TestCase(@"c:\SomeLocation\SomeOtherLocation\SomeFile.exe", true)]
[TestCase(@"\\SomeNetworkLocation\SomeFile.exe", true)]
public void WhenGivenQuerySearchString_ThenShouldIndicateIfIsLocationPathString(string querySearchString, bool expectedResult) public void WhenGivenQuerySearchString_ThenShouldIndicateIfIsLocationPathString(string querySearchString, bool expectedResult)
{ {
// When, Given // When, Given

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

View file

@ -26,7 +26,6 @@
<system:String x:Key="plugin_pluginsmanager_update_prompt">{0} by {1} {2}{3}Would you like to update this plugin? After the update Flow will automatically restart.</system:String> <system:String x:Key="plugin_pluginsmanager_update_prompt">{0} by {1} {2}{3}Would you like to update this plugin? After the update Flow will automatically restart.</system:String>
<system:String x:Key="plugin_pluginsmanager_update_prompt_no_restart">{0} by {1} {2}{2}Would you like to update this plugin?</system:String> <system:String x:Key="plugin_pluginsmanager_update_prompt_no_restart">{0} by {1} {2}{2}Would you like to update this plugin?</system:String>
<system:String x:Key="plugin_pluginsmanager_update_title">Plugin Update</system:String> <system:String x:Key="plugin_pluginsmanager_update_title">Plugin Update</system:String>
<system:String x:Key="plugin_pluginsmanager_update_exists">This plugin has an update, would you like to see it?</system:String>
<system:String x:Key="plugin_pluginsmanager_update_alreadyexists">This plugin is already installed</system:String> <system:String x:Key="plugin_pluginsmanager_update_alreadyexists">This plugin is already installed</system:String>
<system:String x:Key="plugin_pluginsmanager_update_failed_title">Plugin Manifest Download Failed</system:String> <system:String x:Key="plugin_pluginsmanager_update_failed_title">Plugin Manifest Download Failed</system:String>
<system:String x:Key="plugin_pluginsmanager_update_failed_subtitle">Please check if you can connect to github.com. This error means you may not be able to install or update plugins.</system:String> <system:String x:Key="plugin_pluginsmanager_update_failed_subtitle">Please check if you can connect to github.com. This error means you may not be able to install or update plugins.</system:String>

View file

@ -3,11 +3,9 @@ using Flow.Launcher.Core.Plugin;
using Flow.Launcher.Infrastructure; using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.Http; using Flow.Launcher.Infrastructure.Http;
using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin.SharedCommands; using Flow.Launcher.Plugin.SharedCommands;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net.Http; using System.Net.Http;
@ -99,13 +97,12 @@ namespace Flow.Launcher.Plugin.PluginsManager
if (Context.API.GetAllPlugins() if (Context.API.GetAllPlugins()
.Any(x => x.Metadata.ID == plugin.ID && x.Metadata.Version.CompareTo(plugin.Version) < 0)) .Any(x => x.Metadata.ID == plugin.ID && x.Metadata.Version.CompareTo(plugin.Version) < 0))
{ {
if (MessageBox.Show(Context.API.GetTranslation("plugin_pluginsmanager_update_exists"), var updateDetail = !plugin.IsFromLocalInstallPath ? plugin.Name : plugin.LocalInstallPath;
Context.API.GetTranslation("plugin_pluginsmanager_update_title"),
MessageBoxButton.YesNo) == MessageBoxResult.Yes) Context
Context .API
.API .ChangeQuery(
.ChangeQuery( $"{Context.CurrentPluginMetadata.ActionKeywords.FirstOrDefault()} {Settings.UpdateCommand} {updateDetail}");
$"{Context.CurrentPluginMetadata.ActionKeywords.FirstOrDefault()} {Settings.UpdateCommand} {plugin.Name}");
var mainWindow = Application.Current.MainWindow; var mainWindow = Application.Current.MainWindow;
mainWindow.Show(); mainWindow.Show();
@ -147,12 +144,17 @@ namespace Flow.Launcher.Plugin.PluginsManager
try try
{ {
if (File.Exists(filePath)) if (!plugin.IsFromLocalInstallPath)
{ {
File.Delete(filePath); if (File.Exists(filePath))
} File.Delete(filePath);
await Http.DownloadAsync(plugin.UrlDownload, filePath).ConfigureAwait(false); await Http.DownloadAsync(plugin.UrlDownload, filePath).ConfigureAwait(false);
}
else
{
filePath = plugin.LocalInstallPath;
}
Install(plugin, filePath); Install(plugin, filePath);
} }
@ -193,24 +195,38 @@ namespace Flow.Launcher.Plugin.PluginsManager
{ {
await PluginsManifest.UpdateManifestAsync(token, usePrimaryUrlOnly); await PluginsManifest.UpdateManifestAsync(token, usePrimaryUrlOnly);
var pluginFromLocalPath = null as UserPlugin;
var updateFromLocalPath = false;
if (FilesFolders.IsZipFilePath(search, checkFileExists: true))
{
pluginFromLocalPath = Utilities.GetPluginInfoFromZip(search);
pluginFromLocalPath.LocalInstallPath = search;
updateFromLocalPath = true;
}
var updateSource = !updateFromLocalPath
? PluginsManifest.UserPlugins
: new List<UserPlugin> { pluginFromLocalPath };
var resultsForUpdate = ( var resultsForUpdate = (
from existingPlugin in Context.API.GetAllPlugins() from existingPlugin in Context.API.GetAllPlugins()
join pluginFromManifest in PluginsManifest.UserPlugins join pluginUpdateSource in updateSource
on existingPlugin.Metadata.ID equals pluginFromManifest.ID on existingPlugin.Metadata.ID equals pluginUpdateSource.ID
where String.Compare(existingPlugin.Metadata.Version, pluginFromManifest.Version, where string.Compare(existingPlugin.Metadata.Version, pluginUpdateSource.Version,
StringComparison.InvariantCulture) < StringComparison.InvariantCulture) <
0 // if current version precedes manifest version 0 // if current version precedes version of the plugin from update source (e.g. PluginsManifest)
&& !PluginManager.PluginModified(existingPlugin.Metadata.ID) && !PluginManager.PluginModified(existingPlugin.Metadata.ID)
select select
new new
{ {
pluginFromManifest.Name, pluginUpdateSource.Name,
pluginFromManifest.Author, pluginUpdateSource.Author,
CurrentVersion = existingPlugin.Metadata.Version, CurrentVersion = existingPlugin.Metadata.Version,
NewVersion = pluginFromManifest.Version, NewVersion = pluginUpdateSource.Version,
existingPlugin.Metadata.IcoPath, existingPlugin.Metadata.IcoPath,
PluginExistingMetadata = existingPlugin.Metadata, PluginExistingMetadata = existingPlugin.Metadata,
PluginNewUserPlugin = pluginFromManifest PluginNewUserPlugin = pluginUpdateSource
}).ToList(); }).ToList();
if (!resultsForUpdate.Any()) if (!resultsForUpdate.Any())
@ -261,13 +277,21 @@ namespace Flow.Launcher.Plugin.PluginsManager
_ = Task.Run(async delegate _ = Task.Run(async delegate
{ {
if (File.Exists(downloadToFilePath)) if (!x.PluginNewUserPlugin.IsFromLocalInstallPath)
{ {
File.Delete(downloadToFilePath); if (File.Exists(downloadToFilePath))
} {
File.Delete(downloadToFilePath);
}
await Http.DownloadAsync(x.PluginNewUserPlugin.UrlDownload, downloadToFilePath) await Http.DownloadAsync(x.PluginNewUserPlugin.UrlDownload, downloadToFilePath)
.ConfigureAwait(false); .ConfigureAwait(false);
}
else
{
downloadToFilePath = x.PluginNewUserPlugin.LocalInstallPath;
}
PluginManager.UpdatePlugin(x.PluginExistingMetadata, x.PluginNewUserPlugin, PluginManager.UpdatePlugin(x.PluginExistingMetadata, x.PluginNewUserPlugin,
downloadToFilePath); downloadToFilePath);
@ -396,7 +420,7 @@ namespace Flow.Launcher.Plugin.PluginsManager
results = results.Prepend(updateAllResult); results = results.Prepend(updateAllResult);
} }
return Search(results, search); return !updateFromLocalPath ? Search(results, search) : results.ToList();
} }
internal bool PluginExists(string id) internal bool PluginExists(string id)
@ -470,6 +494,42 @@ namespace Flow.Launcher.Plugin.PluginsManager
return new List<Result> { result }; return new List<Result> { result };
} }
internal List<Result> InstallFromLocalPath(string localPath)
{
var plugin = Utilities.GetPluginInfoFromZip(localPath);
plugin.LocalInstallPath = localPath;
return new List<Result>
{
new Result
{
Title = $"{plugin.Name} by {plugin.Author}",
SubTitle = plugin.Description,
IcoPath = plugin.IcoPath,
Action = e =>
{
if (Settings.WarnFromUnknownSource)
{
if (!InstallSourceKnown(plugin.Website)
&& 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;
}
Application.Current.MainWindow.Hide();
_ = InstallOrUpdateAsync(plugin);
return ShouldHideWindow;
}
}
};
}
private bool InstallSourceKnown(string url) private bool InstallSourceKnown(string url)
{ {
var author = url.Split('/')[3]; var author = url.Split('/')[3];
@ -489,6 +549,9 @@ namespace Flow.Launcher.Plugin.PluginsManager
&& search.Split('.').Last() == zip) && search.Split('.').Last() == zip)
return InstallFromWeb(search); return InstallFromWeb(search);
if (FilesFolders.IsZipFilePath(search, checkFileExists: true))
return InstallFromLocalPath(search);
var results = var results =
PluginsManifest PluginsManifest
.UserPlugins .UserPlugins
@ -522,10 +585,13 @@ namespace Flow.Launcher.Plugin.PluginsManager
if (!File.Exists(downloadedFilePath)) if (!File.Exists(downloadedFilePath))
throw new FileNotFoundException($"Plugin {plugin.ID} zip file not found at {downloadedFilePath}", throw new FileNotFoundException($"Plugin {plugin.ID} zip file not found at {downloadedFilePath}",
downloadedFilePath); downloadedFilePath);
try try
{ {
PluginManager.InstallPlugin(plugin, downloadedFilePath); PluginManager.InstallPlugin(plugin, downloadedFilePath);
File.Delete(downloadedFilePath);
if (!plugin.IsFromLocalInstallPath)
File.Delete(downloadedFilePath);
} }
catch (FileNotFoundException e) catch (FileNotFoundException e)
{ {

View file

@ -1,5 +1,10 @@
using ICSharpCode.SharpZipLib.Zip; using Flow.Launcher.Core.ExternalPlugins;
using Flow.Launcher.Infrastructure.UserSettings;
using ICSharpCode.SharpZipLib.Zip;
using Newtonsoft.Json;
using System.IO; using System.IO;
using System.IO.Compression;
using System.Linq;
namespace Flow.Launcher.Plugin.PluginsManager namespace Flow.Launcher.Plugin.PluginsManager
{ {
@ -55,5 +60,28 @@ namespace Flow.Launcher.Plugin.PluginsManager
return string.Empty; return string.Empty;
} }
internal static UserPlugin GetPluginInfoFromZip(string filePath)
{
var plugin = null as UserPlugin;
using (ZipArchive archive = System.IO.Compression.ZipFile.OpenRead(filePath))
{
var pluginJsonPath = archive.Entries.FirstOrDefault(x => x.Name == "plugin.json").ToString();
ZipArchiveEntry pluginJsonEntry = archive.GetEntry(pluginJsonPath);
if (pluginJsonEntry != null)
{
using (StreamReader reader = new StreamReader(pluginJsonEntry.Open()))
{
string pluginJsonContent = reader.ReadToEnd();
plugin = JsonConvert.DeserializeObject<UserPlugin>(pluginJsonContent);
plugin.IcoPath = "Images\\zipfolder.png";
}
}
}
return plugin;
}
} }
} }