diff --git a/README.md b/README.md
index 5161547..27596dd 100644
--- a/README.md
+++ b/README.md
@@ -7,13 +7,13 @@
## ✨ 特性
- 🐳 **Docker 镜像加速** - 单域名实现 Docker Hub、GHCR、Quay 等多个镜像仓库加速,以及优化拉取速度。
-- 🐳 **离线镜像包** - 支持批量下载离线镜像包。
+- 🐳 **离线镜像包** - 支持下载离线镜像包,流式传输加防抖设计。
- 📁 **GitHub 文件加速** - 加速 GitHub Release、Raw 文件下载,支持`api.github.com`,脚本嵌套加速等等
- 🤖 **AI 模型库支持** - 支持 Hugging Face 模型下载加速
- 🛡️ **智能限流** - IP 限流保护,防止滥用
- 🚫 **仓库审计** - 强大的自定义黑名单,白名单,同时审计镜像仓库,和GitHub仓库
- 🔍 **镜像搜索** - 在线搜索 Docker 镜像
-- ⚡ **轻量高效** - 基于 Go 语言,单二进制文件运行,资源占用低
+- ⚡ **轻量高效** - 基于 Go 语言,单二进制文件运行,资源占用低,优雅的内存清理机制。
- 🔧 **配置热重载** - 统一配置管理,部分配置项支持热重载,无需重启服务
## 🚀 快速开始
diff --git a/src/access_control.go b/src/access_control.go
index c589c86..c2a3dee 100644
--- a/src/access_control.go
+++ b/src/access_control.go
@@ -31,13 +31,10 @@ var GlobalAccessController = &AccessController{}
// ParseDockerImage 解析Docker镜像名称
func (ac *AccessController) ParseDockerImage(image string) DockerImageInfo {
- // 移除可能的协议前缀
image = strings.TrimPrefix(image, "docker://")
- // 分离标签
var tag string
if idx := strings.LastIndex(image, ":"); idx != -1 {
- // 检查是否是端口号而不是标签(包含斜杠)
part := image[idx+1:]
if !strings.Contains(part, "/") {
tag = part
@@ -48,15 +45,11 @@ func (ac *AccessController) ParseDockerImage(image string) DockerImageInfo {
tag = "latest"
}
- // 分离命名空间和仓库名
var namespace, repository string
if strings.Contains(image, "/") {
- // 处理自定义registry的情况,如 registry.com/user/repo
parts := strings.Split(image, "/")
if len(parts) >= 2 {
- // 检查第一部分是否是域名(包含.)
if strings.Contains(parts[0], ".") {
- // 跳过registry域名,取用户名和仓库名
if len(parts) >= 3 {
namespace = parts[1]
repository = parts[2]
@@ -65,13 +58,11 @@ func (ac *AccessController) ParseDockerImage(image string) DockerImageInfo {
repository = parts[1]
}
} else {
- // 标准格式:user/repo
namespace = parts[0]
repository = parts[1]
}
}
} else {
- // 官方镜像,如 nginx
namespace = "library"
repository = image
}
@@ -171,7 +162,6 @@ func (ac *AccessController) matchImageInList(imageInfo DockerImageInfo, list []s
}
}
- // 5. 子仓库匹配(防止 user/repo 匹配到 user/repo-fork)
if strings.HasPrefix(fullName, item+"/") {
return true
}
@@ -185,7 +175,6 @@ func (ac *AccessController) checkList(matches, list []string) bool {
return false
}
- // 组合用户名和仓库名,处理.git后缀
username := strings.ToLower(strings.TrimSpace(matches[0]))
repoName := strings.ToLower(strings.TrimSpace(strings.TrimSuffix(matches[1], ".git")))
fullRepo := username + "/" + repoName
@@ -196,10 +185,7 @@ func (ac *AccessController) checkList(matches, list []string) bool {
continue
}
- // 支持多种匹配模式:
- // 1. 精确匹配: "vaxilu/x-ui"
- // 2. 用户级匹配: "vaxilu/*" 或 "vaxilu"
- // 3. 前缀匹配: "vaxilu/x-ui-*"
+ // 支持多种匹配模式
if fullRepo == item {
return true
}
@@ -225,15 +211,10 @@ func (ac *AccessController) checkList(matches, list []string) bool {
return false
}
-// 🔥 Reload 热重载访问控制规则
+// Reload 热重载访问控制规则
func (ac *AccessController) Reload() {
ac.mu.Lock()
defer ac.mu.Unlock()
- // 访问控制器本身不缓存配置,每次检查时都会调用GetConfig()
- // 所以这里只需要确保锁的原子性,实际的重载在GetConfig()中完成
- // 可以在这里添加一些初始化逻辑,比如预编译正则表达式等
-
- // 目前访问控制器设计为无状态的,每次检查都读取最新配置
- // 这样设计的好处是配置更新后无需额外处理,自动生效
+ // 访问控制器本身不缓存配置
}
\ No newline at end of file
diff --git a/src/config.go b/src/config.go
index 3a205a0..0d20aa9 100644
--- a/src/config.go
+++ b/src/config.go
@@ -48,10 +48,8 @@ type AppConfig struct {
MaxImages int `toml:"maxImages"` // 单次下载最大镜像数量限制
} `toml:"download"`
- // 新增:Registry映射配置
Registries map[string]RegistryMapping `toml:"registries"`
- // Token缓存配置
TokenCache struct {
Enabled bool `toml:"enabled"` // 是否启用token缓存
DefaultTTL string `toml:"defaultTTL"` // 默认缓存时间
@@ -64,7 +62,6 @@ var (
isViperEnabled bool
viperInstance *viper.Viper
- // ✅ 配置缓存变量
cachedConfig *AppConfig
configCacheTime time.Time
configCacheTTL = 5 * time.Second
@@ -147,7 +144,6 @@ func DefaultConfig() *AppConfig {
// GetConfig 安全地获取配置副本
func GetConfig() *AppConfig {
- // ✅ 快速缓存检查,减少深拷贝开销
configCacheMutex.RLock()
if cachedConfig != nil && time.Since(configCacheTime) < configCacheTTL {
config := cachedConfig
@@ -194,7 +190,6 @@ func setConfig(cfg *AppConfig) {
defer appConfigLock.Unlock()
appConfig = cfg
- // ✅ 配置更新时清除缓存
configCacheMutex.Lock()
cachedConfig = nil
configCacheMutex.Unlock()
@@ -220,17 +215,13 @@ func LoadConfig() error {
// 设置配置
setConfig(cfg)
- // 🔥 首次加载后启用Viper热重载
if !isViperEnabled {
go enableViperHotReload()
}
- // 配置加载成功,详细信息在启动时统一显示
-
return nil
}
-// 🔥 启用Viper自动热重载
func enableViperHotReload() {
if isViperEnabled {
return
@@ -251,9 +242,7 @@ func enableViperHotReload() {
}
isViperEnabled = true
- // 热重载已启用,不显示额外信息
- // 🚀 启用文件监听
viperInstance.WatchConfig()
viperInstance.OnConfigChange(func(e fsnotify.Event) {
fmt.Printf("检测到配置文件变化: %s\n", e.Name)
@@ -261,7 +250,6 @@ func enableViperHotReload() {
})
}
-// 🔥 使用Viper进行热重载
func hotReloadWithViper() {
start := time.Now()
fmt.Println("🔄 自动热重载...")
@@ -275,10 +263,8 @@ func hotReloadWithViper() {
return
}
- // 从环境变量覆盖(保持原有功能)
overrideFromEnv(cfg)
- // 原子性更新配置
setConfig(cfg)
// 异步更新受影响的组件
@@ -288,7 +274,6 @@ func hotReloadWithViper() {
}()
}
-// 🔧 更新受配置影响的组件
func updateAffectedComponents() {
// 重新初始化限流器
if globalLimiter != nil {
@@ -302,7 +287,6 @@ func updateAffectedComponents() {
GlobalAccessController.Reload()
}
- // 🔥 刷新Registry配置映射
fmt.Println("🌐 更新Registry配置映射...")
reloadRegistryConfig()
@@ -310,7 +294,6 @@ func updateAffectedComponents() {
fmt.Println("🔧 组件更新完成")
}
-// 🔥 重新加载Registry配置
func reloadRegistryConfig() {
cfg := GetConfig()
enabledCount := 0
@@ -324,8 +307,6 @@ func reloadRegistryConfig() {
fmt.Printf("🌐 Registry配置已更新: %d个启用\n", enabledCount)
- // Registry配置是动态读取的,每次请求都会调用GetConfig()
- // 所以这里只需要简单通知,实际生效是自动的
}
// overrideFromEnv 从环境变量覆盖配置
diff --git a/src/docker.go b/src/docker.go
index fd3c740..4b4ebf1 100644
--- a/src/docker.go
+++ b/src/docker.go
@@ -78,8 +78,6 @@ func initDockerProxy() {
registry: registry,
options: options,
}
-
- // Docker代理初始化完成
}
// ProxyDockerRegistryGin 标准Docker Registry API v2代理
@@ -105,7 +103,6 @@ func handleRegistryRequest(c *gin.Context, path string) {
// 移除 /v2/ 前缀
pathWithoutV2 := strings.TrimPrefix(path, "/v2/")
- // 🔍 新增:Registry域名检测和路由
if registryDomain, remainingPath := registryDetector.detectRegistryDomain(pathWithoutV2); registryDomain != "" {
if registryDetector.isRegistryEnabled(registryDomain) {
// 设置目标Registry信息到Context
@@ -118,7 +115,6 @@ func handleRegistryRequest(c *gin.Context, path string) {
}
}
- // 原有逻辑完全保持(零改动)
imageName, apiType, reference := parseRegistryPath(pathWithoutV2)
if imageName == "" || apiType == "" {
c.String(http.StatusBadRequest, "Invalid path format")
@@ -392,9 +388,7 @@ func (r *ResponseRecorder) Write(data []byte) (int, error) {
return r.ResponseWriter.Write(data)
}
-// proxyDockerAuthOriginal Docker认证代理(原始逻辑,保持不变)
func proxyDockerAuthOriginal(c *gin.Context) {
- // 检查是否有目标Registry域名(来自Context)
var authURL string
if targetDomain, exists := c.Get("target_registry_domain"); exists {
if mapping, found := registryDetector.getRegistryMapping(targetDomain.(string)); found {
@@ -672,17 +666,11 @@ func createUpstreamOptions(mapping RegistryMapping) []remote.Option {
remote.WithUserAgent("hubproxy/go-containerregistry"),
}
- // 根据Registry类型添加特定的认证选项
+ // 根据Registry类型添加特定的认证选项(方便后续扩展)
switch mapping.AuthType {
case "github":
- // GitHub Container Registry 通常使用匿名访问
- // 如需要认证,可在此处添加
case "google":
- // Google Container Registry 配置
- // 如需要认证,可在此处添加
case "quay":
- // Quay.io 配置
- // 如需要认证,可在此处添加
}
return options
diff --git a/src/imagetar.go b/src/imagetar.go
index b5b0404..64c8fce 100644
--- a/src/imagetar.go
+++ b/src/imagetar.go
@@ -361,7 +361,7 @@ func (is *ImageStreamer) streamDockerFormatWithReturn(ctx context.Context, tarWr
default:
}
- if err := func() error { // ✅ 匿名函数确保资源立即释放
+ if err := func() error {
digest, err := layer.Digest()
if err != nil {
return err
@@ -859,7 +859,7 @@ func (is *ImageStreamer) StreamMultipleImages(ctx context.Context, imageRefs []s
log.Printf("处理镜像 %d/%d: %s", i+1, len(imageRefs), imageRef)
- // ✅ 添加超时保护,防止单个镜像处理时间过长
+ // 防止单个镜像处理时间过长
timeoutCtx, cancel := context.WithTimeout(ctx, 15*time.Minute)
manifest, repositories, err := is.streamSingleImageForBatch(timeoutCtx, tarWriter, imageRef, options)
cancel()
diff --git a/src/main.go b/src/main.go
index 94022c6..3a186f0 100644
--- a/src/main.go
+++ b/src/main.go
@@ -46,7 +46,7 @@ var (
}
globalLimiter *IPRateLimiter
- // ✅ 服务启动时间追踪
+ // 服务启动时间
serviceStartTime = time.Now()
)
@@ -75,7 +75,7 @@ func main() {
gin.SetMode(gin.ReleaseMode)
router := gin.Default()
- // ✅ 添加全局Panic恢复保护
+ // 全局Panic恢复保护
router.Use(gin.CustomRecovery(func(c *gin.Context, recovered interface{}) {
log.Printf("🚨 Panic recovered: %v", recovered)
c.JSON(http.StatusInternalServerError, gin.H{
@@ -84,13 +84,13 @@ func main() {
})
}))
- // ✅ 初始化监控端点 (优先级最高,避免中间件影响)
+ // 初始化监控端点
initHealthRoutes(router)
// 初始化镜像tar下载路由
initImageTarRoutes(router)
- // 静态文件路由(使用嵌入文件)
+ // 静态文件路由
router.GET("/", func(c *gin.Context) {
serveEmbedFile(c, "public/index.html")
})
@@ -120,7 +120,7 @@ func main() {
router.Any("/v2/*path", RateLimitMiddleware(globalLimiter), ProxyDockerRegistryGin)
- // 注册NoRoute处理器,应用限流中间件
+ // 注册NoRoute处理器
router.NoRoute(RateLimitMiddleware(globalLimiter), handler)
cfg := GetConfig()
@@ -226,7 +226,6 @@ func proxyWithRedirect(c *gin.Context, u string, redirectCount int) {
resp.Header.Del("Referrer-Policy")
resp.Header.Del("Strict-Transport-Security")
- // 智能处理系统 - 自动识别需要加速的内容
// 获取真实域名
realHost := c.Request.Header.Get("X-Forwarded-Host")
if realHost == "" {
@@ -237,15 +236,11 @@ func proxyWithRedirect(c *gin.Context, u string, redirectCount int) {
realHost = "https://" + realHost
}
- // 🚀 高性能预筛选:仅对.sh文件进行智能处理
if strings.HasSuffix(strings.ToLower(u), ".sh") {
- // 检查是否为gzip压缩内容
isGzipCompressed := resp.Header.Get("Content-Encoding") == "gzip"
- // 仅对shell脚本使用智能处理器
processedBody, processedSize, err := ProcessSmart(resp.Body, isGzipCompressed, realHost)
if err != nil {
- // 优雅降级 - 处理失败时使用直接代理模式
fmt.Printf("智能处理失败,回退到直接代理: %v\n", err)
processedBody = resp.Body
processedSize = 0
@@ -253,7 +248,6 @@ func proxyWithRedirect(c *gin.Context, u string, redirectCount int) {
// 智能设置响应头
if processedSize > 0 {
- // 内容被处理过,清理压缩相关头,使用chunked传输
resp.Header.Del("Content-Length")
resp.Header.Del("Content-Encoding")
resp.Header.Set("Transfer-Encoding", "chunked")
@@ -282,8 +276,6 @@ func proxyWithRedirect(c *gin.Context, u string, redirectCount int) {
return
}
} else {
- // 🔥 非.sh文件:直接高性能流式代理,零内存消耗
- // 复制所有响应头
for key, values := range resp.Header {
for _, value := range values {
c.Header(key, value)
@@ -302,7 +294,7 @@ func proxyWithRedirect(c *gin.Context, u string, redirectCount int) {
c.Status(resp.StatusCode)
- // 直接流式转发,零内存拷贝
+ // 直接流式转发
if _, err := io.Copy(c.Writer, resp.Body); err != nil {
fmt.Printf("直接代理失败: %v\n", err)
}
@@ -318,9 +310,9 @@ func checkURL(u string) []string {
return nil
}
-// ✅ 初始化健康监控路由
+// 初始化健康监控路由
func initHealthRoutes(router *gin.Engine) {
- // 健康检查端点 - 最轻量级,无依赖检查
+ // 健康检查端点
router.GET("/health", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"status": "healthy",
@@ -330,12 +322,11 @@ func initHealthRoutes(router *gin.Engine) {
})
})
- // 就绪检查端点 - 检查关键组件状态
+ // 就绪检查端点
router.GET("/ready", func(c *gin.Context) {
checks := make(map[string]string)
allReady := true
- // 检查配置状态
if GetConfig() != nil {
checks["config"] = "ok"
} else {
diff --git a/src/proxysh.go b/src/proxysh.go
index 5f7e7bd..f3f7f58 100644
--- a/src/proxysh.go
+++ b/src/proxysh.go
@@ -16,7 +16,6 @@ var githubRegex = regexp.MustCompile(`https?://(?:github\.com|raw\.githubusercon
func ProcessSmart(input io.ReadCloser, isCompressed bool, host string) (io.Reader, int64, error) {
defer input.Close()
- // 读取Shell脚本内容
content, err := readShellContent(input, isCompressed)
if err != nil {
return nil, 0, fmt.Errorf("内容读取失败: %v", err)
@@ -26,38 +25,31 @@ func ProcessSmart(input io.ReadCloser, isCompressed bool, host string) (io.Reade
return strings.NewReader(""), 0, nil
}
- // Shell脚本大小检查 (10MB限制)
if len(content) > 10*1024*1024 {
return strings.NewReader(content), int64(len(content)), nil
}
- // 快速检查是否包含GitHub URL
if !strings.Contains(content, "github.com") && !strings.Contains(content, "githubusercontent.com") {
return strings.NewReader(content), int64(len(content)), nil
}
- // 执行GitHub URL替换
processed := processGitHubURLs(content, host)
return strings.NewReader(processed), int64(len(processed)), nil
}
-// readShellContent 读取Shell脚本内容
func readShellContent(input io.ReadCloser, isCompressed bool) (string, error) {
var reader io.Reader = input
// 处理gzip压缩
if isCompressed {
- // 读取前2字节检查gzip魔数
peek := make([]byte, 2)
n, err := input.Read(peek)
if err != nil && err != io.EOF {
return "", fmt.Errorf("读取数据失败: %v", err)
}
- // 检查gzip魔数 (0x1f, 0x8b)
if n >= 2 && peek[0] == 0x1f && peek[1] == 0x8b {
- // 合并peek数据和剩余流
combinedReader := io.MultiReader(bytes.NewReader(peek[:n]), input)
gzReader, err := gzip.NewReader(combinedReader)
if err != nil {
@@ -66,12 +58,10 @@ func readShellContent(input io.ReadCloser, isCompressed bool) (string, error) {
defer gzReader.Close()
reader = gzReader
} else {
- // 不是gzip格式,合并peek数据
reader = io.MultiReader(bytes.NewReader(peek[:n]), input)
}
}
- // 读取全部内容
data, err := io.ReadAll(reader)
if err != nil {
return "", fmt.Errorf("读取内容失败: %v", err)
@@ -80,7 +70,6 @@ func readShellContent(input io.ReadCloser, isCompressed bool) (string, error) {
return string(data), nil
}
-// processGitHubURLs 处理GitHub URL替换
func processGitHubURLs(content, host string) string {
return githubRegex.ReplaceAllStringFunc(content, func(url string) string {
return transformURL(url, host)
@@ -89,19 +78,15 @@ func processGitHubURLs(content, host string) string {
// transformURL URL转换函数
func transformURL(url, host string) string {
- // 避免重复处理
if strings.Contains(url, host) {
return url
}
- // 协议标准化为https
if strings.HasPrefix(url, "http://") {
url = "https" + url[4:]
} else if !strings.HasPrefix(url, "https://") && !strings.HasPrefix(url, "//") {
url = "https://" + url
}
-
- // 清理host格式
cleanHost := strings.TrimPrefix(host, "https://")
cleanHost = strings.TrimPrefix(cleanHost, "http://")
cleanHost = strings.TrimSuffix(cleanHost, "/")
diff --git a/src/public/images.html b/src/public/images.html
index 8393d94..b82f010 100644
--- a/src/public/images.html
+++ b/src/public/images.html
@@ -348,7 +348,6 @@
margin-top: 0.25rem;
}
- /* 响应式设计 */
@media (max-width: 768px) {
.navbar-container {
padding: 0 0.5rem;
@@ -385,7 +384,6 @@
}
}
- /* 加载动画 */
.loading {
display: inline-block;
width: 1rem;
@@ -405,7 +403,6 @@
display: none;
}
- /* 移动端菜单样式 - 与其他页面完全一致 */
.mobile-menu-toggle {
display: none;
}
@@ -480,7 +477,6 @@
-
-
-
-
单镜像下载
@@ -548,13 +541,12 @@
type="text"
id="imageInput"
class="form-input"
- placeholder="例如: nginx:latest, ubuntu:20.04, redis:alpine"
- value="nginx:latest"
+ placeholder="例如: nginx:alpine"
>
-
+
-
多个镜像批量下载
@@ -591,7 +582,7 @@
@@ -634,7 +625,6 @@
});
}
- // 显示状态消息
function showStatus(elementId, message, type = 'success') {
const element = document.getElementById(elementId);
element.className = `status status-${type}`;
@@ -642,12 +632,10 @@
element.classList.remove('hidden');
}
- // 隐藏状态消息
function hideStatus(elementId) {
document.getElementById(elementId).classList.add('hidden');
}
- // 设置按钮加载状态
function setButtonLoading(btnId, textId, loadingId, loading) {
const btn = document.getElementById(btnId);
const text = document.getElementById(textId);
@@ -663,13 +651,10 @@
}
}
- // 构建下载URL
function buildDownloadUrl(imageName, platform = '') {
- // 将斜杠替换为下划线以适应URL路径
const encodedImage = imageName.replace(/\//g, '_');
let url = `/api/image/download/${encodedImage}`;
- // 只有指定平台时才添加查询参数
if (platform && platform.trim()) {
url += `?platform=${encodeURIComponent(platform.trim())}`;
}
@@ -677,7 +662,6 @@
return url;
}
- // 单镜像下载
document.getElementById('singleForm').addEventListener('submit', function(e) {
e.preventDefault();
@@ -692,26 +676,22 @@
hideStatus('singleStatus');
setButtonLoading('downloadBtn', 'downloadText', 'downloadLoading', true);
- // 创建下载链接并触发下载
const downloadUrl = buildDownloadUrl(imageName, platform);
const link = document.createElement('a');
link.href = downloadUrl;
- link.download = ''; // 让浏览器决定文件名
+ link.download = '';
link.style.display = 'none';
document.body.appendChild(link);
- // 触发下载
link.click();
document.body.removeChild(link);
- // 显示成功消息
const platformText = platform ? ` (${platform})` : '';
showStatus('singleStatus', `开始下载 ${imageName}${platformText}`, 'success');
setButtonLoading('downloadBtn', 'downloadText', 'downloadLoading', false);
});
- // 批量下载
document.getElementById('batchForm').addEventListener('submit', async function(e) {
e.preventDefault();
@@ -736,7 +716,6 @@
images: images
};
- // 如果指定了平台,添加到选项中
if (platform) {
options.platform = platform;
}
@@ -754,7 +733,6 @@
});
if (response.ok) {
- // 获取文件名
const contentDisposition = response.headers.get('Content-Disposition');
let filename = `batch_${images.length}_images.tar`;
@@ -763,7 +741,6 @@
if (matches) filename = matches[1];
}
- // 创建blob并下载
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
@@ -805,7 +782,6 @@
}
}
- // 初始化
initTheme();
initMobileMenu();
diff --git a/src/public/index.html b/src/public/index.html
index e7323ab..89b8df1 100644
--- a/src/public/index.html
+++ b/src/public/index.html
@@ -543,14 +543,13 @@
padding: 1.5rem;
}
- /* 移动端快速生成加速链接区域优化 */
.input-container {
flex-direction: column;
gap: 1rem;
}
.input {
- font-size: 16px; /* 防止iOS缩放 */
+ font-size: 16px;
}
.button {
@@ -572,7 +571,6 @@
-
-
-
GitHub 文件加速
@@ -610,7 +606,6 @@
-
-
-