From d2e37617d4f93ab46c49f0e5eeb80b5d48b9032d Mon Sep 17 00:00:00 2001 From: Zoe <62722391+juls0730@users.noreply.github.com> Date: Tue, 7 Jan 2025 20:09:56 +0000 Subject: [PATCH] fix ub and add deleting plugins --- DOCUMENTATION.md | 10 +++++- README.md | 2 +- gloomi/main.go | 12 +++++++ main.go | 86 +++++++++++++++++++++++++++++++++++++++++++++++- schema.sql | 2 +- 5 files changed, 108 insertions(+), 4 deletions(-) diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index 67c9703..f03d373 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -12,6 +12,12 @@ The `Plugin` interface is the main interface for plugins to implement. It has th - `Name()` - This method returns the name of the plugin. - `RegisterRoutes(router fiber.Router)` - This method is called when the plugin is loaded and is responsible for registering routes to the router. +Furthermore, your plugin should export a symbol named `Plugin` that implements the `Plugin` interface. The easiest way to do this in Go is simply + +```go +var Plugin MyPluginDataStruct +``` + An example plugin is provided in the `plugin` directory and can be built using the following command: ```bash @@ -22,10 +28,11 @@ This will generate a `plugin.so` file in the `plugin` directory. ## RPC -GLoom exposes an RPC server on port 7143. This server is used for GLoom's plugin management system. Gloom currently provides two methods: +GLoom exposes an RPC server on port 7143. This server is used for GLoom's plugin management system. Gloom currently provides three methods: - `ListPlugins(struct{}, reply *[]PluginData) error` - This method returns a list of all registered plugins and their domains. - `UploadPlugin(PluginUpload, reply *string) error` - This method uploads a plugin to GLoom. +- `DeletePlugin(pluginName string, reply *string) error` - This method deletes a plugin from GLoom. PluginData is a struct that looks like this: @@ -54,3 +61,4 @@ GLoomI is the included plugin management interface for GLoom, it utilizes the GL - `POST /api/plugins` - This endpoint uploads a plugin to GLoom. it takes a multipart/form-data request with the following fields: - `plugin` - The plugin file to upload. - `domains` - A comma-separated list of domains to associate with the plugin. +- `DELETE /api/plugins/:pluginName` - This endpoint deletes a plugin from GLoom. `pluginName` is the string returned by the `Name()` method of the plugin and is case-sensitive. diff --git a/README.md b/README.md index 2df9d4a..4eee454 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # GLoom -GLoom is a plugin-based web server written in Go. GLoom's focus is to provide and simple and efficient way to host micro-web apps easily. +GLoom is a plugin-based web app manager written in Go. GLoom's focus is to provide and simple and efficient way to host micro-web apps easily. ## Features diff --git a/gloomi/main.go b/gloomi/main.go index 2b60da1..39e9b79 100644 --- a/gloomi/main.go +++ b/gloomi/main.go @@ -101,6 +101,18 @@ func (p *GLoomI) RegisterRoutes(router fiber.Router) { return c.Status(fiber.StatusOK).SendString("Plugin uploaded successfully") }) + + apiRouter.Delete("/plugins/:pluginName", func(c fiber.Ctx) error { + pluginName := c.Params("pluginName") + var response string + err := p.client.Call("GloomRPC.DeletePlugin", pluginName, &response) + if err != nil { + return c.Status(fiber.StatusInternalServerError).SendString("Failed to list plugins: " + err.Error()) + } + + c.Status(fiber.StatusOK).SendString(response) + return nil + }) } } diff --git a/main.go b/main.go index 274ab4e..caa75db 100644 --- a/main.go +++ b/main.go @@ -30,6 +30,7 @@ type Plugin interface { type PluginInstance struct { Plugin Plugin Name string + Path string Router *fiber.App } @@ -121,6 +122,7 @@ func (gloom *GLoom) RegisterPlugin(pluginPath string, domains []string) { pluginInstance := PluginInstance{ Plugin: pluginLib, Name: pluginLib.Name(), + Path: pluginPath, Router: router, } @@ -131,6 +133,15 @@ func (gloom *GLoom) RegisterPlugin(pluginPath string, domains []string) { } } +func (gloom *GLoom) DeletePlugin(pluginName string) { + gloom.domainMap.Range(func(domain string, plugin *PluginInstance) bool { + if plugin.Name == pluginName { + gloom.domainMap.Delete(domain) + } + return true + }) +} + func (gloom *GLoom) StartRPCServer() error { rpcServer := &GloomRPC{gloom: gloom} err := rpc.Register(rpcServer) @@ -197,11 +208,57 @@ type PluginUpload struct { func (rpc *GloomRPC) UploadPlugin(plugin PluginUpload, reply *string) error { slog.Info("Uploading plugin", "plugin", plugin.Name, "domains", plugin.Domains) + var plugExists bool + rpc.gloom.DB.QueryRow("SELECT path FROM plugins WHERE path = ?", "plugs/"+plugin.Name).Scan(&plugExists) + + var domains []string + if plugExists { + // if plugin exists, we need to not check for domains that this plug has already registered, but instead check for new domains this plugin is registering + domains = make([]string, 0) + var existingDomains []string + err := rpc.gloom.DB.QueryRow("SELECT domains FROM plugins WHERE path = ?", "plugs/"+plugin.Name).Scan(&existingDomains) + if err != nil { + return err + } + + for _, domain := range existingDomains { + var found bool + for _, domainToCheck := range plugin.Domains { + if domain == domainToCheck { + found = true + break + } + } + if !found { + domains = append(domains, domain) + } + + found = false + } + } else { + domains = plugin.Domains + } + + for _, domain := range domains { + _, ok := rpc.gloom.domainMap.Load(domain) + if ok { + *reply = fmt.Sprintf("Domain %s already exists", domain) + return nil + } + } + + // regardless of if plugin exists or not, we'll upload the file since this could be an update to an existing plugin if err := os.WriteFile(fmt.Sprintf("plugs/%s", plugin.Name), plugin.Data, 0644); err != nil { return err } - fmt.Print("Plugin uploaded successfully") + fmt.Println("Plugin uploaded successfully") + + if plugExists { + // exit out early otherwise we risk creating multiple of the same plugin and causing undefined behavior + *reply = "Plugin updated successfully" + return nil + } rpc.gloom.DB.Exec("INSERT INTO plugins (path, domains) VALUES (?, ?)", "plugs/"+plugin.Name, strings.Join(plugin.Domains, ",")) rpc.gloom.RegisterPlugin("plugs/"+plugin.Name, plugin.Domains) @@ -209,6 +266,33 @@ func (rpc *GloomRPC) UploadPlugin(plugin PluginUpload, reply *string) error { return nil } +func (rpc *GloomRPC) DeletePlugin(pluginName string, reply *string) error { + var targetPlugin PluginInstance + for _, plugin := range rpc.gloom.Plugins { + if plugin.Name == pluginName { + targetPlugin = plugin + break + } + } + + _, err := rpc.gloom.DB.Exec("DELETE FROM plugins WHERE path = ?", targetPlugin.Path) + if err != nil { + *reply = "Plugin not found" + return err + } + + err = os.Remove(targetPlugin.Path) + if err != nil { + *reply = "Plugin not found" + return err + } + + rpc.gloom.DeletePlugin(pluginName) + + *reply = "Plugin deleted successfully" + return nil +} + func init() { if err := godotenv.Load(); err != nil { fmt.Println("No .env file found") diff --git a/schema.sql b/schema.sql index daca7c6..3633ece 100644 --- a/schema.sql +++ b/schema.sql @@ -1,5 +1,5 @@ CREATE TABLE IF NOT EXISTS plugins ( id INTEGER PRIMARY KEY AUTOINCREMENT, - path TEXT NOT NULL, + path TEXT NOT NULL UNIQUE, domains TEXT NOT NULL );