diff --git a/src/main.go b/src/main.go index 7f6e5d6..b222506 100644 --- a/src/main.go +++ b/src/main.go @@ -209,18 +209,36 @@ func proxyWithRedirect(c *gin.Context, u string, redirectCount int) { realHost = "https://" + realHost } + // 检查是否为gzip压缩内容 + isGzipCompressed := resp.Header.Get("Content-Encoding") == "gzip" + // 使用智能处理器自动处理所有内容 - processedBody, processedSize, err := ProcessSmart(resp.Body, resp.Header.Get("Content-Encoding") == "gzip", realHost) + processedBody, processedSize, err := ProcessSmart(resp.Body, isGzipCompressed, realHost) if err != nil { - // 优雅降级 - 处理失败时直接返回原内容 - c.String(http.StatusInternalServerError, fmt.Sprintf("内容处理错误: %v", err)) + // 优雅降级 - 处理失败时使用直接代理模式 + fmt.Printf("智能处理失败,回退到直接代理: %v\n", err) + + // 复制原始响应头 + for key, values := range resp.Header { + for _, value := range values { + c.Header(key, value) + } + } + + c.Status(resp.StatusCode) + + // 直接转发原始内容 + if _, err := io.Copy(c.Writer, resp.Body); err != nil { + fmt.Printf("直接代理模式复制内容失败: %v\n", err) + } return } // 智能设置响应头 if processedSize > 0 { - // 内容被处理过,使用chunked传输以获得最佳性能 + // 内容被处理过,清理压缩相关头,使用chunked传输 resp.Header.Del("Content-Length") + resp.Header.Del("Content-Encoding") // 重要:清理压缩头,防止浏览器重复解压 resp.Header.Set("Transfer-Encoding", "chunked") } diff --git a/src/proxysh.go b/src/proxysh.go index 3a06ffa..587574c 100644 --- a/src/proxysh.go +++ b/src/proxysh.go @@ -95,8 +95,12 @@ func ProcessSmart(input io.ReadCloser, isCompressed bool, host string) (io.Reade // 快速读取内容 content, err := processor.readContent(input, isCompressed) - if err != nil || len(content) == 0 { - // 优雅降级:返回空读取器而不是已消费的input + if err != nil { + // 优雅降级:读取错误时返回错误,让上层处理 + return nil, 0, fmt.Errorf("内容读取失败: %v", err) + } + if len(content) == 0 { + // 空内容,返回空读取器 return strings.NewReader(""), 0, nil } @@ -209,18 +213,6 @@ func (sp *SmartProcessor) isBinaryContent(content string) bool { func (sp *SmartProcessor) readContent(input io.ReadCloser, isCompressed bool) (string, error) { defer input.Close() - var reader io.Reader = input - - // 压缩处理 - if isCompressed { - gzReader, err := gzip.NewReader(input) - if err != nil { - return "", err - } - defer gzReader.Close() - reader = gzReader - } - // 使用缓冲池 buffer := sp.bufferPool.Get().([]byte) defer sp.bufferPool.Put(buffer) @@ -228,6 +220,36 @@ func (sp *SmartProcessor) readContent(input io.ReadCloser, isCompressed bool) (s var result strings.Builder result.Grow(SMART_BUFFER_SIZE) // 预分配 + var reader io.Reader = input + var gzReader *gzip.Reader + + // 智能压缩处理 - 防止双重解压 + if isCompressed { + // 先读取一小部分数据来检测是否真的是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 { + // 确认是gzip格式,创建MultiReader组合peek数据和剩余数据 + combinedReader := io.MultiReader(bytes.NewReader(peek[:n]), input) + gzReader, err = gzip.NewReader(combinedReader) + if err != nil { + return "", fmt.Errorf("gzip解压失败: %v", err) + } + defer gzReader.Close() + reader = gzReader + } else { + // 不是gzip格式或者HTTP客户端已经解压,使用原始数据 + combinedReader := io.MultiReader(bytes.NewReader(peek[:n]), input) + reader = combinedReader + } + } + + // 读取内容 for { n, err := reader.Read(buffer) if n > 0 { @@ -237,7 +259,7 @@ func (sp *SmartProcessor) readContent(input io.ReadCloser, isCompressed bool) (s break } if err != nil { - return "", err + return "", fmt.Errorf("读取内容失败: %v", err) } // 大文件保护 diff --git a/src/public/index.html b/src/public/index.html index f5fc353..6417d5a 100644 --- a/src/public/index.html +++ b/src/public/index.html @@ -363,6 +363,15 @@ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); } + /* 黑夜模式下的Docker按钮样式 */ + .dark .docker-button { + background: linear-gradient(135deg, #374151, #4b5563); + } + + .dark .docker-button:hover { + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); + } + /* 模态框设计 */ .modal { position: fixed; diff --git a/src/public/search.html b/src/public/search.html index cb8b3dd..18ba8ed 100644 --- a/src/public/search.html +++ b/src/public/search.html @@ -855,14 +855,14 @@ elements.backToSearch.classList.remove('show'); try { - const response = await fetch(`/api/search?q=${encodeURIComponent(term)}&page=${page}`); + const response = await fetch(`/search?q=${encodeURIComponent(term)}&page=${page}`); const data = await response.json(); - if (data.success) { + if (data && data.results) { searchResults = data.results; - displaySearchResults(data.results, data.total, page); + displaySearchResults(data.results, data.count, page); } else { - showToast('搜索失败:' + (data.message || '未知错误'), 'error'); + showToast('搜索失败:' + (data.error || '未知错误'), 'error'); } } catch (error) { console.error('搜索错误:', error); @@ -894,7 +894,7 @@ function createResultCard(result) { const card = document.createElement('div'); card.className = 'result-card'; - card.onclick = () => viewImageTags(result.name); + card.onclick = () => viewImageTags(result.repo_name || result.name); const badges = []; if (result.is_official) badges.push('官方'); @@ -902,21 +902,21 @@ card.innerHTML = `
- 🐳 ${result.name} + 🐳 ${result.repo_name || result.name} ${badges.join('')}
- ${result.description || '暂无描述'} + ${result.short_description || result.description || '暂无描述'}
📈 - ${formatUtils.formatNumber(result.star_count)} Stars + ${formatUtils.formatNumber(result.star_count || 0)} Stars
📥 - ${formatUtils.formatNumber(result.pull_count)} Pulls + ${formatUtils.formatNumber(result.pull_count || 0)} Pulls
@@ -934,15 +934,28 @@ elements.searchResults.classList.remove('show'); try { - const response = await fetch(`/api/tags?image=${encodeURIComponent(imageName)}`); + // 解析镜像名称 + let namespace = 'library'; + let repoName = imageName; + + if (imageName.includes('/')) { + const parts = imageName.split('/'); + namespace = parts[0]; + repoName = parts[1]; + } else { + // 官方镜像 + namespace = 'library'; + } + + const response = await fetch(`/tags/${namespace}/${repoName}`); const data = await response.json(); - if (data.success) { - allTags = data.tags; - displayImageTags(imageName, data.tags); + if (Array.isArray(data)) { + allTags = data; + displayImageTags(imageName, data); elements.backToSearch.classList.add('show'); } else { - showToast('获取标签失败:' + (data.message || '未知错误'), 'error'); + showToast('获取标签失败:' + (data.error || '未知错误'), 'error'); } } catch (error) { console.error('获取标签错误:', error); diff --git a/src/public/skopeo.html b/src/public/skopeo.html index 39fbbba..d4ba38d 100644 --- a/src/public/skopeo.html +++ b/src/public/skopeo.html @@ -364,6 +364,18 @@ transform: none; } + .input-info { + padding: 0.75rem 1rem; + background-color: var(--muted); + border: 1px solid var(--border); + border-radius: var(--radius); + color: var(--muted-foreground); + font-size: 0.875rem; + display: flex; + align-items: center; + gap: 0.5rem; + } + /* 架构选择区域 */ .arch-buttons { display: flex; @@ -679,11 +691,9 @@
- +
+ 📦 系统自动使用 Docker Archive (.tar) 格式 +
@@ -767,7 +777,6 @@ const elements = { imageListInput: document.getElementById('imageListInput'), architectureInput: document.getElementById('architectureInput'), - formatSelect: document.getElementById('formatSelect'), startDownload: document.getElementById('startDownload'), pauseDownload: document.getElementById('pauseDownload'), stopDownload: document.getElementById('stopDownload'), @@ -898,7 +907,6 @@ async function startDownload() { const imageList = parseImageList(elements.imageListInput.value); const architecture = elements.architectureInput.value.trim(); - const format = elements.formatSelect.value; if (imageList.length === 0) { showToast('请输入要下载的镜像列表', 'error'); @@ -915,7 +923,6 @@ id: index, image: image, architecture: architecture, - format: format, status: 'pending' })); @@ -959,26 +966,26 @@ try { // 调用后端API开始下载 - const response = await fetch('/api/skopeo/download', { + const response = await fetch('/api/download', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ - image: task.image, - architecture: task.architecture, - format: task.format + images: [task.image], + platform: task.architecture }) }); const result = await response.json(); - if (result.success) { - task.status = 'completed'; - updateTaskStatus(currentTaskIndex, 'completed', '下载完成'); + if (result.taskId) { + // 下载任务已创建,等待完成 + task.taskId = result.taskId; + await waitForTaskCompletion(task); } else { task.status = 'failed'; - updateTaskStatus(currentTaskIndex, 'failed', result.message || '下载失败'); + updateTaskStatus(currentTaskIndex, 'failed', result.error || '下载失败'); } } catch (error) { console.error('下载错误:', error); @@ -995,6 +1002,37 @@ } } + // 等待任务完成 + async function waitForTaskCompletion(task) { + return new Promise((resolve) => { + const checkStatus = async () => { + try { + const response = await fetch(`/api/task/${task.taskId}`); + const data = await response.json(); + + if (data.status === 'completed') { + task.status = 'completed'; + updateTaskStatus(currentTaskIndex, 'completed', '下载完成'); + resolve(); + } else if (data.status === 'failed') { + task.status = 'failed'; + updateTaskStatus(currentTaskIndex, 'failed', '下载失败'); + resolve(); + } else { + // 继续检查 + setTimeout(checkStatus, 2000); + } + } catch (error) { + task.status = 'failed'; + updateTaskStatus(currentTaskIndex, 'failed', '状态查询失败'); + resolve(); + } + }; + + checkStatus(); + }); + } + // 暂停下载 function pauseDownload() { isDownloading = false;