11
11
using Flow . Launcher . Infrastructure . UserSettings ;
12
12
using Flow . Launcher . Plugin ;
13
13
using ISavable = Flow . Launcher . Plugin . ISavable ;
14
+ using Flow . Launcher . Plugin . SharedCommands ;
15
+ using System . Text . Json ;
14
16
15
17
namespace Flow . Launcher . Core . Plugin
16
18
{
@@ -27,9 +29,9 @@ public static class PluginManager
27
29
28
30
public static IPublicAPI API { private set ; get ; }
29
31
30
- // todo happlebao, this should not be public, the indicator function should be embeded
31
- public static PluginsSettings Settings ;
32
+ private static PluginsSettings Settings ;
32
33
private static List < PluginMetadata > _metadatas ;
34
+ private static List < string > _modifiedPlugins = new List < string > ( ) ;
33
35
34
36
/// <summary>
35
37
/// Directories that will hold Flow Launcher plugin directory
@@ -343,5 +345,156 @@ public static void ReplaceActionKeyword(string id, string oldActionKeyword, stri
343
345
RemoveActionKeyword ( id , oldActionKeyword ) ;
344
346
}
345
347
}
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
346
499
}
347
500
}
0 commit comments