Plugin Store URLs
+diff --git a/src/api.go b/src/api.go
index 0991823..52f6499 100644
--- a/src/api.go
+++ b/src/api.go
@@ -234,6 +234,9 @@ func RegisterPluginAPIs(authRouter *auth.RouterDef) {
authRouter.HandleFunc("/api/plugins/groups/add", pluginManager.HandleAddPluginToGroup)
authRouter.HandleFunc("/api/plugins/groups/remove", pluginManager.HandleRemovePluginFromGroup)
authRouter.HandleFunc("/api/plugins/groups/deleteTag", pluginManager.HandleRemovePluginGroup)
+
+ authRouter.HandleFunc("/api/plugins/store/list", pluginManager.HandleListDownloadablePlugins)
+ authRouter.HandleFunc("/api/plugins/store/resync", pluginManager.HandleResyncPluginList)
}
// Register the APIs for Auth functions, due to scoping issue some functions are defined here
diff --git a/src/mod/plugins/handler.go b/src/mod/plugins/handler.go
index 8e95805..93930ab 100644
--- a/src/mod/plugins/handler.go
+++ b/src/mod/plugins/handler.go
@@ -249,3 +249,5 @@ func (m *Manager) HandleDisablePlugin(w http.ResponseWriter, r *http.Request) {
utils.SendOK(w)
}
+
+/* Plugin Store */
diff --git a/src/mod/plugins/plugins.go b/src/mod/plugins/plugins.go
index bb4c64b..5971ea6 100644
--- a/src/mod/plugins/plugins.go
+++ b/src/mod/plugins/plugins.go
@@ -82,7 +82,7 @@ func (m *Manager) LoadPluginsFromDisk() error {
m.Log("Loaded plugin: "+thisPlugin.Spec.Name, nil)
// If the plugin was enabled, start it now
- fmt.Println("Plugin enabled state", m.GetPluginPreviousEnableState(thisPlugin.Spec.ID))
+ //fmt.Println("Plugin enabled state", m.GetPluginPreviousEnableState(thisPlugin.Spec.ID))
if m.GetPluginPreviousEnableState(thisPlugin.Spec.ID) {
err = m.StartPlugin(thisPlugin.Spec.ID)
if err != nil {
diff --git a/src/mod/plugins/store.go b/src/mod/plugins/store.go
new file mode 100644
index 0000000..eff3fcd
--- /dev/null
+++ b/src/mod/plugins/store.go
@@ -0,0 +1,118 @@
+package plugins
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "strings"
+ "time"
+
+ "imuslab.com/zoraxy/mod/plugins/zoraxy_plugin"
+ "imuslab.com/zoraxy/mod/utils"
+)
+
+/*
+ Plugin Store
+*/
+
+// See https://github.com/aroz-online/zoraxy-official-plugins/blob/main/directories/index.json for the standard format
+
+type Checksums struct {
+ LinuxAmd64 string `json:"linux_amd64"`
+ Linux386 string `json:"linux_386"`
+ LinuxArm string `json:"linux_arm"`
+ LinuxArm64 string `json:"linux_arm64"`
+ LinuxMipsle string `json:"linux_mipsle"`
+ LinuxRiscv64 string `json:"linux_riscv64"`
+ WindowsAmd64 string `json:"windows_amd64"`
+}
+
+type DownloadablePlugin struct {
+ IconPath string
+ PluginIntroSpect zoraxy_plugin.IntroSpect //Plugin introspect information
+ ChecksumsSHA256 Checksums //Checksums for the plugin binary
+ DownloadURLs map[string]string //Download URLs for different platforms
+}
+
+/* Plugin Store Index List Sync */
+//Update the plugin list from the plugin store URLs
+func (m *Manager) UpdateDownloadablePluginList() error {
+ //Get downloadable plugins from each of the plugin store URLS
+ m.Options.DownloadablePluginCache = []*DownloadablePlugin{}
+ for _, url := range m.Options.PluginStoreURLs {
+ pluginList, err := m.getPluginListFromURL(url)
+ if err != nil {
+ return fmt.Errorf("failed to get plugin list from %s: %w", url, err)
+ }
+ m.Options.DownloadablePluginCache = append(m.Options.DownloadablePluginCache, pluginList...)
+ }
+
+ m.Options.LastSuccPluginSyncTime = time.Now().Unix()
+
+ return nil
+}
+
+// Get the plugin list from the URL
+func (m *Manager) getPluginListFromURL(url string) ([]*DownloadablePlugin, error) {
+ //Get the plugin list from the URL
+ resp, err := http.Get(url)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ return nil, fmt.Errorf("failed to get plugin list from %s: %s", url, resp.Status)
+ }
+
+ var pluginList []*DownloadablePlugin
+ content, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return nil, fmt.Errorf("failed to read plugin list from %s: %w", url, err)
+ }
+ content = []byte(strings.TrimSpace(string(content)))
+
+ err = json.Unmarshal(content, &pluginList)
+ if err != nil {
+ return nil, fmt.Errorf("failed to unmarshal plugin list from %s: %w", url, err)
+ }
+
+ return pluginList, nil
+}
+
+func (m *Manager) ListDownloadablePlugins() []*DownloadablePlugin {
+ //List all downloadable plugins
+ if len(m.Options.DownloadablePluginCache) == 0 {
+ return []*DownloadablePlugin{}
+ }
+ return m.Options.DownloadablePluginCache
+}
+
+/*
+ Handlers for Plugin Store
+*/
+
+func (m *Manager) HandleListDownloadablePlugins(w http.ResponseWriter, r *http.Request) {
+ //List all downloadable plugins
+ plugins := m.ListDownloadablePlugins()
+ js, _ := json.Marshal(plugins)
+ utils.SendJSONResponse(w, string(js))
+}
+
+// HandleResyncPluginList is the handler for resyncing the plugin list from the plugin store URLs
+func (m *Manager) HandleResyncPluginList(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodPost {
+ //Make sure this function require csrf token
+ utils.SendErrorResponse(w, "Method not allowed")
+ return
+ }
+
+ //Resync the plugin list from the plugin store URLs
+ err := m.UpdateDownloadablePluginList()
+ if err != nil {
+ utils.SendErrorResponse(w, "Failed to resync plugin list: "+err.Error())
+ return
+ }
+ utils.SendOK(w)
+}
diff --git a/src/mod/plugins/store_test.go b/src/mod/plugins/store_test.go
new file mode 100644
index 0000000..058648a
--- /dev/null
+++ b/src/mod/plugins/store_test.go
@@ -0,0 +1,52 @@
+package plugins
+
+import (
+ "testing"
+)
+
+func TestUpdateDownloadablePluginList(t *testing.T) {
+ mockManager := &Manager{
+ Options: &ManagerOptions{
+ DownloadablePluginCache: []*DownloadablePlugin{},
+ PluginStoreURLs: []string{},
+ },
+ }
+
+ //Inject a mock URL for testing
+ mockManager.Options.PluginStoreURLs = []string{"https://raw.githubusercontent.com/aroz-online/zoraxy-official-plugins/refs/heads/main/directories/index.json"}
+
+ err := mockManager.UpdateDownloadablePluginList()
+ if err != nil {
+ t.Fatalf("expected no error, got %v", err)
+ }
+
+ if len(mockManager.Options.DownloadablePluginCache) == 0 {
+ t.Fatalf("expected plugin cache to be updated, but it was empty")
+ }
+
+ if mockManager.Options.LastSuccPluginSyncTime == 0 {
+ t.Fatalf("expected LastSuccPluginSyncTime to be updated, but it was not")
+ }
+}
+
+func TestGetPluginListFromURL(t *testing.T) {
+ mockManager := &Manager{
+ Options: &ManagerOptions{
+ DownloadablePluginCache: []*DownloadablePlugin{},
+ PluginStoreURLs: []string{},
+ },
+ }
+
+ pluginList, err := mockManager.getPluginListFromURL("https://raw.githubusercontent.com/aroz-online/zoraxy-official-plugins/refs/heads/main/directories/index.json")
+ if err != nil {
+ t.Fatalf("expected no error, got %v", err)
+ }
+
+ if len(pluginList) == 0 {
+ t.Fatalf("expected plugin list to be populated, but it was empty")
+ }
+
+ for _, plugin := range pluginList {
+ t.Logf("Plugin: %+v", plugin)
+ }
+}
diff --git a/src/mod/plugins/typdef.go b/src/mod/plugins/typdef.go
index 991651c..23ad083 100644
--- a/src/mod/plugins/typdef.go
+++ b/src/mod/plugins/typdef.go
@@ -29,10 +29,16 @@ type Plugin struct {
}
type ManagerOptions struct {
+ /* Plugins */
PluginDir string //The directory where the plugins are stored
PluginGroups map[string][]string //The plugin groups,key is the tag name and the value is an array of plugin IDs
PluginGroupsConfig string //The group / tag configuration file, if set the plugin groups will be loaded from this file
+ /* Plugin Downloader */
+ PluginStoreURLs []string //The plugin store URLs, used to download the plugins
+ DownloadablePluginCache []*DownloadablePlugin //The cache for the downloadable plugins, key is the plugin ID and value is the DownloadablePlugin struct
+ LastSuccPluginSyncTime int64 //The last sync time for the plugin store URLs, used to check if the plugin store URLs need to be synced again
+
/* Runtime */
SystemConst *zoraxyPlugin.RuntimeConstantValue //The system constant value
CSRFTokenGen func(*http.Request) string `json:"-"` //The CSRF token generator function
diff --git a/src/start.go b/src/start.go
index 2f3c79e..83ed7f1 100644
--- a/src/start.go
+++ b/src/start.go
@@ -1,7 +1,6 @@
package main
import (
- "imuslab.com/zoraxy/mod/auth/sso/authentik"
"log"
"net/http"
"os"
@@ -10,6 +9,8 @@ import (
"strings"
"time"
+ "imuslab.com/zoraxy/mod/auth/sso/authentik"
+
"github.com/gorilla/csrf"
"imuslab.com/zoraxy/mod/access"
"imuslab.com/zoraxy/mod/acme"
@@ -322,6 +323,9 @@ func startupSequence() {
ZoraxyUUID: nodeUUID,
DevelopmentBuild: DEVELOPMENT_BUILD,
},
+ PluginStoreURLs: []string{
+ "https://raw.githubusercontent.com/aroz-online/zoraxy-official-plugins/refs/heads/main/directories/index.json",
+ },
Database: sysdb,
Logger: SystemWideLogger,
PluginGroupsConfig: CONF_PLUGIN_GROUPS,
@@ -330,9 +334,19 @@ func startupSequence() {
},
})
+ //Sync latest plugin list from the plugin store
+ go func() {
+ err = pluginManager.UpdateDownloadablePluginList()
+ if err != nil {
+ SystemWideLogger.PrintAndLog("plugin-manager", "Failed to sync plugin list from plugin store", err)
+ } else {
+ SystemWideLogger.PrintAndLog("plugin-manager", "Plugin list synced from plugin store", nil)
+ }
+ }()
+
err = pluginManager.LoadPluginsFromDisk()
if err != nil {
- SystemWideLogger.PrintAndLog("Plugin Manager", "Failed to load plugins", err)
+ SystemWideLogger.PrintAndLog("plugin-manager", "Failed to load plugins", err)
}
/* Docker UX Optimizer */
diff --git a/src/web/components/plugins.html b/src/web/components/plugins.html
index bf2d9ef..bae23ba 100644
--- a/src/web/components/plugins.html
+++ b/src/web/components/plugins.html
@@ -185,6 +185,8 @@
+
+
diff --git a/src/web/main.css b/src/web/main.css
index 713cb12..7359506 100644
--- a/src/web/main.css
+++ b/src/web/main.css
@@ -197,6 +197,12 @@ body.darkTheme .menubar{
max-width: calc(80% - 1em);
}
+@media screen and (max-width: 478px) {
+ .sideWrapper.extendedMode {
+ max-width: calc(100% - 1em);
+ }
+}
+
.sideWrapper .content{
height: 100%;
width: 100%;
diff --git a/src/web/snippet/pluginstore.html b/src/web/snippet/pluginstore.html
new file mode 100644
index 0000000..a7a4b91
--- /dev/null
+++ b/src/web/snippet/pluginstore.html
@@ -0,0 +1,258 @@
+
+
+