mirror of
https://github.com/tobychui/zoraxy.git
synced 2025-08-08 06:08:30 +02:00
Added working plugin store prototype
- Added plugin install and remove api
This commit is contained in:
@@ -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()
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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)
|
||||
}
|
||||
|
Reference in New Issue
Block a user