修复离线镜像小细节
This commit is contained in:
120
src/imagetar.go
120
src/imagetar.go
@@ -160,48 +160,9 @@ func (is *ImageStreamer) StreamImageToGin(ctx context.Context, imageRef string,
|
||||
|
||||
// streamMultiArchImage 处理多架构镜像
|
||||
func (is *ImageStreamer) streamMultiArchImage(ctx context.Context, desc *remote.Descriptor, writer io.Writer, options *StreamOptions, remoteOptions []remote.Option, imageRef string) error {
|
||||
index, err := desc.ImageIndex()
|
||||
img, err := is.selectPlatformImage(desc, options)
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取镜像索引失败: %w", err)
|
||||
}
|
||||
|
||||
manifest, err := index.IndexManifest()
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取索引清单失败: %w", err)
|
||||
}
|
||||
|
||||
// 选择合适的平台
|
||||
var selectedDesc *v1.Descriptor
|
||||
for _, m := range manifest.Manifests {
|
||||
if m.Platform == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if options.Platform != "" {
|
||||
platformParts := strings.Split(options.Platform, "/")
|
||||
if len(platformParts) == 2 &&
|
||||
m.Platform.OS == platformParts[0] &&
|
||||
m.Platform.Architecture == platformParts[1] {
|
||||
selectedDesc = &m
|
||||
break
|
||||
}
|
||||
} else if m.Platform.OS == "linux" && m.Platform.Architecture == "amd64" {
|
||||
selectedDesc = &m
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if selectedDesc == nil && len(manifest.Manifests) > 0 {
|
||||
selectedDesc = &manifest.Manifests[0]
|
||||
}
|
||||
|
||||
if selectedDesc == nil {
|
||||
return fmt.Errorf("未找到合适的平台镜像")
|
||||
}
|
||||
|
||||
img, err := index.Image(selectedDesc.Digest)
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取选中镜像失败: %w", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return is.streamImageLayers(ctx, img, writer, options, imageRef)
|
||||
@@ -427,7 +388,28 @@ func (is *ImageStreamer) streamSingleImageForBatch(ctx context.Context, tarWrite
|
||||
|
||||
switch desc.MediaType {
|
||||
case types.OCIImageIndex, types.DockerManifestList:
|
||||
return nil, nil, fmt.Errorf("批量下载暂不支持多架构镜像")
|
||||
// 处理多架构镜像,复用单个下载的逻辑
|
||||
img, err := is.selectPlatformImage(desc, options)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("选择平台镜像失败: %w", err)
|
||||
}
|
||||
|
||||
layers, err := img.Layers()
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("获取镜像层失败: %w", err)
|
||||
}
|
||||
|
||||
configFile, err := img.ConfigFile()
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("获取镜像配置失败: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("镜像包含 %d 层", len(layers))
|
||||
|
||||
err = is.streamDockerFormatWithReturn(ctx, tarWriter, img, layers, configFile, imageRef, &manifest, &repositories)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
case types.OCIManifestSchema1, types.DockerManifestSchema2:
|
||||
img, err := desc.Image()
|
||||
if err != nil {
|
||||
@@ -477,6 +459,55 @@ func (is *ImageStreamer) streamSingleImageForBatch(ctx context.Context, tarWrite
|
||||
return manifest, repositories, nil
|
||||
}
|
||||
|
||||
// selectPlatformImage 从多架构镜像中选择合适的平台镜像
|
||||
func (is *ImageStreamer) selectPlatformImage(desc *remote.Descriptor, options *StreamOptions) (v1.Image, error) {
|
||||
index, err := desc.ImageIndex()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取镜像索引失败: %w", err)
|
||||
}
|
||||
|
||||
manifest, err := index.IndexManifest()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取索引清单失败: %w", err)
|
||||
}
|
||||
|
||||
// 选择合适的平台
|
||||
var selectedDesc *v1.Descriptor
|
||||
for _, m := range manifest.Manifests {
|
||||
if m.Platform == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if options.Platform != "" {
|
||||
platformParts := strings.Split(options.Platform, "/")
|
||||
if len(platformParts) == 2 &&
|
||||
m.Platform.OS == platformParts[0] &&
|
||||
m.Platform.Architecture == platformParts[1] {
|
||||
selectedDesc = &m
|
||||
break
|
||||
}
|
||||
} else if m.Platform.OS == "linux" && m.Platform.Architecture == "amd64" {
|
||||
selectedDesc = &m
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if selectedDesc == nil && len(manifest.Manifests) > 0 {
|
||||
selectedDesc = &manifest.Manifests[0]
|
||||
}
|
||||
|
||||
if selectedDesc == nil {
|
||||
return nil, fmt.Errorf("未找到合适的平台镜像")
|
||||
}
|
||||
|
||||
img, err := index.Image(selectedDesc.Digest)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取选中镜像失败: %w", err)
|
||||
}
|
||||
|
||||
return img, nil
|
||||
}
|
||||
|
||||
var globalImageStreamer *ImageStreamer
|
||||
|
||||
// initImageStreamer 初始化镜像下载器
|
||||
@@ -569,17 +600,16 @@ func handleSimpleBatchDownload(c *gin.Context) {
|
||||
|
||||
options := &StreamOptions{
|
||||
Platform: req.Platform,
|
||||
Compression: true,
|
||||
Compression: false,
|
||||
}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
log.Printf("批量下载 %d 个镜像 (平台: %s)", len(req.Images), formatPlatformText(req.Platform))
|
||||
|
||||
filename := fmt.Sprintf("batch_%d_images.docker.gz", len(req.Images))
|
||||
filename := fmt.Sprintf("batch_%d_images.tar", len(req.Images))
|
||||
|
||||
c.Header("Content-Type", "application/octet-stream")
|
||||
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename))
|
||||
c.Header("Content-Encoding", "gzip")
|
||||
|
||||
if err := globalImageStreamer.StreamMultipleImages(ctx, req.Images, c.Writer, options); err != nil {
|
||||
log.Printf("批量镜像下载失败: %v", err)
|
||||
|
||||
@@ -513,7 +513,7 @@
|
||||
<!-- 页面头部 -->
|
||||
<div class="header">
|
||||
<h1 class="title">Docker离线镜像下载</h1>
|
||||
<p class="subtitle">即点即下,无需等待打包,完全符合docker load标准</p>
|
||||
<p class="subtitle">即点即下,无需等待打包,完全符合docker load加载标准</p>
|
||||
|
||||
<div class="features">
|
||||
<div class="feature">
|
||||
@@ -582,7 +582,7 @@
|
||||
|
||||
<form id="batchForm">
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="imagesTextarea">镜像列表,每行一个,会将多个镜像合并为tar格式,完全兼容docker load</label>
|
||||
<label class="form-label" for="imagesTextarea">镜像列表,每行一个,会将多个镜像自动合并,符合官方标准,完全兼容docker load</label>
|
||||
<textarea
|
||||
id="imagesTextarea"
|
||||
class="textarea"
|
||||
@@ -756,7 +756,7 @@
|
||||
if (response.ok) {
|
||||
// 获取文件名
|
||||
const contentDisposition = response.headers.get('Content-Disposition');
|
||||
let filename = `batch_${images.length}_images.docker.gz`;
|
||||
let filename = `batch_${images.length}_images.tar`;
|
||||
|
||||
if (contentDisposition) {
|
||||
const matches = contentDisposition.match(/filename="(.+)"/);
|
||||
|
||||
Reference in New Issue
Block a user