Files
siyuan/kernel/model/bazaar.go

464 lines
14 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 model
import (
"context"
"errors"
"fmt"
"os"
"path/filepath"
"sync"
"github.com/88250/gulu"
"github.com/emirpasic/gods/sets/hashset"
"github.com/siyuan-note/logging"
"github.com/siyuan-note/siyuan/kernel/bazaar"
"github.com/siyuan-note/siyuan/kernel/util"
"golang.org/x/mod/semver"
"golang.org/x/sync/singleflight"
)
// 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")
}
}
// installMeta 记录安装前后的状态,供安装后处理使用
type installMeta struct {
update bool
}
// batchInstallItem 同类型批量安装时单个包的结果
type batchInstallItem struct {
name string
meta installMeta
}
// updatePackages 更新一组集市包;同类型批量更新时,安装后处理只执行一次
func updatePackages(packages []*bazaar.Package, pkgType string, successCount *int, planned int) {
items := make([]batchInstallItem, 0, len(packages))
for _, pkg := range packages {
meta, err := installBazaarPackage(pkgType, pkg.RepoURL, pkg.RepoHash, 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)
continue
}
items = append(items, batchInstallItem{name: pkg.Name, meta: meta})
*successCount++
util.PushEndlessProgress(fmt.Sprintf(Conf.language(236), *successCount, planned, pkg.Name))
}
finishInstall(pkgType, items, 0)
}
// filterUpdatableBazaarPackages 过滤出允许更新的集市包
func filterUpdatableBazaarPackages(packages []*bazaar.Package) []*bazaar.Package {
updatable := make([]*bazaar.Package, 0, len(packages))
for _, pkg := range packages {
if !pkg.DisallowUpdate {
updatable = append(updatable, pkg)
}
}
return updatable
}
// BatchUpdatePackages 更新所有集市包
func BatchUpdatePackages(frontend string) {
plugins, widgets, icons, themes, templates := GetUpdatedPackages(frontend)
plugins = filterUpdatableBazaarPackages(plugins)
widgets = filterUpdatableBazaarPackages(widgets)
icons = filterUpdatableBazaarPackages(icons)
themes = filterUpdatableBazaarPackages(themes)
templates = filterUpdatableBazaarPackages(templates)
planned := len(plugins) + len(widgets) + len(icons) + len(themes) + len(templates)
if 1 > planned {
return
}
defer util.PushClearProgress()
successCount := 0
updatePackages(plugins, "plugins", &successCount, planned)
updatePackages(themes, "themes", &successCount, planned)
updatePackages(icons, "icons", &successCount, planned)
updatePackages(templates, "templates", &successCount, planned)
updatePackages(widgets, "widgets", &successCount, planned)
if 0 < successCount {
util.PushMsg(fmt.Sprintf(Conf.language(237), successCount), 5000)
}
}
// GetUpdatedPackages 获取所有类型集市包的更新列表
//
// - frontend 仅用于插件环境兼容性判断
func GetUpdatedPackages(frontend string) (plugins, widgets, icons, themes, templates []*bazaar.Package) {
wg := &sync.WaitGroup{}
wg.Go(func() {
plugins = getUpdatedPackages("plugins", frontend)
})
wg.Go(func() {
themes = getUpdatedPackages("themes", "")
})
wg.Go(func() {
icons = getUpdatedPackages("icons", "")
})
wg.Go(func() {
templates = getUpdatedPackages("templates", "")
})
wg.Go(func() {
widgets = getUpdatedPackages("widgets", "")
})
wg.Wait()
return
}
// getUpdatedPackages 获取单个类型集市包的更新列表
func getUpdatedPackages(pkgType, frontend string) (updatedPackages []*bazaar.Package) {
installedPackages := GetInstalledPackages(pkgType, frontend, "")
updatedPackages = []*bazaar.Package{} // 确保返回空切片而非 nil
for _, pkg := range installedPackages {
if !pkg.Outdated {
continue
}
updatedPackages = append(updatedPackages, pkg)
}
return
}
// 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
}
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() (any, 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
}
bazaarPackagesMap := bazaar.GetBazaarPackagesMap(pkgType, frontend)
for _, info := range installedInfos {
pkg := info.Pkg
installPath := filepath.Join(basePath, info.DirName)
baseURLPath := baseURLPathPrefix + info.DirName + "/"
// 设置本地集市包的通用元数据
if !bazaar.SetInstalledPackageMetadata(pkg, installPath, baseURLPath, pkgType, frontend, 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":
installedIncompatible := bazaar.IsIncompatiblePlugin(pkg, frontend)
pkg.InstalledIncompatible = &installedIncompatible
var bazaarIncompatible bool
if onlinePkg := bazaarPackagesMap[pkg.Name]; nil != onlinePkg {
bazaarIncompatible = bazaar.IsIncompatiblePlugin(onlinePkg, frontend)
}
pkg.BazaarIncompatible = &bazaarIncompatible
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
}
// installBazaarPackage 下载并安装集市包
func installBazaarPackage(pkgType, repoURL, repoHash, packageName string) (meta installMeta, err error) {
installPath, jsonFileName, err := getPackageInstallPath(pkgType, packageName)
if err != nil {
return
}
installedPkg, parseErr := bazaar.ParsePackageJSON(filepath.Join(installPath, jsonFileName))
meta.update = parseErr == nil && installedPkg != nil && installedPkg.Name == packageName
err = bazaar.InstallPackage(repoURL, repoHash, installPath, Conf.System.ID, pkgType, packageName)
if err != nil {
err = fmt.Errorf(Conf.Language(46), packageName, err)
}
return
}
// finishInstall 集市包安装后的处理(刷新外观、推送插件重载等);批量更新时同类型只执行一次
//
// - themeMode0 浅色 / 1 深色仅在新安装主题meta.update 为 false时写入外观批量覆盖更新不会用到
func finishInstall(pkgType string, items []batchInstallItem, themeMode int) {
if 1 > len(items) {
return
}
switch pkgType {
case "plugins":
reloadPluginSet := hashset.New()
for _, item := range items {
if !item.meta.update {
continue
}
petal := GetPetalByName(item.name)
if nil != petal && petal.Enabled {
_, err := SetPetalEnabled(petal.Name, petal.Enabled) // 重新加载插件内容
if err != nil {
logging.LogErrorf("reload plugin [%s] after update failed: %s", item.name, err)
util.PushErrMsg(err.Error(), 5000)
continue
}
reloadPluginSet.Add(item.name)
}
}
if 0 < reloadPluginSet.Size() {
PushReloadPlugin(nil, nil, reloadPluginSet, nil, "")
}
case "themes":
for _, item := range items {
if !item.meta.update {
// 新安装主题时才自动切换 https://github.com/siyuan-note/siyuan/issues/4966
if 0 == themeMode {
Conf.Appearance.ThemeLight = item.name
} else {
Conf.Appearance.ThemeDark = item.name
}
Conf.Appearance.Mode = themeMode
Conf.Appearance.ThemeJS = gulu.File.IsExist(filepath.Join(util.ThemesPath, item.name, "theme.js"))
Conf.Save()
}
}
InitAppearance()
WatchThemes()
util.BroadcastByType("main", "setAppearance", 0, "", Conf.Appearance)
case "icons":
for _, item := range items {
if !item.meta.update {
// 新安装图标时才自动切换
Conf.Appearance.Icon = item.name
Conf.Save()
}
}
InitAppearance()
util.BroadcastByType("main", "setAppearance", 0, "", Conf.Appearance)
}
}
// InstallBazaarPackage 安装集市包themeMode 仅在 pkgType 为 "themes" 时生效
func InstallBazaarPackage(pkgType, repoURL, repoHash, packageName string, themeMode int) error {
meta, err := installBazaarPackage(pkgType, repoURL, repoHash, packageName)
if err != nil {
return err
}
finishInstall(pkgType, []batchInstallItem{{name: packageName, meta: meta}}, themeMode)
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 "litheness" == name
}