1111using Flow . Launcher . Infrastructure . UserSettings ;
1212using Flow . Launcher . Plugin ;
1313using ISavable = Flow . Launcher . Plugin . ISavable ;
14+ using Flow . Launcher . Plugin . SharedCommands ;
15+ using System . Text . Json ;
1416
1517namespace Flow . Launcher . Core . Plugin
1618{
@@ -27,9 +29,9 @@ public static class PluginManager
2729
2830 public static IPublicAPI API { private set ; get ; }
2931
30- // todo happlebao, this should not be public, the indicator function should be embeded
31- public static PluginsSettings Settings ;
32+ private static PluginsSettings Settings ;
3233 private static List < PluginMetadata > _metadatas ;
34+ private static List < string > _modifiedPlugins = new List < string > ( ) ;
3335
3436 /// <summary>
3537 /// Directories that will hold Flow Launcher plugin directory
@@ -331,5 +333,156 @@ public static void ReplaceActionKeyword(string id, string oldActionKeyword, stri
331333 RemoveActionKeyword ( id , oldActionKeyword ) ;
332334 }
333335 }
336+
337+ private static string GetContainingFolderPathAfterUnzip ( string unzippedParentFolderPath )
338+ {
339+ var unzippedFolderCount = Directory . GetDirectories ( unzippedParentFolderPath ) . Length ;
340+ var unzippedFilesCount = Directory . GetFiles ( unzippedParentFolderPath ) . Length ;
341+
342+ // adjust path depending on how the plugin is zipped up
343+ // the recommended should be to zip up the folder not the contents
344+ if ( unzippedFolderCount == 1 && unzippedFilesCount == 0 )
345+ // folder is zipped up, unzipped plugin directory structure: tempPath/unzippedParentPluginFolder/pluginFolderName/
346+ return Directory . GetDirectories ( unzippedParentFolderPath ) [ 0 ] ;
347+
348+ if ( unzippedFilesCount > 1 )
349+ // content is zipped up, unzipped plugin directory structure: tempPath/unzippedParentPluginFolder/
350+ return unzippedParentFolderPath ;
351+
352+ return string . Empty ;
353+ }
354+
355+ private static bool SameOrLesserPluginVersionExists ( string metadataPath )
356+ {
357+ var newMetadata = JsonSerializer . Deserialize < PluginMetadata > ( File . ReadAllText ( metadataPath ) ) ;
358+ return AllPlugins . Any ( x => x . Metadata . ID == newMetadata . ID
359+ && newMetadata . Version . CompareTo ( x . Metadata . Version ) <= 0 ) ;
360+ }
361+
362+ #region Public functions
363+
364+ public static bool PluginModified ( string uuid )
365+ {
366+ return _modifiedPlugins . Contains ( uuid ) ;
367+ }
368+
369+
370+ /// <summary>
371+ /// Update a plugin to new version, from a zip file. Will Delete zip after updating.
372+ /// </summary>
373+ public static void UpdatePlugin ( PluginMetadata existingVersion , UserPlugin newVersion , string zipFilePath )
374+ {
375+ InstallPlugin ( newVersion , zipFilePath , checkModified : false ) ;
376+ UninstallPlugin ( existingVersion , removeSettings : false , checkModified : false ) ;
377+ _modifiedPlugins . Add ( existingVersion . ID ) ;
378+ }
379+
380+ /// <summary>
381+ /// Install a plugin. Will Delete zip after updating.
382+ /// </summary>
383+ public static void InstallPlugin ( UserPlugin plugin , string zipFilePath )
384+ {
385+ InstallPlugin ( plugin , zipFilePath , true ) ;
386+ }
387+
388+ /// <summary>
389+ /// Uninstall a plugin.
390+ /// </summary>
391+ public static void UninstallPlugin ( PluginMetadata plugin , bool removeSettings = true )
392+ {
393+ UninstallPlugin ( plugin , removeSettings , true ) ;
394+ }
395+
396+ #endregion
397+
398+ #region Internal functions
399+
400+ internal static void InstallPlugin ( UserPlugin plugin , string zipFilePath , bool checkModified )
401+ {
402+ if ( checkModified && PluginModified ( plugin . ID ) )
403+ {
404+ // Distinguish exception from installing same or less version
405+ throw new ArgumentException ( $ "Plugin { plugin . Name } { plugin . ID } has been modified.", nameof ( plugin ) ) ;
406+ }
407+
408+ // Unzip plugin files to temp folder
409+ var tempFolderPluginPath = Path . Combine ( Path . GetTempPath ( ) , Guid . NewGuid ( ) . ToString ( ) ) ;
410+ System . IO . Compression . ZipFile . ExtractToDirectory ( zipFilePath , tempFolderPluginPath ) ;
411+ File . Delete ( zipFilePath ) ;
412+
413+ var pluginFolderPath = GetContainingFolderPathAfterUnzip ( tempFolderPluginPath ) ;
414+
415+ var metadataJsonFilePath = string . Empty ;
416+ if ( File . Exists ( Path . Combine ( pluginFolderPath , Constant . PluginMetadataFileName ) ) )
417+ metadataJsonFilePath = Path . Combine ( pluginFolderPath , Constant . PluginMetadataFileName ) ;
418+
419+ if ( string . IsNullOrEmpty ( metadataJsonFilePath ) || string . IsNullOrEmpty ( pluginFolderPath ) )
420+ {
421+ throw new FileNotFoundException ( $ "Unable to find plugin.json from the extracted zip file, or this path { pluginFolderPath } does not exist") ;
422+ }
423+
424+ if ( SameOrLesserPluginVersionExists ( metadataJsonFilePath ) )
425+ {
426+ throw new InvalidOperationException ( $ "A plugin with the same ID and version already exists, or the version is greater than this downloaded plugin { plugin . Name } ") ;
427+ }
428+
429+ var folderName = string . IsNullOrEmpty ( plugin . Version ) ? $ "{ plugin . Name } -{ Guid . NewGuid ( ) } " : $ "{ plugin . Name } -{ plugin . Version } ";
430+
431+ var defaultPluginIDs = new List < string >
432+ {
433+ "0ECADE17459B49F587BF81DC3A125110" , // BrowserBookmark
434+ "CEA0FDFC6D3B4085823D60DC76F28855" , // Calculator
435+ "572be03c74c642baae319fc283e561a8" , // Explorer
436+ "6A122269676E40EB86EB543B945932B9" , // PluginIndicator
437+ "9f8f9b14-2518-4907-b211-35ab6290dee7" , // PluginsManager
438+ "b64d0a79-329a-48b0-b53f-d658318a1bf6" , // ProcessKiller
439+ "791FC278BA414111B8D1886DFE447410" , // Program
440+ "D409510CD0D2481F853690A07E6DC426" , // Shell
441+ "CEA08895D2544B019B2E9C5009600DF4" , // Sys
442+ "0308FD86DE0A4DEE8D62B9B535370992" , // URL
443+ "565B73353DBF4806919830B9202EE3BF" , // WebSearch
444+ "5043CETYU6A748679OPA02D27D99677A" // WindowsSettings
445+ } ;
446+
447+ // Treat default plugin differently, it needs to be removable along with each flow release
448+ var installDirectory = ! defaultPluginIDs . Any ( x => x == plugin . ID )
449+ ? DataLocation . PluginsDirectory
450+ : Constant . PreinstalledDirectory ;
451+
452+ var newPluginPath = Path . Combine ( installDirectory , folderName ) ;
453+
454+ FilesFolders . CopyAll ( pluginFolderPath , newPluginPath ) ;
455+
456+ Directory . Delete ( tempFolderPluginPath , true ) ;
457+
458+ if ( checkModified )
459+ {
460+ _modifiedPlugins . Add ( plugin . ID ) ;
461+ }
462+ }
463+
464+ internal static void UninstallPlugin ( PluginMetadata plugin , bool removeSettings , bool checkModified )
465+ {
466+ if ( checkModified && PluginModified ( plugin . ID ) )
467+ {
468+ throw new ArgumentException ( $ "Plugin { plugin . Name } has been modified") ;
469+ }
470+
471+ if ( removeSettings )
472+ {
473+ Settings . Plugins . Remove ( plugin . ID ) ;
474+ AllPlugins . RemoveAll ( p => p . Metadata . ID == plugin . ID ) ;
475+ }
476+
477+ // Marked for deletion. Will be deleted on next start up
478+ using var _ = File . CreateText ( Path . Combine ( plugin . PluginDirectory , "NeedDelete.txt" ) ) ;
479+
480+ if ( checkModified )
481+ {
482+ _modifiedPlugins . Add ( plugin . ID ) ;
483+ }
484+ }
485+
486+ #endregion
334487 }
335488}
0 commit comments