add support for multi-target docker image(oci) proxy
This commit is contained in:
53
main.go
53
main.go
@@ -17,6 +17,7 @@ import (
|
|||||||
"ghproxy/middleware/loggin"
|
"ghproxy/middleware/loggin"
|
||||||
"ghproxy/proxy"
|
"ghproxy/proxy"
|
||||||
"ghproxy/rate"
|
"ghproxy/rate"
|
||||||
|
"ghproxy/weakcache"
|
||||||
|
|
||||||
"github.com/WJQSERVER-STUDIO/logger"
|
"github.com/WJQSERVER-STUDIO/logger"
|
||||||
"github.com/hertz-contrib/http2/factory"
|
"github.com/hertz-contrib/http2/factory"
|
||||||
@@ -50,6 +51,10 @@ var (
|
|||||||
pagesFS embed.FS
|
pagesFS embed.FS
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
wcache *weakcache.Cache[string] // docker token缓存
|
||||||
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
logw = logger.Logw
|
logw = logger.Logw
|
||||||
logDump = logger.LogDump
|
logDump = logger.LogDump
|
||||||
@@ -360,6 +365,9 @@ func init() {
|
|||||||
setMemLimit(cfg)
|
setMemLimit(cfg)
|
||||||
loadlist(cfg)
|
loadlist(cfg)
|
||||||
setupRateLimit(cfg)
|
setupRateLimit(cfg)
|
||||||
|
if cfg.Docker.Enabled {
|
||||||
|
wcache = proxy.InitWeakCache()
|
||||||
|
}
|
||||||
|
|
||||||
if cfg.Server.Debug {
|
if cfg.Server.Debug {
|
||||||
runMode = "dev"
|
runMode = "dev"
|
||||||
@@ -468,23 +476,7 @@ func main() {
|
|||||||
proxy.RoutingHandler(cfg, limiter, iplimiter)(ctx, c)
|
proxy.RoutingHandler(cfg, limiter, iplimiter)(ctx, c)
|
||||||
})
|
})
|
||||||
|
|
||||||
// for 3.4.0
|
r.GET("/v2/", func(ctx context.Context, c *app.RequestContext) {
|
||||||
|
|
||||||
/*
|
|
||||||
r.GET("/v2/", func(ctx context.Context, c *app.RequestContext) {
|
|
||||||
proxy.GhcrRouting(cfg)(ctx, c)
|
|
||||||
|
|
||||||
/*
|
|
||||||
//proxy.GhcrRouting(cfg)(ctx, c)
|
|
||||||
// 返回200与空json
|
|
||||||
//c.JSON(200, map[string]interface{}{})
|
|
||||||
emptyJSON := "{}"
|
|
||||||
//emptyJSON := `{"name":"disable-list-tags","tags":[]}`
|
|
||||||
c.Header("Content-Type", "application/json")
|
|
||||||
c.Header("Content-Length", fmt.Sprint(len(emptyJSON)))
|
|
||||||
c.String(200, emptyJSON)
|
|
||||||
*/
|
|
||||||
/*
|
|
||||||
emptyJSON := "{}"
|
emptyJSON := "{}"
|
||||||
c.Header("Content-Type", "application/json")
|
c.Header("Content-Type", "application/json")
|
||||||
c.Header("Content-Length", fmt.Sprint(len(emptyJSON)))
|
c.Header("Content-Length", fmt.Sprint(len(emptyJSON)))
|
||||||
@@ -493,30 +485,18 @@ func main() {
|
|||||||
|
|
||||||
c.Status(200)
|
c.Status(200)
|
||||||
c.Write([]byte(emptyJSON))
|
c.Write([]byte(emptyJSON))
|
||||||
*/
|
})
|
||||||
|
|
||||||
|
r.Any("/v2/:target/:user/:repo/*filepath", func(ctx context.Context, c *app.RequestContext) {
|
||||||
|
proxy.GhcrWithImageRouting(cfg)(ctx, c)
|
||||||
|
})
|
||||||
|
|
||||||
/*
|
/*
|
||||||
w := adaptor.GetCompatResponseWriter(&c.Response)
|
|
||||||
|
|
||||||
const emptyJSON = "{}"
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
w.Header().Set("Content-Length", fmt.Sprint(len(emptyJSON)))
|
|
||||||
w.Header().Del("Server")
|
|
||||||
|
|
||||||
fmt.Fprint(w, emptyJSON)
|
|
||||||
*/
|
|
||||||
/*
|
|
||||||
})
|
|
||||||
|
|
||||||
r.Any("/v2/:target/*filepath", func(ctx context.Context, c *app.RequestContext) {
|
r.Any("/v2/:target/*filepath", func(ctx context.Context, c *app.RequestContext) {
|
||||||
proxy.GhcrRouting(cfg)(ctx, c)
|
proxy.GhcrRouting(cfg)(ctx, c)
|
||||||
})
|
})
|
||||||
*/
|
*/
|
||||||
|
|
||||||
r.Any("/v2/*filepath", func(ctx context.Context, c *app.RequestContext) {
|
|
||||||
proxy.GhcrRouting(cfg)(ctx, c)
|
|
||||||
})
|
|
||||||
|
|
||||||
r.NoRoute(func(ctx context.Context, c *app.RequestContext) {
|
r.NoRoute(func(ctx context.Context, c *app.RequestContext) {
|
||||||
proxy.NoRouteHandler(cfg, limiter, iplimiter)(ctx, c)
|
proxy.NoRouteHandler(cfg, limiter, iplimiter)(ctx, c)
|
||||||
})
|
})
|
||||||
@@ -531,7 +511,7 @@ func main() {
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
r.Spin()
|
defer wcache.StopCleanup()
|
||||||
defer logger.Close()
|
defer logger.Close()
|
||||||
defer func() {
|
defer func() {
|
||||||
if hertZfile != nil {
|
if hertZfile != nil {
|
||||||
@@ -541,5 +521,8 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
r.Spin()
|
||||||
|
|
||||||
fmt.Println("Program Exit")
|
fmt.Println("Program Exit")
|
||||||
}
|
}
|
||||||
|
|||||||
62
proxy/authparse.go
Normal file
62
proxy/authparse.go
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
package proxy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BearerAuthParams 用于存放解析出的 Bearer 认证参数
|
||||||
|
type BearerAuthParams struct {
|
||||||
|
Realm string
|
||||||
|
Service string
|
||||||
|
Scope string
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseBearerWWWAuthenticateHeader 解析 Bearer 方案的 Www-Authenticate Header。
|
||||||
|
// 它期望格式为 'Bearer key1="value1",key2="value2",...'
|
||||||
|
// 并尝试将已知参数解析到 BearerAuthParams struct 中。
|
||||||
|
func parseBearerWWWAuthenticateHeader(headerValue string) (*BearerAuthParams, error) {
|
||||||
|
if headerValue == "" {
|
||||||
|
return nil, fmt.Errorf("header value is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查 Scheme 是否是 "Bearer"
|
||||||
|
parts := strings.SplitN(headerValue, " ", 2)
|
||||||
|
if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" {
|
||||||
|
return nil, fmt.Errorf("invalid or non-bearer header format: got '%s'", headerValue)
|
||||||
|
}
|
||||||
|
paramsStr := parts[1]
|
||||||
|
|
||||||
|
paramPairs := strings.Split(paramsStr, ",")
|
||||||
|
tempMap := make(map[string]string)
|
||||||
|
|
||||||
|
for _, pair := range paramPairs {
|
||||||
|
trimmedPair := strings.TrimSpace(pair)
|
||||||
|
keyValue := strings.SplitN(trimmedPair, "=", 2)
|
||||||
|
if len(keyValue) != 2 {
|
||||||
|
logWarning("Skipping malformed parameter '%s' in Www-Authenticate header: %s", pair, headerValue)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
key := strings.TrimSpace(keyValue[0])
|
||||||
|
value := strings.TrimSpace(keyValue[1])
|
||||||
|
if strings.HasPrefix(value, "\"") && strings.HasSuffix(value, "\"") {
|
||||||
|
value = value[1 : len(value)-1]
|
||||||
|
}
|
||||||
|
tempMap[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
//从 map 中提取值并填充到 struct
|
||||||
|
authParams := &BearerAuthParams{}
|
||||||
|
|
||||||
|
if realm, ok := tempMap["realm"]; ok {
|
||||||
|
authParams.Realm = realm
|
||||||
|
}
|
||||||
|
if service, ok := tempMap["service"]; ok {
|
||||||
|
authParams.Service = service
|
||||||
|
}
|
||||||
|
if scope, ok := tempMap["scope"]; ok {
|
||||||
|
authParams.Scope = scope
|
||||||
|
}
|
||||||
|
|
||||||
|
return authParams, nil
|
||||||
|
}
|
||||||
247
proxy/docker.go
247
proxy/docker.go
@@ -2,39 +2,132 @@ package proxy
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"ghproxy/config"
|
"ghproxy/config"
|
||||||
|
"ghproxy/weakcache"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/WJQSERVER-STUDIO/go-utils/limitreader"
|
"github.com/WJQSERVER-STUDIO/go-utils/limitreader"
|
||||||
"github.com/cloudwego/hertz/pkg/app"
|
"github.com/cloudwego/hertz/pkg/app"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
dockerhubTarget = "registry-1.docker.io"
|
||||||
|
ghcrTarget = "ghcr.io"
|
||||||
|
)
|
||||||
|
|
||||||
|
var cache *weakcache.Cache[string]
|
||||||
|
|
||||||
|
type imageInfo struct {
|
||||||
|
User string
|
||||||
|
Repo string
|
||||||
|
Image string
|
||||||
|
}
|
||||||
|
|
||||||
|
func InitWeakCache() *weakcache.Cache[string] {
|
||||||
|
cache = weakcache.NewCache[string](weakcache.DefaultExpiration, 100)
|
||||||
|
return cache
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
func GhcrRouting(cfg *config.Config) app.HandlerFunc {
|
func GhcrRouting(cfg *config.Config) app.HandlerFunc {
|
||||||
return func(ctx context.Context, c *app.RequestContext) {
|
return func(ctx context.Context, c *app.RequestContext) {
|
||||||
if cfg.Docker.Enabled {
|
|
||||||
|
charToFind := '.'
|
||||||
|
reqTarget := c.Param("target")
|
||||||
|
path := ""
|
||||||
|
target := ""
|
||||||
|
|
||||||
|
if strings.ContainsRune(reqTarget, charToFind) {
|
||||||
|
|
||||||
|
path = c.Param("filepath")
|
||||||
|
if reqTarget == "docker.io" {
|
||||||
|
target = dockerhubTarget
|
||||||
|
} else if reqTarget == "ghcr.io" {
|
||||||
|
target = ghcrTarget
|
||||||
|
} else {
|
||||||
|
target = reqTarget
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
path = string(c.Request.RequestURI())
|
||||||
|
}
|
||||||
|
|
||||||
|
GhcrToTarget(ctx, c, cfg, target, path, nil)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
func GhcrWithImageRouting(cfg *config.Config) app.HandlerFunc {
|
||||||
|
return func(ctx context.Context, c *app.RequestContext) {
|
||||||
|
|
||||||
|
charToFind := '.'
|
||||||
|
reqTarget := c.Param("target")
|
||||||
|
reqImageUser := c.Param("user")
|
||||||
|
reqImageName := c.Param("repo")
|
||||||
|
reqFilePath := c.Param("filepath")
|
||||||
|
|
||||||
|
path := fmt.Sprintf("%s/%s/%s", reqImageUser, reqImageName, reqFilePath)
|
||||||
|
target := ""
|
||||||
|
|
||||||
|
if strings.ContainsRune(reqTarget, charToFind) {
|
||||||
|
|
||||||
|
if reqTarget == "docker.io" {
|
||||||
|
target = dockerhubTarget
|
||||||
|
} else if reqTarget == "ghcr.io" {
|
||||||
|
target = ghcrTarget
|
||||||
|
} else {
|
||||||
|
target = reqTarget
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
path = string(c.Request.RequestURI())
|
||||||
|
reqImageUser = c.Param("target")
|
||||||
|
reqImageName = c.Param("user")
|
||||||
|
}
|
||||||
|
image := &imageInfo{
|
||||||
|
User: reqImageUser,
|
||||||
|
Repo: reqImageName,
|
||||||
|
Image: fmt.Sprintf("%s/%s", reqImageUser, reqImageName),
|
||||||
|
}
|
||||||
|
|
||||||
|
GhcrToTarget(ctx, c, cfg, target, path, image)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func GhcrToTarget(ctx context.Context, c *app.RequestContext, cfg *config.Config, target string, path string, image *imageInfo) {
|
||||||
|
if cfg.Docker.Enabled {
|
||||||
|
if target != "" {
|
||||||
|
GhcrRequest(ctx, c, "https://"+target+"/v2/"+path+string(c.Request.QueryString()), image, cfg, target)
|
||||||
|
|
||||||
|
} else {
|
||||||
if cfg.Docker.Target == "ghcr" {
|
if cfg.Docker.Target == "ghcr" {
|
||||||
GhcrRequest(ctx, c, "https://ghcr.io"+string(c.Request.RequestURI()), cfg, "ghcr")
|
GhcrRequest(ctx, c, "https://"+ghcrTarget+string(c.Request.RequestURI()), image, cfg, ghcrTarget)
|
||||||
} else if cfg.Docker.Target == "dockerhub" {
|
} else if cfg.Docker.Target == "dockerhub" {
|
||||||
GhcrRequest(ctx, c, "https://registry-1.docker.io"+string(c.Request.RequestURI()), cfg, "dockerhub")
|
GhcrRequest(ctx, c, "https://"+dockerhubTarget+string(c.Request.RequestURI()), image, cfg, dockerhubTarget)
|
||||||
} else if cfg.Docker.Target != "" {
|
} else if cfg.Docker.Target != "" {
|
||||||
// 自定义taget
|
// 自定义taget
|
||||||
GhcrRequest(ctx, c, "https://"+cfg.Docker.Target+string(c.Request.RequestURI()), cfg, "custom")
|
GhcrRequest(ctx, c, "https://"+cfg.Docker.Target+string(c.Request.RequestURI()), image, cfg, cfg.Docker.Target)
|
||||||
} else {
|
} else {
|
||||||
// 配置为空
|
// 配置为空
|
||||||
ErrorPage(c, NewErrorWithStatusLookup(403, "Docker Target is not set"))
|
ErrorPage(c, NewErrorWithStatusLookup(403, "Docker Target is not set"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
|
||||||
ErrorPage(c, NewErrorWithStatusLookup(403, "Docker is not Allowed"))
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
ErrorPage(c, NewErrorWithStatusLookup(403, "Docker is not Allowed"))
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func GhcrRequest(ctx context.Context, c *app.RequestContext, u string, cfg *config.Config, matcher string) {
|
func GhcrRequest(ctx context.Context, c *app.RequestContext, u string, image *imageInfo, cfg *config.Config, target string) {
|
||||||
|
|
||||||
var (
|
var (
|
||||||
method []byte
|
method []byte
|
||||||
@@ -55,12 +148,11 @@ func GhcrRequest(ctx context.Context, c *app.RequestContext, u string, cfg *conf
|
|||||||
|
|
||||||
method = c.Request.Method()
|
method = c.Request.Method()
|
||||||
|
|
||||||
rb := client.NewRequestBuilder(string(method), u)
|
rb := ghcrclient.NewRequestBuilder(string(method), u)
|
||||||
rb.NoDefaultHeaders()
|
rb.NoDefaultHeaders()
|
||||||
rb.SetBody(c.Request.BodyStream())
|
rb.SetBody(c.Request.BodyStream())
|
||||||
rb.WithContext(ctx)
|
rb.WithContext(ctx)
|
||||||
|
|
||||||
//req, err = client.NewRequest(string(method), u, c.Request.BodyStream())
|
|
||||||
req, err = rb.Build()
|
req, err = rb.Build()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
HandleError(c, fmt.Sprintf("Failed to create request: %v", err))
|
HandleError(c, fmt.Sprintf("Failed to create request: %v", err))
|
||||||
@@ -73,14 +165,62 @@ func GhcrRequest(ctx context.Context, c *app.RequestContext, u string, cfg *conf
|
|||||||
req.Header.Add(headerKey, headerValue)
|
req.Header.Add(headerKey, headerValue)
|
||||||
})
|
})
|
||||||
|
|
||||||
resp, err = client.Do(req)
|
req.Header.Set("Host", target)
|
||||||
|
token, exist := cache.Get(image.Image)
|
||||||
|
if exist {
|
||||||
|
logDebug("Use Cache Token: %s", token)
|
||||||
|
req.Header.Set("Authorization", "Bearer "+token)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err = ghcrclient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
HandleError(c, fmt.Sprintf("Failed to send request: %v", err))
|
HandleError(c, fmt.Sprintf("Failed to send request: %v", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 错误处理(404)
|
// 处理状态码
|
||||||
if resp.StatusCode == 404 {
|
if resp.StatusCode == 401 {
|
||||||
|
// 请求target /v2/路径
|
||||||
|
if string(c.Request.URI().Path()) != "/v2/" {
|
||||||
|
resp.Body.Close()
|
||||||
|
token := ChallengeReq(target, image, ctx, c)
|
||||||
|
|
||||||
|
// 更新kv
|
||||||
|
if token != "" {
|
||||||
|
logDump("Update Cache Token: %s", token)
|
||||||
|
cache.Put(image.Image, token)
|
||||||
|
}
|
||||||
|
|
||||||
|
rb := ghcrclient.NewRequestBuilder(string(method), u)
|
||||||
|
rb.NoDefaultHeaders()
|
||||||
|
rb.SetBody(c.Request.BodyStream())
|
||||||
|
rb.WithContext(ctx)
|
||||||
|
|
||||||
|
req, err = rb.Build()
|
||||||
|
if err != nil {
|
||||||
|
HandleError(c, fmt.Sprintf("Failed to create request: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Request.Header.VisitAll(func(key, value []byte) {
|
||||||
|
headerKey := string(key)
|
||||||
|
headerValue := string(value)
|
||||||
|
req.Header.Add(headerKey, headerValue)
|
||||||
|
})
|
||||||
|
|
||||||
|
req.Header.Set("Host", target)
|
||||||
|
if token != "" {
|
||||||
|
req.Header.Set("Authorization", "Bearer "+token)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err = ghcrclient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
HandleError(c, fmt.Sprintf("Failed to send request: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if resp.StatusCode == 404 { // 错误处理(404)
|
||||||
ErrorPage(c, NewErrorWithStatusLookup(404, "Page Not Found (From Github)"))
|
ErrorPage(c, NewErrorWithStatusLookup(404, "Page Not Found (From Github)"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -101,8 +241,7 @@ func GhcrRequest(ctx context.Context, c *app.RequestContext, u string, cfg *conf
|
|||||||
bodySize = -1
|
bodySize = -1
|
||||||
}
|
}
|
||||||
if err == nil && bodySize > sizelimit {
|
if err == nil && bodySize > sizelimit {
|
||||||
var finalURL string
|
finalURL := resp.Request.URL.String()
|
||||||
finalURL = resp.Request.URL.String()
|
|
||||||
err = resp.Body.Close()
|
err = resp.Body.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logError("Failed to close response body: %v", err)
|
logError("Failed to close response body: %v", err)
|
||||||
@@ -116,7 +255,6 @@ func GhcrRequest(ctx context.Context, c *app.RequestContext, u string, cfg *conf
|
|||||||
// 复制响应头,排除需要移除的 header
|
// 复制响应头,排除需要移除的 header
|
||||||
for key, values := range resp.Header {
|
for key, values := range resp.Header {
|
||||||
for _, value := range values {
|
for _, value := range values {
|
||||||
//c.Header(key, value)
|
|
||||||
c.Response.Header.Add(key, value)
|
c.Response.Header.Add(key, value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -136,3 +274,78 @@ func GhcrRequest(ctx context.Context, c *app.RequestContext, u string, cfg *conf
|
|||||||
c.SetBodyStream(bodyReader, -1)
|
c.SetBodyStream(bodyReader, -1)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AuthToken struct {
|
||||||
|
Token string `json:"token"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func ChallengeReq(target string, image *imageInfo, ctx context.Context, c *app.RequestContext) (token string) {
|
||||||
|
var resp401 *http.Response
|
||||||
|
var req401 *http.Request
|
||||||
|
var err error
|
||||||
|
|
||||||
|
rb401 := ghcrclient.NewRequestBuilder("GET", "https://"+target+"/v2/")
|
||||||
|
rb401.NoDefaultHeaders()
|
||||||
|
rb401.WithContext(ctx)
|
||||||
|
rb401.AddHeader("User-Agent", "docker/28.1.1 go/go1.23.8 git-commit/01f442b kernel/6.12.25-amd64 os/linux arch/amd64 UpstreamClient(Docker-Client/28.1.1 ")
|
||||||
|
req401, err = rb401.Build()
|
||||||
|
if err != nil {
|
||||||
|
HandleError(c, fmt.Sprintf("Failed to create request: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
req401.Header.Set("Host", target)
|
||||||
|
|
||||||
|
resp401, err = ghcrclient.Do(req401)
|
||||||
|
if err != nil {
|
||||||
|
HandleError(c, fmt.Sprintf("Failed to send request: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer resp401.Body.Close()
|
||||||
|
bearer, err := parseBearerWWWAuthenticateHeader(resp401.Header.Get("Www-Authenticate"))
|
||||||
|
if err != nil {
|
||||||
|
logError("Failed to parse Www-Authenticate header: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
scope := fmt.Sprintf("repository:%s:pull", image.Image)
|
||||||
|
|
||||||
|
getAuthRB := ghcrclient.NewRequestBuilder("GET", bearer.Realm).
|
||||||
|
NoDefaultHeaders().
|
||||||
|
WithContext(ctx).
|
||||||
|
AddHeader("User-Agent", "docker/28.1.1 go/go1.23.8 git-commit/01f442b kernel/6.12.25-amd64 os/linux arch/amd64 UpstreamClient(Docker-Client/28.1.1 ").
|
||||||
|
SetHeader("Host", bearer.Service).
|
||||||
|
AddQueryParam("service", bearer.Service).
|
||||||
|
AddQueryParam("scope", scope)
|
||||||
|
|
||||||
|
getAuthReq, err := getAuthRB.Build()
|
||||||
|
if err != nil {
|
||||||
|
logError("Failed to create request: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
authResp, err := ghcrclient.Do(getAuthReq)
|
||||||
|
if err != nil {
|
||||||
|
logError("Failed to send request: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
defer authResp.Body.Close()
|
||||||
|
|
||||||
|
bodyBytes, err := io.ReadAll(authResp.Body)
|
||||||
|
if err != nil {
|
||||||
|
logError("Failed to read auth response body: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解码json
|
||||||
|
var authToken AuthToken
|
||||||
|
err = json.Unmarshal(bodyBytes, &authToken)
|
||||||
|
if err != nil {
|
||||||
|
logError("Failed to decode auth response body: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
token = authToken.Token
|
||||||
|
|
||||||
|
return token
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,10 +12,12 @@ import (
|
|||||||
var BufferSize int = 32 * 1024 // 32KB
|
var BufferSize int = 32 * 1024 // 32KB
|
||||||
|
|
||||||
var (
|
var (
|
||||||
tr *http.Transport
|
tr *http.Transport
|
||||||
gittr *http.Transport
|
gittr *http.Transport
|
||||||
client *httpc.Client
|
client *httpc.Client
|
||||||
gitclient *httpc.Client
|
gitclient *httpc.Client
|
||||||
|
ghcrtr *http.Transport
|
||||||
|
ghcrclient *httpc.Client
|
||||||
)
|
)
|
||||||
|
|
||||||
func InitReq(cfg *config.Config) error {
|
func InitReq(cfg *config.Config) error {
|
||||||
@@ -23,11 +25,13 @@ func InitReq(cfg *config.Config) error {
|
|||||||
if cfg.GitClone.Mode == "cache" {
|
if cfg.GitClone.Mode == "cache" {
|
||||||
initGitHTTPClient(cfg)
|
initGitHTTPClient(cfg)
|
||||||
}
|
}
|
||||||
|
initGhcrHTTPClient(cfg)
|
||||||
err := SetGlobalRateLimit(cfg)
|
err := SetGlobalRateLimit(cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func initHTTPClient(cfg *config.Config) {
|
func initHTTPClient(cfg *config.Config) {
|
||||||
@@ -77,6 +81,7 @@ func initHTTPClient(cfg *config.Config) {
|
|||||||
httpc.WithTransport(tr),
|
httpc.WithTransport(tr),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func initGitHTTPClient(cfg *config.Config) {
|
func initGitHTTPClient(cfg *config.Config) {
|
||||||
@@ -147,3 +152,51 @@ func initGitHTTPClient(cfg *config.Config) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func initGhcrHTTPClient(cfg *config.Config) {
|
||||||
|
var proTolcols = new(http.Protocols)
|
||||||
|
proTolcols.SetHTTP1(true)
|
||||||
|
proTolcols.SetHTTP2(true)
|
||||||
|
if cfg.Httpc.Mode == "auto" {
|
||||||
|
|
||||||
|
ghcrtr = &http.Transport{
|
||||||
|
IdleConnTimeout: 30 * time.Second,
|
||||||
|
WriteBufferSize: 32 * 1024, // 32KB
|
||||||
|
ReadBufferSize: 32 * 1024, // 32KB
|
||||||
|
Protocols: proTolcols,
|
||||||
|
}
|
||||||
|
} else if cfg.Httpc.Mode == "advanced" {
|
||||||
|
ghcrtr = &http.Transport{
|
||||||
|
MaxIdleConns: cfg.Httpc.MaxIdleConns,
|
||||||
|
MaxConnsPerHost: cfg.Httpc.MaxConnsPerHost,
|
||||||
|
MaxIdleConnsPerHost: cfg.Httpc.MaxIdleConnsPerHost,
|
||||||
|
WriteBufferSize: 32 * 1024, // 32KB
|
||||||
|
ReadBufferSize: 32 * 1024, // 32KB
|
||||||
|
Protocols: proTolcols,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 错误的模式
|
||||||
|
logError("unknown httpc mode: %s", cfg.Httpc.Mode)
|
||||||
|
fmt.Println("unknown httpc mode: ", cfg.Httpc.Mode)
|
||||||
|
logWarning("use Auto to Run HTTP Client")
|
||||||
|
fmt.Println("use Auto to Run HTTP Client")
|
||||||
|
ghcrtr = &http.Transport{
|
||||||
|
IdleConnTimeout: 30 * time.Second,
|
||||||
|
WriteBufferSize: 32 * 1024, // 32KB
|
||||||
|
ReadBufferSize: 32 * 1024, // 32KB
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if cfg.Outbound.Enabled {
|
||||||
|
initTransport(cfg, ghcrtr)
|
||||||
|
}
|
||||||
|
if cfg.Server.Debug {
|
||||||
|
ghcrclient = httpc.New(
|
||||||
|
httpc.WithTransport(ghcrtr),
|
||||||
|
httpc.WithDumpLog(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
ghcrclient = httpc.New(
|
||||||
|
httpc.WithTransport(ghcrtr),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -17,9 +17,12 @@ func Matcher(rawPath string, cfg *config.Config) (string, string, string, *GHPro
|
|||||||
// 匹配 "https://github.com"开头的链接
|
// 匹配 "https://github.com"开头的链接
|
||||||
if strings.HasPrefix(rawPath, "https://github.com") {
|
if strings.HasPrefix(rawPath, "https://github.com") {
|
||||||
remainingPath := strings.TrimPrefix(rawPath, "https://github.com")
|
remainingPath := strings.TrimPrefix(rawPath, "https://github.com")
|
||||||
if strings.HasPrefix(remainingPath, "/") {
|
/*
|
||||||
remainingPath = strings.TrimPrefix(remainingPath, "/")
|
if strings.HasPrefix(remainingPath, "/") {
|
||||||
}
|
remainingPath = strings.TrimPrefix(remainingPath, "/")
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
remainingPath = strings.TrimPrefix(remainingPath, "/")
|
||||||
// 预期格式/user/repo/more...
|
// 预期格式/user/repo/more...
|
||||||
// 取出user和repo和最后部分
|
// 取出user和repo和最后部分
|
||||||
parts := strings.Split(remainingPath, "/")
|
parts := strings.Split(remainingPath, "/")
|
||||||
|
|||||||
@@ -13,8 +13,7 @@ func listCheck(cfg *config.Config, c *app.RequestContext, user string, repo stri
|
|||||||
|
|
||||||
// 白名单检查
|
// 白名单检查
|
||||||
if cfg.Whitelist.Enabled {
|
if cfg.Whitelist.Enabled {
|
||||||
var whitelist bool
|
whitelist := auth.CheckWhitelist(user, repo)
|
||||||
whitelist = auth.CheckWhitelist(user, repo)
|
|
||||||
if !whitelist {
|
if !whitelist {
|
||||||
ErrorPage(c, NewErrorWithStatusLookup(403, fmt.Sprintf("Whitelist Blocked repo: %s/%s", user, repo)))
|
ErrorPage(c, NewErrorWithStatusLookup(403, fmt.Sprintf("Whitelist Blocked repo: %s/%s", user, repo)))
|
||||||
logInfo("%s %s %s %s %s Whitelist Blocked repo: %s/%s", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), user, repo)
|
logInfo("%s %s %s %s %s Whitelist Blocked repo: %s/%s", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), user, repo)
|
||||||
@@ -24,8 +23,7 @@ func listCheck(cfg *config.Config, c *app.RequestContext, user string, repo stri
|
|||||||
|
|
||||||
// 黑名单检查
|
// 黑名单检查
|
||||||
if cfg.Blacklist.Enabled {
|
if cfg.Blacklist.Enabled {
|
||||||
var blacklist bool
|
blacklist := auth.CheckBlacklist(user, repo)
|
||||||
blacklist = auth.CheckBlacklist(user, repo)
|
|
||||||
if blacklist {
|
if blacklist {
|
||||||
ErrorPage(c, NewErrorWithStatusLookup(403, fmt.Sprintf("Blacklist Blocked repo: %s/%s", user, repo)))
|
ErrorPage(c, NewErrorWithStatusLookup(403, fmt.Sprintf("Blacklist Blocked repo: %s/%s", user, repo)))
|
||||||
logInfo("%s %s %s %s %s Blacklist Blocked repo: %s/%s", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), user, repo)
|
logInfo("%s %s %s %s %s Blacklist Blocked repo: %s/%s", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), user, repo)
|
||||||
|
|||||||
258
weakcache/weakcache.go
Normal file
258
weakcache/weakcache.go
Normal file
@@ -0,0 +1,258 @@
|
|||||||
|
package weakcache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"container/list"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
"weak" // Go 1.24 引入的 weak 包
|
||||||
|
)
|
||||||
|
|
||||||
|
// DefaultExpiration 默认过期时间,这里设置为 15 分钟。
|
||||||
|
// 这是一个导出的常量,方便用户使用包时引用默认值。
|
||||||
|
const DefaultExpiration = 5 * time.Minute
|
||||||
|
|
||||||
|
// cleanupInterval 是后台清理 Go routine 的扫描间隔,这里设置为 5 分钟。
|
||||||
|
// 这是一个内部常量,不导出。
|
||||||
|
const cleanupInterval = 2 * time.Minute
|
||||||
|
|
||||||
|
// cacheEntry 缓存项的内部结构。不导出。
|
||||||
|
type cacheEntry[T any] struct {
|
||||||
|
Value T
|
||||||
|
Expiration time.Time
|
||||||
|
key string // 存储key,方便在list.Element中引用
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache 是一个基于 weak.Pointer, 带有过期和大小上限 (FIFO) 的泛型缓存。
|
||||||
|
// 这是一个导出的类型。
|
||||||
|
type Cache[T any] struct {
|
||||||
|
mu sync.RWMutex
|
||||||
|
|
||||||
|
// 修正:缓存存储:key -> weak.Pointer 到 cacheEntry 结构体 (而不是指向结构体的指针)
|
||||||
|
// weak.Make(*cacheEntry[T]) 返回 weak.Pointer[cacheEntry[T]]
|
||||||
|
data map[string]weak.Pointer[cacheEntry[T]]
|
||||||
|
|
||||||
|
// FIFO 链表:存储 key 的 list.Element
|
||||||
|
// 链表头部是最近放入的,尾部是最早放入的(最老的)
|
||||||
|
fifoList *list.List
|
||||||
|
// FIFO 元素的映射:key -> *list.Element
|
||||||
|
fifoMap map[string]*list.Element
|
||||||
|
|
||||||
|
defaultExpiration time.Duration
|
||||||
|
maxSize int // 缓存最大容量,0 表示无限制
|
||||||
|
|
||||||
|
stopCleanup chan struct{}
|
||||||
|
wg sync.WaitGroup // 用于等待清理 Go routine 退出
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCache 创建一个新的缓存实例。
|
||||||
|
// expiration: 新添加项的默认过期时间。如果为 0,则使用 DefaultExpiration。
|
||||||
|
// maxSize: 缓存的最大容量,0 表示无限制。当达到上限时,采用 FIFO 策略淘汰。
|
||||||
|
// 这是一个导出的构造函数。
|
||||||
|
func NewCache[T any](expiration time.Duration, maxSize int) *Cache[T] {
|
||||||
|
if expiration <= 0 {
|
||||||
|
expiration = DefaultExpiration
|
||||||
|
}
|
||||||
|
|
||||||
|
c := &Cache[T]{
|
||||||
|
// 修正:初始化 map,值类型已修正
|
||||||
|
data: make(map[string]weak.Pointer[cacheEntry[T]]),
|
||||||
|
fifoList: list.New(),
|
||||||
|
fifoMap: make(map[string]*list.Element),
|
||||||
|
defaultExpiration: expiration,
|
||||||
|
maxSize: maxSize,
|
||||||
|
stopCleanup: make(chan struct{}),
|
||||||
|
}
|
||||||
|
// 启动后台清理 Go routine
|
||||||
|
c.wg.Add(1)
|
||||||
|
go c.cleanupLoop()
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Put 将值放入缓存。如果 key 已存在,会更新其值和过期时间。
|
||||||
|
// 这是导出的方法。
|
||||||
|
func (c *Cache[T]) Put(key string, value T) {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
expiration := now.Add(c.defaultExpiration)
|
||||||
|
|
||||||
|
// 如果 key 已经存在,更新其值和过期时间。
|
||||||
|
// 在 FIFO 策略中, Put 更新不改变其在链表中的位置,除非旧的 entry 已经被 GC。
|
||||||
|
if elem, ok := c.fifoMap[key]; ok {
|
||||||
|
// 从 data map 中获取弱引用,wp 的类型现在是 weak.Pointer[cacheEntry[T]]
|
||||||
|
if wp, dataOk := c.data[key]; dataOk {
|
||||||
|
// wp.Value() 返回 *cacheEntry[T], entry 的类型现在是 *cacheEntry[T]
|
||||||
|
entry := wp.Value()
|
||||||
|
if entry != nil {
|
||||||
|
// 旧的 cacheEntry 仍在内存中,直接更新
|
||||||
|
entry.Value = value
|
||||||
|
entry.Expiration = expiration
|
||||||
|
// 在严格 FIFO 中,更新不移动位置
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 如果 weak.Pointer.Value() 为 nil,说明之前的 cacheEntry 已经被 GC 了
|
||||||
|
// 此时需要创建一个新的 entry,并将其从旧位置移除,再重新添加
|
||||||
|
c.fifoList.Remove(elem)
|
||||||
|
delete(c.fifoMap, key)
|
||||||
|
} else {
|
||||||
|
c.fifoList.Remove(elem)
|
||||||
|
delete(c.fifoMap, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新建缓存项 (注意这里是结构体值,而不是指针)
|
||||||
|
// weak.Make 接收的是指针 *T
|
||||||
|
entry := &cacheEntry[T]{ // 创建结构体指针
|
||||||
|
Value: value,
|
||||||
|
Expiration: expiration,
|
||||||
|
key: key, // 存储 key
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将新的 *cacheEntry[T] 包装成 weak.Pointer[cacheEntry[T]] 存入 data map
|
||||||
|
// weak.Make(entry) 现在返回 weak.Pointer[cacheEntry[T]],类型匹配 data map 的值类型
|
||||||
|
c.data[key] = weak.Make(entry)
|
||||||
|
|
||||||
|
// 添加到 FIFO 链表头部 (最近放入/更新的在头部)
|
||||||
|
// PushFront 返回新的 list.Element
|
||||||
|
c.fifoMap[key] = c.fifoList.PushFront(key)
|
||||||
|
|
||||||
|
// 检查大小上限并进行淘汰 (淘汰尾部的最老项)
|
||||||
|
c.evictIfNeeded()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get 从缓存中获取值。返回获取到的值和是否存在/是否有效。
|
||||||
|
// 这是导出的方法。
|
||||||
|
func (c *Cache[T]) Get(key string) (T, bool) {
|
||||||
|
c.mu.RLock() // 先读锁
|
||||||
|
// 从 data map 中获取弱引用,wp 的类型现在是 weak.Pointer[cacheEntry[T]]
|
||||||
|
wp, ok := c.data[key]
|
||||||
|
c.mu.RUnlock() // 立即释放读锁,如果需要写操作(removeEntry)可以获得锁
|
||||||
|
|
||||||
|
var zero T // 零值
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
return zero, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试获取实际的 cacheEntry 指针
|
||||||
|
// wp.Value() 返回 *cacheEntry[T], entry 的类型现在是 *cacheEntry[T]
|
||||||
|
entry := wp.Value()
|
||||||
|
|
||||||
|
if entry == nil {
|
||||||
|
// 对象已被GC回收,需要清理此弱引用
|
||||||
|
c.removeEntry(key) // 内部会加写锁
|
||||||
|
return zero, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查过期时间 (通过 entry 指针访问字段)
|
||||||
|
if time.Now().After(entry.Expiration) {
|
||||||
|
// 逻辑上已过期
|
||||||
|
c.removeEntry(key) // 内部会加写锁
|
||||||
|
return zero, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在 FIFO 缓存中,Get 操作不改变项在链表中的位置
|
||||||
|
return entry.Value, true // 通过 entry 指针访问值字段
|
||||||
|
}
|
||||||
|
|
||||||
|
// removeEntry 从缓存中移除项。
|
||||||
|
// 这个方法是内部使用的,不导出。需要被调用者确保持有写锁,或者内部自己加锁。
|
||||||
|
// 考虑到 Get 和 cleanupLoop 可能会调用,让其内部自己加锁更安全。
|
||||||
|
func (c *Cache[T]) removeEntry(key string) {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
|
// 从 data map 中删除
|
||||||
|
delete(c.data, key)
|
||||||
|
|
||||||
|
// 从 FIFO 链表和 fifoMap 中删除
|
||||||
|
if elem, ok := c.fifoMap[key]; ok {
|
||||||
|
c.fifoList.Remove(elem)
|
||||||
|
delete(c.fifoMap, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// evictIfNeeded 检查是否需要淘汰最老(FIFO 链表尾部)的项。
|
||||||
|
// 这个方法是内部使用的,不导出。必须在持有写锁的情况下调用。
|
||||||
|
func (c *Cache[T]) evictIfNeeded() {
|
||||||
|
if c.maxSize > 0 && c.fifoList.Len() > c.maxSize {
|
||||||
|
// 淘汰 FIFO 链表尾部的元素 (最老的)
|
||||||
|
oldest := c.fifoList.Back()
|
||||||
|
if oldest != nil {
|
||||||
|
keyToEvict := oldest.Value.(string) // 链表元素存储的是 key
|
||||||
|
c.fifoList.Remove(oldest)
|
||||||
|
delete(c.fifoMap, keyToEvict)
|
||||||
|
delete(c.data, keyToEvict) // 移除弱引用
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Size 返回当前缓存中的弱引用项数量。
|
||||||
|
// 注意:这个数量可能包含已被 GC 回收但尚未清理的项。
|
||||||
|
// 这是一个导出的方法。
|
||||||
|
func (c *Cache[T]) Size() int {
|
||||||
|
c.mu.RLock()
|
||||||
|
defer c.mu.RUnlock()
|
||||||
|
return len(c.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanupLoop 后台清理 Go routine。不导出。
|
||||||
|
func (c *Cache[T]) cleanupLoop() {
|
||||||
|
defer c.wg.Done()
|
||||||
|
// 使用内部常量 cleanupInterval
|
||||||
|
ticker := time.NewTicker(cleanupInterval)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
c.cleanupExpiredAndGCed()
|
||||||
|
case <-c.stopCleanup:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanupExpiredAndGCed 扫描并清理已过期或已被 GC 回收的项。不导出。
|
||||||
|
func (c *Cache[T]) cleanupExpiredAndGCed() {
|
||||||
|
c.mu.Lock() // 清理时需要写锁
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
keysToRemove := make([]string, 0, len(c.data)) // 预估容量
|
||||||
|
|
||||||
|
// 遍历 data map 查找需要清理的键
|
||||||
|
for key, wp := range c.data {
|
||||||
|
// wp 的类型是 weak.Pointer[cacheEntry[T]]
|
||||||
|
// wp.Value() 返回 *cacheEntry[T], entry 的类型是 *cacheEntry[T]
|
||||||
|
entry := wp.Value() // 尝试获取强引用
|
||||||
|
|
||||||
|
if entry == nil {
|
||||||
|
// 已被 GC 回收
|
||||||
|
keysToRemove = append(keysToRemove, key)
|
||||||
|
} else if now.After(entry.Expiration) {
|
||||||
|
// 逻辑过期 (通过 entry 指针访问字段)
|
||||||
|
keysToRemove = append(keysToRemove, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行删除操作
|
||||||
|
for _, key := range keysToRemove {
|
||||||
|
// 从 data map 中删除
|
||||||
|
delete(c.data, key)
|
||||||
|
// 从 FIFO 链表和 fifoMap 中删除
|
||||||
|
// 需要再次检查 fifoMap,因为在持有锁期间,evictIfNeeded 可能已经移除了这个 key
|
||||||
|
if elem, ok := c.fifoMap[key]; ok {
|
||||||
|
c.fifoList.Remove(elem)
|
||||||
|
delete(c.fifoMap, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// StopCleanup 停止后台清理 Go routine。
|
||||||
|
// 这是一个导出的方法。
|
||||||
|
func (c *Cache[T]) StopCleanup() {
|
||||||
|
close(c.stopCleanup)
|
||||||
|
c.wg.Wait() // 等待 Go routine 退出
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user