siyuan/kernel/bazaar/stage.go
2026-03-08 15:49:18 +08:00

224 lines
6.6 KiB
Go
Raw 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 bazaar
import (
"context"
"errors"
"sync"
"time"
gcache "github.com/patrickmn/go-cache"
"github.com/siyuan-note/httpclient"
"github.com/siyuan-note/logging"
"github.com/siyuan-note/siyuan/kernel/util"
"golang.org/x/sync/singleflight"
)
// cachedStageIndex 缓存 stage 索引
var cachedStageIndex = gcache.New(time.Duration(util.RhyCacheDuration)*time.Second, time.Duration(util.RhyCacheDuration)*time.Second/6)
type StageBazaarResult struct {
StageIndex *StageIndex // stage 索引
BazaarStats map[string]*bazaarStats // 统计信息
Online bool // online 状态
StageErr error // stage 错误
}
var stageBazaarFlight singleflight.Group
var bazaarStatsFlight singleflight.Group
// getStageAndBazaar 获取 stage 索引和 bazaar 索引,相同 pkgType 的并发调用会合并为一次实际请求 (single-flight)
func getStageAndBazaar(pkgType string) (result StageBazaarResult) {
key := "stageBazaar:" + pkgType
v, err, _ := stageBazaarFlight.Do(key, func() (interface{}, error) {
return getStageAndBazaar0(pkgType), nil
})
if err != nil {
return
}
result = v.(StageBazaarResult)
return
}
// getStageAndBazaar0 执行一次 stage 和 bazaar 索引拉取
func getStageAndBazaar0(pkgType string) (result StageBazaarResult) {
stageIndex, stageErr := getStageIndexFromCache(pkgType)
bazaarStats := getBazaarStatsFromCache()
if nil != stageIndex && nil != bazaarStats {
// 两者都从缓存返回,不需要 online 检查
return StageBazaarResult{
StageIndex: stageIndex,
BazaarStats: bazaarStats,
Online: true,
StageErr: stageErr,
}
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
var onlineResult bool
onlineDone := make(chan bool, 1)
wg := &sync.WaitGroup{}
wg.Add(3)
go func() {
defer wg.Done()
onlineResult = isBazaarOnline()
onlineDone <- true
}()
go func() {
defer wg.Done()
stageIndex, stageErr = getStageIndex(ctx, pkgType)
}()
go func() {
defer wg.Done()
bazaarStats = getBazaarStats(ctx)
}()
<-onlineDone
if !onlineResult {
// 不在线时立即取消其他请求并返回结果,避免等待 HTTP 请求超时
cancel()
return StageBazaarResult{
StageIndex: stageIndex,
BazaarStats: bazaarStats,
Online: false,
StageErr: stageErr,
}
}
// 在线时等待所有请求完成
wg.Wait()
return StageBazaarResult{
StageIndex: stageIndex,
BazaarStats: bazaarStats,
Online: onlineResult,
StageErr: stageErr,
}
}
// getStageIndexFromCache 仅从缓存获取 stage 索引,过期或无缓存时返回 nil
func getStageIndexFromCache(pkgType string) (ret *StageIndex, err error) {
if val, found := cachedStageIndex.Get(pkgType); found {
ret = val.(*StageIndex)
}
return
}
// getStageIndex 获取 stage 索引
func getStageIndex(ctx context.Context, pkgType string) (ret *StageIndex, err error) {
if cached, cacheErr := getStageIndexFromCache(pkgType); nil != cached {
ret = cached
err = cacheErr
return
}
var rhyRet map[string]interface{}
rhyRet, err = util.GetRhyResult(ctx, false)
if nil != err {
return
}
bazaarHash := rhyRet["bazaar"].(string)
ret = &StageIndex{}
request := httpclient.NewBrowserRequest()
u := util.BazaarOSSServer + "/bazaar@" + bazaarHash + "/stage/" + pkgType + ".json" // pkgType 单词为复数形式
resp, reqErr := request.SetContext(ctx).SetSuccessResult(ret).Get(u)
if nil != reqErr {
logging.LogErrorf("get community stage index [%s] failed: %s", u, reqErr)
err = reqErr
return
}
if 200 != resp.StatusCode {
logging.LogErrorf("get community stage index [%s] failed: %d", u, resp.StatusCode)
err = errors.New("get stage index failed")
return
}
cachedStageIndex.SetDefault(pkgType, ret)
return
}
// getStageRepoByURL 根据 pkgType 与 urlowner/repo@hash获取 StageRepo
func getStageRepoByURL(ctx context.Context, pkgType, url string) *StageRepo {
stageIndex, _ := getStageIndex(ctx, pkgType)
if nil == stageIndex {
return nil
}
stageIndex.reposOnce.Do(func() {
stageIndex.reposByURL = make(map[string]*StageRepo, len(stageIndex.Repos))
for _, r := range stageIndex.Repos {
stageIndex.reposByURL[r.URL] = r
}
})
return stageIndex.reposByURL[url]
}
func isBazaarOnline() (ret bool) {
// Improve marketplace loading when offline https://github.com/siyuan-note/siyuan/issues/12050
ret = util.IsOnline(util.BazaarOSSServer+"/204", true, 3000)
if !ret {
util.PushErrMsg(util.Langs[util.Lang][24], 5000)
}
return
}
// bazaarStats 集市包统计信息
type bazaarStats struct {
Downloads int `json:"downloads"` // 下载次数
}
// cachedBazaarStats 缓存集市包统计信息
var cachedBazaarStats = gcache.New(time.Duration(util.RhyCacheDuration)*time.Second, time.Duration(util.RhyCacheDuration)*time.Second/6)
// getBazaarStatsFromCache 仅从缓存获取集市包统计信息,过期或无缓存时返回 nil
func getBazaarStatsFromCache() (ret map[string]*bazaarStats) {
if val, found := cachedBazaarStats.Get("index"); found {
ret = val.(map[string]*bazaarStats)
}
return
}
// getBazaarStats 获取集市包统计信息
func getBazaarStats(ctx context.Context) map[string]*bazaarStats {
if cached := getBazaarStatsFromCache(); nil != cached {
return cached
}
v, _, _ := bazaarStatsFlight.Do("bazaarStats", func() (interface{}, error) {
return getBazaarStats0(ctx), nil
})
return v.(map[string]*bazaarStats)
}
func getBazaarStats0(ctx context.Context) map[string]*bazaarStats {
var result map[string]*bazaarStats
request := httpclient.NewBrowserRequest()
u := util.BazaarStatServer + "/bazaar/index.json"
resp, reqErr := request.SetContext(ctx).SetSuccessResult(&result).Get(u)
if nil != reqErr {
logging.LogErrorf("get bazaar stats [%s] failed: %s", u, reqErr)
return result
}
if 200 != resp.StatusCode {
logging.LogErrorf("get bazaar stats [%s] failed: %d", u, resp.StatusCode)
return result
}
cachedBazaarStats.SetDefault("index", result)
return result
}