♻️ Unified marketplace Package Type Model (#17152)

This commit is contained in:
Jeffrey Chen 2026-03-08 11:09:46 +08:00 committed by GitHub
parent ab83e5d987
commit 3cac07dfd9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 1108 additions and 1919 deletions

108
kernel/bazaar/bazaar.go Normal file
View file

@ -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 <https://www.gnu.org/licenses/>.
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
}

View file

@ -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 <https://www.gnu.org/licenses/>.
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)
}

148
kernel/bazaar/install.go Normal file
View file

@ -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 <https://www.gnu.org/licenses/>.
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
}

278
kernel/bazaar/installed.go Normal file
View file

@ -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 <https://www.gnu.org/licenses/>.
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()
}

View file

@ -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

View file

@ -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
}

View file

@ -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
}

View file

@ -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

View file

@ -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 <https://www.gnu.org/licenses/>.
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
}

View file

@ -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 <https://www.gnu.org/licenses/>.
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)
}

View file

@ -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 <https://www.gnu.org/licenses/>.
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)
}