From de79d2ec1dbc3b09be1930506fd0cc74c1da6057 Mon Sep 17 00:00:00 2001 From: NewName Date: Sat, 17 May 2025 13:51:24 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84=E8=BF=9B=E5=BA=A6=E6=98=BE?= =?UTF-8?q?=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ghproxy/Dockerfile | 2 +- ghproxy/go.mod | 5 +- ghproxy/go.sum | 2 + ghproxy/public/skopeo.html | 35 ++++++-- ghproxy/skopeo_service.go | 175 ++++++++++++++++++++++++++++++++----- 5 files changed, 188 insertions(+), 31 deletions(-) diff --git a/ghproxy/Dockerfile b/ghproxy/Dockerfile index 2049314..3aff630 100644 --- a/ghproxy/Dockerfile +++ b/ghproxy/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.22.5-alpine AS builder +FROM golang:1.23-alpine AS builder WORKDIR /app COPY go.mod go.sum ./ diff --git a/ghproxy/go.mod b/ghproxy/go.mod index ac9bbe7..baaf850 100644 --- a/ghproxy/go.mod +++ b/ghproxy/go.mod @@ -1,10 +1,13 @@ module ghproxy -go 1.22.5 +go 1.23.0 + +toolchain go1.24.1 require ( github.com/gin-gonic/gin v1.10.0 github.com/gorilla/websocket v1.5.1 + golang.org/x/sync v0.14.0 ) require ( diff --git a/ghproxy/go.sum b/ghproxy/go.sum index 9ced7b0..6a170de 100644 --- a/ghproxy/go.sum +++ b/ghproxy/go.sum @@ -72,6 +72,8 @@ golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= +golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= diff --git a/ghproxy/public/skopeo.html b/ghproxy/public/skopeo.html index b30963a..e5e24b7 100644 --- a/ghproxy/public/skopeo.html +++ b/ghproxy/public/skopeo.html @@ -222,6 +222,13 @@ display: none; } + .total-progress { + margin-bottom: 20px; + padding: 15px; + background-color: var(--inputcolor); + border-radius: 10px; + } + .progress-bar { height: 20px; background-color: #39c5bb; @@ -231,8 +238,10 @@ } .progress-text { - margin-top: 5px; - font-size: 14px; + margin-top: 10px; + font-size: 16px; + font-weight: bold; + text-align: center; } .image-progress { @@ -328,10 +337,12 @@

整体进度

-
-
+
+
+
+
+
0%
-
0%
@@ -425,6 +436,10 @@ currentTaskId = data.taskId; showProgressUI(); connectWebSocket(currentTaskId); + + // 显示初始任务总数 + const totalCount = data.totalCount || images.length; + totalProgressText.textContent = `0/${totalCount} - 0%`; } else { showToast('下载任务创建失败'); } @@ -441,6 +456,11 @@ imageInput.disabled = true; platformInput.disabled = true; + // 设置初始总进度显示 + const totalCount = images.length; + totalProgressBar.style.width = '0%'; + totalProgressText.textContent = `0/${totalCount} - 0%`; + // 初始化每个镜像的进度条 imageProgressList.innerHTML = ''; images.forEach(image => { @@ -529,8 +549,9 @@ // 更新总体进度 function updateProgress(data) { // 更新总进度 - totalProgressBar.style.width = `${data.totalProgress}%`; - totalProgressText.textContent = `${Math.round(data.totalProgress)}%`; + const progressPercent = data.totalCount > 0 ? (data.completedCount / data.totalCount) * 100 : 0; + totalProgressBar.style.width = `${progressPercent}%`; + totalProgressText.textContent = `${data.completedCount}/${data.totalCount} - ${Math.round(progressPercent)}%`; // 更新各个镜像的进度 data.images.forEach(imgData => { diff --git a/ghproxy/skopeo_service.go b/ghproxy/skopeo_service.go index 7a0b94a..2450b00 100644 --- a/ghproxy/skopeo_service.go +++ b/ghproxy/skopeo_service.go @@ -15,7 +15,6 @@ import ( "path/filepath" "strings" "sync" - "sync/atomic" "time" "github.com/gin-gonic/gin" @@ -47,7 +46,8 @@ type ImageTask struct { type DownloadTask struct { ID string `json:"id"` Images []*ImageTask `json:"images"` - TotalProgress float64 `json:"totalProgress"` + CompletedCount int `json:"completedCount"` // 已完成任务数 + TotalCount int `json:"totalCount"` // 总任务数 Status TaskStatus `json:"status"` OutputFile string `json:"-"` // 最终输出文件 TempDir string `json:"-"` // 临时目录 @@ -189,7 +189,8 @@ func getTaskStatus(c *gin.Context) { // 创建任务状态副本以避免序列化过程中的锁 taskCopy := &DownloadTask{ ID: task.ID, - TotalProgress: 0, + CompletedCount: 0, + TotalCount: len(task.Images), Status: TaskStatus(""), Images: nil, } @@ -200,7 +201,7 @@ func getTaskStatus(c *gin.Context) { task.StatusLock.RUnlock() task.ProgressLock.RLock() - taskCopy.TotalProgress = task.TotalProgress + taskCopy.CompletedCount = task.CompletedCount task.ProgressLock.RUnlock() // 复制镜像信息 @@ -250,21 +251,42 @@ func initTask(task *DownloadTask) { imgTask := task.Images[update.ImageIndex] task.ImageLock.RUnlock() + statusChanged := false + prevStatus := "" + // 更新镜像进度和状态 imgTask.lock.Lock() if update.Progress > 0 { imgTask.Progress = update.Progress } - if update.Status != "" { + if update.Status != "" && update.Status != imgTask.Status { + prevStatus = imgTask.Status imgTask.Status = update.Status + statusChanged = true } if update.Error != "" { imgTask.Error = update.Error } imgTask.lock.Unlock() - // 更新总进度 - updateTaskTotalProgress(task) + // 检查状态变化并更新完成计数 + if statusChanged { + task.ProgressLock.Lock() + // 如果之前不是Completed,现在是Completed,增加计数 + if prevStatus != string(StatusCompleted) && update.Status == string(StatusCompleted) { + task.CompletedCount++ + fmt.Printf("任务 %s: 镜像 %d 完成,当前完成数: %d/%d\n", + task.ID, update.ImageIndex, task.CompletedCount, task.TotalCount) + } + // 如果之前是Completed,现在不是,减少计数 + if prevStatus == string(StatusCompleted) && update.Status != string(StatusCompleted) { + task.CompletedCount-- + if task.CompletedCount < 0 { + task.CompletedCount = 0 + } + } + task.ProgressLock.Unlock() + } // 发送更新到客户端 sendTaskUpdate(task) @@ -289,30 +311,35 @@ func sendProgressUpdate(task *DownloadTask, index int, progress float64, status } } -// 更新总进度 - 不需要外部锁 +// 更新总进度 - 重新计算已完成任务数 func updateTaskTotalProgress(task *DownloadTask) { task.ProgressLock.Lock() defer task.ProgressLock.Unlock() - totalProgress := 0.0 + completedCount := 0 task.ImageLock.RLock() - imageCount := len(task.Images) + totalCount := len(task.Images) task.ImageLock.RUnlock() - if imageCount == 0 { + if totalCount == 0 { return } task.ImageLock.RLock() for _, img := range task.Images { img.lock.Lock() - totalProgress += img.Progress + if img.Status == string(StatusCompleted) { + completedCount++ + } img.lock.Unlock() } task.ImageLock.RUnlock() - task.TotalProgress = totalProgress / float64(imageCount) + task.CompletedCount = completedCount + task.TotalCount = totalCount + + fmt.Printf("任务 %s: 进度更新 %d/%d 已完成\n", task.ID, completedCount, totalCount) } // 处理下载请求 @@ -351,7 +378,8 @@ func handleDownload(c *gin.Context) { task := &DownloadTask{ ID: taskID, Images: imageTasks, - TotalProgress: 0, + CompletedCount: 0, + TotalCount: len(imageTasks), Status: StatusPending, TempDir: tempDir, } @@ -374,6 +402,7 @@ func handleDownload(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "taskId": taskID, "status": "started", + "totalCount": len(imageTasks), }) } @@ -384,6 +413,16 @@ func processDownloadTask(task *DownloadTask, platform string) { task.Status = StatusRunning task.StatusLock.Unlock() + // 初始化总任务数 + task.ImageLock.RLock() + imageCount := len(task.Images) + task.ImageLock.RUnlock() + + task.ProgressLock.Lock() + task.TotalCount = imageCount + task.CompletedCount = 0 + task.ProgressLock.Unlock() + // 通知客户端任务已开始 sendTaskUpdate(task) @@ -395,7 +434,7 @@ func processDownloadTask(task *DownloadTask, platform string) { // 启动并发下载 task.ImageLock.RLock() - imageCount := len(task.Images) + imageCount = len(task.Images) task.ImageLock.RUnlock() // 创建工作池限制并发数 @@ -428,6 +467,9 @@ func processDownloadTask(task *DownloadTask, platform string) { // 等待所有下载完成 err := g.Wait() + // 再次计算已完成任务数,确保正确 + updateTaskTotalProgress(task) + // 检查是否有错误发生 if err != nil { task.StatusLock.Lock() @@ -496,15 +538,20 @@ func processDownloadTask(task *DownloadTask, platform string) { task.OutputFile = finalFilePath task.Status = StatusCompleted - // 设置总进度为100% + // 设置完成计数为总任务数 task.ProgressLock.Lock() - task.TotalProgress = 100 + task.CompletedCount = task.TotalCount task.ProgressLock.Unlock() task.StatusLock.Unlock() // 发送最终状态更新 sendTaskUpdate(task) + + // 确保所有进度都达到100% + ensureTaskCompletion(task) + + fmt.Printf("任务 %s 全部完成: %d/%d\n", task.ID, task.CompletedCount, task.TotalCount) } // 下载单个镜像(带上下文控制) @@ -575,7 +622,6 @@ func downloadImageWithContext(ctx context.Context, task *DownloadTask, index int } // 使用进度通道传递进度信息 - progressChan := make(chan float64, 10) outputChan := make(chan string, 20) done := make(chan struct{}) @@ -606,7 +652,11 @@ func downloadImageWithContext(ctx context.Context, task *DownloadTask, index int return case <-done: - // 命令完成 + // 命令完成,强制更新到100% + if lastProgress < 100 { + fmt.Printf("镜像 %s 下载完成,强制更新进度到100%%\n", imgTask.Image) + sendProgressUpdate(task, index, 100, string(StatusCompleted), "") + } return case output := <-outputChan: @@ -654,6 +704,14 @@ func downloadImageWithContext(ctx context.Context, task *DownloadTask, index int } } + // 如果发现完成标记,立即更新到100% + if checkForCompletionMarkers(output) { + fmt.Printf("镜像 %s 检测到完成标记\n", imgTask.Image) + lastProgress = 100 + sendProgressUpdate(task, index, 100, string(StatusCompleted), "") + stagnantTime = 0 + } + case <-ticker.C: // 如果进度长时间无变化,缓慢增加 stagnantTime += 100 // 100ms @@ -716,7 +774,7 @@ func downloadImageWithContext(ctx context.Context, task *DownloadTask, index int return fmt.Errorf(errMsg) } - // 更新状态为已完成 + // 确保更新状态为已完成,进度为100% sendProgressUpdate(task, index, 100, string(StatusCompleted), "") return nil } @@ -796,7 +854,8 @@ func sendTaskUpdate(task *DownloadTask) { // 复制任务状态避免序列化时锁定 taskCopy := &DownloadTask{ ID: task.ID, - TotalProgress: 0, + CompletedCount: 0, + TotalCount: len(task.Images), Status: TaskStatus(""), Images: nil, } @@ -807,7 +866,7 @@ func sendTaskUpdate(task *DownloadTask) { task.StatusLock.RUnlock() task.ProgressLock.RLock() - taskCopy.TotalProgress = task.TotalProgress + taskCopy.CompletedCount = task.CompletedCount task.ProgressLock.RUnlock() // 复制镜像信息 @@ -879,6 +938,16 @@ func serveFile(c *gin.Context) { return } + // 确保任务状态为已完成,并且所有进度都是100% + task.StatusLock.RLock() + isCompleted := task.Status == StatusCompleted + task.StatusLock.RUnlock() + + if isCompleted { + // 确保所有进度达到100% + ensureTaskCompletion(task) + } + // 检查文件是否存在 filePath := task.OutputFile if filePath == "" || !fileExists(filePath) { @@ -940,4 +1009,66 @@ func cleanupTempFiles() { fmt.Printf("清理临时文件失败: %v\n", err) } } +} + +// 完成任务处理函数,确保进度是100% +func ensureTaskCompletion(task *DownloadTask) { + // 重新检查一遍所有镜像的进度 + task.ImageLock.RLock() + completedCount := 0 + totalCount := len(task.Images) + + for i, img := range task.Images { + img.lock.Lock() + if img.Status == string(StatusCompleted) { + // 确保进度为100% + if img.Progress < 100 { + img.Progress = 100 + fmt.Printf("确保镜像 %d 进度为100%%\n", i) + } + completedCount++ + } + img.lock.Unlock() + } + task.ImageLock.RUnlock() + + // 更新完成计数 + task.ProgressLock.Lock() + task.CompletedCount = completedCount + task.TotalCount = totalCount + task.ProgressLock.Unlock() + + // 如果任务状态为已完成,但计数不匹配,修正计数 + task.StatusLock.RLock() + isCompleted := task.Status == StatusCompleted + task.StatusLock.RUnlock() + + if isCompleted && completedCount != totalCount { + task.ProgressLock.Lock() + task.CompletedCount = totalCount + task.ProgressLock.Unlock() + fmt.Printf("任务 %s 状态已完成,强制设置计数为 %d/%d\n", task.ID, totalCount, totalCount) + } + + // 发送最终更新 + sendTaskUpdate(task) +} + +// 处理下载单个镜像的输出中的完成标记 +func checkForCompletionMarkers(output string) bool { + // 已知的完成标记 + completionMarkers := []string{ + "Writing manifest to image destination", + "Copying config complete", + "Storing signatures", + "Writing manifest complete", + } + + for _, marker := range completionMarkers { + if strings.Contains(output, marker) { + return true + } + } + + return false } \ No newline at end of file