diff --git a/src/api.go b/src/api.go
index 52f6499..1caedeb 100644
--- a/src/api.go
+++ b/src/api.go
@@ -237,6 +237,8 @@ func RegisterPluginAPIs(authRouter *auth.RouterDef) {
authRouter.HandleFunc("/api/plugins/store/list", pluginManager.HandleListDownloadablePlugins)
authRouter.HandleFunc("/api/plugins/store/resync", pluginManager.HandleResyncPluginList)
+ authRouter.HandleFunc("/api/plugins/store/install", pluginManager.HandleInstallPlugin)
+ authRouter.HandleFunc("/api/plugins/store/uninstall", pluginManager.HandleUninstallPlugin)
}
// Register the APIs for Auth functions, due to scoping issue some functions are defined here
diff --git a/src/mod/plugins/lifecycle.go b/src/mod/plugins/lifecycle.go
index e7b2161..914acf7 100644
--- a/src/mod/plugins/lifecycle.go
+++ b/src/mod/plugins/lifecycle.go
@@ -274,13 +274,10 @@ func (m *Manager) StopPlugin(pluginID string) error {
}
// Check if the plugin is still running
-func (m *Manager) PluginStillRunning(pluginID string) bool {
+func (m *Manager) PluginIsRunning(pluginID string) bool {
plugin, err := m.GetPluginByID(pluginID)
if err != nil {
return false
}
- if plugin.process == nil {
- return false
- }
- return plugin.process.ProcessState == nil
+ return plugin.IsRunning()
}
diff --git a/src/mod/plugins/plugins.go b/src/mod/plugins/plugins.go
index 5971ea6..039eaa1 100644
--- a/src/mod/plugins/plugins.go
+++ b/src/mod/plugins/plugins.go
@@ -58,6 +58,59 @@ func NewPluginManager(options *ManagerOptions) *Manager {
}
}
+// Reload all plugins from disk
+func (m *Manager) ReloadPluginFromDisk() {
+ //Check each of the current plugins if the directory exists
+ //If not, remove the plugin from the loaded plugins list
+ m.loadedPluginsMutex.Lock()
+ for pluginID, plugin := range m.LoadedPlugins {
+ if !utils.FileExists(plugin.RootDir) {
+ m.Log("Plugin directory not found, removing plugin from runtime: "+pluginID, nil)
+ delete(m.LoadedPlugins, pluginID)
+ //Remove the plugin enable state from the database
+ m.Options.Database.Delete("plugins", pluginID)
+ }
+ }
+
+ m.loadedPluginsMutex.Unlock()
+
+ //Scan the plugin directory for new plugins
+ foldersInPluginDir, err := os.ReadDir(m.Options.PluginDir)
+ if err != nil {
+ m.Log("Failed to read plugin directory", err)
+ return
+ }
+
+ for _, folder := range foldersInPluginDir {
+ if folder.IsDir() {
+ pluginPath := filepath.Join(m.Options.PluginDir, folder.Name())
+ thisPlugin, err := m.LoadPluginSpec(pluginPath)
+ if err != nil {
+ m.Log("Failed to load plugin: "+filepath.Base(pluginPath), err)
+ continue
+ }
+
+ //Check if the plugin id is already loaded into the runtime
+ m.loadedPluginsMutex.RLock()
+ _, ok := m.LoadedPlugins[thisPlugin.Spec.ID]
+ m.loadedPluginsMutex.RUnlock()
+ if ok {
+ //Plugin already loaded, skip it
+ continue
+ }
+
+ thisPlugin.RootDir = filepath.ToSlash(pluginPath)
+ thisPlugin.staticRouteProxy = make(map[string]*dpcore.ReverseProxy)
+ m.loadedPluginsMutex.Lock()
+ m.LoadedPlugins[thisPlugin.Spec.ID] = thisPlugin
+ m.loadedPluginsMutex.Unlock()
+ m.Log("Added new plugin: "+thisPlugin.Spec.Name, nil)
+
+ // The default state of the plugin is disabled, so no need to start it
+ }
+ }
+}
+
// LoadPluginsFromDisk loads all plugins from the plugin directory
func (m *Manager) LoadPluginsFromDisk() error {
// Load all plugins from the plugin directory
@@ -258,3 +311,8 @@ func (p *Plugin) HandleStaticRoute(w http.ResponseWriter, r *http.Request, longe
})
}
+
+// IsRunning checks if the plugin is currently running
+func (p *Plugin) IsRunning() bool {
+ return p.process != nil && p.process.Process != nil
+}
diff --git a/src/mod/plugins/store.go b/src/mod/plugins/store.go
index eff3fcd..fc17833 100644
--- a/src/mod/plugins/store.go
+++ b/src/mod/plugins/store.go
@@ -1,10 +1,14 @@
package plugins
import (
+ "crypto/sha256"
"encoding/json"
"fmt"
"io"
"net/http"
+ "os"
+ "path/filepath"
+ "runtime"
"strings"
"time"
@@ -89,6 +93,180 @@ func (m *Manager) ListDownloadablePlugins() []*DownloadablePlugin {
return m.Options.DownloadablePluginCache
}
+// InstallPlugin installs the given plugin by moving it to the PluginDir.
+func (m *Manager) InstallPlugin(plugin *DownloadablePlugin) error {
+ pluginDir := filepath.Join(m.Options.PluginDir, plugin.PluginIntroSpect.Name)
+ pluginFile := plugin.PluginIntroSpect.Name
+ if runtime.GOOS == "windows" {
+ pluginFile += ".exe"
+ }
+
+ //Check if the plugin id already exists in runtime plugin map
+ if _, ok := m.LoadedPlugins[plugin.PluginIntroSpect.ID]; ok {
+ return fmt.Errorf("plugin already installed: %s", plugin.PluginIntroSpect.ID)
+ }
+
+ // Create the plugin directory if it doesn't exist
+ err := os.MkdirAll(pluginDir, os.ModePerm)
+ if err != nil {
+ return fmt.Errorf("failed to create plugin directory: %w", err)
+ }
+
+ // Download the plugin binary
+ downloadURL, ok := plugin.DownloadURLs[runtime.GOOS+"_"+runtime.GOARCH]
+ if !ok {
+ return fmt.Errorf("no download URL available for the current platform")
+ }
+
+ resp, err := http.Get(downloadURL)
+ if err != nil {
+ return fmt.Errorf("failed to download plugin: %w", err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ return fmt.Errorf("failed to download plugin: %s", resp.Status)
+ }
+
+ // Write the plugin binary to the plugin directory
+ pluginPath := filepath.Join(pluginDir, pluginFile)
+ out, err := os.Create(pluginPath)
+ if err != nil {
+ return fmt.Errorf("failed to create plugin file: %w", err)
+ }
+
+ _, err = io.Copy(out, resp.Body)
+ if err != nil {
+ out.Close()
+ return fmt.Errorf("failed to write plugin file: %w", err)
+ }
+
+ // Make the plugin executable
+ err = os.Chmod(pluginPath, 0755)
+ if err != nil {
+ out.Close()
+ return fmt.Errorf("failed to set executable permissions: %w", err)
+ }
+
+ // Verify the checksum of the downloaded plugin binary
+ checksums, err := plugin.ChecksumsSHA256.GetCurrentPlatformChecksum()
+ if err == nil {
+ if !verifyChecksumForFile(pluginPath, checksums) {
+ out.Close()
+ return fmt.Errorf("checksum verification failed for plugin binary")
+ }
+ }
+
+ //Ok, also download the icon if exists
+ if plugin.IconPath != "" {
+ iconURL := strings.TrimSpace(plugin.IconPath)
+ if iconURL != "" {
+ resp, err := http.Get(iconURL)
+ if err != nil {
+ return fmt.Errorf("failed to download plugin icon: %w", err)
+ }
+ defer resp.Body.Close()
+
+ //Save the icon to the plugin directory
+ iconPath := filepath.Join(pluginDir, "icon.png")
+ out, err := os.Create(iconPath)
+ if err != nil {
+ return fmt.Errorf("failed to create plugin icon file: %w", err)
+ }
+ defer out.Close()
+
+ io.Copy(out, resp.Body)
+ }
+ }
+ //Close the plugin exeutable
+ out.Close()
+
+ //Reload the plugin list
+ m.ReloadPluginFromDisk()
+
+ return nil
+}
+
+// UninstallPlugin uninstalls the plugin by removing its directory.
+func (m *Manager) UninstallPlugin(pluginID string) error {
+
+ //Stop the plugin process if it's running
+ plugin, ok := m.LoadedPlugins[pluginID]
+ if !ok {
+ return fmt.Errorf("plugin not found: %s", pluginID)
+ }
+
+ if plugin.IsRunning() {
+ err := m.StopPlugin(plugin.Spec.ID)
+ if err != nil {
+ return fmt.Errorf("failed to stop plugin: %w", err)
+ }
+ }
+
+ //Make sure the plugin process is stopped
+ m.Options.Logger.PrintAndLog("plugin-manager", "Removing plugin in 3 seconds...", nil)
+ time.Sleep(3 * time.Second)
+
+ // Remove the plugin directory
+ err := os.RemoveAll(plugin.RootDir)
+ if err != nil {
+ return fmt.Errorf("failed to remove plugin directory: %w", err)
+ }
+
+ //Reload the plugin list
+ m.ReloadPluginFromDisk()
+ return nil
+}
+
+// GetCurrentPlatformChecksum returns the checksum for the current platform
+func (c *Checksums) GetCurrentPlatformChecksum() (string, error) {
+ switch runtime.GOOS {
+ case "linux":
+ switch runtime.GOARCH {
+ case "amd64":
+ return c.LinuxAmd64, nil
+ case "386":
+ return c.Linux386, nil
+ case "arm":
+ return c.LinuxArm, nil
+ case "arm64":
+ return c.LinuxArm64, nil
+ case "mipsle":
+ return c.LinuxMipsle, nil
+ case "riscv64":
+ return c.LinuxRiscv64, nil
+ default:
+ return "", fmt.Errorf("unsupported architecture: %s", runtime.GOARCH)
+ }
+ case "windows":
+ switch runtime.GOARCH {
+ case "amd64":
+ return c.WindowsAmd64, nil
+ default:
+ return "", fmt.Errorf("unsupported architecture: %s", runtime.GOARCH)
+ }
+ default:
+ return "", fmt.Errorf("unsupported operating system: %s", runtime.GOOS)
+ }
+}
+
+// VerifyChecksum verifies the checksum of the downloaded plugin binary.
+func verifyChecksumForFile(filePath string, checksum string) bool {
+ file, err := os.Open(filePath)
+ if err != nil {
+ return false
+ }
+ defer file.Close()
+
+ hash := sha256.New()
+ if _, err := io.Copy(hash, file); err != nil {
+ return false
+ }
+ calculatedChecksum := fmt.Sprintf("%x", hash.Sum(nil))
+
+ return calculatedChecksum == checksum
+}
+
/*
Handlers for Plugin Store
*/
@@ -116,3 +294,63 @@ func (m *Manager) HandleResyncPluginList(w http.ResponseWriter, r *http.Request)
}
utils.SendOK(w)
}
+
+// HandleInstallPlugin is the handler for installing a plugin
+func (m *Manager) HandleInstallPlugin(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodPost {
+ utils.SendErrorResponse(w, "Method not allowed")
+ return
+ }
+
+ pluginID, err := utils.PostPara(r, "pluginID")
+ if err != nil {
+ utils.SendErrorResponse(w, "pluginID is required")
+ return
+ }
+
+ // Find the plugin info from cache
+ var plugin *DownloadablePlugin
+ for _, p := range m.Options.DownloadablePluginCache {
+ if p.PluginIntroSpect.ID == pluginID {
+ plugin = p
+ break
+ }
+ }
+
+ if plugin == nil {
+ utils.SendErrorResponse(w, "Plugin not found")
+ return
+ }
+
+ // Install the plugin (implementation depends on your system)
+ err = m.InstallPlugin(plugin)
+ if err != nil {
+ utils.SendErrorResponse(w, "Failed to install plugin: "+err.Error())
+ return
+ }
+
+ utils.SendOK(w)
+}
+
+// HandleUninstallPlugin is the handler for uninstalling a plugin
+func (m *Manager) HandleUninstallPlugin(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodPost {
+ utils.SendErrorResponse(w, "Method not allowed")
+ return
+ }
+
+ pluginID, err := utils.PostPara(r, "pluginID")
+ if err != nil {
+ utils.SendErrorResponse(w, "pluginID is required")
+ return
+ }
+
+ // Uninstall the plugin (implementation depends on your system)
+ err = m.UninstallPlugin(pluginID)
+ if err != nil {
+ utils.SendErrorResponse(w, "Failed to uninstall plugin: "+err.Error())
+ return
+ }
+
+ utils.SendOK(w)
+}
diff --git a/src/web/components/plugins.html b/src/web/components/plugins.html
index bae23ba..9592723 100644
--- a/src/web/components/plugins.html
+++ b/src/web/components/plugins.html
@@ -186,7 +186,7 @@
-
+
diff --git a/src/web/snippet/pluginstore.html b/src/web/snippet/pluginstore.html
index a7a4b91..3f479d4 100644
--- a/src/web/snippet/pluginstore.html
+++ b/src/web/snippet/pluginstore.html
@@ -43,6 +43,10 @@
${plugin.PluginIntroSpect.description}