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