diff --git a/app/src/config/bazaar.ts b/app/src/config/bazaar.ts index 0ba9a74f3..71115fe6d 100644 --- a/app/src/config/bazaar.ts +++ b/app/src/config/bazaar.ts @@ -700,7 +700,7 @@ type="checkbox"> }, async response => { bazaar._onBazaar(response, bazaarType); bazaar._genMyHTML(bazaarType, app, false); - if (bazaarType === "plugins") { + if (response.code === 0 && bazaarType === "plugins") { if (window.siyuan.config.bazaar.petalDisabled) { confirmDialog(window.siyuan.languages.confirm, window.siyuan.languages.enablePluginTip2); } else { @@ -756,27 +756,17 @@ type="checkbox"> packageName: dataObj.name, repoHash: dataObj.repoHash, mode: dataObj.themeMode === "dark" ? 1 : 0, - update: true, frontend: getFrontend() }, async response => { this._genMyHTML(bazaarType, app); bazaar._onBazaar(response, bazaarType); + // TODO 集市包的相关逻辑应完全由内核处理并推送到所有前端实例,下面的代码需要确认 // https://github.com/siyuan-note/siyuan/issues/15177 if (bazaarType === "themes" && response.data.appearance?.themeVer) { window.siyuan.config.appearance.themeVer = response.data.appearance.themeVer; } // 更新主题后不需要对该主题进行切换 https://github.com/siyuan-note/siyuan/issues/4966 // https://github.com/siyuan-note/siyuan/issues/5411 - if (bazaarType === "plugins") { - app.plugins.find((item: Plugin) => { - if (item.name === dataObj.name) { - reloadPlugin(app, { - reloadPlugins: [dataObj.name], - }); - return true; - } - }); - } }); }); } @@ -1099,12 +1089,6 @@ type="checkbox"> }); }, _onBazaar(response: IWebSocketData, bazaarType: TBazaarType) { - if (bazaar.element.querySelector("#configBazaarReadme").classList.contains("config-bazaar__readme--show")) { - const dataObj = JSON.parse(bazaar.element.querySelector("#configBazaarReadme > .item__side").getAttribute("data-obj")); - bazaar._renderReadme((dataObj.bazaarType) as TBazaarType, - response.data.packages.find((item: IBazaarItem) => item.repoURL === dataObj.repoURL), - dataObj.downloaded); - } let id = "#configBazaarTemplate"; if (bazaarType === "themes") { id = "#configBazaarTheme"; @@ -1117,10 +1101,18 @@ type="checkbox"> } const element = bazaar.element.querySelector(id); if (response.code === 1) { + // 安装集市包 /api/bazaar/installBazaar* 失败 showMessage(response.msg); element.querySelectorAll("img[data-type='img-loading']").forEach((item) => { item.remove(); }); + return; + } + if (bazaar.element.querySelector("#configBazaarReadme").classList.contains("config-bazaar__readme--show")) { + const dataObj = JSON.parse(bazaar.element.querySelector("#configBazaarReadme > .item__side").getAttribute("data-obj")); + bazaar._renderReadme((dataObj.bazaarType) as TBazaarType, + response.data.packages.find((item: IBazaarItem) => item.repoURL === dataObj.repoURL), + dataObj.downloaded); } let html = ""; response.data.packages.forEach((item: IBazaarItem) => { diff --git a/app/src/types/index.d.ts b/app/src/types/index.d.ts index c173202ca..7439737a9 100644 --- a/app/src/types/index.d.ts +++ b/app/src/types/index.d.ts @@ -878,8 +878,6 @@ interface IMenu { } interface IBazaarItem { - incompatible?: boolean; // 仅 plugin - enabled: boolean; preferredName: string; minAppVersion: string; preferredDesc: string; @@ -895,13 +893,11 @@ interface IBazaarItem { outdated: false; name: string; previewURL: string; - previewURLThumb: string; repoHash: string; repoURL: string; url: string; openIssues: number; version: string; - modes: string[]; hSize: string; hInstallSize: string; hInstallDate: string; @@ -909,6 +905,9 @@ interface IBazaarItem { preferredFunding: string; disallowUpdate: boolean; updateRequiredMinAppVer: string; + incompatible?: boolean; // 仅 plugin + enabled?: boolean; // 仅 plugin + modes?: string[]; // 仅 theme } interface IAV { diff --git a/kernel/api/bazaar.go b/kernel/api/bazaar.go index df965b415..d9a9ac5b6 100644 --- a/kernel/api/bazaar.go +++ b/kernel/api/bazaar.go @@ -46,7 +46,7 @@ func batchUpdatePackage(c *gin.Context) { if !util.ParseJsonArgs(arg, ret, util.BindJsonArg("frontend", true, &frontend)) { return } - model.BatchUpdateBazaarPackages(frontend) + model.BatchUpdatePackages(frontend) } func getUpdatedPackage(c *gin.Context) { @@ -62,7 +62,7 @@ func getUpdatedPackage(c *gin.Context) { if !util.ParseJsonArgs(arg, ret, util.BindJsonArg("frontend", true, &frontend)) { return } - plugins, widgets, icons, themes, templates := model.UpdatedPackages(frontend) + plugins, widgets, icons, themes, templates := model.GetUpdatedPackages(frontend) ret.Data = map[string]interface{}{ "plugins": plugins, "widgets": widgets, @@ -117,7 +117,7 @@ func getBazaarPlugin(c *gin.Context) { } ret.Data = map[string]interface{}{ - "packages": model.BazaarPlugins(frontend, keyword), + "packages": model.GetBazaarPackages("plugins", frontend, keyword), } } @@ -139,7 +139,7 @@ func getInstalledPlugin(c *gin.Context) { } ret.Data = map[string]interface{}{ - "packages": model.InstalledPlugins(frontend, keyword), + "packages": model.GetInstalledPackages("plugins", frontend, keyword), } } @@ -162,7 +162,7 @@ func installBazaarPlugin(c *gin.Context) { ) { return } - err := model.InstallBazaarPlugin(repoURL, repoHash, packageName) + err := model.InstallBazaarPackage("plugins", repoURL, repoHash, packageName, 0) if err != nil { ret.Code = 1 ret.Msg = err.Error() @@ -171,7 +171,7 @@ func installBazaarPlugin(c *gin.Context) { util.PushMsg(model.Conf.Language(69), 3000) ret.Data = map[string]interface{}{ - "packages": model.BazaarPlugins(frontend, keyword), + "packages": model.GetBazaarPackages("plugins", frontend, keyword), } } @@ -192,7 +192,7 @@ func uninstallBazaarPlugin(c *gin.Context) { ) { return } - err := model.UninstallBazaarPlugin(packageName, frontend) + err := model.UninstallPackage("plugins", packageName) if err != nil { ret.Code = -1 ret.Msg = err.Error() @@ -200,7 +200,7 @@ func uninstallBazaarPlugin(c *gin.Context) { } ret.Data = map[string]interface{}{ - "packages": model.BazaarPlugins(frontend, keyword), + "packages": model.GetBazaarPackages("plugins", frontend, keyword), } } @@ -219,7 +219,7 @@ func getBazaarWidget(c *gin.Context) { } ret.Data = map[string]interface{}{ - "packages": model.BazaarWidgets(keyword), + "packages": model.GetBazaarPackages("widgets", "", keyword), } } @@ -238,7 +238,7 @@ func getInstalledWidget(c *gin.Context) { } ret.Data = map[string]interface{}{ - "packages": model.InstalledWidgets(keyword), + "packages": model.GetInstalledPackages("widgets", "", keyword), } } @@ -260,7 +260,7 @@ func installBazaarWidget(c *gin.Context) { ) { return } - err := model.InstallBazaarWidget(repoURL, repoHash, packageName) + err := model.InstallBazaarPackage("widgets", repoURL, repoHash, packageName, 0) if err != nil { ret.Code = 1 ret.Msg = err.Error() @@ -269,7 +269,7 @@ func installBazaarWidget(c *gin.Context) { util.PushMsg(model.Conf.Language(69), 3000) ret.Data = map[string]interface{}{ - "packages": model.BazaarWidgets(keyword), + "packages": model.GetBazaarPackages("widgets", "", keyword), } } @@ -289,7 +289,7 @@ func uninstallBazaarWidget(c *gin.Context) { ) { return } - err := model.UninstallBazaarWidget(packageName) + err := model.UninstallPackage("widgets", packageName) if err != nil { ret.Code = -1 ret.Msg = err.Error() @@ -297,7 +297,7 @@ func uninstallBazaarWidget(c *gin.Context) { } ret.Data = map[string]interface{}{ - "packages": model.BazaarWidgets(keyword), + "packages": model.GetBazaarPackages("widgets", "", keyword), } } @@ -316,7 +316,7 @@ func getBazaarIcon(c *gin.Context) { } ret.Data = map[string]interface{}{ - "packages": model.BazaarIcons(keyword), + "packages": model.GetBazaarPackages("icons", "", keyword), } } @@ -335,7 +335,7 @@ func getInstalledIcon(c *gin.Context) { } ret.Data = map[string]interface{}{ - "packages": model.InstalledIcons(keyword), + "packages": model.GetInstalledPackages("icons", "", keyword), } } @@ -357,7 +357,7 @@ func installBazaarIcon(c *gin.Context) { ) { return } - err := model.InstallBazaarIcon(repoURL, repoHash, packageName) + err := model.InstallBazaarPackage("icons", repoURL, repoHash, packageName, 0) if err != nil { ret.Code = 1 ret.Msg = err.Error() @@ -366,7 +366,7 @@ func installBazaarIcon(c *gin.Context) { util.PushMsg(model.Conf.Language(69), 3000) ret.Data = map[string]interface{}{ - "packages": model.BazaarIcons(keyword), + "packages": model.GetBazaarPackages("icons", "", keyword), "appearance": model.Conf.Appearance, } } @@ -387,7 +387,7 @@ func uninstallBazaarIcon(c *gin.Context) { ) { return } - err := model.UninstallBazaarIcon(packageName) + err := model.UninstallPackage("icons", packageName) if err != nil { ret.Code = -1 ret.Msg = err.Error() @@ -395,7 +395,7 @@ func uninstallBazaarIcon(c *gin.Context) { } ret.Data = map[string]interface{}{ - "packages": model.BazaarIcons(keyword), + "packages": model.GetBazaarPackages("icons", "", keyword), "appearance": model.Conf.Appearance, } } @@ -415,7 +415,7 @@ func getBazaarTemplate(c *gin.Context) { } ret.Data = map[string]interface{}{ - "packages": model.BazaarTemplates(keyword), + "packages": model.GetBazaarPackages("templates", "", keyword), } } @@ -434,7 +434,7 @@ func getInstalledTemplate(c *gin.Context) { } ret.Data = map[string]interface{}{ - "packages": model.InstalledTemplates(keyword), + "packages": model.GetInstalledPackages("templates", "", keyword), } } @@ -447,15 +447,16 @@ func installBazaarTemplate(c *gin.Context) { return } - var keyword string - if keywordArg := arg["keyword"]; nil != keywordArg { - keyword = keywordArg.(string) + var keyword, repoURL, repoHash, packageName string + if !util.ParseJsonArgs(arg, ret, + util.BindJsonArg("keyword", false, &keyword), + util.BindJsonArg("repoURL", true, &repoURL), + util.BindJsonArg("repoHash", true, &repoHash), + util.BindJsonArg("packageName", true, &packageName), + ) { + return } - - repoURL := arg["repoURL"].(string) - repoHash := arg["repoHash"].(string) - packageName := arg["packageName"].(string) - err := model.InstallBazaarTemplate(repoURL, repoHash, packageName) + err := model.InstallBazaarPackage("templates", repoURL, repoHash, packageName, 0) if err != nil { ret.Code = 1 ret.Msg = err.Error() @@ -463,7 +464,7 @@ func installBazaarTemplate(c *gin.Context) { } ret.Data = map[string]interface{}{ - "packages": model.BazaarTemplates(keyword), + "packages": model.GetBazaarPackages("templates", "", keyword), } util.PushMsg(model.Conf.Language(69), 3000) @@ -478,13 +479,14 @@ func uninstallBazaarTemplate(c *gin.Context) { return } - var keyword string - if keywordArg := arg["keyword"]; nil != keywordArg { - keyword = keywordArg.(string) + var keyword, packageName string + if !util.ParseJsonArgs(arg, ret, + util.BindJsonArg("keyword", false, &keyword), + util.BindJsonArg("packageName", true, &packageName), + ) { + return } - - packageName := arg["packageName"].(string) - err := model.UninstallBazaarTemplate(packageName) + err := model.UninstallPackage("templates", packageName) if err != nil { ret.Code = -1 ret.Msg = err.Error() @@ -492,7 +494,7 @@ func uninstallBazaarTemplate(c *gin.Context) { } ret.Data = map[string]interface{}{ - "packages": model.BazaarTemplates(keyword), + "packages": model.GetBazaarPackages("templates", "", keyword), } } @@ -506,12 +508,12 @@ func getBazaarTheme(c *gin.Context) { } var keyword string - if keywordArg := arg["keyword"]; nil != keywordArg { - keyword = keywordArg.(string) + if !util.ParseJsonArgs(arg, ret, util.BindJsonArg("keyword", false, &keyword)) { + return } ret.Data = map[string]interface{}{ - "packages": model.BazaarThemes(keyword), + "packages": model.GetBazaarPackages("themes", "", keyword), } } @@ -525,12 +527,12 @@ func getInstalledTheme(c *gin.Context) { } var keyword string - if keywordArg := arg["keyword"]; nil != keywordArg { - keyword = keywordArg.(string) + if !util.ParseJsonArgs(arg, ret, util.BindJsonArg("keyword", false, &keyword)) { + return } ret.Data = map[string]interface{}{ - "packages": model.InstalledThemes(keyword), + "packages": model.GetInstalledPackages("themes", "", keyword), } } @@ -554,11 +556,7 @@ func installBazaarTheme(c *gin.Context) { ) { return } - update := false - if nil != arg["update"] { - update = arg["update"].(bool) - } - err := model.InstallBazaarTheme(repoURL, repoHash, packageName, int(mode), update) + err := model.InstallBazaarPackage("themes", repoURL, repoHash, packageName, int(mode)) if err != nil { ret.Code = 1 ret.Msg = err.Error() @@ -572,7 +570,7 @@ func installBazaarTheme(c *gin.Context) { util.PushMsg(model.Conf.Language(69), 3000) ret.Data = map[string]interface{}{ - "packages": model.BazaarThemes(keyword), + "packages": model.GetBazaarPackages("themes", "", keyword), "appearance": model.Conf.Appearance, } } @@ -586,13 +584,14 @@ func uninstallBazaarTheme(c *gin.Context) { return } - var keyword string - if keywordArg := arg["keyword"]; nil != keywordArg { - keyword = keywordArg.(string) + var keyword, packageName string + if !util.ParseJsonArgs(arg, ret, + util.BindJsonArg("keyword", false, &keyword), + util.BindJsonArg("packageName", true, &packageName), + ) { + return } - - packageName := arg["packageName"].(string) - err := model.UninstallBazaarTheme(packageName) + err := model.UninstallPackage("themes", packageName) if err != nil { ret.Code = -1 ret.Msg = err.Error() @@ -600,7 +599,7 @@ func uninstallBazaarTheme(c *gin.Context) { } ret.Data = map[string]interface{}{ - "packages": model.BazaarThemes(keyword), + "packages": model.GetBazaarPackages("themes", "", keyword), "appearance": model.Conf.Appearance, } } diff --git a/kernel/bazaar/bazaar.go b/kernel/bazaar/bazaar.go new file mode 100644 index 000000000..3db03b1d0 --- /dev/null +++ b/kernel/bazaar/bazaar.go @@ -0,0 +1,108 @@ +// SiYuan - Refactor your thinking +// 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 ( + "strings" + "time" + + "github.com/88250/go-humanize" + "github.com/araddon/dateparse" + "github.com/siyuan-note/siyuan/kernel/util" +) + +// GetBazaarPackages 返回指定类型的在线集市包列表(plugins 类型需要传递 frontend 参数)。 +func GetBazaarPackages(pkgType string, frontend string) (packages []*Package) { + result := getStageAndBazaar(pkgType) + + if !result.Online || nil != result.StageErr || nil == result.StageIndex { + return make([]*Package, 0) + } + + packages = make([]*Package, 0, len(result.StageIndex.Repos)) + for _, repo := range result.StageIndex.Repos { + pkg := buildBazaarPackageWithMetadata(repo, result.BazaarIndex, pkgType, frontend) + if nil == pkg { + continue + } + packages = append(packages, pkg) + } + return +} + +// buildBazaarPackageWithMetadata 从 StageRepo 构建带有在线元数据的集市包。 +func buildBazaarPackageWithMetadata(repo *StageRepo, bazaarIndex map[string]*bazaarPackage, pkgType string, frontend string) *Package { + if nil == repo || nil == repo.Package { + return nil + } + + pkg := *repo.Package + pkg.URL = strings.TrimSuffix(pkg.URL, "/") + repoURLHash := strings.Split(repo.URL, "@") + if 2 != len(repoURLHash) { + return nil + } + pkg.RepoURL = "https://github.com/" + repoURLHash[0] + pkg.RepoHash = repoURLHash[1] + + // 展示信息 + pkg.IconURL = util.BazaarOSSServer + "/package/" + repo.URL + "/icon.png" + pkg.PreviewURL = util.BazaarOSSServer + "/package/" + repo.URL + "/preview.png?imageslim" + pkg.PreferredName = GetPreferredLocaleString(pkg.DisplayName, pkg.Name) + pkg.PreferredDesc = GetPreferredLocaleString(pkg.Description, "") + pkg.PreferredFunding = getPreferredFunding(pkg.Funding) + + // 更新信息 + disallow := isBelowRequiredAppVersion(&pkg) + pkg.DisallowInstall = disallow + pkg.DisallowUpdate = disallow + pkg.UpdateRequiredMinAppVer = pkg.MinAppVersion + if "plugins" == pkgType { + incompatible := IsIncompatiblePlugin(&pkg, frontend) + pkg.Incompatible = &incompatible + } + + // 统计信息 + pkg.Updated = repo.Updated + pkg.HUpdated = formatUpdated(pkg.Updated) + pkg.Stars = repo.Stars + pkg.OpenIssues = repo.OpenIssues + pkg.Size = repo.Size + pkg.HSize = humanize.BytesCustomCeil(uint64(pkg.Size), 2) + pkg.InstallSize = repo.InstallSize + pkg.HInstallSize = humanize.BytesCustomCeil(uint64(pkg.InstallSize), 2) + if bp := bazaarIndex[repoURLHash[0]]; nil != bp { + pkg.Downloads = bp.Downloads + } + packageInstallSizeCache.SetDefault(pkg.RepoURL, pkg.InstallSize) + return &pkg +} + +// formatUpdated 格式化发布日期字符串。 +func formatUpdated(updated string) (ret string) { + t, e := dateparse.ParseIn(updated, time.Now().Location()) + if nil == e { + ret = t.Format("2006-01-02") + } else { + if strings.Contains(updated, "T") { + ret = updated[:strings.Index(updated, "T")] + } else { + ret = strings.ReplaceAll(strings.ReplaceAll(updated, "T", ""), "Z", "") + } + } + return +} diff --git a/kernel/bazaar/icon.go b/kernel/bazaar/icon.go deleted file mode 100644 index a6d7549d3..000000000 --- a/kernel/bazaar/icon.go +++ /dev/null @@ -1,187 +0,0 @@ -// SiYuan - Refactor your thinking -// 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 ( - "os" - "path/filepath" - "sort" - "strings" - - "github.com/88250/go-humanize" - "github.com/siyuan-note/logging" - "github.com/siyuan-note/siyuan/kernel/util" -) - -type Icon struct { - *Package -} - -// Icons 返回集市图标列表 -func Icons() (icons []*Icon) { - icons = []*Icon{} - result := getStageAndBazaar("icons") - - if !result.Online { - return - } - if result.StageErr != nil { - return - } - if 1 > len(result.BazaarIndex) { - return - } - - for _, repo := range result.StageIndex.Repos { - if nil == repo.Package { - continue - } - icon := buildIconFromStageRepo(repo, result.BazaarIndex) - if nil != icon { - icons = append(icons, icon) - } - } - - sort.Slice(icons, func(i, j int) bool { return icons[i].Updated > icons[j].Updated }) - return -} - -// buildIconFromStageRepo 使用 stage 内嵌的 package 构建 *Icon,不发起 HTTP 请求。 -func buildIconFromStageRepo(repo *StageRepo, bazaarIndex map[string]*bazaarPackage) *Icon { - pkg := *repo.Package - pkg.URL = strings.TrimSuffix(pkg.URL, "/") - repoURLHash := strings.Split(repo.URL, "@") - if 2 != len(repoURLHash) { - return nil - } - pkg.RepoURL = "https://github.com/" + repoURLHash[0] - pkg.RepoHash = repoURLHash[1] - pkg.PreviewURL = util.BazaarOSSServer + "/package/" + repo.URL + "/preview.png?imageslim" - pkg.PreviewURLThumb = util.BazaarOSSServer + "/package/" + repo.URL + "/preview.png?imageView2/2/w/436/h/232" - pkg.IconURL = util.BazaarOSSServer + "/package/" + repo.URL + "/icon.png" - pkg.Updated = repo.Updated - pkg.Stars = repo.Stars - pkg.OpenIssues = repo.OpenIssues - pkg.Size = repo.Size - pkg.HSize = humanize.BytesCustomCeil(uint64(pkg.Size), 2) - pkg.InstallSize = repo.InstallSize - pkg.HInstallSize = humanize.BytesCustomCeil(uint64(pkg.InstallSize), 2) - pkg.HUpdated = formatUpdated(pkg.Updated) - pkg.PreferredFunding = getPreferredFunding(pkg.Funding) - pkg.PreferredName = GetPreferredName(&pkg) - pkg.PreferredDesc = getPreferredDesc(pkg.Description) - pkg.DisallowInstall = disallowInstallBazaarPackage(&pkg) - pkg.DisallowUpdate = disallowInstallBazaarPackage(&pkg) - pkg.UpdateRequiredMinAppVer = pkg.MinAppVersion - if bp := bazaarIndex[repoURLHash[0]]; nil != bp { - pkg.Downloads = bp.Downloads - } - packageInstallSizeCache.SetDefault(pkg.RepoURL, pkg.InstallSize) - return &Icon{Package: &pkg} -} - -func InstalledIcons() (ret []*Icon) { - ret = []*Icon{} - - if !util.IsPathRegularDirOrSymlinkDir(util.IconsPath) { - return - } - - iconDirs, err := os.ReadDir(util.IconsPath) - if err != nil { - logging.LogWarnf("read icons folder failed: %s", err) - return - } - - bazaarIcons := Icons() - - for _, iconDir := range iconDirs { - if !util.IsDirRegularOrSymlink(iconDir) { - continue - } - dirName := iconDir.Name() - if isBuiltInIcon(dirName) { - continue - } - - icon, parseErr := IconJSON(dirName) - if nil != parseErr || nil == icon { - continue - } - - icon.RepoURL = icon.URL - icon.DisallowInstall = disallowInstallBazaarPackage(icon.Package) - if bazaarPkg := getBazaarIcon(icon.Name, bazaarIcons); nil != bazaarPkg { - icon.DisallowUpdate = disallowInstallBazaarPackage(bazaarPkg.Package) - icon.UpdateRequiredMinAppVer = bazaarPkg.MinAppVersion - icon.RepoURL = bazaarPkg.RepoURL - } - - installPath := filepath.Join(util.IconsPath, dirName) - icon.Installed = true - icon.PreviewURL = "/appearance/icons/" + dirName + "/preview.png" - icon.PreviewURLThumb = "/appearance/icons/" + dirName + "/preview.png" - icon.IconURL = "/appearance/icons/" + dirName + "/icon.png" - icon.PreferredFunding = getPreferredFunding(icon.Funding) - icon.PreferredName = GetPreferredName(icon.Package) - icon.PreferredDesc = getPreferredDesc(icon.Description) - info, statErr := os.Stat(filepath.Join(installPath, "icon.json")) - if nil != statErr { - logging.LogWarnf("stat install icon.json failed: %s", statErr) - continue - } - icon.HInstallDate = info.ModTime().Format("2006-01-02") - if installSize, ok := packageInstallSizeCache.Get(icon.RepoURL); ok { - icon.InstallSize = installSize.(int64) - } else { - is, _ := util.SizeOfDirectory(installPath) - icon.InstallSize = is - packageInstallSizeCache.SetDefault(icon.RepoURL, is) - } - icon.HInstallSize = humanize.BytesCustomCeil(uint64(icon.InstallSize), 2) - icon.PreferredReadme = getInstalledPackageREADME(installPath, "/appearance/icons/"+dirName+"/", icon.Readme) - icon.Outdated = isOutdatedIcon(icon, bazaarIcons) - ret = append(ret, icon) - } - return -} - -func isBuiltInIcon(dirName string) bool { - return "ant" == dirName || "material" == dirName -} - -func getBazaarIcon(name string, icon []*Icon) *Icon { - for _, p := range icon { - if p.Name == name { - return p - } - } - return nil -} - -func InstallIcon(repoURL, repoHash, installPath string, systemID string) error { - repoURLHash := repoURL + "@" + repoHash - data, err := downloadPackage(repoURLHash, true, systemID) - if err != nil { - return err - } - return installPackage(data, installPath, repoURLHash) -} - -func UninstallIcon(installPath string) error { - return uninstallPackage(installPath) -} diff --git a/kernel/bazaar/install.go b/kernel/bazaar/install.go new file mode 100644 index 000000000..b41a6a1f2 --- /dev/null +++ b/kernel/bazaar/install.go @@ -0,0 +1,148 @@ +// SiYuan - Refactor your thinking +// 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 ( + "bytes" + "errors" + "fmt" + "os" + "path/filepath" + "strings" + "time" + + "github.com/88250/gulu" + "github.com/imroc/req/v3" + "github.com/siyuan-note/filelock" + "github.com/siyuan-note/httpclient" + "github.com/siyuan-note/logging" + "github.com/siyuan-note/siyuan/kernel/util" + "golang.org/x/sync/singleflight" +) + +var downloadPackageFlight singleflight.Group + +// downloadBazaarFile 下载集市文件 +func downloadBazaarFile(repoURLHash string, pushProgress bool) (data []byte, err error) { + repoURLHashTrimmed := strings.TrimPrefix(repoURLHash, "https://github.com/") + v, err, _ := downloadPackageFlight.Do(repoURLHash, func() (interface{}, error) { + // repoURLHash: https://github.com/88250/Comfortably-Numb@6286912c381ef3f83e455d06ba4d369c498238dc 或带路径 /README.md + repoURL := repoURLHash[:strings.LastIndex(repoURLHash, "@")] + u := util.BazaarOSSServer + "/package/" + repoURLHashTrimmed + buf := &bytes.Buffer{} + resp, err := httpclient.NewCloudFileRequest2m().SetOutput(buf).SetDownloadCallback(func(info req.DownloadInfo) { + if pushProgress { + progress := float32(info.DownloadedSize) / float32(info.Response.ContentLength) + util.PushDownloadProgress(repoURL, progress) + } + }).Get(u) + if err != nil { + logging.LogErrorf("get bazaar package [%s] failed: %s", u, err) + return nil, errors.New("get bazaar package failed, please check your network") + } + if 200 != resp.StatusCode { + logging.LogErrorf("get bazaar package [%s] failed: %d", u, resp.StatusCode) + return nil, errors.New("get bazaar package failed: " + resp.Status) + } + data := buf.Bytes() + return data, nil + }) + if err != nil { + return nil, err + } + return v.([]byte), nil +} + +// incPackageDownloads 增加集市包下载次数 +func incPackageDownloads(repoURL, systemID string) { + if "" == systemID { + return + } + repo := strings.TrimPrefix(repoURL, "https://github.com/") + u := util.GetCloudServer() + "/apis/siyuan/bazaar/addBazaarPackageDownloadCount" + httpclient.NewCloudRequest30s().SetBody( + map[string]interface{}{ + "systemID": systemID, + "repo": repo, + }).Post(u) +} + +// InstallPackage 安装集市包 +func InstallPackage(repoURL, repoHash, installPath, systemID, pkgType, packageName string) error { + repoURLHash := repoURL + "@" + repoHash + data, err := downloadBazaarFile(repoURLHash, true) + if err != nil { + return err + } + if err = installPackage(data, installPath); err != nil { + return err + } + + // 记录安装时间 + now := time.Now() + setPackageInstallTime(pkgType, packageName, now) + + // 文件夹的修改时间设置为当前安装时间 + if err = os.Chtimes(installPath, now, now); err != nil { + logging.LogWarnf("set package [%s] folder mtime failed: %s", packageName, err) + } + + go incPackageDownloads(repoURL, systemID) + return nil +} + +func installPackage(data []byte, installPath string) (err error) { + tmpPackage := filepath.Join(util.TempDir, "bazaar", "package") + if err = os.MkdirAll(tmpPackage, 0755); err != nil { + return + } + name := gulu.Rand.String(7) + tmp := filepath.Join(tmpPackage, name+".zip") + if err = os.WriteFile(tmp, data, 0644); err != nil { + return + } + + unzipPath := filepath.Join(tmpPackage, name) + if err = gulu.Zip.Unzip(tmp, unzipPath); err != nil { + logging.LogErrorf("write file [%s] failed: %s", installPath, err) + return + } + + dirs, err := os.ReadDir(unzipPath) + if err != nil { + return + } + + srcPath := unzipPath + if 1 == len(dirs) && dirs[0].IsDir() { + srcPath = filepath.Join(unzipPath, dirs[0].Name()) + } + + if err = filelock.Copy(srcPath, installPath); err != nil { + return + } + return +} + +// UninstallPackage 卸载集市包 +func UninstallPackage(installPath string) (err error) { + if err = os.RemoveAll(installPath); err != nil { + logging.LogErrorf("remove [%s] failed: %s", installPath, err) + return fmt.Errorf("remove community package [%s] failed", filepath.Base(installPath)) + } + return +} diff --git a/kernel/bazaar/installed.go b/kernel/bazaar/installed.go new file mode 100644 index 000000000..917a86d8a --- /dev/null +++ b/kernel/bazaar/installed.go @@ -0,0 +1,278 @@ +// SiYuan - Refactor your thinking +// 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 ( + "os" + "path/filepath" + "sync" + "time" + + "github.com/88250/go-humanize" + "github.com/88250/gulu" + gcache "github.com/patrickmn/go-cache" + "github.com/siyuan-note/filelock" + "github.com/siyuan-note/logging" + "github.com/siyuan-note/siyuan/kernel/util" + "golang.org/x/mod/semver" + "golang.org/x/sync/singleflight" +) + +// packageInstallSizeCache 缓存集市包的安装大小,与 cachedStageIndex 使用相同的缓存时间 +var packageInstallSizeCache = gcache.New(time.Duration(util.RhyCacheDuration)*time.Second, time.Duration(util.RhyCacheDuration)*time.Second/6) // [repoURL]*int64 + +// CleanBazaarPackageCache 清空集市包相关缓存(如切换语言后需刷新展示名等) +func CleanBazaarPackageCache() { + packageInstallSizeCache.Flush() +} + +// ReadInstalledPackageDirs 读取本地集市包的目录列表 +func ReadInstalledPackageDirs(basePath string) ([]os.DirEntry, error) { + if !util.IsPathRegularDirOrSymlinkDir(basePath) { + return []os.DirEntry{}, nil + } + + entries, err := os.ReadDir(basePath) + if err != nil { + return nil, err + } + + dirs := make([]os.DirEntry, 0, len(entries)) + for _, e := range entries { + if util.IsDirRegularOrSymlink(e) { + dirs = append(dirs, e) + } + } + return dirs, nil +} + +// SetInstalledPackageMetadata 设置本地集市包的通用元数据 +func SetInstalledPackageMetadata(pkg *Package, installPath, baseURLPath, pkgType string, bazaarPackagesMap map[string]*Package) bool { + // 展示信息 + pkg.IconURL = baseURLPath + "icon.png" + pkg.PreviewURL = baseURLPath + "preview.png" + pkg.PreferredName = GetPreferredLocaleString(pkg.DisplayName, pkg.Name) + pkg.PreferredDesc = GetPreferredLocaleString(pkg.Description, "") + pkg.PreferredReadme = getInstalledPackageREADME(installPath, baseURLPath, pkg.Readme) + pkg.PreferredFunding = getPreferredFunding(pkg.Funding) + + // 更新信息 + pkg.Installed = true + pkg.DisallowInstall = isBelowRequiredAppVersion(pkg) + if bazaarPkg := bazaarPackagesMap[pkg.Name]; nil != bazaarPkg { + pkg.DisallowUpdate = isBelowRequiredAppVersion(bazaarPkg) + pkg.UpdateRequiredMinAppVer = bazaarPkg.MinAppVersion + pkg.RepoURL = bazaarPkg.RepoURL // 更新链接使用在线数据,避免本地元数据的链接错误 + + if 0 > semver.Compare("v"+pkg.Version, "v"+bazaarPkg.Version) { + pkg.RepoHash = bazaarPkg.RepoHash + pkg.Outdated = true + } + } else { + pkg.RepoURL = pkg.URL + } + + // 安装信息 + pkg.HInstallDate = getPackageHInstallDate(pkgType, pkg.Name, installPath) + // TODO 本地安装大小的缓存改成 1 分钟有效,打开集市包 README 的时候才遍历集市包文件夹进行统计,异步返回结果到前端显示 https://github.com/siyuan-note/siyuan/issues/16983 + // 目前优先使用在线 stage 数据:不耗时,但可能不准确,比如本地旧版本与云端最新版本的安装大小可能不一致;其次使用本地目录大小:耗时,但准确 + if installSize, ok := packageInstallSizeCache.Get(pkg.RepoURL); ok { + pkg.InstallSize = installSize.(int64) + } else { + size, _ := util.SizeOfDirectory(installPath) + pkg.InstallSize = size + packageInstallSizeCache.SetDefault(pkg.RepoURL, size) + } + pkg.HInstallSize = humanize.BytesCustomCeil(uint64(pkg.InstallSize), 2) + + return true +} + +// Add marketplace package config item `minAppVersion` https://github.com/siyuan-note/siyuan/issues/8330 +func isBelowRequiredAppVersion(pkg *Package) bool { + // 如果包没有指定 minAppVersion,则允许安装 + if "" == pkg.MinAppVersion { + return false + } + + // 如果包要求的 minAppVersion 大于当前版本,则不允许安装 + if 0 < semver.Compare("v"+pkg.MinAppVersion, "v"+util.Ver) { + return true + } + return false +} + +// BazaarInfo 集市的持久化信息 +type BazaarInfo struct { + Packages map[string]map[string]*PackageInfo `json:"packages"` +} + +// PackageInfo 集市包的持久化信息 +type PackageInfo struct { + InstallTime int64 `json:"installTime"` // 安装时间戳(毫秒) +} + +var ( + bazaarInfoCache *BazaarInfo + bazaarInfoModTime time.Time + bazaarInfoCacheLock = sync.RWMutex{} + bazaarInfoSingleFlight singleflight.Group +) + +// getBazaarInfo 确保集市持久化信息已加载到 bazaarInfoCache +func getBazaarInfo() { + infoPath := filepath.Join(util.DataDir, "storage", "bazaar.json") + info, err := os.Stat(infoPath) + + bazaarInfoCacheLock.RLock() + cache := bazaarInfoCache + modTime := bazaarInfoModTime + bazaarInfoCacheLock.RUnlock() + // 文件修改时间没变则认为缓存有效 + if cache != nil && err == nil && info.ModTime().Equal(modTime) { + return + } + + _, _, _ = bazaarInfoSingleFlight.Do("loadBazaarInfo", func() (interface{}, error) { + // 缓存失效时从磁盘加载 + newRet := loadBazaarInfo() + // 更新缓存和修改时间 + bazaarInfoCacheLock.Lock() + bazaarInfoCache = newRet + if err == nil { + bazaarInfoModTime = info.ModTime() + } + bazaarInfoCacheLock.Unlock() + return newRet, nil + }) +} + +// loadBazaarInfo 从磁盘加载集市持久化信息 +func loadBazaarInfo() (ret *BazaarInfo) { + // 初始化一个空的 BazaarInfo,后续使用时无需判断 nil + ret = &BazaarInfo{ + Packages: make(map[string]map[string]*PackageInfo), + } + + infoDir := filepath.Join(util.DataDir, "storage") + if err := os.MkdirAll(infoDir, 0755); err != nil { + logging.LogErrorf("create bazaar info dir [%s] failed: %s", infoDir, err) + return + } + + infoPath := filepath.Join(infoDir, "bazaar.json") + if !filelock.IsExist(infoPath) { + return + } + + data, err := filelock.ReadFile(infoPath) + if err != nil { + logging.LogErrorf("read bazaar info [%s] failed: %s", infoPath, err) + return + } + + if err = gulu.JSON.UnmarshalJSON(data, &ret); err != nil { + logging.LogErrorf("unmarshal bazaar info [%s] failed: %s", infoPath, err) + ret = &BazaarInfo{ + Packages: make(map[string]map[string]*PackageInfo), + } + } + + return +} + +// saveBazaarInfo 保存集市持久化信息(调用者需持有写锁) +func saveBazaarInfo() { + infoPath := filepath.Join(util.DataDir, "storage", "bazaar.json") + + data, err := gulu.JSON.MarshalIndentJSON(bazaarInfoCache, "", "\t") + if err != nil { + logging.LogErrorf("marshal bazaar info [%s] failed: %s", infoPath, err) + return + } + if err = filelock.WriteFile(infoPath, data); err != nil { + logging.LogErrorf("write bazaar info [%s] failed: %s", infoPath, err) + return + } + + if fi, statErr := os.Stat(infoPath); statErr == nil { + bazaarInfoModTime = fi.ModTime() + } +} + +// setPackageInstallTime 设置集市包的安装时间 +func setPackageInstallTime(pkgType, pkgName string, installTime time.Time) { + getBazaarInfo() + + bazaarInfoCacheLock.Lock() + defer bazaarInfoCacheLock.Unlock() + + if bazaarInfoCache == nil { + return + } + if bazaarInfoCache.Packages[pkgType] == nil { + bazaarInfoCache.Packages[pkgType] = make(map[string]*PackageInfo) + } + p := bazaarInfoCache.Packages[pkgType][pkgName] + if p == nil { + p = &PackageInfo{} + bazaarInfoCache.Packages[pkgType][pkgName] = p + } + p.InstallTime = installTime.UnixMilli() + saveBazaarInfo() +} + +// getPackageHInstallDate 获取集市包的安装日期 +func getPackageHInstallDate(pkgType, pkgName, installPath string) string { + getBazaarInfo() + bazaarInfoCacheLock.RLock() + var installTime int64 + if bazaarInfoCache != nil && bazaarInfoCache.Packages[pkgType] != nil { + if p := bazaarInfoCache.Packages[pkgType][pkgName]; p != nil { + installTime = p.InstallTime + } + } + bazaarInfoCacheLock.RUnlock() + + if installTime > 0 { + return time.UnixMilli(installTime).Format("2006-01-02") + } + + // 如果 bazaar.json 中没有记录,使用文件夹修改时间并记录到 bazaar.json 中 + fi, err := os.Stat(installPath) + if err != nil { + logging.LogWarnf("stat install package folder [%s] failed: %s", installPath, err) + return time.Now().Format("2006-01-02") + } + setPackageInstallTime(pkgType, pkgName, fi.ModTime()) + + return fi.ModTime().Format("2006-01-02") +} + +// RemovePackageInfo 删除集市包的持久化信息 +func RemovePackageInfo(pkgType, pkgName string) { + getBazaarInfo() + + bazaarInfoCacheLock.Lock() + defer bazaarInfoCacheLock.Unlock() + + if bazaarInfoCache != nil && bazaarInfoCache.Packages[pkgType] != nil { + delete(bazaarInfoCache.Packages[pkgType], pkgName) + } + + saveBazaarInfo() +} diff --git a/kernel/bazaar/package.go b/kernel/bazaar/package.go index 6ecaae1c7..78efb8f65 100644 --- a/kernel/bazaar/package.go +++ b/kernel/bazaar/package.go @@ -17,24 +17,16 @@ package bazaar import ( - "bytes" - "errors" - "fmt" + "html" "os" - "path/filepath" + "path" "strings" "sync" - "time" "github.com/88250/gulu" - "github.com/araddon/dateparse" - "github.com/imroc/req/v3" - gcache "github.com/patrickmn/go-cache" "github.com/siyuan-note/filelock" - "github.com/siyuan-note/httpclient" "github.com/siyuan-note/logging" "github.com/siyuan-note/siyuan/kernel/util" - "golang.org/x/mod/semver" ) // LocaleStrings 表示按语种 key 的字符串表,key 为语种如 "default"、"en_US"、"zh_CN" 等 @@ -47,6 +39,8 @@ type Funding struct { Custom []string `json:"custom"` } +// Package 描述了集市包元数据和传递给前端的其他信息。 +// - 集市包新增元数据字段需要同步修改 bazaar 的工作流,参考 https://github.com/siyuan-note/bazaar/commit/aa36d0003139c52d8e767c6e18a635be006323e2 type Package struct { Author string `json:"author"` URL string `json:"url"` @@ -66,12 +60,11 @@ type Package struct { PreferredDesc string `json:"preferredDesc"` PreferredReadme string `json:"preferredReadme"` - Name string `json:"name"` - RepoURL string `json:"repoURL"` - RepoHash string `json:"repoHash"` - PreviewURL string `json:"previewURL"` - PreviewURLThumb string `json:"previewURLThumb"` - IconURL string `json:"iconURL"` + Name string `json:"name"` // 包名,不一定是仓库名 + RepoURL string `json:"repoURL"` // 形式为 https://github.com/owner/repo + RepoHash string `json:"repoHash"` + PreviewURL string `json:"previewURL"` + IconURL string `json:"iconURL"` Installed bool `json:"installed"` Outdated bool `json:"outdated"` @@ -90,11 +83,14 @@ type Package struct { DisallowUpdate bool `json:"disallowUpdate"` UpdateRequiredMinAppVer string `json:"updateRequiredMinAppVer"` - Incompatible bool `json:"incompatible"` + // 专用字段,nil 时不序列化 + Incompatible *bool `json:"incompatible,omitempty"` // Plugin:是否不兼容 + Enabled *bool `json:"enabled,omitempty"` // Plugin:是否启用 + Modes *[]string `json:"modes,omitempty"` // Theme:支持的模式列表 } type StageRepo struct { - URL string `json:"url"` + URL string `json:"url"` // owner/repo@hash 形式 Updated string `json:"updated"` Stars int `json:"stars"` OpenIssues int `json:"openIssues"` @@ -107,10 +103,48 @@ type StageRepo struct { type StageIndex struct { Repos []*StageRepo `json:"repos"` + + reposByURL map[string]*StageRepo // 不序列化,首次按 URL 查找时懒构建 + reposOnce sync.Once } -// getPreferredLocaleString 从 LocaleStrings 中按当前语种取值,无则回退 default、en_US,再回退 fallback。 -func getPreferredLocaleString(m LocaleStrings, fallback string) string { +// ParsePackageJSON 解析集市包 JSON 文件 +func ParsePackageJSON(filePath string) (ret *Package, err error) { + if !filelock.IsExist(filePath) { + err = os.ErrNotExist + return + } + data, err := filelock.ReadFile(filePath) + if err != nil { + logging.LogErrorf("read [%s] failed: %s", filePath, err) + return + } + if err = gulu.JSON.UnmarshalJSON(data, &ret); err != nil { + logging.LogErrorf("parse [%s] failed: %s", filePath, err) + return + } + + // 仅对本地集市包做 HTML 转义,在线 stage 由 bazaar 工作流处理 + sanitizePackageDisplayStrings(ret) + ret.URL = strings.TrimSuffix(ret.URL, "/") + return +} + +// sanitizePackageDisplayStrings 对集市包直接显示的信息做 HTML 转义,避免 XSS。 +func sanitizePackageDisplayStrings(pkg *Package) { + if pkg == nil { + return + } + for k, v := range pkg.DisplayName { + pkg.DisplayName[k] = html.EscapeString(v) + } + for k, v := range pkg.Description { + pkg.Description[k] = html.EscapeString(v) + } +} + +// GetPreferredLocaleString 从 LocaleStrings 中按当前语种取值,无则回退 default、en_US,再回退 fallback。 +func GetPreferredLocaleString(m LocaleStrings, fallback string) string { if len(m) == 0 { return fallback } @@ -126,40 +160,19 @@ func getPreferredLocaleString(m LocaleStrings, fallback string) string { return fallback } -func GetPreferredName(pkg *Package) string { - return getPreferredLocaleString(pkg.DisplayName, pkg.Name) -} - -func getPreferredDesc(desc LocaleStrings) string { - return getPreferredLocaleString(desc, "") -} - -func getPreferredReadme(readme LocaleStrings) string { - return getPreferredLocaleString(readme, "README.md") -} - +// getPreferredFunding 获取包的首选赞助链接 func getPreferredFunding(funding *Funding) string { if nil == funding { return "" } - - if "" != funding.OpenCollective { - if strings.HasPrefix(funding.OpenCollective, "http://") || strings.HasPrefix(funding.OpenCollective, "https://") { - return funding.OpenCollective - } - return "https://opencollective.com/" + funding.OpenCollective + if v := normalizeFundingURL(funding.OpenCollective, "https://opencollective.com/"); "" != v { + return v } - if "" != funding.Patreon { - if strings.HasPrefix(funding.Patreon, "http://") || strings.HasPrefix(funding.Patreon, "https://") { - return funding.Patreon - } - return "https://www.patreon.com/" + funding.Patreon + if v := normalizeFundingURL(funding.Patreon, "https://www.patreon.com/"); "" != v { + return v } - if "" != funding.GitHub { - if strings.HasPrefix(funding.GitHub, "http://") || strings.HasPrefix(funding.GitHub, "https://") { - return funding.GitHub - } - return "https://github.com/sponsors/" + funding.GitHub + if v := normalizeFundingURL(funding.GitHub, "https://github.com/sponsors/"); "" != v { + return v } if 0 < len(funding.Custom) { return funding.Custom[0] @@ -167,249 +180,82 @@ func getPreferredFunding(funding *Funding) string { return "" } -func PluginJSON(pluginDirName string) (ret *Plugin, err error) { - p := filepath.Join(util.DataDir, "plugins", pluginDirName, "plugin.json") - if !filelock.IsExist(p) { - err = os.ErrNotExist - return +func normalizeFundingURL(s, base string) string { + if "" == s { + return "" } - data, err := filelock.ReadFile(p) - if err != nil { - logging.LogErrorf("read plugin.json [%s] failed: %s", p, err) - return + if strings.HasPrefix(s, "https://") || strings.HasPrefix(s, "http://") { + return s } - if err = gulu.JSON.UnmarshalJSON(data, &ret); err != nil { - logging.LogErrorf("parse plugin.json [%s] failed: %s", p, err) - return - } - - ret.URL = strings.TrimSuffix(ret.URL, "/") - return + return base + s } -func WidgetJSON(widgetDirName string) (ret *Widget, err error) { - p := filepath.Join(util.DataDir, "widgets", widgetDirName, "widget.json") - if !filelock.IsExist(p) { - err = os.ErrNotExist - return +// FilterPackages 按关键词过滤集市包列表 +func FilterPackages(packages []*Package, keyword string) []*Package { + keywords := getSearchKeywords(keyword) + if 0 == len(keywords) { + return packages } - data, err := filelock.ReadFile(p) - if err != nil { - logging.LogErrorf("read widget.json [%s] failed: %s", p, err) - return - } - if err = gulu.JSON.UnmarshalJSON(data, &ret); err != nil { - logging.LogErrorf("parse widget.json [%s] failed: %s", p, err) - return - } - - ret.URL = strings.TrimSuffix(ret.URL, "/") - return -} - -func IconJSON(iconDirName string) (ret *Icon, err error) { - p := filepath.Join(util.IconsPath, iconDirName, "icon.json") - if !gulu.File.IsExist(p) { - err = os.ErrNotExist - return - } - data, err := os.ReadFile(p) - if err != nil { - logging.LogErrorf("read icon.json [%s] failed: %s", p, err) - return - } - if err = gulu.JSON.UnmarshalJSON(data, &ret); err != nil { - logging.LogErrorf("parse icon.json [%s] failed: %s", p, err) - return - } - - ret.URL = strings.TrimSuffix(ret.URL, "/") - return -} - -func TemplateJSON(templateDirName string) (ret *Template, err error) { - p := filepath.Join(util.DataDir, "templates", templateDirName, "template.json") - if !filelock.IsExist(p) { - err = os.ErrNotExist - return - } - data, err := filelock.ReadFile(p) - if err != nil { - logging.LogErrorf("read template.json [%s] failed: %s", p, err) - return - } - if err = gulu.JSON.UnmarshalJSON(data, &ret); err != nil { - logging.LogErrorf("parse template.json [%s] failed: %s", p, err) - return - } - - ret.URL = strings.TrimSuffix(ret.URL, "/") - return -} - -func ThemeJSON(themeDirName string) (ret *Theme, err error) { - p := filepath.Join(util.ThemesPath, themeDirName, "theme.json") - if !gulu.File.IsExist(p) { - err = os.ErrNotExist - return - } - data, err := os.ReadFile(p) - if err != nil { - logging.LogErrorf("read theme.json [%s] failed: %s", p, err) - return - } - - ret = &Theme{} - if err = gulu.JSON.UnmarshalJSON(data, &ret); err != nil { - logging.LogErrorf("parse theme.json [%s] failed: %s", p, err) - return - } - - ret.URL = strings.TrimSuffix(ret.URL, "/") - return -} - -var ( - packageLocks = map[string]*sync.Mutex{} - packageLocksLock = sync.Mutex{} -) - -func downloadPackage(repoURLHash string, pushProgress bool, systemID string) (data []byte, err error) { - packageLocksLock.Lock() - defer packageLocksLock.Unlock() - - // repoURLHash: https://github.com/88250/Comfortably-Numb@6286912c381ef3f83e455d06ba4d369c498238dc - repoURL := repoURLHash[:strings.LastIndex(repoURLHash, "@")] - lock, ok := packageLocks[repoURLHash] - if !ok { - lock = &sync.Mutex{} - packageLocks[repoURLHash] = lock - } - lock.Lock() - defer lock.Unlock() - - repoURLHash = strings.TrimPrefix(repoURLHash, "https://github.com/") - u := util.BazaarOSSServer + "/package/" + repoURLHash - buf := &bytes.Buffer{} - resp, err := httpclient.NewCloudFileRequest2m().SetOutput(buf).SetDownloadCallback(func(info req.DownloadInfo) { - if pushProgress { - progress := float32(info.DownloadedSize) / float32(info.Response.ContentLength) - //logging.LogDebugf("downloading bazaar package [%f]", progress) - util.PushDownloadProgress(repoURL, progress) + ret := []*Package{} + for _, pkg := range packages { + if packageContainsKeywords(pkg, keywords) { + ret = append(ret, pkg) } - }).Get(u) - if err != nil { - logging.LogErrorf("get bazaar package [%s] failed: %s", u, err) - return nil, errors.New("get bazaar package failed, please check your network") } - if 200 != resp.StatusCode { - logging.LogErrorf("get bazaar package [%s] failed: %d", u, resp.StatusCode) - return nil, errors.New("get bazaar package failed: " + resp.Status) - } - data = buf.Bytes() - - go incPackageDownloads(repoURLHash, systemID) - return + return ret } -func incPackageDownloads(repoURLHash, systemID string) { - if strings.Contains(repoURLHash, ".md") || "" == systemID { +func getSearchKeywords(query string) (ret []string) { + query = strings.TrimSpace(query) + if "" == query { return } - - repo := strings.Split(repoURLHash, "@")[0] - u := util.GetCloudServer() + "/apis/siyuan/bazaar/addBazaarPackageDownloadCount" - httpclient.NewCloudRequest30s().SetBody( - map[string]interface{}{ - "systemID": systemID, - "repo": repo, - }).Post(u) -} - -func uninstallPackage(installPath string) (err error) { - if err = os.RemoveAll(installPath); err != nil { - logging.LogErrorf("remove [%s] failed: %s", installPath, err) - return fmt.Errorf("remove community package [%s] failed", filepath.Base(installPath)) - } - packageCache.Flush() - return -} - -func installPackage(data []byte, installPath, repoURLHash string) (err error) { - err = installPackage0(data, installPath) - if err != nil { - return - } - - packageCache.Delete(strings.TrimPrefix(repoURLHash, "https://github.com/")) - return -} - -func installPackage0(data []byte, installPath string) (err error) { - tmpPackage := filepath.Join(util.TempDir, "bazaar", "package") - if err = os.MkdirAll(tmpPackage, 0755); err != nil { - return - } - name := gulu.Rand.String(7) - tmp := filepath.Join(tmpPackage, name+".zip") - if err = os.WriteFile(tmp, data, 0644); err != nil { - return - } - - unzipPath := filepath.Join(tmpPackage, name) - if err = gulu.Zip.Unzip(tmp, unzipPath); err != nil { - logging.LogErrorf("write file [%s] failed: %s", installPath, err) - return - } - - dirs, err := os.ReadDir(unzipPath) - if err != nil { - return - } - - srcPath := unzipPath - if 1 == len(dirs) && dirs[0].IsDir() { - srcPath = filepath.Join(unzipPath, dirs[0].Name()) - } - - if err = filelock.Copy(srcPath, installPath); err != nil { - return - } - return -} - -func formatUpdated(updated string) (ret string) { - t, e := dateparse.ParseIn(updated, time.Now().Location()) - if nil == e { - ret = t.Format("2006-01-02") - } else { - if strings.Contains(updated, "T") { - ret = updated[:strings.Index(updated, "T")] - } else { - ret = strings.ReplaceAll(strings.ReplaceAll(updated, "T", ""), "Z", "") + keywords := strings.Split(query, " ") + for _, k := range keywords { + if "" != k { + ret = append(ret, strings.ToLower(k)) } } return } -// Add marketplace package config item `minAppVersion` https://github.com/siyuan-note/siyuan/issues/8330 -func disallowInstallBazaarPackage(pkg *Package) bool { - // 如果包没有指定 minAppVersion,则允许安装 - if "" == pkg.MinAppVersion { +func packageContainsKeywords(pkg *Package, keywords []string) bool { + if 0 == len(keywords) { + return true + } + if nil == pkg { return false } + for _, kw := range keywords { + if !packageContainsKeyword(pkg, kw) { + return false + } + } + return true +} - // 如果包要求的 minAppVersion 大于当前版本,则不允许安装 - if 0 < semver.Compare("v"+pkg.MinAppVersion, "v"+util.Ver) { +func packageContainsKeyword(pkg *Package, kw string) bool { + if strings.Contains(strings.ToLower(pkg.Name), kw) || // https://github.com/siyuan-note/siyuan/issues/10515 + strings.Contains(strings.ToLower(pkg.Author), kw) { // https://github.com/siyuan-note/siyuan/issues/11673 + return true + } + for _, s := range pkg.DisplayName { + if strings.Contains(strings.ToLower(s), kw) { + return true + } + } + for _, s := range pkg.Description { + if strings.Contains(strings.ToLower(s), kw) { + return true + } + } + for _, s := range pkg.Keywords { + if strings.Contains(strings.ToLower(s), kw) { + return true + } + } + if strings.Contains(strings.ToLower(path.Base(pkg.RepoURL)), kw) { // 仓库名,不一定是包名 return true } return false } - -var packageCache = gcache.New(6*time.Hour, 30*time.Minute) // [repoURL]*Package - -func CleanBazaarPackageCache() { - packageCache.Flush() -} - -var packageInstallSizeCache = gcache.New(48*time.Hour, 6*time.Hour) // [repoURL]*int64 diff --git a/kernel/bazaar/plugin.go b/kernel/bazaar/plugin.go index 190444f69..1264b4848 100644 --- a/kernel/bazaar/plugin.go +++ b/kernel/bazaar/plugin.go @@ -20,83 +20,11 @@ import ( "os" "path/filepath" "runtime" - "sort" - "strings" - "github.com/88250/go-humanize" "github.com/siyuan-note/logging" "github.com/siyuan-note/siyuan/kernel/util" ) -type Plugin struct { - *Package - Enabled bool `json:"enabled"` -} - -// Plugins 返回集市插件列表 -func Plugins(frontend string) (plugins []*Plugin) { - plugins = []*Plugin{} - result := getStageAndBazaar("plugins") - - if !result.Online { - return - } - if result.StageErr != nil { - return - } - if 1 > len(result.BazaarIndex) { - return - } - - for _, repo := range result.StageIndex.Repos { - if nil == repo.Package { - continue - } - plugin := buildPluginFromStageRepo(repo, frontend, result.BazaarIndex) - if nil != plugin { - plugins = append(plugins, plugin) - } - } - - sort.Slice(plugins, func(i, j int) bool { return plugins[i].Updated > plugins[j].Updated }) - return -} - -// buildPluginFromStageRepo 使用 stage 内嵌的 package 构建 *Plugin,不发起 HTTP 请求。 -func buildPluginFromStageRepo(repo *StageRepo, frontend string, bazaarIndex map[string]*bazaarPackage) *Plugin { - pkg := *repo.Package - pkg.URL = strings.TrimSuffix(pkg.URL, "/") - repoURLHash := strings.Split(repo.URL, "@") - if 2 != len(repoURLHash) { - return nil - } - pkg.RepoURL = "https://github.com/" + repoURLHash[0] - pkg.RepoHash = repoURLHash[1] - pkg.PreviewURL = util.BazaarOSSServer + "/package/" + repo.URL + "/preview.png?imageslim" - pkg.PreviewURLThumb = util.BazaarOSSServer + "/package/" + repo.URL + "/preview.png?imageView2/2/w/436/h/232" - pkg.IconURL = util.BazaarOSSServer + "/package/" + repo.URL + "/icon.png" - pkg.Updated = repo.Updated - pkg.Stars = repo.Stars - pkg.OpenIssues = repo.OpenIssues - pkg.Size = repo.Size - pkg.HSize = humanize.BytesCustomCeil(uint64(pkg.Size), 2) - pkg.InstallSize = repo.InstallSize - pkg.HInstallSize = humanize.BytesCustomCeil(uint64(pkg.InstallSize), 2) - pkg.HUpdated = formatUpdated(pkg.Updated) - pkg.PreferredFunding = getPreferredFunding(pkg.Funding) - pkg.PreferredName = GetPreferredName(&pkg) - pkg.PreferredDesc = getPreferredDesc(pkg.Description) - pkg.DisallowInstall = disallowInstallBazaarPackage(&pkg) - pkg.DisallowUpdate = disallowInstallBazaarPackage(&pkg) - pkg.UpdateRequiredMinAppVer = pkg.MinAppVersion - pkg.Incompatible = isIncompatiblePlugin(&Plugin{Package: &pkg}, frontend) - if bp := bazaarIndex[repoURLHash[0]]; nil != bp { - pkg.Downloads = bp.Downloads - } - packageInstallSizeCache.SetDefault(pkg.RepoURL, pkg.InstallSize) - return &Plugin{Package: &pkg} -} - func ParseInstalledPlugin(name, frontend string) (found bool, displayName string, incompatible, disabledInPublish, disallowInstall bool) { pluginsPath := filepath.Join(util.DataDir, "plugins") if !util.IsPathRegularDirOrSymlinkDir(pluginsPath) { @@ -118,142 +46,57 @@ func ParseInstalledPlugin(name, frontend string) (found bool, displayName string continue } - plugin, parseErr := PluginJSON(dirName) + plugin, parseErr := ParsePackageJSON(filepath.Join(util.DataDir, "plugins", dirName, "plugin.json")) if nil != parseErr || nil == plugin { return } found = true - displayName = GetPreferredName(plugin.Package) - incompatible = isIncompatiblePlugin(plugin, frontend) + displayName = GetPreferredLocaleString(plugin.DisplayName, plugin.Name) + incompatible = IsIncompatiblePlugin(plugin, frontend) disabledInPublish = plugin.DisabledInPublish - disallowInstall = disallowInstallBazaarPackage(plugin.Package) + disallowInstall = isBelowRequiredAppVersion(plugin) } return } -func InstalledPlugins(frontend string) (ret []*Plugin) { - ret = []*Plugin{} - - pluginsPath := filepath.Join(util.DataDir, "plugins") - if !util.IsPathRegularDirOrSymlinkDir(pluginsPath) { - return +// IsIncompatiblePlugin 判断插件是否与当前环境不兼容 +func IsIncompatiblePlugin(plugin *Package, frontend string) bool { + backend := getCurrentBackend() + if !isTargetSupported(plugin.Backends, backend) { + return true } - pluginDirs, err := os.ReadDir(pluginsPath) - if err != nil { - logging.LogWarnf("read plugins folder failed: %s", err) - return + if !isTargetSupported(plugin.Frontends, frontend) { + return true } - bazaarPlugins := Plugins(frontend) - - for _, pluginDir := range pluginDirs { - if !util.IsDirRegularOrSymlink(pluginDir) { - continue - } - dirName := pluginDir.Name() - - plugin, parseErr := PluginJSON(dirName) - if nil != parseErr || nil == plugin { - continue - } - - plugin.RepoURL = plugin.URL - plugin.DisallowInstall = disallowInstallBazaarPackage(plugin.Package) - if bazaarPkg := getBazaarPlugin(plugin.Name, bazaarPlugins); nil != bazaarPkg { - plugin.DisallowUpdate = disallowInstallBazaarPackage(bazaarPkg.Package) - plugin.UpdateRequiredMinAppVer = bazaarPkg.MinAppVersion - plugin.RepoURL = bazaarPkg.RepoURL - } - - installPath := filepath.Join(util.DataDir, "plugins", dirName) - plugin.Installed = true - plugin.PreviewURL = "/plugins/" + dirName + "/preview.png" - plugin.PreviewURLThumb = "/plugins/" + dirName + "/preview.png" - plugin.IconURL = "/plugins/" + dirName + "/icon.png" - plugin.PreferredFunding = getPreferredFunding(plugin.Funding) - plugin.PreferredName = GetPreferredName(plugin.Package) - plugin.PreferredDesc = getPreferredDesc(plugin.Description) - info, statErr := os.Stat(filepath.Join(installPath, "plugin.json")) - if nil != statErr { - logging.LogWarnf("stat install plugin.json failed: %s", statErr) - continue - } - plugin.HInstallDate = info.ModTime().Format("2006-01-02") - if installSize, ok := packageInstallSizeCache.Get(plugin.RepoURL); ok { - plugin.InstallSize = installSize.(int64) - } else { - is, _ := util.SizeOfDirectory(installPath) - plugin.InstallSize = is - packageInstallSizeCache.SetDefault(plugin.RepoURL, is) - } - plugin.HInstallSize = humanize.BytesCustomCeil(uint64(plugin.InstallSize), 2) - plugin.PreferredReadme = getInstalledPackageREADME(installPath, "/plugins/"+dirName+"/", plugin.Readme) - plugin.Outdated = isOutdatedPlugin(plugin, bazaarPlugins) - plugin.Incompatible = isIncompatiblePlugin(plugin, frontend) - ret = append(ret, plugin) - } - return + return false } -func getBazaarPlugin(name string, plugins []*Plugin) *Plugin { - for _, p := range plugins { - if p.Name == name { - return p - } - } - return nil -} - -func InstallPlugin(repoURL, repoHash, installPath string, systemID string) error { - repoURLHash := repoURL + "@" + repoHash - data, err := downloadPackage(repoURLHash, true, systemID) - if err != nil { - return err - } - return installPackage(data, installPath, repoURLHash) -} - -func UninstallPlugin(installPath string) error { - return uninstallPackage(installPath) -} - -func isIncompatiblePlugin(plugin *Plugin, currentFrontend string) bool { - if 1 > len(plugin.Backends) { - return false - } - - currentBackend := getCurrentBackend() - backendOk := false - for _, backend := range plugin.Backends { - if backend == currentBackend || "all" == backend { - backendOk = true - break - } - } - - frontendOk := false - for _, frontend := range plugin.Frontends { - if frontend == currentFrontend || "all" == frontend { - frontendOk = true - break - } - } - return !backendOk || !frontendOk -} +var cachedBackend string func getCurrentBackend() string { - switch util.Container { - case util.ContainerDocker: - return "docker" - case util.ContainerIOS: - return "ios" - case util.ContainerAndroid: - return "android" - case util.ContainerHarmony: - return "harmony" - default: - return runtime.GOOS + if cachedBackend == "" { + if util.Container == util.ContainerStd { + cachedBackend = runtime.GOOS + } else { + cachedBackend = util.Container + } } + return cachedBackend +} + +// isTargetSupported 检查 platforms 中是否包含 target 或 "all" +func isTargetSupported(platforms []string, target string) bool { + // 缺失字段时跳过检查,相当于 all + if len(platforms) == 0 { + return true + } + for _, v := range platforms { + if v == target || v == "all" { + return true + } + } + return false } diff --git a/kernel/bazaar/readme.go b/kernel/bazaar/readme.go index c68c8f8d4..82a709ffb 100644 --- a/kernel/bazaar/readme.go +++ b/kernel/bazaar/readme.go @@ -36,7 +36,7 @@ import ( // getReadmeFileCandidates 根据包的 README 配置返回去重的按优先级排序的 README 候选文件名列表:当前语言首选、default、README.md。 func getReadmeFileCandidates(readme LocaleStrings) []string { - preferred := getPreferredReadme(readme) + preferred := GetPreferredLocaleString(readme, "README.md") defaultName := "README.md" if v := strings.TrimSpace(readme["default"]); v != "" { defaultName = v @@ -76,7 +76,7 @@ func GetBazaarPackageREADME(ctx context.Context, repoURL, repoHash, packageType var loadErr error var errMsgs []string for _, name := range candidates { - data, loadErr = downloadPackage(repoURLHash+"/"+name, false, "") + data, loadErr = downloadBazaarFile(repoURLHash+"/"+name, false) if loadErr == nil { break } diff --git a/kernel/bazaar/stage.go b/kernel/bazaar/stage.go index 6b90f4797..2f66b8302 100644 --- a/kernel/bazaar/stage.go +++ b/kernel/bazaar/stage.go @@ -19,14 +19,12 @@ package bazaar import ( "context" "errors" - "strings" "sync" "time" "github.com/siyuan-note/httpclient" "github.com/siyuan-note/logging" "github.com/siyuan-note/siyuan/kernel/util" - "golang.org/x/mod/semver" "golang.org/x/sync/singleflight" ) @@ -164,106 +162,6 @@ func getStageIndex(ctx context.Context, pkgType string) (ret *StageIndex, err er return } -func isOutdatedTheme(theme *Theme, bazaarThemes []*Theme) bool { - if !strings.HasPrefix(theme.URL, "https://github.com/") { - return false - } - - repo := strings.TrimPrefix(theme.URL, "https://github.com/") - parts := strings.Split(repo, "/") - if 2 != len(parts) || "" == strings.TrimSpace(parts[1]) { - return false - } - - for _, pkg := range bazaarThemes { - if theme.Name == pkg.Name && 0 > semver.Compare("v"+theme.Version, "v"+pkg.Version) { - theme.RepoHash = pkg.RepoHash - return true - } - } - return false -} - -func isOutdatedIcon(icon *Icon, bazaarIcons []*Icon) bool { - if !strings.HasPrefix(icon.URL, "https://github.com/") { - return false - } - - repo := strings.TrimPrefix(icon.URL, "https://github.com/") - parts := strings.Split(repo, "/") - if 2 != len(parts) || "" == strings.TrimSpace(parts[1]) { - return false - } - - for _, pkg := range bazaarIcons { - if icon.Name == pkg.Name && 0 > semver.Compare("v"+icon.Version, "v"+pkg.Version) { - icon.RepoHash = pkg.RepoHash - return true - } - } - 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.Name == pkg.Name && 0 > semver.Compare("v"+plugin.Version, "v"+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 - } - - repo := strings.TrimPrefix(widget.URL, "https://github.com/") - parts := strings.Split(repo, "/") - if 2 != len(parts) || "" == strings.TrimSpace(parts[1]) { - return false - } - - for _, pkg := range bazaarWidgets { - if widget.Name == pkg.Name && 0 > semver.Compare("v"+widget.Version, "v"+pkg.Version) { - widget.RepoHash = pkg.RepoHash - return true - } - } - return false -} - -func isOutdatedTemplate(template *Template, bazaarTemplates []*Template) bool { - if !strings.HasPrefix(template.URL, "https://github.com/") { - return false - } - - repo := strings.TrimPrefix(template.URL, "https://github.com/") - parts := strings.Split(repo, "/") - if 2 != len(parts) || "" == strings.TrimSpace(parts[1]) { - return false - } - - for _, pkg := range bazaarTemplates { - if template.Name == pkg.Name && 0 > semver.Compare("v"+template.Version, "v"+pkg.Version) { - template.RepoHash = pkg.RepoHash - return true - } - } - return false -} - func isBazzarOnline() bool { v, err, _ := onlineCheckFlight.Do("bazaarOnline", func() (interface{}, error) { return isBazzarOnline0(), nil diff --git a/kernel/bazaar/template.go b/kernel/bazaar/template.go deleted file mode 100644 index dfab139a8..000000000 --- a/kernel/bazaar/template.go +++ /dev/null @@ -1,202 +0,0 @@ -// SiYuan - Refactor your thinking -// 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 ( - "os" - "path/filepath" - "sort" - "strings" - "time" - - "github.com/88250/go-humanize" - "github.com/siyuan-note/logging" - "github.com/siyuan-note/siyuan/kernel/util" -) - -type Template struct { - *Package -} - -// Templates 返回集市模板列表 -func Templates() (templates []*Template) { - templates = []*Template{} - result := getStageAndBazaar("templates") - - if !result.Online { - return - } - if result.StageErr != nil { - return - } - if 1 > len(result.BazaarIndex) { - return - } - - for _, repo := range result.StageIndex.Repos { - if nil == repo.Package { - continue - } - template := buildTemplateFromStageRepo(repo, result.BazaarIndex) - if nil != template { - templates = append(templates, template) - } - } - - templates = filterLegacyTemplates(templates) - - sort.Slice(templates, func(i, j int) bool { return templates[i].Updated > templates[j].Updated }) - return -} - -// buildTemplateFromStageRepo 使用 stage 内嵌的 package 构建 *Template,不发起 HTTP 请求。 -func buildTemplateFromStageRepo(repo *StageRepo, bazaarIndex map[string]*bazaarPackage) *Template { - pkg := *repo.Package - pkg.URL = strings.TrimSuffix(pkg.URL, "/") - repoURLHash := strings.Split(repo.URL, "@") - if 2 != len(repoURLHash) { - return nil - } - pkg.RepoURL = "https://github.com/" + repoURLHash[0] - pkg.RepoHash = repoURLHash[1] - pkg.PreviewURL = util.BazaarOSSServer + "/package/" + repo.URL + "/preview.png?imageslim" - pkg.PreviewURLThumb = util.BazaarOSSServer + "/package/" + repo.URL + "/preview.png?imageView2/2/w/436/h/232" - pkg.IconURL = util.BazaarOSSServer + "/package/" + repo.URL + "/icon.png" - pkg.Updated = repo.Updated - pkg.Stars = repo.Stars - pkg.OpenIssues = repo.OpenIssues - pkg.Size = repo.Size - pkg.HSize = humanize.BytesCustomCeil(uint64(pkg.Size), 2) - pkg.InstallSize = repo.InstallSize - pkg.HInstallSize = humanize.BytesCustomCeil(uint64(pkg.InstallSize), 2) - pkg.HUpdated = formatUpdated(pkg.Updated) - pkg.PreferredFunding = getPreferredFunding(pkg.Funding) - pkg.PreferredName = GetPreferredName(&pkg) - pkg.PreferredDesc = getPreferredDesc(pkg.Description) - pkg.DisallowInstall = disallowInstallBazaarPackage(&pkg) - pkg.DisallowUpdate = disallowInstallBazaarPackage(&pkg) - pkg.UpdateRequiredMinAppVer = pkg.MinAppVersion - if bp := bazaarIndex[repoURLHash[0]]; nil != bp { - pkg.Downloads = bp.Downloads - } - packageInstallSizeCache.SetDefault(pkg.RepoURL, pkg.InstallSize) - return &Template{Package: &pkg} -} - -func InstalledTemplates() (ret []*Template) { - ret = []*Template{} - - templatesPath := filepath.Join(util.DataDir, "templates") - if !util.IsPathRegularDirOrSymlinkDir(templatesPath) { - return - } - - templateDirs, err := os.ReadDir(templatesPath) - if err != nil { - logging.LogWarnf("read templates folder failed: %s", err) - return - } - - bazaarTemplates := Templates() - - for _, templateDir := range templateDirs { - if !util.IsDirRegularOrSymlink(templateDir) { - continue - } - dirName := templateDir.Name() - - template, parseErr := TemplateJSON(dirName) - if nil != parseErr || nil == template { - continue - } - - template.RepoURL = template.URL - template.DisallowInstall = disallowInstallBazaarPackage(template.Package) - if bazaarPkg := getBazaarTemplate(template.Name, bazaarTemplates); nil != bazaarPkg { - template.DisallowUpdate = disallowInstallBazaarPackage(bazaarPkg.Package) - template.UpdateRequiredMinAppVer = bazaarPkg.MinAppVersion - template.RepoURL = bazaarPkg.RepoURL - } - - installPath := filepath.Join(util.DataDir, "templates", dirName) - template.Installed = true - template.PreviewURL = "/templates/" + dirName + "/preview.png" - template.PreviewURLThumb = "/templates/" + dirName + "/preview.png" - template.IconURL = "/templates/" + dirName + "/icon.png" - template.PreferredFunding = getPreferredFunding(template.Funding) - template.PreferredName = GetPreferredName(template.Package) - template.PreferredDesc = getPreferredDesc(template.Description) - info, statErr := os.Stat(filepath.Join(installPath, "template.json")) - if nil != statErr { - logging.LogWarnf("stat install template.json failed: %s", statErr) - continue - } - template.HInstallDate = info.ModTime().Format("2006-01-02") - if installSize, ok := packageInstallSizeCache.Get(template.RepoURL); ok { - template.InstallSize = installSize.(int64) - } else { - is, _ := util.SizeOfDirectory(installPath) - template.InstallSize = is - packageInstallSizeCache.SetDefault(template.RepoURL, is) - } - template.HInstallSize = humanize.BytesCustomCeil(uint64(template.InstallSize), 2) - template.PreferredReadme = getInstalledPackageREADME(installPath, "/templates/"+dirName+"/", template.Readme) - template.Outdated = isOutdatedTemplate(template, bazaarTemplates) - ret = append(ret, template) - } - return -} - -func getBazaarTemplate(name string, templates []*Template) *Template { - for _, p := range templates { - if p.Name == name { - return p - } - } - return nil -} - -func InstallTemplate(repoURL, repoHash, installPath string, systemID string) error { - repoURLHash := repoURL + "@" + repoHash - data, err := downloadPackage(repoURLHash, true, systemID) - if err != nil { - return err - } - return installPackage(data, installPath, repoURLHash) -} - -func UninstallTemplate(installPath string) error { - return uninstallPackage(installPath) -} - -func filterLegacyTemplates(templates []*Template) (ret []*Template) { - verTime, _ := time.Parse("2006-01-02T15:04:05", "2021-05-12T00:00:00") - for _, theme := range templates { - if "" != theme.Updated { - updated := theme.Updated[:len("2006-01-02T15:04:05")] - t, err := time.Parse("2006-01-02T15:04:05", updated) - if err != nil { - logging.LogErrorf("convert update time [%s] failed: %s", updated, err) - continue - } - if t.After(verTime) { - ret = append(ret, theme) - } - } - } - return -} diff --git a/kernel/bazaar/theme.go b/kernel/bazaar/theme.go deleted file mode 100644 index a2e2f76fd..000000000 --- a/kernel/bazaar/theme.go +++ /dev/null @@ -1,190 +0,0 @@ -// SiYuan - Refactor your thinking -// 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 ( - "os" - "path/filepath" - "sort" - "strings" - - "github.com/88250/go-humanize" - "github.com/siyuan-note/logging" - "github.com/siyuan-note/siyuan/kernel/util" -) - -type Theme struct { - *Package - - Modes []string `json:"modes"` -} - -// Themes 返回集市主题列表 -func Themes() (ret []*Theme) { - ret = []*Theme{} - result := getStageAndBazaar("themes") - - if !result.Online { - return - } - if result.StageErr != nil { - return - } - if 1 > len(result.BazaarIndex) { - return - } - - for _, repo := range result.StageIndex.Repos { - if nil == repo.Package { - continue - } - theme := buildThemeFromStageRepo(repo, result.BazaarIndex) - if nil != theme { - ret = append(ret, theme) - } - } - - sort.Slice(ret, func(i, j int) bool { return ret[i].Updated > ret[j].Updated }) - return -} - -// buildThemeFromStageRepo 使用 stage 内嵌的 package 构建 *Theme,不发起 HTTP 请求。 -func buildThemeFromStageRepo(repo *StageRepo, bazaarIndex map[string]*bazaarPackage) *Theme { - pkg := *repo.Package - pkg.URL = strings.TrimSuffix(pkg.URL, "/") - repoURLHash := strings.Split(repo.URL, "@") - if 2 != len(repoURLHash) { - return nil - } - pkg.RepoURL = "https://github.com/" + repoURLHash[0] - pkg.RepoHash = repoURLHash[1] - pkg.PreviewURL = util.BazaarOSSServer + "/package/" + repo.URL + "/preview.png?imageslim" - pkg.PreviewURLThumb = util.BazaarOSSServer + "/package/" + repo.URL + "/preview.png?imageView2/2/w/436/h/232" - pkg.IconURL = util.BazaarOSSServer + "/package/" + repo.URL + "/icon.png" - pkg.Updated = repo.Updated - pkg.Stars = repo.Stars - pkg.OpenIssues = repo.OpenIssues - pkg.Size = repo.Size - pkg.HSize = humanize.BytesCustomCeil(uint64(pkg.Size), 2) - pkg.InstallSize = repo.InstallSize - pkg.HInstallSize = humanize.BytesCustomCeil(uint64(pkg.InstallSize), 2) - pkg.HUpdated = formatUpdated(pkg.Updated) - pkg.PreferredFunding = getPreferredFunding(pkg.Funding) - pkg.PreferredName = GetPreferredName(&pkg) - pkg.PreferredDesc = getPreferredDesc(pkg.Description) - pkg.DisallowInstall = disallowInstallBazaarPackage(&pkg) - pkg.DisallowUpdate = disallowInstallBazaarPackage(&pkg) - pkg.UpdateRequiredMinAppVer = pkg.MinAppVersion - if bp := bazaarIndex[repoURLHash[0]]; nil != bp { - pkg.Downloads = bp.Downloads - } - packageInstallSizeCache.SetDefault(pkg.RepoURL, pkg.InstallSize) - theme := &Theme{Package: &pkg, Modes: []string{}} - return theme -} - -func InstalledThemes() (ret []*Theme) { - ret = []*Theme{} - - if !util.IsPathRegularDirOrSymlinkDir(util.ThemesPath) { - return - } - - themeDirs, err := os.ReadDir(util.ThemesPath) - if err != nil { - logging.LogWarnf("read appearance themes folder failed: %s", err) - return - } - - bazaarThemes := Themes() - - for _, themeDir := range themeDirs { - if !util.IsDirRegularOrSymlink(themeDir) { - continue - } - dirName := themeDir.Name() - if isBuiltInTheme(dirName) { - continue - } - - theme, parseErr := ThemeJSON(dirName) - if nil != parseErr || nil == theme { - continue - } - - theme.RepoURL = theme.URL - theme.DisallowInstall = disallowInstallBazaarPackage(theme.Package) - if bazaarPkg := getBazaarTheme(theme.Name, bazaarThemes); nil != bazaarPkg { - theme.DisallowUpdate = disallowInstallBazaarPackage(bazaarPkg.Package) - theme.UpdateRequiredMinAppVer = bazaarPkg.MinAppVersion - theme.RepoURL = bazaarPkg.RepoURL - } - - installPath := filepath.Join(util.ThemesPath, dirName) - theme.Installed = true - theme.PreviewURL = "/appearance/themes/" + dirName + "/preview.png" - theme.PreviewURLThumb = "/appearance/themes/" + dirName + "/preview.png" - theme.IconURL = "/appearance/themes/" + dirName + "/icon.png" - theme.PreferredFunding = getPreferredFunding(theme.Funding) - theme.PreferredName = GetPreferredName(theme.Package) - theme.PreferredDesc = getPreferredDesc(theme.Description) - info, statErr := os.Stat(filepath.Join(installPath, "theme.json")) - if nil != statErr { - logging.LogWarnf("stat install theme.json failed: %s", statErr) - continue - } - theme.HInstallDate = info.ModTime().Format("2006-01-02") - if installSize, ok := packageInstallSizeCache.Get(theme.RepoURL); ok { - theme.InstallSize = installSize.(int64) - } else { - is, _ := util.SizeOfDirectory(installPath) - theme.InstallSize = is - packageInstallSizeCache.SetDefault(theme.RepoURL, is) - } - theme.HInstallSize = humanize.BytesCustomCeil(uint64(theme.InstallSize), 2) - theme.PreferredReadme = getInstalledPackageREADME(installPath, "/appearance/themes/"+dirName+"/", theme.Readme) - theme.Outdated = isOutdatedTheme(theme, bazaarThemes) - ret = append(ret, theme) - } - return -} - -func isBuiltInTheme(dirName string) bool { - return "daylight" == dirName || "midnight" == dirName -} - -func getBazaarTheme(name string, themes []*Theme) *Theme { - for _, p := range themes { - if p.Name == name { - return p - } - } - return nil -} - -func InstallTheme(repoURL, repoHash, installPath string, systemID string) error { - repoURLHash := repoURL + "@" + repoHash - data, err := downloadPackage(repoURLHash, true, systemID) - if err != nil { - return err - } - return installPackage(data, installPath, repoURLHash) -} - -func UninstallTheme(installPath string) error { - return uninstallPackage(installPath) -} diff --git a/kernel/bazaar/widget.go b/kernel/bazaar/widget.go deleted file mode 100644 index 8009a14d8..000000000 --- a/kernel/bazaar/widget.go +++ /dev/null @@ -1,181 +0,0 @@ -// SiYuan - Refactor your thinking -// 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 ( - "os" - "path/filepath" - "sort" - "strings" - - "github.com/88250/go-humanize" - "github.com/siyuan-note/logging" - "github.com/siyuan-note/siyuan/kernel/util" -) - -type Widget struct { - *Package -} - -// Widgets 返回集市挂件列表 -func Widgets() (widgets []*Widget) { - widgets = []*Widget{} - result := getStageAndBazaar("widgets") - - if !result.Online { - return - } - if result.StageErr != nil { - return - } - if 1 > len(result.BazaarIndex) { - return - } - - for _, repo := range result.StageIndex.Repos { - if nil == repo.Package { - continue - } - widget := buildWidgetFromStageRepo(repo, result.BazaarIndex) - if nil != widget { - widgets = append(widgets, widget) - } - } - - sort.Slice(widgets, func(i, j int) bool { return widgets[i].Updated > widgets[j].Updated }) - return -} - -// buildWidgetFromStageRepo 使用 stage 内嵌的 package 构建 *Widget,不发起 HTTP 请求。 -func buildWidgetFromStageRepo(repo *StageRepo, bazaarIndex map[string]*bazaarPackage) *Widget { - pkg := *repo.Package - pkg.URL = strings.TrimSuffix(pkg.URL, "/") - repoURLHash := strings.Split(repo.URL, "@") - if 2 != len(repoURLHash) { - return nil - } - pkg.RepoURL = "https://github.com/" + repoURLHash[0] - pkg.RepoHash = repoURLHash[1] - pkg.PreviewURL = util.BazaarOSSServer + "/package/" + repo.URL + "/preview.png?imageslim" - pkg.PreviewURLThumb = util.BazaarOSSServer + "/package/" + repo.URL + "/preview.png?imageView2/2/w/436/h/232" - pkg.IconURL = util.BazaarOSSServer + "/package/" + repo.URL + "/icon.png" - pkg.Updated = repo.Updated - pkg.Stars = repo.Stars - pkg.OpenIssues = repo.OpenIssues - pkg.Size = repo.Size - pkg.HSize = humanize.BytesCustomCeil(uint64(pkg.Size), 2) - pkg.InstallSize = repo.InstallSize - pkg.HInstallSize = humanize.BytesCustomCeil(uint64(pkg.InstallSize), 2) - pkg.HUpdated = formatUpdated(pkg.Updated) - pkg.PreferredFunding = getPreferredFunding(pkg.Funding) - pkg.PreferredName = GetPreferredName(&pkg) - pkg.PreferredDesc = getPreferredDesc(pkg.Description) - pkg.DisallowInstall = disallowInstallBazaarPackage(&pkg) - pkg.DisallowUpdate = disallowInstallBazaarPackage(&pkg) - pkg.UpdateRequiredMinAppVer = pkg.MinAppVersion - if bp := bazaarIndex[repoURLHash[0]]; nil != bp { - pkg.Downloads = bp.Downloads - } - packageInstallSizeCache.SetDefault(pkg.RepoURL, pkg.InstallSize) - return &Widget{Package: &pkg} -} - -func InstalledWidgets() (ret []*Widget) { - ret = []*Widget{} - - widgetsPath := filepath.Join(util.DataDir, "widgets") - if !util.IsPathRegularDirOrSymlinkDir(widgetsPath) { - return - } - - widgetDirs, err := os.ReadDir(widgetsPath) - if err != nil { - logging.LogWarnf("read widgets folder failed: %s", err) - return - } - - bazaarWidgets := Widgets() - - for _, widgetDir := range widgetDirs { - if !util.IsDirRegularOrSymlink(widgetDir) { - continue - } - dirName := widgetDir.Name() - - widget, parseErr := WidgetJSON(dirName) - if nil != parseErr || nil == widget { - continue - } - - widget.RepoURL = widget.URL - widget.DisallowInstall = disallowInstallBazaarPackage(widget.Package) - if bazaarPkg := getBazaarWidget(widget.Name, bazaarWidgets); nil != bazaarPkg { - widget.DisallowUpdate = disallowInstallBazaarPackage(bazaarPkg.Package) - widget.UpdateRequiredMinAppVer = bazaarPkg.MinAppVersion - widget.RepoURL = bazaarPkg.RepoURL - } - - installPath := filepath.Join(util.DataDir, "widgets", dirName) - widget.Installed = true - widget.PreviewURL = "/widgets/" + dirName + "/preview.png" - widget.PreviewURLThumb = "/widgets/" + dirName + "/preview.png" - widget.IconURL = "/widgets/" + dirName + "/icon.png" - widget.PreferredFunding = getPreferredFunding(widget.Funding) - widget.PreferredName = GetPreferredName(widget.Package) - widget.PreferredDesc = getPreferredDesc(widget.Description) - info, statErr := os.Stat(filepath.Join(installPath, "widget.json")) - if nil != statErr { - logging.LogWarnf("stat install widget.json failed: %s", statErr) - continue - } - widget.HInstallDate = info.ModTime().Format("2006-01-02") - if installSize, ok := packageInstallSizeCache.Get(widget.RepoURL); ok { - widget.InstallSize = installSize.(int64) - } else { - is, _ := util.SizeOfDirectory(installPath) - widget.InstallSize = is - packageInstallSizeCache.SetDefault(widget.RepoURL, is) - } - widget.HInstallSize = humanize.BytesCustomCeil(uint64(widget.InstallSize), 2) - widget.PreferredReadme = getInstalledPackageREADME(installPath, "/widgets/"+dirName+"/", widget.Readme) - widget.Outdated = isOutdatedWidget(widget, bazaarWidgets) - ret = append(ret, widget) - } - return -} - -func getBazaarWidget(name string, widgets []*Widget) *Widget { - for _, p := range widgets { - if p.Name == name { - return p - } - } - return nil -} - -func InstallWidget(repoURL, repoHash, installPath string, systemID string) error { - repoURLHash := repoURL + "@" + repoHash - data, err := downloadPackage(repoURLHash, true, systemID) - if err != nil { - return err - } - return installPackage(data, installPath, repoURLHash) -} - -func UninstallWidget(installPath string) error { - return uninstallPackage(installPath) -} diff --git a/kernel/model/appearance.go b/kernel/model/appearance.go index 60da7e0f1..dc100a326 100644 --- a/kernel/model/appearance.go +++ b/kernel/model/appearance.go @@ -97,12 +97,16 @@ func loadThemes() { continue } name := themeDir.Name() - themeConf, parseErr := bazaar.ThemeJSON(name) + themeConf, parseErr := bazaar.ParsePackageJSON(filepath.Join(util.ThemesPath, name, "theme.json")) if nil != parseErr || nil == themeConf { continue } - for _, mode := range themeConf.Modes { + var modes []string + if nil != themeConf.Modes { + modes = *themeConf.Modes + } + for _, mode := range modes { t := &conf.AppearanceTheme{Name: name} if isBuiltInTheme(name) { t.Label = name + Conf.Language(281) @@ -174,7 +178,7 @@ func LoadIcons() { continue } name := iconDir.Name() - iconConf, err := bazaar.IconJSON(name) + iconConf, err := bazaar.ParsePackageJSON(filepath.Join(util.IconsPath, name, "icon.json")) if err != nil || nil == iconConf { continue } diff --git a/kernel/model/bazaar.go b/kernel/model/bazaar.go index a9f3bf488..376def7fd 100644 --- a/kernel/model/bazaar.go +++ b/kernel/model/bazaar.go @@ -20,9 +20,8 @@ import ( "context" "errors" "fmt" - "path" + "os" "path/filepath" - "strings" "sync" "time" @@ -33,10 +32,55 @@ import ( "github.com/siyuan-note/siyuan/kernel/task" "github.com/siyuan-note/siyuan/kernel/util" "golang.org/x/mod/semver" + "golang.org/x/sync/singleflight" ) -func BatchUpdateBazaarPackages(frontend string) { - plugins, widgets, icons, themes, templates := UpdatedPackages(frontend) +// installedPackageInfo 描述了本地集市包的包与目录名信息 +type installedPackageInfo struct { + Pkg *bazaar.Package + DirName string +} + +func getPackageInstallPath(pkgType, packageName string) (string, string, error) { + switch pkgType { + case "plugins": + return filepath.Join(util.DataDir, "plugins", packageName), "plugin.json", nil + case "themes": + return filepath.Join(util.ThemesPath, packageName), "theme.json", nil + case "icons": + return filepath.Join(util.IconsPath, packageName), "icon.json", nil + case "templates": + return filepath.Join(util.DataDir, "templates", packageName), "template.json", nil + case "widgets": + return filepath.Join(util.DataDir, "widgets", packageName), "widget.json", nil + default: + logging.LogErrorf("invalid package type: %s", pkgType) + return "", "", errors.New("invalid package type") + } +} + +// updatePackages 更新一组集市包 +func updatePackages(packages []*bazaar.Package, pkgType string, count *int, total int) bool { + for _, pkg := range packages { + installPath, _, err := getPackageInstallPath(pkgType, pkg.Name) + if err != nil { + return false + } + err = bazaar.InstallPackage(pkg.RepoURL, pkg.RepoHash, installPath, Conf.System.ID, pkgType, pkg.Name) + if err != nil { + logging.LogErrorf("update %s [%s] failed: %s", pkgType, pkg.Name, err) + util.PushErrMsg(fmt.Sprintf(Conf.language(238), pkg.Name), 5000) + return false + } + *count++ + util.PushEndlessProgress(fmt.Sprintf(Conf.language(236), *count, total, pkg.Name)) + } + return true +} + +// BatchUpdatePackages 更新所有集市包 +func BatchUpdatePackages(frontend string) { + plugins, widgets, icons, themes, templates := GetUpdatedPackages(frontend) total := len(plugins) + len(widgets) + len(icons) + len(themes) + len(templates) if 1 > total { @@ -46,544 +90,334 @@ func BatchUpdateBazaarPackages(frontend string) { util.PushEndlessProgress(fmt.Sprintf(Conf.language(235), 1, total)) defer util.PushClearProgress() count := 1 - for _, plugin := range plugins { - err := bazaar.InstallPlugin(plugin.RepoURL, plugin.RepoHash, filepath.Join(util.DataDir, "plugins", plugin.Name), Conf.System.ID) - if err != nil { - logging.LogErrorf("update plugin [%s] failed: %s", plugin.Name, err) - util.PushErrMsg(fmt.Sprintf(Conf.language(238), plugin.Name), 5000) - return - } - count++ - util.PushEndlessProgress(fmt.Sprintf(Conf.language(236), count, total, plugin.Name)) + if !updatePackages(plugins, "plugins", &count, total) { + return } - - for _, widget := range widgets { - err := bazaar.InstallWidget(widget.RepoURL, widget.RepoHash, filepath.Join(util.DataDir, "widgets", widget.Name), Conf.System.ID) - if err != nil { - logging.LogErrorf("update widget [%s] failed: %s", widget.Name, err) - util.PushErrMsg(fmt.Sprintf(Conf.language(238), widget.Name), 5000) - return - } - - count++ - util.PushEndlessProgress(fmt.Sprintf(Conf.language(236), count, total, widget.Name)) + if !updatePackages(themes, "themes", &count, total) { + return } - - for _, icon := range icons { - err := bazaar.InstallIcon(icon.RepoURL, icon.RepoHash, filepath.Join(util.IconsPath, icon.Name), Conf.System.ID) - if err != nil { - logging.LogErrorf("update icon [%s] failed: %s", icon.Name, err) - util.PushErrMsg(fmt.Sprintf(Conf.language(238), icon.Name), 5000) - return - } - - count++ - util.PushEndlessProgress(fmt.Sprintf(Conf.language(236), count, total, icon.Name)) + if !updatePackages(icons, "icons", &count, total) { + return } - - for _, template := range templates { - err := bazaar.InstallTemplate(template.RepoURL, template.RepoHash, filepath.Join(util.DataDir, "templates", template.Name), Conf.System.ID) - if err != nil { - logging.LogErrorf("update template [%s] failed: %s", template.Name, err) - util.PushErrMsg(fmt.Sprintf(Conf.language(238), template.Name), 5000) - return - } - - count++ - util.PushEndlessProgress(fmt.Sprintf(Conf.language(236), count, total, template.Name)) + if !updatePackages(templates, "templates", &count, total) { + return } - - for _, theme := range themes { - err := bazaar.InstallTheme(theme.RepoURL, theme.RepoHash, filepath.Join(util.ThemesPath, theme.Name), Conf.System.ID) - if err != nil { - logging.LogErrorf("update theme [%s] failed: %s", theme.Name, err) - util.PushErrMsg(fmt.Sprintf(Conf.language(238), theme.Name), 5000) - return - } - - count++ - util.PushEndlessProgress(fmt.Sprintf(Conf.language(236), count, total, theme.Name)) + if !updatePackages(widgets, "widgets", &count, total) { + return } util.ReloadUI() task.AppendAsyncTaskWithDelay(task.PushMsg, 3*time.Second, util.PushMsg, fmt.Sprintf(Conf.language(237), total), 5000) - return } -func UpdatedPackages(frontend string) (plugins []*bazaar.Plugin, widgets []*bazaar.Widget, icons []*bazaar.Icon, themes []*bazaar.Theme, templates []*bazaar.Template) { +// GetUpdatedPackages 获取所有类型集市包的更新列表 +func GetUpdatedPackages(frontend string) (plugins, widgets, icons, themes, templates []*bazaar.Package) { wg := &sync.WaitGroup{} wg.Add(5) - go func() { - defer wg.Done() - tmp := InstalledPlugins(frontend, "") - for _, plugin := range tmp { - if plugin.Outdated { - plugins = append(plugins, plugin) - } - plugin.PreferredReadme = "" // 清空这个字段,前端会请求在线的 README - } - }() go func() { defer wg.Done() - tmp := InstalledWidgets("") - for _, widget := range tmp { - if widget.Outdated { - widgets = append(widgets, widget) - } - widget.PreferredReadme = "" - } + plugins = getUpdatedPackages("plugins", frontend, "") }() - go func() { defer wg.Done() - tmp := InstalledIcons("") - for _, icon := range tmp { - if icon.Outdated { - icons = append(icons, icon) - } - icon.PreferredReadme = "" - } + themes = getUpdatedPackages("themes", "", "") }() - go func() { defer wg.Done() - tmp := InstalledThemes("") - for _, theme := range tmp { - if theme.Outdated { - themes = append(themes, theme) - } - theme.PreferredReadme = "" - } + icons = getUpdatedPackages("icons", "", "") }() - go func() { defer wg.Done() - tmp := InstalledTemplates("") - for _, template := range tmp { - if template.Outdated { - templates = append(templates, template) - } - template.PreferredReadme = "" - } + templates = getUpdatedPackages("templates", "", "") + }() + go func() { + defer wg.Done() + widgets = getUpdatedPackages("widgets", "", "") }() wg.Wait() + return +} - if 1 > len(plugins) { - plugins = []*bazaar.Plugin{} - } - - if 1 > len(widgets) { - widgets = []*bazaar.Widget{} - } - - if 1 > len(icons) { - icons = []*bazaar.Icon{} - } - - if 1 > len(themes) { - themes = []*bazaar.Theme{} - } - - if 1 > len(templates) { - templates = []*bazaar.Template{} +// getUpdatedPackages 获取单个类型集市包的更新列表 +func getUpdatedPackages(pkgType, frontend, keyword string) (updatedPackages []*bazaar.Package) { + installedPackages := GetInstalledPackages(pkgType, frontend, keyword) + updatedPackages = []*bazaar.Package{} // 确保返回空切片而非 nil + for _, pkg := range installedPackages { + if !pkg.Outdated { + continue + } + updatedPackages = append(updatedPackages, pkg) + pkg.PreferredReadme = "" // 清空这个字段,前端会请求在线的 README } return } -// GetBazaarPackageREADME 获取集市包的在线 README。 -func GetBazaarPackageREADME(ctx context.Context, repoURL, repoHash, packageType string) (ret string) { - ret = bazaar.GetBazaarPackageREADME(ctx, repoURL, repoHash, packageType) - return -} - -func BazaarPlugins(frontend, keyword string) (plugins []*bazaar.Plugin) { - plugins = bazaar.Plugins(frontend) - plugins = filterPlugins(plugins, keyword) - for _, plugin := range plugins { - plugin.Installed = util.IsPathRegularDirOrSymlinkDir(filepath.Join(util.DataDir, "plugins", plugin.Name)) - if plugin.Installed { - if pluginConf, err := bazaar.PluginJSON(plugin.Name); err == nil && nil != plugin { - plugin.Outdated = 0 > semver.Compare("v"+pluginConf.Version, "v"+plugin.Version) - } - } else { - plugin.Outdated = false - } - } - return -} - -func filterPlugins(plugins []*bazaar.Plugin, keyword string) (ret []*bazaar.Plugin) { - keywords := getSearchKeywords(keyword) - if 0 == len(keywords) { - return plugins - } - ret = []*bazaar.Plugin{} - for _, plugin := range plugins { - if matchPackage(keywords, plugin.Package) { - ret = append(ret, plugin) - } - } - return -} - -func InstalledPlugins(frontend, keyword string) (plugins []*bazaar.Plugin) { - plugins = bazaar.InstalledPlugins(frontend) - plugins = filterPlugins(plugins, keyword) - petals := getPetals() - for _, plugin := range plugins { - petal := getPetalByName(plugin.Name, petals) - if nil != petal { - plugin.Enabled = petal.Enabled - } - } - 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 err != nil { - return errors.New(fmt.Sprintf(Conf.Language(46), pluginName, err)) - } - return nil -} - -func UninstallBazaarPlugin(pluginName, frontend string) error { - installPath := filepath.Join(util.DataDir, "plugins", pluginName) - err := bazaar.UninstallPlugin(installPath) - if err != nil { - return errors.New(fmt.Sprintf(Conf.Language(47), err.Error())) - } - - petals := getPetals() - var tmp []*Petal - for i, petal := range petals { - if petal.Name != pluginName { - tmp = append(tmp, petals[i]) - } - } - petals = tmp - savePetals(petals) - - uninstallPluginSet := hashset.New(pluginName) - PushReloadPlugin(uninstallPluginSet, nil, nil, nil, "") - return nil -} - -func BazaarWidgets(keyword string) (widgets []*bazaar.Widget) { - widgets = bazaar.Widgets() - widgets = filterWidgets(widgets, keyword) - for _, widget := range widgets { - widget.Installed = util.IsPathRegularDirOrSymlinkDir(filepath.Join(util.DataDir, "widgets", widget.Name)) - if widget.Installed { - if widgetConf, err := bazaar.WidgetJSON(widget.Name); err == nil && nil != widget { - widget.Outdated = 0 > semver.Compare("v"+widgetConf.Version, "v"+widget.Version) - } - } else { - widget.Outdated = false - } - } - return -} - -func filterWidgets(widgets []*bazaar.Widget, keyword string) (ret []*bazaar.Widget) { - keywords := getSearchKeywords(keyword) - if 0 == len(keywords) { - return widgets - } - ret = []*bazaar.Widget{} - for _, w := range widgets { - if matchPackage(keywords, w.Package) { - ret = append(ret, w) - } - } - return -} - -func InstalledWidgets(keyword string) (widgets []*bazaar.Widget) { - widgets = bazaar.InstalledWidgets() - widgets = filterWidgets(widgets, keyword) - return -} - -func InstallBazaarWidget(repoURL, repoHash, widgetName string) error { - installPath := filepath.Join(util.DataDir, "widgets", widgetName) - err := bazaar.InstallWidget(repoURL, repoHash, installPath, Conf.System.ID) - if err != nil { - return errors.New(fmt.Sprintf(Conf.Language(46), widgetName, err)) - } - return nil -} - -func UninstallBazaarWidget(widgetName string) error { - installPath := filepath.Join(util.DataDir, "widgets", widgetName) - err := bazaar.UninstallWidget(installPath) - if err != nil { - return errors.New(fmt.Sprintf(Conf.Language(47), err.Error())) - } - return nil -} - -func BazaarIcons(keyword string) (icons []*bazaar.Icon) { - icons = bazaar.Icons() - icons = filterIcons(icons, keyword) - for _, installed := range Conf.Appearance.Icons { - for _, icon := range icons { - if installed == icon.Name { - icon.Installed = true - if iconConf, err := bazaar.IconJSON(icon.Name); err == nil { - icon.Outdated = 0 > semver.Compare("v"+iconConf.Version, "v"+icon.Version) - } - } - icon.Current = icon.Name == Conf.Appearance.Icon - } - } - return -} - -func filterIcons(icons []*bazaar.Icon, keyword string) (ret []*bazaar.Icon) { - keywords := getSearchKeywords(keyword) - if 0 == len(keywords) { - return icons - } - ret = []*bazaar.Icon{} - for _, i := range icons { - if matchPackage(keywords, i.Package) { - ret = append(ret, i) - } - } - return -} - -func InstalledIcons(keyword string) (icons []*bazaar.Icon) { - icons = bazaar.InstalledIcons() - icons = filterIcons(icons, keyword) - for _, icon := range icons { - icon.Current = icon.Name == Conf.Appearance.Icon - } - return -} - -func InstallBazaarIcon(repoURL, repoHash, iconName string) error { - installPath := filepath.Join(util.IconsPath, iconName) - err := bazaar.InstallIcon(repoURL, repoHash, installPath, Conf.System.ID) - if err != nil { - return errors.New(fmt.Sprintf(Conf.Language(46), iconName, err)) - } - Conf.Appearance.Icon = iconName - Conf.Save() - InitAppearance() - util.BroadcastByType("main", "setAppearance", 0, "", Conf.Appearance) - return nil -} - -func UninstallBazaarIcon(iconName string) error { - installPath := filepath.Join(util.IconsPath, iconName) - err := bazaar.UninstallIcon(installPath) - if err != nil { - return errors.New(fmt.Sprintf(Conf.Language(47), err.Error())) - } - - InitAppearance() - return nil -} - -func BazaarThemes(keyword string) (ret []*bazaar.Theme) { - ret = bazaar.Themes() - ret = filterThemes(ret, keyword) - installs := Conf.Appearance.DarkThemes - installs = append(installs, Conf.Appearance.LightThemes...) - for _, installed := range installs { - for _, theme := range ret { - if installed.Name == theme.Name { - theme.Installed = true - if themeConf, err := bazaar.ThemeJSON(theme.Name); err == nil { - theme.Outdated = 0 > semver.Compare("v"+themeConf.Version, "v"+theme.Version) - } - theme.Current = theme.Name == Conf.Appearance.ThemeDark || theme.Name == Conf.Appearance.ThemeLight - } - } - } - return -} - -func filterThemes(themes []*bazaar.Theme, keyword string) (ret []*bazaar.Theme) { - keywords := getSearchKeywords(keyword) - if 0 == len(keywords) { - return themes - } - ret = []*bazaar.Theme{} - for _, t := range themes { - if matchPackage(keywords, t.Package) { - ret = append(ret, t) - } - } - return -} - -func InstalledThemes(keyword string) (ret []*bazaar.Theme) { - ret = bazaar.InstalledThemes() - ret = filterThemes(ret, keyword) - for _, theme := range ret { - theme.Current = theme.Name == Conf.Appearance.ThemeDark || theme.Name == Conf.Appearance.ThemeLight - } - return -} - -func InstallBazaarTheme(repoURL, repoHash, themeName string, mode int, update bool) error { - CloseWatchThemes() - - installPath := filepath.Join(util.ThemesPath, themeName) - err := bazaar.InstallTheme(repoURL, repoHash, installPath, Conf.System.ID) - if err != nil { - return errors.New(fmt.Sprintf(Conf.Language(46), themeName, err)) - } - - if !update { - // 更新主题后不需要对该主题进行切换 https://github.com/siyuan-note/siyuan/issues/4966 - if 0 == mode { - Conf.Appearance.ThemeLight = themeName - } else { - Conf.Appearance.ThemeDark = themeName - } - Conf.Appearance.Mode = mode - Conf.Appearance.ThemeJS = gulu.File.IsExist(filepath.Join(installPath, "theme.js")) - Conf.Save() - } - - InitAppearance() - util.BroadcastByType("main", "setAppearance", 0, "", Conf.Appearance) - return nil -} - -func UninstallBazaarTheme(themeName string) error { - CloseWatchThemes() - - installPath := filepath.Join(util.ThemesPath, themeName) - err := bazaar.UninstallTheme(installPath) - if err != nil { - return errors.New(fmt.Sprintf(Conf.Language(47), err.Error())) - } - - InitAppearance() - return nil -} - -func BazaarTemplates(keyword string) (templates []*bazaar.Template) { - templates = bazaar.Templates() - templates = filterTemplates(templates, keyword) - for _, template := range templates { - template.Installed = util.IsPathRegularDirOrSymlinkDir(filepath.Join(util.DataDir, "templates", template.Name)) - if template.Installed { - if templateConf, err := bazaar.TemplateJSON(template.Name); err == nil && nil != templateConf { - template.Outdated = 0 > semver.Compare("v"+templateConf.Version, "v"+template.Version) - } - } else { - template.Outdated = false - } - } - return -} - -func filterTemplates(templates []*bazaar.Template, keyword string) (ret []*bazaar.Template) { - keywords := getSearchKeywords(keyword) - if 0 == len(keywords) { - return templates - } - ret = []*bazaar.Template{} - for _, t := range templates { - if matchPackage(keywords, t.Package) { - ret = append(ret, t) - } - } - return -} - -func InstalledTemplates(keyword string) (templates []*bazaar.Template) { - templates = bazaar.InstalledTemplates() - templates = filterTemplates(templates, keyword) - return -} - -func InstallBazaarTemplate(repoURL, repoHash, templateName string) error { - installPath := filepath.Join(util.DataDir, "templates", templateName) - err := bazaar.InstallTemplate(repoURL, repoHash, installPath, Conf.System.ID) - if err != nil { - return errors.New(fmt.Sprintf(Conf.Language(46), templateName, err)) - } - return nil -} - -func UninstallBazaarTemplate(templateName string) error { - installPath := filepath.Join(util.DataDir, "templates", templateName) - err := bazaar.UninstallTemplate(installPath) - if err != nil { - return errors.New(fmt.Sprintf(Conf.Language(47), err.Error())) - } - return nil -} - -func matchPackage(keywords []string, pkg *bazaar.Package) bool { - if 1 > len(keywords) { - return true - } - - if nil == pkg { - return false - } - - for _, kw := range keywords { - if !packageContainsKeyword(pkg, kw) { - return false - } - } - - // 全部关键词匹配 - return true -} - -func packageContainsKeyword(pkg *bazaar.Package, kw string) bool { - if strings.Contains(strings.ToLower(path.Base(pkg.RepoURL)), kw) || - strings.Contains(strings.ToLower(pkg.Author), kw) { - return true - } - for _, s := range pkg.DisplayName { - if strings.Contains(strings.ToLower(s), kw) { - return true - } - } - for _, s := range pkg.Description { - if strings.Contains(strings.ToLower(s), kw) { - return true - } - } - for _, s := range pkg.Keywords { - if strings.Contains(strings.ToLower(s), kw) { - return true - } - } - return false -} - -func getSearchKeywords(query string) (ret []string) { - query = strings.TrimSpace(query) - if "" == query { +// GetInstalledPackageInfos 获取本地集市包信息,并返回路径相关字段供调用方复用 +func GetInstalledPackageInfos(pkgType string) (installedPackageInfos []installedPackageInfo, basePath, baseURLPathPrefix string, err error) { + var jsonFileName string + switch pkgType { + case "plugins": + basePath, jsonFileName, baseURLPathPrefix = filepath.Join(util.DataDir, "plugins"), "plugin.json", "/plugins/" + case "themes": + basePath, jsonFileName, baseURLPathPrefix = util.ThemesPath, "theme.json", "/appearance/themes/" + case "icons": + basePath, jsonFileName, baseURLPathPrefix = util.IconsPath, "icon.json", "/appearance/icons/" + case "templates": + basePath, jsonFileName, baseURLPathPrefix = filepath.Join(util.DataDir, "templates"), "template.json", "/templates/" + case "widgets": + basePath, jsonFileName, baseURLPathPrefix = filepath.Join(util.DataDir, "widgets"), "widget.json", "/widgets/" + default: + logging.LogErrorf("invalid package type: %s", pkgType) + err = errors.New("invalid package type") return } - keywords := strings.Split(query, " ") - for _, k := range keywords { - if "" != k { - ret = append(ret, strings.ToLower(k)) + dirs, err := bazaar.ReadInstalledPackageDirs(basePath) + if err != nil { + logging.LogWarnf("read %s folder failed: %s", pkgType, err) + return + } + if len(dirs) == 0 { + return + } + + // 过滤内置包 + switch pkgType { + case "themes": + filtered := make([]os.DirEntry, 0, len(dirs)) + for _, d := range dirs { + if isBuiltInTheme(d.Name()) { + continue + } + filtered = append(filtered, d) + } + dirs = filtered + case "icons": + filtered := make([]os.DirEntry, 0, len(dirs)) + for _, d := range dirs { + if isBuiltInIcon(d.Name()) { + continue + } + filtered = append(filtered, d) + } + dirs = filtered + } + + for _, dir := range dirs { + dirName := dir.Name() + pkg, parseErr := bazaar.ParsePackageJSON(filepath.Join(basePath, dirName, jsonFileName)) + if nil != parseErr || nil == pkg { + continue + } + installedPackageInfos = append(installedPackageInfos, installedPackageInfo{Pkg: pkg, DirName: dirName}) + } + return +} + +var getInstalledPackagesFlight singleflight.Group + +// GetInstalledPackages 获取本地集市包列表 +func GetInstalledPackages(pkgType, frontend, keyword string) (installedPackages []*bazaar.Package) { + key := "getInstalledPackages:" + pkgType + ":" + frontend + ":" + keyword + v, err, _ := getInstalledPackagesFlight.Do(key, func() (interface{}, error) { + return getInstalledPackages0(pkgType, frontend, keyword), nil + }) + if err != nil { + return []*bazaar.Package{} + } + return v.([]*bazaar.Package) +} + +func getInstalledPackages0(pkgType, frontend, keyword string) (installedPackages []*bazaar.Package) { + installedPackages = []*bazaar.Package{} + + installedInfos, basePath, baseURLPathPrefix, err := GetInstalledPackageInfos(pkgType) + if err != nil { + return + } + // 本地没有该类型的集市包时,直接返回,避免请求云端数据 + if len(installedInfos) == 0 { + return + } + + bazaarPackages := bazaar.GetBazaarPackages(pkgType, frontend) + bazaarPackagesMap := make(map[string]*bazaar.Package, len(bazaarPackages)) + for _, pkg := range bazaarPackages { + if "" != pkg.Name { + bazaarPackagesMap[pkg.Name] = pkg + } + } + + for _, info := range installedInfos { + pkg := info.Pkg + installPath := filepath.Join(basePath, info.DirName) + baseURLPath := baseURLPathPrefix + info.DirName + "/" + // 设置本地集市包的通用元数据 + if !bazaar.SetInstalledPackageMetadata(pkg, installPath, baseURLPath, pkgType, bazaarPackagesMap) { + continue + } + installedPackages = append(installedPackages, pkg) + } + + installedPackages = bazaar.FilterPackages(installedPackages, keyword) + + // 设置本地集市包的额外元数据 + var petals []*Petal + if pkgType == "plugins" { + petals = getPetals() + } + for _, pkg := range installedPackages { + switch pkgType { + case "plugins": + incompatible := bazaar.IsIncompatiblePlugin(pkg, frontend) + pkg.Incompatible = &incompatible + petal := getPetalByName(pkg.Name, petals) + if nil != petal { + enabled := petal.Enabled + pkg.Enabled = &enabled + } + case "themes": + pkg.Current = pkg.Name == Conf.Appearance.ThemeDark || pkg.Name == Conf.Appearance.ThemeLight + case "icons": + pkg.Current = pkg.Name == Conf.Appearance.Icon } } return } +// GetBazaarPackages 获取在线集市包列表 +func GetBazaarPackages(pkgType, frontend, keyword string) (bazaarPackages []*bazaar.Package) { + bazaarPackages = bazaar.GetBazaarPackages(pkgType, frontend) + bazaarPackages = bazaar.FilterPackages(bazaarPackages, keyword) + installedInfos, _, _, err := GetInstalledPackageInfos(pkgType) + if err != nil { + return + } + installedMap := make(map[string]*bazaar.Package, len(installedInfos)) + for _, info := range installedInfos { + installedMap[info.Pkg.Name] = info.Pkg + } + for _, pkg := range bazaarPackages { + installedPkg, ok := installedMap[pkg.Name] + if !ok { + continue + } + pkg.Installed = true + pkg.Outdated = 0 > semver.Compare("v"+installedPkg.Version, "v"+pkg.Version) + switch pkgType { + case "themes": + pkg.Current = pkg.Name == Conf.Appearance.ThemeDark || pkg.Name == Conf.Appearance.ThemeLight + case "icons": + pkg.Current = pkg.Name == Conf.Appearance.Icon + } + } + return +} + +func GetBazaarPackageREADME(ctx context.Context, repoURL, repoHash, pkgType string) (ret string) { + ret = bazaar.GetBazaarPackageREADME(ctx, repoURL, repoHash, pkgType) + return +} + +func InstallBazaarPackage(pkgType, repoURL, repoHash, packageName string, themeMode int) error { + installPath, jsonFileName, err := getPackageInstallPath(pkgType, packageName) + if err != nil { + return err + } + + installedPkg, parseErr := bazaar.ParsePackageJSON(filepath.Join(installPath, jsonFileName)) + update := parseErr == nil && installedPkg != nil && installedPkg.Name == packageName + + err = bazaar.InstallPackage(repoURL, repoHash, installPath, Conf.System.ID, pkgType, packageName) + if err != nil { + return fmt.Errorf(Conf.Language(46), packageName, err) + } + + switch pkgType { + case "plugins": + if update { + // 已启用的插件更新之后需要重载 + petals := getPetals() + petal := getPetalByName(packageName, petals) + if nil != petal && petal.Enabled { + reloadPluginSet := hashset.New(packageName) + PushReloadPlugin(nil, nil, reloadPluginSet, nil, "") + } + } + case "themes": + if !update { + // 更新主题后不需要切换到该主题 https://github.com/siyuan-note/siyuan/issues/4966 + if 0 == themeMode { + Conf.Appearance.ThemeLight = packageName + } else { + Conf.Appearance.ThemeDark = packageName + } + Conf.Appearance.Mode = themeMode + Conf.Appearance.ThemeJS = gulu.File.IsExist(filepath.Join(util.ThemesPath, packageName, "theme.js")) + Conf.Save() + } + InitAppearance() + WatchThemes() + util.BroadcastByType("main", "setAppearance", 0, "", Conf.Appearance) + case "icons": + if !update { + // 更新图标后不需要切换到该图标 + Conf.Appearance.Icon = packageName + Conf.Save() + } + InitAppearance() + util.BroadcastByType("main", "setAppearance", 0, "", Conf.Appearance) + } + return nil +} + +func UninstallPackage(pkgType, packageName string) error { + installPath, _, err := getPackageInstallPath(pkgType, packageName) + if err != nil { + return err + } + + err = bazaar.UninstallPackage(installPath) + if err != nil { + return fmt.Errorf(Conf.Language(47), err.Error()) + } + + // 删除集市包的持久化信息 + bazaar.RemovePackageInfo(pkgType, packageName) + + switch pkgType { + case "plugins": + petals := getPetals() + var tmp []*Petal + for i, petal := range petals { + if petal.Name != packageName { + tmp = append(tmp, petals[i]) + } + } + petals = tmp + savePetals(petals) + + uninstallPluginSet := hashset.New(packageName) + PushReloadPlugin(uninstallPluginSet, nil, nil, nil, "") + case "themes": + InitAppearance() + WatchThemes() + case "icons": + InitAppearance() + } + + return nil +} + // isBuiltInTheme 通过包名或目录名判断是否为内置主题 func isBuiltInTheme(name string) bool { return "daylight" == name || "midnight" == name } + +// isBuiltInIcon 通过包名或目录名判断是否为内置图标 +func isBuiltInIcon(name string) bool { + return "ant" == name || "material" == name +} diff --git a/kernel/model/widget.go b/kernel/model/widget.go index 851b887db..5cf86defe 100644 --- a/kernel/model/widget.go +++ b/kernel/model/widget.go @@ -34,24 +34,24 @@ type WidgetSearchResult struct { func SearchWidget(keyword string) (ret []*WidgetSearchResult) { ret = []*WidgetSearchResult{} - widgetsDir := filepath.Join(util.DataDir, "widgets") - entries, err := os.ReadDir(widgetsDir) + widgetsDirPath := filepath.Join(util.DataDir, "widgets") + widgetsDir, err := os.ReadDir(widgetsDirPath) if err != nil { - logging.LogErrorf("read dir [%s] failed: %s", widgetsDir, err) + logging.LogErrorf("read dir [%s] failed: %s", widgetsDirPath, err) return } - k := strings.ToLower(keyword) - var widgets []*bazaar.Widget - for _, entry := range entries { - if !util.IsDirRegularOrSymlink(entry) { + var widgets []*bazaar.Package + for _, dir := range widgetsDir { + if !util.IsDirRegularOrSymlink(dir) { continue } - if strings.HasPrefix(entry.Name(), ".") { + dirName := dir.Name() + if strings.HasPrefix(dirName, ".") { continue } - widget, _ := bazaar.WidgetJSON(entry.Name()) + widget, _ := bazaar.ParsePackageJSON(filepath.Join(widgetsDirPath, dirName, "widget.json")) if nil == widget { continue } @@ -59,10 +59,10 @@ func SearchWidget(keyword string) (ret []*WidgetSearchResult) { widgets = append(widgets, widget) } - widgets = filterWidgets(widgets, k) + widgets = bazaar.FilterPackages(widgets, keyword) for _, widget := range widgets { b := &WidgetSearchResult{ - Name: bazaar.GetPreferredName(widget.Package), + Name: bazaar.GetPreferredLocaleString(widget.DisplayName, widget.Name), Content: widget.Name, } ret = append(ret, b)