4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
.DS_Store
|
||||||
|
hubproxy*
|
||||||
10
README.md
10
README.md
@@ -138,11 +138,17 @@ blackList = [
|
|||||||
"baduser/*"
|
"baduser/*"
|
||||||
]
|
]
|
||||||
|
|
||||||
# SOCKS5代理配置,支持有用户名/密码认证和无认证模式
|
# 代理配置,支持有用户名/密码认证和无认证模式
|
||||||
# 无认证: socks5://127.0.0.1:1080
|
# 无认证: socks5://127.0.0.1:1080
|
||||||
# 有认证: socks5://username:password@127.0.0.1:1080
|
# 有认证: socks5://username:password@127.0.0.1:1080
|
||||||
|
# HTTP 代理示例
|
||||||
|
# http://username:password@127.0.0.1:7890
|
||||||
|
# SOCKS5 代理示例
|
||||||
|
# socks5://username:password@127.0.0.1:1080
|
||||||
|
# SOCKS5H 代理示例
|
||||||
|
# socks5h://username:password@127.0.0.1:1080
|
||||||
# 留空不使用代理
|
# 留空不使用代理
|
||||||
socks5 = ""
|
proxy = ""
|
||||||
|
|
||||||
[download]
|
[download]
|
||||||
# 批量下载离线镜像数量限制
|
# 批量下载离线镜像数量限制
|
||||||
|
|||||||
@@ -85,15 +85,15 @@ func (ac *AccessController) CheckDockerAccess(image string) (allowed bool, reaso
|
|||||||
imageInfo := ac.ParseDockerImage(image)
|
imageInfo := ac.ParseDockerImage(image)
|
||||||
|
|
||||||
// 检查白名单(如果配置了白名单,则只允许白名单中的镜像)
|
// 检查白名单(如果配置了白名单,则只允许白名单中的镜像)
|
||||||
if len(cfg.Proxy.WhiteList) > 0 {
|
if len(cfg.Access.WhiteList) > 0 {
|
||||||
if !ac.matchImageInList(imageInfo, cfg.Proxy.WhiteList) {
|
if !ac.matchImageInList(imageInfo, cfg.Access.WhiteList) {
|
||||||
return false, "不在Docker镜像白名单内"
|
return false, "不在Docker镜像白名单内"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查黑名单
|
// 检查黑名单
|
||||||
if len(cfg.Proxy.BlackList) > 0 {
|
if len(cfg.Access.BlackList) > 0 {
|
||||||
if ac.matchImageInList(imageInfo, cfg.Proxy.BlackList) {
|
if ac.matchImageInList(imageInfo, cfg.Access.BlackList) {
|
||||||
return false, "Docker镜像在黑名单内"
|
return false, "Docker镜像在黑名单内"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -110,12 +110,12 @@ func (ac *AccessController) CheckGitHubAccess(matches []string) (allowed bool, r
|
|||||||
cfg := GetConfig()
|
cfg := GetConfig()
|
||||||
|
|
||||||
// 检查白名单
|
// 检查白名单
|
||||||
if len(cfg.Proxy.WhiteList) > 0 && !ac.checkList(matches, cfg.Proxy.WhiteList) {
|
if len(cfg.Access.WhiteList) > 0 && !ac.checkList(matches, cfg.Access.WhiteList) {
|
||||||
return false, "不在GitHub仓库白名单内"
|
return false, "不在GitHub仓库白名单内"
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查黑名单
|
// 检查黑名单
|
||||||
if len(cfg.Proxy.BlackList) > 0 && ac.checkList(matches, cfg.Proxy.BlackList) {
|
if len(cfg.Access.BlackList) > 0 && ac.checkList(matches, cfg.Access.BlackList) {
|
||||||
return false, "GitHub仓库在黑名单内"
|
return false, "GitHub仓库在黑名单内"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,5 +210,3 @@ func (ac *AccessController) checkList(matches, list []string) bool {
|
|||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -37,10 +37,10 @@ type AppConfig struct {
|
|||||||
BlackList []string `toml:"blackList"` // 黑名单IP/CIDR列表
|
BlackList []string `toml:"blackList"` // 黑名单IP/CIDR列表
|
||||||
} `toml:"security"`
|
} `toml:"security"`
|
||||||
|
|
||||||
Proxy struct {
|
Access struct {
|
||||||
WhiteList []string `toml:"whiteList"` // 代理白名单(仓库级别)
|
WhiteList []string `toml:"whiteList"` // 代理白名单(仓库级别)
|
||||||
BlackList []string `toml:"blackList"` // 代理黑名单(仓库级别)
|
BlackList []string `toml:"blackList"` // 代理黑名单(仓库级别)
|
||||||
Socks5 string `toml:"socks5"` // SOCKS5代理地址: socks5://[user:pass@]host:port
|
Proxy string `toml:"proxy"` // 代理地址: 支持 http/https/socks5/socks5h
|
||||||
} `toml:"proxy"`
|
} `toml:"proxy"`
|
||||||
|
|
||||||
Download struct {
|
Download struct {
|
||||||
@@ -65,6 +65,7 @@ var (
|
|||||||
configCacheMutex sync.RWMutex
|
configCacheMutex sync.RWMutex
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// todo:Refactoring is needed
|
||||||
// DefaultConfig 返回默认配置
|
// DefaultConfig 返回默认配置
|
||||||
func DefaultConfig() *AppConfig {
|
func DefaultConfig() *AppConfig {
|
||||||
return &AppConfig{
|
return &AppConfig{
|
||||||
@@ -91,14 +92,14 @@ func DefaultConfig() *AppConfig {
|
|||||||
WhiteList: []string{},
|
WhiteList: []string{},
|
||||||
BlackList: []string{},
|
BlackList: []string{},
|
||||||
},
|
},
|
||||||
Proxy: struct {
|
Access: struct {
|
||||||
WhiteList []string `toml:"whiteList"`
|
WhiteList []string `toml:"whiteList"`
|
||||||
BlackList []string `toml:"blackList"`
|
BlackList []string `toml:"blackList"`
|
||||||
Socks5 string `toml:"socks5"`
|
Proxy string `toml:"proxy"`
|
||||||
}{
|
}{
|
||||||
WhiteList: []string{},
|
WhiteList: []string{},
|
||||||
BlackList: []string{},
|
BlackList: []string{},
|
||||||
Socks5: "", // 默认不使用代理
|
Proxy: "", // 默认不使用代理
|
||||||
},
|
},
|
||||||
Download: struct {
|
Download: struct {
|
||||||
MaxImages int `toml:"maxImages"`
|
MaxImages int `toml:"maxImages"`
|
||||||
@@ -173,8 +174,8 @@ func GetConfig() *AppConfig {
|
|||||||
configCopy := *appConfig
|
configCopy := *appConfig
|
||||||
configCopy.Security.WhiteList = append([]string(nil), appConfig.Security.WhiteList...)
|
configCopy.Security.WhiteList = append([]string(nil), appConfig.Security.WhiteList...)
|
||||||
configCopy.Security.BlackList = append([]string(nil), appConfig.Security.BlackList...)
|
configCopy.Security.BlackList = append([]string(nil), appConfig.Security.BlackList...)
|
||||||
configCopy.Proxy.WhiteList = append([]string(nil), appConfig.Proxy.WhiteList...)
|
configCopy.Access.WhiteList = append([]string(nil), appConfig.Access.WhiteList...)
|
||||||
configCopy.Proxy.BlackList = append([]string(nil), appConfig.Proxy.BlackList...)
|
configCopy.Access.BlackList = append([]string(nil), appConfig.Access.BlackList...)
|
||||||
appConfigLock.RUnlock()
|
appConfigLock.RUnlock()
|
||||||
|
|
||||||
cachedConfig = &configCopy
|
cachedConfig = &configCopy
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ blackList = [
|
|||||||
"192.168.100.0/24"
|
"192.168.100.0/24"
|
||||||
]
|
]
|
||||||
|
|
||||||
[proxy]
|
[access]
|
||||||
# 代理服务白名单(支持GitHub仓库和Docker镜像,支持通配符)
|
# 代理服务白名单(支持GitHub仓库和Docker镜像,支持通配符)
|
||||||
# 只允许访问白名单中的仓库/镜像,为空时不限制
|
# 只允许访问白名单中的仓库/镜像,为空时不限制
|
||||||
whiteList = []
|
whiteList = []
|
||||||
@@ -39,11 +39,17 @@ blackList = [
|
|||||||
"baduser/*"
|
"baduser/*"
|
||||||
]
|
]
|
||||||
|
|
||||||
# SOCKS5代理配置,支持有用户名/密码认证和无认证模式
|
# 代理配置,支持有用户名/密码认证和无认证模式
|
||||||
# 无认证: socks5://127.0.0.1:1080
|
# 无认证: socks5://127.0.0.1:1080
|
||||||
# 有认证: socks5://username:password@127.0.0.1:1080
|
# 有认证: socks5://username:password@127.0.0.1:1080
|
||||||
|
# HTTP 代理示例
|
||||||
|
# http://username:password@127.0.0.1:7890
|
||||||
|
# SOCKS5 代理示例
|
||||||
|
# socks5://username:password@127.0.0.1:1080
|
||||||
|
# SOCKS5H 代理示例
|
||||||
|
# socks5h://username:password@127.0.0.1:1080
|
||||||
# 留空不使用代理
|
# 留空不使用代理
|
||||||
socks5 = ""
|
proxy = ""
|
||||||
|
|
||||||
[download]
|
[download]
|
||||||
# 批量下载离线镜像数量限制
|
# 批量下载离线镜像数量限制
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ require (
|
|||||||
github.com/gin-gonic/gin v1.10.0
|
github.com/gin-gonic/gin v1.10.0
|
||||||
github.com/google/go-containerregistry v0.20.5
|
github.com/google/go-containerregistry v0.20.5
|
||||||
github.com/pelletier/go-toml/v2 v2.2.3
|
github.com/pelletier/go-toml/v2 v2.2.3
|
||||||
golang.org/x/net v0.33.0
|
|
||||||
golang.org/x/time v0.11.0
|
golang.org/x/time v0.11.0
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -44,6 +43,7 @@ require (
|
|||||||
github.com/vbatts/tar-split v0.12.1 // indirect
|
github.com/vbatts/tar-split v0.12.1 // indirect
|
||||||
golang.org/x/arch v0.8.0 // indirect
|
golang.org/x/arch v0.8.0 // indirect
|
||||||
golang.org/x/crypto v0.32.0 // indirect
|
golang.org/x/crypto v0.32.0 // indirect
|
||||||
|
golang.org/x/net v0.33.0 // indirect
|
||||||
golang.org/x/sync v0.14.0 // indirect
|
golang.org/x/sync v0.14.0 // indirect
|
||||||
golang.org/x/sys v0.33.0 // indirect
|
golang.org/x/sys v0.33.0 // indirect
|
||||||
golang.org/x/text v0.21.0 // indirect
|
golang.org/x/text v0.21.0 // indirect
|
||||||
|
|||||||
@@ -1,14 +1,10 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"log"
|
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"golang.org/x/net/proxy"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -22,63 +18,18 @@ var (
|
|||||||
func initHTTPClients() {
|
func initHTTPClients() {
|
||||||
cfg := GetConfig()
|
cfg := GetConfig()
|
||||||
|
|
||||||
// 创建DialContext函数,支持SOCKS5代理
|
if p := cfg.Access.Proxy; p != "" {
|
||||||
createDialContext := func(timeout time.Duration) func(ctx context.Context, network, addr string) (net.Conn, error) {
|
os.Setenv("HTTP_PROXY", p)
|
||||||
if cfg.Proxy.Socks5 == "" {
|
os.Setenv("HTTPS_PROXY", p)
|
||||||
// 没有配置代理,使用直连
|
|
||||||
dialer := &net.Dialer{
|
|
||||||
Timeout: timeout,
|
|
||||||
KeepAlive: 30 * time.Second,
|
|
||||||
}
|
|
||||||
return dialer.DialContext
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解析SOCKS5代理URL
|
|
||||||
proxyURL, err := url.Parse(cfg.Proxy.Socks5)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("SOCKS5代理配置错误,使用直连: %v", err)
|
|
||||||
dialer := &net.Dialer{
|
|
||||||
Timeout: timeout,
|
|
||||||
KeepAlive: 30 * time.Second,
|
|
||||||
}
|
|
||||||
return dialer.DialContext
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建基础dialer
|
|
||||||
baseDialer := &net.Dialer{
|
|
||||||
Timeout: timeout,
|
|
||||||
KeepAlive: 30 * time.Second,
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建SOCKS5代理dialer
|
|
||||||
var auth *proxy.Auth
|
|
||||||
if proxyURL.User != nil {
|
|
||||||
if password, ok := proxyURL.User.Password(); ok {
|
|
||||||
auth = &proxy.Auth{
|
|
||||||
User: proxyURL.User.Username(),
|
|
||||||
Password: password,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
socks5Dialer, err := proxy.SOCKS5("tcp", proxyURL.Host, auth, baseDialer)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("创建SOCKS5代理失败,使用直连: %v", err)
|
|
||||||
return baseDialer.DialContext
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("使用SOCKS5代理: %s", proxyURL.Host)
|
|
||||||
|
|
||||||
// 返回带上下文的dial函数
|
|
||||||
return func(ctx context.Context, network, addr string) (net.Conn, error) {
|
|
||||||
return socks5Dialer.Dial(network, addr)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 代理客户端配置 - 适用于大文件传输
|
// 代理客户端配置 - 适用于大文件传输
|
||||||
globalHTTPClient = &http.Client{
|
globalHTTPClient = &http.Client{
|
||||||
Transport: &http.Transport{
|
Transport: &http.Transport{
|
||||||
DialContext: createDialContext(30 * time.Second),
|
Proxy: http.ProxyFromEnvironment,
|
||||||
|
DialContext: (&net.Dialer{
|
||||||
|
Timeout: 30 * time.Second,
|
||||||
|
KeepAlive: 30 * time.Second,
|
||||||
|
}).DialContext,
|
||||||
MaxIdleConns: 1000,
|
MaxIdleConns: 1000,
|
||||||
MaxIdleConnsPerHost: 1000,
|
MaxIdleConnsPerHost: 1000,
|
||||||
IdleConnTimeout: 90 * time.Second,
|
IdleConnTimeout: 90 * time.Second,
|
||||||
@@ -92,7 +43,11 @@ func initHTTPClients() {
|
|||||||
searchHTTPClient = &http.Client{
|
searchHTTPClient = &http.Client{
|
||||||
Timeout: 10 * time.Second,
|
Timeout: 10 * time.Second,
|
||||||
Transport: &http.Transport{
|
Transport: &http.Transport{
|
||||||
DialContext: createDialContext(5 * time.Second),
|
Proxy: http.ProxyFromEnvironment,
|
||||||
|
DialContext: (&net.Dialer{
|
||||||
|
Timeout: 5 * time.Second,
|
||||||
|
KeepAlive: 30 * time.Second,
|
||||||
|
}).DialContext,
|
||||||
MaxIdleConns: 100,
|
MaxIdleConns: 100,
|
||||||
MaxIdleConnsPerHost: 10,
|
MaxIdleConnsPerHost: 10,
|
||||||
IdleConnTimeout: 90 * time.Second,
|
IdleConnTimeout: 90 * time.Second,
|
||||||
|
|||||||
@@ -385,12 +385,11 @@ func (is *ImageStreamer) streamDockerFormatWithReturn(ctx context.Context, tarWr
|
|||||||
log.Printf("已处理层 %d/%d", i+1, len(layers))
|
log.Printf("已处理层 %d/%d", i+1, len(layers))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// 构建单个镜像的manifest信息
|
// 构建单个镜像的manifest信息
|
||||||
singleManifest := map[string]interface{}{
|
singleManifest := map[string]interface{}{
|
||||||
"Config": configDigest.String() + ".json",
|
"Config": configDigest.String() + ".json",
|
||||||
"RepoTags": []string{imageRef},
|
"RepoTags": []string{imageRef},
|
||||||
"Layers": func() []string {
|
"Layers": func() []string {
|
||||||
var layers []string
|
var layers []string
|
||||||
for _, digest := range layerDigests {
|
for _, digest := range layerDigests {
|
||||||
layers = append(layers, digest+"/layer.tar")
|
layers = append(layers, digest+"/layer.tar")
|
||||||
@@ -549,8 +548,8 @@ func (is *ImageStreamer) selectPlatformImage(desc *remote.Descriptor, options *S
|
|||||||
}
|
}
|
||||||
|
|
||||||
if m.Platform.OS == targetOS &&
|
if m.Platform.OS == targetOS &&
|
||||||
m.Platform.Architecture == targetArch &&
|
m.Platform.Architecture == targetArch &&
|
||||||
m.Platform.Variant == targetVariant {
|
m.Platform.Variant == targetVariant {
|
||||||
selectedDesc = &m
|
selectedDesc = &m
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -632,7 +631,7 @@ func handleDirectImageDownload(c *gin.Context) {
|
|||||||
|
|
||||||
if !singleImageDebouncer.ShouldAllow(userID, contentKey) {
|
if !singleImageDebouncer.ShouldAllow(userID, contentKey) {
|
||||||
c.JSON(http.StatusTooManyRequests, gin.H{
|
c.JSON(http.StatusTooManyRequests, gin.H{
|
||||||
"error": "请求过于频繁,请稍后再试",
|
"error": "请求过于频繁,请稍后再试",
|
||||||
"retry_after": 5,
|
"retry_after": 5,
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
@@ -692,7 +691,7 @@ func handleSimpleBatchDownload(c *gin.Context) {
|
|||||||
|
|
||||||
if !batchImageDebouncer.ShouldAllow(userID, contentKey) {
|
if !batchImageDebouncer.ShouldAllow(userID, contentKey) {
|
||||||
c.JSON(http.StatusTooManyRequests, gin.H{
|
c.JSON(http.StatusTooManyRequests, gin.H{
|
||||||
"error": "批量下载请求过于频繁,请稍后再试",
|
"error": "批量下载请求过于频繁,请稍后再试",
|
||||||
"retry_after": 60,
|
"retry_after": 60,
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -122,7 +122,6 @@ func main() {
|
|||||||
// 注册Docker Registry代理路由
|
// 注册Docker Registry代理路由
|
||||||
router.Any("/v2/*path", ProxyDockerRegistryGin)
|
router.Any("/v2/*path", ProxyDockerRegistryGin)
|
||||||
|
|
||||||
|
|
||||||
// 注册NoRoute处理器
|
// 注册NoRoute处理器
|
||||||
router.NoRoute(handler)
|
router.NoRoute(handler)
|
||||||
|
|
||||||
@@ -177,12 +176,10 @@ func handler(c *gin.Context) {
|
|||||||
proxyRequest(c, rawPath)
|
proxyRequest(c, rawPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func proxyRequest(c *gin.Context, u string) {
|
func proxyRequest(c *gin.Context, u string) {
|
||||||
proxyWithRedirect(c, u, 0)
|
proxyWithRedirect(c, u, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func proxyWithRedirect(c *gin.Context, u string, redirectCount int) {
|
func proxyWithRedirect(c *gin.Context, u string, redirectCount int) {
|
||||||
// 限制最大重定向次数,防止无限递归
|
// 限制最大重定向次数,防止无限递归
|
||||||
const maxRedirects = 20
|
const maxRedirects = 20
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import (
|
|||||||
const (
|
const (
|
||||||
// 清理间隔
|
// 清理间隔
|
||||||
CleanupInterval = 10 * time.Minute
|
CleanupInterval = 10 * time.Minute
|
||||||
MaxIPCacheSize = 10000
|
MaxIPCacheSize = 10000
|
||||||
)
|
)
|
||||||
|
|
||||||
// IPRateLimiter IP限流器结构体
|
// IPRateLimiter IP限流器结构体
|
||||||
@@ -233,7 +233,7 @@ func RateLimitMiddleware(limiter *IPRateLimiter) gin.HandlerFunc {
|
|||||||
// 静态文件豁免:跳过限流检查
|
// 静态文件豁免:跳过限流检查
|
||||||
path := c.Request.URL.Path
|
path := c.Request.URL.Path
|
||||||
if path == "/" || path == "/favicon.ico" || path == "/images.html" || path == "/search.html" ||
|
if path == "/" || path == "/favicon.ico" || path == "/images.html" || path == "/search.html" ||
|
||||||
strings.HasPrefix(path, "/public/") {
|
strings.HasPrefix(path, "/public/") {
|
||||||
c.Next()
|
c.Next()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -299,5 +299,3 @@ func RateLimitMiddleware(limiter *IPRateLimiter) gin.HandlerFunc {
|
|||||||
c.Next()
|
c.Next()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -25,27 +25,27 @@ type SearchResult struct {
|
|||||||
|
|
||||||
// Repository 仓库信息
|
// Repository 仓库信息
|
||||||
type Repository struct {
|
type Repository struct {
|
||||||
Name string `json:"repo_name"`
|
Name string `json:"repo_name"`
|
||||||
Description string `json:"short_description"`
|
Description string `json:"short_description"`
|
||||||
IsOfficial bool `json:"is_official"`
|
IsOfficial bool `json:"is_official"`
|
||||||
IsAutomated bool `json:"is_automated"`
|
IsAutomated bool `json:"is_automated"`
|
||||||
StarCount int `json:"star_count"`
|
StarCount int `json:"star_count"`
|
||||||
PullCount int `json:"pull_count"`
|
PullCount int `json:"pull_count"`
|
||||||
RepoOwner string `json:"repo_owner"`
|
RepoOwner string `json:"repo_owner"`
|
||||||
LastUpdated string `json:"last_updated"`
|
LastUpdated string `json:"last_updated"`
|
||||||
Status int `json:"status"`
|
Status int `json:"status"`
|
||||||
Organization string `json:"affiliation"`
|
Organization string `json:"affiliation"`
|
||||||
PullsLastWeek int `json:"pulls_last_week"`
|
PullsLastWeek int `json:"pulls_last_week"`
|
||||||
Namespace string `json:"namespace"`
|
Namespace string `json:"namespace"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TagInfo 标签信息
|
// TagInfo 标签信息
|
||||||
type TagInfo struct {
|
type TagInfo struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
FullSize int64 `json:"full_size"`
|
FullSize int64 `json:"full_size"`
|
||||||
LastUpdated time.Time `json:"last_updated"`
|
LastUpdated time.Time `json:"last_updated"`
|
||||||
LastPusher string `json:"last_pusher"`
|
LastPusher string `json:"last_pusher"`
|
||||||
Images []Image `json:"images"`
|
Images []Image `json:"images"`
|
||||||
Vulnerabilities struct {
|
Vulnerabilities struct {
|
||||||
Critical int `json:"critical"`
|
Critical int `json:"critical"`
|
||||||
High int `json:"high"`
|
High int `json:"high"`
|
||||||
@@ -77,9 +77,9 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Cache struct {
|
type Cache struct {
|
||||||
data map[string]cacheEntry
|
data map[string]cacheEntry
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
maxSize int
|
maxSize int
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -385,9 +385,9 @@ func isRetryableError(err error) bool {
|
|||||||
|
|
||||||
// 网络错误、超时等可以重试
|
// 网络错误、超时等可以重试
|
||||||
if strings.Contains(err.Error(), "timeout") ||
|
if strings.Contains(err.Error(), "timeout") ||
|
||||||
strings.Contains(err.Error(), "connection refused") ||
|
strings.Contains(err.Error(), "connection refused") ||
|
||||||
strings.Contains(err.Error(), "no such host") ||
|
strings.Contains(err.Error(), "no such host") ||
|
||||||
strings.Contains(err.Error(), "too many requests") {
|
strings.Contains(err.Error(), "too many requests") {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,10 +13,10 @@ import (
|
|||||||
|
|
||||||
// CachedItem 通用缓存项,支持Token和Manifest
|
// CachedItem 通用缓存项,支持Token和Manifest
|
||||||
type CachedItem struct {
|
type CachedItem struct {
|
||||||
Data []byte // 缓存数据(token字符串或manifest字节)
|
Data []byte // 缓存数据(token字符串或manifest字节)
|
||||||
ContentType string // 内容类型
|
ContentType string // 内容类型
|
||||||
Headers map[string]string // 额外的响应头
|
Headers map[string]string // 额外的响应头
|
||||||
ExpiresAt time.Time // 过期时间
|
ExpiresAt time.Time // 过期时间
|
||||||
}
|
}
|
||||||
|
|
||||||
// UniversalCache 通用缓存,支持Token和Manifest
|
// UniversalCache 通用缓存,支持Token和Manifest
|
||||||
@@ -86,7 +86,7 @@ func getManifestTTL(reference string) time.Duration {
|
|||||||
|
|
||||||
// mutable tag的智能判断
|
// mutable tag的智能判断
|
||||||
if reference == "latest" || reference == "main" || reference == "master" ||
|
if reference == "latest" || reference == "main" || reference == "master" ||
|
||||||
reference == "dev" || reference == "develop" {
|
reference == "dev" || reference == "develop" {
|
||||||
// 热门可变标签: 短期缓存
|
// 热门可变标签: 短期缓存
|
||||||
return 10 * time.Minute
|
return 10 * time.Minute
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user