diff --git a/kernel/api/bazaar.go b/kernel/api/bazaar.go index 630c2f7d3..62087f010 100644 --- a/kernel/api/bazaar.go +++ b/kernel/api/bazaar.go @@ -41,6 +41,71 @@ func getBazaarPackageREAME(c *gin.Context) { } } +func getBazaarPlugin(c *gin.Context) { + ret := gulu.Ret.NewResult() + defer c.JSON(http.StatusOK, ret) + + ret.Data = map[string]interface{}{ + "packages": model.BazaarPlugins(), + } +} + +func getInstalledPlugin(c *gin.Context) { + ret := gulu.Ret.NewResult() + defer c.JSON(http.StatusOK, ret) + + ret.Data = map[string]interface{}{ + "packages": model.InstalledPlugins(), + } +} + +func installBazaarPlugin(c *gin.Context) { + ret := gulu.Ret.NewResult() + defer c.JSON(http.StatusOK, ret) + + arg, ok := util.JsonArg(c, ret) + if !ok { + return + } + + repoURL := arg["repoURL"].(string) + repoHash := arg["repoHash"].(string) + packageName := arg["packageName"].(string) + err := model.InstallBazaarPlugin(repoURL, repoHash, packageName) + if nil != err { + ret.Code = 1 + ret.Msg = err.Error() + return + } + + util.PushMsg(model.Conf.Language(69), 3000) + ret.Data = map[string]interface{}{ + "packages": model.BazaarPlugins(), + } +} + +func uninstallBazaarPlugin(c *gin.Context) { + ret := gulu.Ret.NewResult() + defer c.JSON(http.StatusOK, ret) + + arg, ok := util.JsonArg(c, ret) + if !ok { + return + } + + packageName := arg["packageName"].(string) + err := model.UninstallBazaarPlugin(packageName) + if nil != err { + ret.Code = -1 + ret.Msg = err.Error() + return + } + + ret.Data = map[string]interface{}{ + "packages": model.BazaarPlugins(), + } +} + func getBazaarWidget(c *gin.Context) { ret := gulu.Ret.NewResult() defer c.JSON(http.StatusOK, ret) diff --git a/kernel/api/router.go b/kernel/api/router.go index 3871daa51..11f69abb7 100644 --- a/kernel/api/router.go +++ b/kernel/api/router.go @@ -275,6 +275,10 @@ func ServeAPI(ginServer *gin.Engine) { ginServer.Handle("POST", "/api/graph/getGraph", model.CheckAuth, getGraph) ginServer.Handle("POST", "/api/graph/getLocalGraph", model.CheckAuth, getLocalGraph) + ginServer.Handle("POST", "/api/bazaar/getBazaarPlugin", model.CheckAuth, getBazaarPlugin) + ginServer.Handle("POST", "/api/bazaar/getInstalledPlugin", model.CheckAuth, getInstalledPlugin) + ginServer.Handle("POST", "/api/bazaar/installBazaarPlugin", model.CheckAuth, model.CheckReadonly, installBazaarPlugin) + ginServer.Handle("POST", "/api/bazaar/uninstallBazaarPlugin", model.CheckAuth, model.CheckReadonly, uninstallBazaarPlugin) ginServer.Handle("POST", "/api/bazaar/getBazaarWidget", model.CheckAuth, getBazaarWidget) ginServer.Handle("POST", "/api/bazaar/getInstalledWidget", model.CheckAuth, getInstalledWidget) ginServer.Handle("POST", "/api/bazaar/installBazaarWidget", model.CheckAuth, model.CheckReadonly, installBazaarWidget) diff --git a/kernel/bazaar/package.go b/kernel/bazaar/package.go index 4b214ba3a..bf8ab61e2 100644 --- a/kernel/bazaar/package.go +++ b/kernel/bazaar/package.go @@ -66,6 +66,28 @@ type Package struct { Downloads int `json:"downloads"` } +func PluginJSON(pluginDirName string) (ret map[string]interface{}, err error) { + p := filepath.Join(util.DataDir, "plugins", pluginDirName, "plugin.json") + if !gulu.File.IsExist(p) { + err = os.ErrNotExist + return + } + data, err := os.ReadFile(p) + if nil != err { + logging.LogErrorf("read plugin.json [%s] failed: %s", p, err) + return + } + if err = gulu.JSON.UnmarshalJSON(data, &ret); nil != err { + logging.LogErrorf("parse plugin.json [%s] failed: %s", p, err) + return + } + if 4 > len(ret) { + logging.LogWarnf("invalid plugin.json [%s]", p) + return nil, errors.New("invalid plugin.json") + } + return +} + func WidgetJSON(widgetDirName string) (ret map[string]interface{}, err error) { p := filepath.Join(util.DataDir, "widgets", widgetDirName, "widget.json") if !gulu.File.IsExist(p) { @@ -216,6 +238,26 @@ func isOutdatedIcon(icon *Icon, bazaarIcons []*Icon) bool { return false } +func isOutdatedPlugin(plugin *Plugin, bazaarPlugins []*Plugin) bool { + if !strings.HasPrefix(plugin.URL, "https://github.com/") { + return false + } + + repo := strings.TrimPrefix(plugin.URL, "https://github.com/") + parts := strings.Split(repo, "/") + if 2 != len(parts) || "" == strings.TrimSpace(parts[1]) { + return false + } + + for _, pkg := range bazaarPlugins { + if plugin.URL == pkg.URL && plugin.Name == pkg.Name && plugin.Author == pkg.Author && plugin.Version < pkg.Version { + plugin.RepoHash = pkg.RepoHash + return true + } + } + return false +} + func isOutdatedWidget(widget *Widget, bazaarWidgets []*Widget) bool { if !strings.HasPrefix(widget.URL, "https://github.com/") { return false diff --git a/kernel/bazaar/plugin.go b/kernel/bazaar/plugin.go new file mode 100644 index 000000000..6a2f665c4 --- /dev/null +++ b/kernel/bazaar/plugin.go @@ -0,0 +1,169 @@ +// SiYuan - Build Your Eternal Digital Garden +// Copyright (c) 2020-present, b3log.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package bazaar + +import ( + "errors" + "os" + "path/filepath" + "sort" + "strings" + "sync" + + "github.com/dustin/go-humanize" + ants "github.com/panjf2000/ants/v2" + "github.com/siyuan-note/httpclient" + "github.com/siyuan-note/logging" + "github.com/siyuan-note/siyuan/kernel/util" +) + +type Plugin struct { + Package +} + +func Plugins() (plugins []*Plugin) { + plugins = []*Plugin{} + + pkgIndex, err := getPkgIndex("plugins") + if nil != err { + return + } + bazaarIndex := getBazaarIndex() + + repos := pkgIndex["repos"].([]interface{}) + waitGroup := &sync.WaitGroup{} + lock := &sync.Mutex{} + p, _ := ants.NewPoolWithFunc(8, func(arg interface{}) { + defer waitGroup.Done() + + repo := arg.(map[string]interface{}) + repoURL := repo["url"].(string) + + plugin := &Plugin{} + innerU := util.BazaarOSSServer + "/package/" + repoURL + "/plugin.json" + innerResp, innerErr := httpclient.NewBrowserRequest().SetSuccessResult(plugin).Get(innerU) + if nil != innerErr { + logging.LogErrorf("get bazaar package [%s] failed: %s", repoURL, innerErr) + return + } + if 200 != innerResp.StatusCode { + logging.LogErrorf("get bazaar package [%s] failed: %d", innerU, innerResp.StatusCode) + return + } + plugin.URL = strings.TrimSuffix(plugin.URL, "/") + + repoURLHash := strings.Split(repoURL, "@") + plugin.RepoURL = "https://github.com/" + repoURLHash[0] + plugin.RepoHash = repoURLHash[1] + plugin.PreviewURL = util.BazaarOSSServer + "/package/" + repoURL + "/preview.png?imageslim" + plugin.PreviewURLThumb = util.BazaarOSSServer + "/package/" + repoURL + "/preview.png?imageView2/2/w/436/h/232" + plugin.Updated = repo["updated"].(string) + plugin.Stars = int(repo["stars"].(float64)) + plugin.OpenIssues = int(repo["openIssues"].(float64)) + plugin.Size = int64(repo["size"].(float64)) + plugin.HSize = humanize.Bytes(uint64(plugin.Size)) + plugin.HUpdated = formatUpdated(plugin.Updated) + pkg := bazaarIndex[strings.Split(repoURL, "@")[0]] + if nil != pkg { + plugin.Downloads = pkg.Downloads + } + lock.Lock() + plugins = append(plugins, plugin) + lock.Unlock() + }) + for _, repo := range repos { + waitGroup.Add(1) + p.Invoke(repo) + } + waitGroup.Wait() + p.Release() + + sort.Slice(plugins, func(i, j int) bool { return plugins[i].Updated > plugins[j].Updated }) + return +} + +func InstalledPlugins() (ret []*Plugin) { + ret = []*Plugin{} + pluginDirs, err := os.ReadDir(filepath.Join(util.DataDir, "plugins")) + if nil != err { + logging.LogWarnf("read plugins folder failed: %s", err) + return + } + + bazaarPlugins := Plugins() + + for _, pluginDir := range pluginDirs { + if !pluginDir.IsDir() { + continue + } + dirName := pluginDir.Name() + + pluginConf, parseErr := PluginJSON(dirName) + if nil != parseErr || nil == pluginConf { + continue + } + + installPath := filepath.Join(util.DataDir, "plugins", dirName) + + plugin := &Plugin{} + plugin.Installed = true + plugin.Name = pluginConf["name"].(string) + plugin.Author = pluginConf["author"].(string) + plugin.URL = pluginConf["url"].(string) + plugin.URL = strings.TrimSuffix(plugin.URL, "/") + plugin.Version = pluginConf["version"].(string) + plugin.RepoURL = plugin.URL + plugin.PreviewURL = "/plugins/" + dirName + "/preview.png" + plugin.PreviewURLThumb = "/plugins/" + dirName + "/preview.png" + info, statErr := os.Stat(filepath.Join(installPath, "README.md")) + if nil != statErr { + logging.LogWarnf("stat install theme README.md failed: %s", statErr) + continue + } + plugin.HInstallDate = info.ModTime().Format("2006-01-02") + installSize, _ := util.SizeOfDirectory(installPath) + plugin.InstallSize = installSize + plugin.HInstallSize = humanize.Bytes(uint64(installSize)) + readme, readErr := os.ReadFile(filepath.Join(installPath, "README.md")) + if nil != readErr { + logging.LogWarnf("read install plugin README.md failed: %s", readErr) + continue + } + plugin.README, _ = renderREADME(plugin.URL, readme) + plugin.Outdated = isOutdatedPlugin(plugin, bazaarPlugins) + ret = append(ret, plugin) + } + return +} + +func InstallPlugin(repoURL, repoHash, installPath string, systemID string) error { + repoURLHash := repoURL + "@" + repoHash + data, err := downloadPackage(repoURLHash, true, systemID) + if nil != err { + return err + } + return installPackage(data, installPath) +} + +func UninstallPlugin(installPath string) error { + if err := os.RemoveAll(installPath); nil != err { + logging.LogErrorf("remove plugin [%s] failed: %s", installPath, err) + return errors.New("remove community plugin failed") + } + //logging.Logger.Infof("uninstalled plugin [%s]", installPath) + return nil +} diff --git a/kernel/model/appearance.go b/kernel/model/appearance.go index b9dc19db3..c70e1bcd5 100644 --- a/kernel/model/appearance.go +++ b/kernel/model/appearance.go @@ -17,7 +17,6 @@ package model import ( - "errors" "fmt" "os" "path/filepath" @@ -142,72 +141,6 @@ func loadThemes() { } } -func iconJSON(iconName string) (ret map[string]interface{}, err error) { - p := filepath.Join(util.IconsPath, iconName, "icon.json") - if !gulu.File.IsExist(p) { - err = os.ErrNotExist - return - } - data, err := os.ReadFile(p) - if nil != err { - logging.LogErrorf("read icon.json [%s] failed: %s", p, err) - return - } - if err = gulu.JSON.UnmarshalJSON(data, &ret); nil != err { - logging.LogErrorf("parse icon.json [%s] failed: %s", p, err) - return - } - if 4 > len(ret) { - logging.LogWarnf("invalid icon.json [%s]", p) - return nil, errors.New("invalid icon.json") - } - return -} - -func templateJSON(templateName string) (ret map[string]interface{}, err error) { - p := filepath.Join(util.DataDir, "templates", templateName, "template.json") - if !gulu.File.IsExist(p) { - err = os.ErrNotExist - return - } - data, err := os.ReadFile(p) - if nil != err { - logging.LogErrorf("read template.json [%s] failed: %s", p, err) - return - } - if err = gulu.JSON.UnmarshalJSON(data, &ret); nil != err { - logging.LogErrorf("parse template.json [%s] failed: %s", p, err) - return - } - if 4 > len(ret) { - logging.LogWarnf("invalid template.json [%s]", p) - return nil, errors.New("invalid template.json") - } - return -} - -func widgetJSON(widgetName string) (ret map[string]interface{}, err error) { - p := filepath.Join(util.DataDir, "widgets", widgetName, "widget.json") - if !gulu.File.IsExist(p) { - err = os.ErrNotExist - return - } - data, err := os.ReadFile(p) - if nil != err { - logging.LogErrorf("read widget.json [%s] failed: %s", p, err) - return - } - if err = gulu.JSON.UnmarshalJSON(data, &ret); nil != err { - logging.LogErrorf("parse widget.json [%s] failed: %s", p, err) - return - } - if 4 > len(ret) { - logging.LogWarnf("invalid widget.json [%s]", p) - return nil, errors.New("invalid widget.json") - } - return -} - func loadIcons() { iconDirs, err := os.ReadDir(util.IconsPath) if nil != err { @@ -222,7 +155,7 @@ func loadIcons() { continue } name := iconDir.Name() - iconConf, err := iconJSON(name) + iconConf, err := bazaar.IconJSON(name) if nil != err || nil == iconConf { continue } diff --git a/kernel/model/bazzar.go b/kernel/model/bazzar.go index 51175f414..a7c6f4b6c 100644 --- a/kernel/model/bazzar.go +++ b/kernel/model/bazzar.go @@ -32,13 +32,53 @@ func GetPackageREADME(repoURL, repoHash string) (ret string) { return } +func BazaarPlugins() (plugins []*bazaar.Plugin) { + plugins = bazaar.Plugins() + for _, plugin := range plugins { + plugin.Installed = gulu.File.IsDir(filepath.Join(util.DataDir, "plugins", plugin.Name)) + if plugin.Installed { + if plugin.Installed { + if pluginConf, err := bazaar.PluginJSON(plugin.Name); nil == err && nil != plugin { + if plugin.Version != pluginConf["version"].(string) { + plugin.Outdated = true + } + } + } + } + } + return +} + +func InstalledPlugins() (plugins []*bazaar.Plugin) { + plugins = bazaar.InstalledPlugins() + return +} + +func InstallBazaarPlugin(repoURL, repoHash, pluginName string) error { + installPath := filepath.Join(util.DataDir, "plugins", pluginName) + err := bazaar.InstallPlugin(repoURL, repoHash, installPath, Conf.System.ID) + if nil != err { + return errors.New(fmt.Sprintf(Conf.Language(46), pluginName)) + } + return nil +} + +func UninstallBazaarPlugin(pluginName string) error { + installPath := filepath.Join(util.DataDir, "plugins", pluginName) + err := bazaar.UninstallPlugin(installPath) + if nil != err { + return errors.New(fmt.Sprintf(Conf.Language(47), err.Error())) + } + return nil +} + func BazaarWidgets() (widgets []*bazaar.Widget) { widgets = bazaar.Widgets() for _, widget := range widgets { widget.Installed = gulu.File.IsDir(filepath.Join(util.DataDir, "widgets", widget.Name)) if widget.Installed { if widget.Installed { - if widgetConf, err := widgetJSON(widget.Name); nil == err && nil != widget { + if widgetConf, err := bazaar.WidgetJSON(widget.Name); nil == err && nil != widget { if widget.Version != widgetConf["version"].(string) { widget.Outdated = true } @@ -78,7 +118,7 @@ func BazaarIcons() (icons []*bazaar.Icon) { for _, icon := range icons { if installed == icon.Name { icon.Installed = true - if themeConf, err := iconJSON(icon.Name); nil == err { + if themeConf, err := bazaar.IconJSON(icon.Name); nil == err { if icon.Version != themeConf["version"].(string) { icon.Outdated = true } @@ -190,7 +230,7 @@ func BazaarTemplates() (templates []*bazaar.Template) { for _, template := range templates { template.Installed = gulu.File.IsExist(filepath.Join(util.DataDir, "templates", template.Name)) if template.Installed { - if themeConf, err := templateJSON(template.Name); nil == err && nil != themeConf { + if themeConf, err := bazaar.TemplateJSON(template.Name); nil == err && nil != themeConf { if template.Version != themeConf["version"].(string) { template.Outdated = true }