修复前端api和后端http压缩BUG
This commit is contained in:
26
src/main.go
26
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")
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
// 大文件保护
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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('<span class="badge badge-official">官方</span>');
|
||||
@@ -902,21 +902,21 @@
|
||||
|
||||
card.innerHTML = `
|
||||
<div class="result-title">
|
||||
🐳 ${result.name}
|
||||
🐳 ${result.repo_name || result.name}
|
||||
${badges.join('')}
|
||||
</div>
|
||||
<div class="result-description">
|
||||
${result.description || '暂无描述'}
|
||||
${result.short_description || result.description || '暂无描述'}
|
||||
</div>
|
||||
<div class="result-meta">
|
||||
<div class="meta-stats">
|
||||
<div class="meta-item">
|
||||
<span>📈</span>
|
||||
${formatUtils.formatNumber(result.star_count)} Stars
|
||||
${formatUtils.formatNumber(result.star_count || 0)} Stars
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<span>📥</span>
|
||||
${formatUtils.formatNumber(result.pull_count)} Pulls
|
||||
${formatUtils.formatNumber(result.pull_count || 0)} Pulls
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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);
|
||||
|
||||
@@ -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 @@
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">输出格式</label>
|
||||
<select class="select" id="formatSelect">
|
||||
<option value="docker-archive">Docker Archive (.tar)</option>
|
||||
<option value="oci-archive">OCI Archive (.tar)</option>
|
||||
<option value="dir">目录格式</option>
|
||||
</select>
|
||||
<div class="input-info">
|
||||
📦 系统自动使用 Docker Archive (.tar) 格式
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 1rem; flex-wrap: wrap;">
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user