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
@@ -331,5 +333,156 @@ public static void ReplaceActionKeyword(string id, string oldActionKeyword, stri
331
333
RemoveActionKeyword ( id , oldActionKeyword ) ;
332
334
}
333
335
}
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
334
487
}
335
488
}
0 commit comments