修复离线镜像小细节
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 处理多架构镜像
|
// streamMultiArchImage 处理多架构镜像
|
||||||
func (is *ImageStreamer) streamMultiArchImage(ctx context.Context, desc *remote.Descriptor, writer io.Writer, options *StreamOptions, remoteOptions []remote.Option, imageRef string) error {
|
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 {
|
if err != nil {
|
||||||
return fmt.Errorf("获取镜像索引失败: %w", err)
|
return 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 is.streamImageLayers(ctx, img, writer, options, imageRef)
|
return is.streamImageLayers(ctx, img, writer, options, imageRef)
|
||||||
@@ -427,7 +388,28 @@ func (is *ImageStreamer) streamSingleImageForBatch(ctx context.Context, tarWrite
|
|||||||
|
|
||||||
switch desc.MediaType {
|
switch desc.MediaType {
|
||||||
case types.OCIImageIndex, types.DockerManifestList:
|
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:
|
case types.OCIManifestSchema1, types.DockerManifestSchema2:
|
||||||
img, err := desc.Image()
|
img, err := desc.Image()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -477,6 +459,55 @@ func (is *ImageStreamer) streamSingleImageForBatch(ctx context.Context, tarWrite
|
|||||||
return manifest, repositories, nil
|
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
|
var globalImageStreamer *ImageStreamer
|
||||||
|
|
||||||
// initImageStreamer 初始化镜像下载器
|
// initImageStreamer 初始化镜像下载器
|
||||||
@@ -569,17 +600,16 @@ func handleSimpleBatchDownload(c *gin.Context) {
|
|||||||
|
|
||||||
options := &StreamOptions{
|
options := &StreamOptions{
|
||||||
Platform: req.Platform,
|
Platform: req.Platform,
|
||||||
Compression: true,
|
Compression: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx := c.Request.Context()
|
ctx := c.Request.Context()
|
||||||
log.Printf("批量下载 %d 个镜像 (平台: %s)", len(req.Images), formatPlatformText(req.Platform))
|
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-Type", "application/octet-stream")
|
||||||
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename))
|
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 {
|
if err := globalImageStreamer.StreamMultipleImages(ctx, req.Images, c.Writer, options); err != nil {
|
||||||
log.Printf("批量镜像下载失败: %v", err)
|
log.Printf("批量镜像下载失败: %v", err)
|
||||||
|
|||||||
@@ -513,7 +513,7 @@
|
|||||||
<!-- 页面头部 -->
|
<!-- 页面头部 -->
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<h1 class="title">Docker离线镜像下载</h1>
|
<h1 class="title">Docker离线镜像下载</h1>
|
||||||
<p class="subtitle">即点即下,无需等待打包,完全符合docker load标准</p>
|
<p class="subtitle">即点即下,无需等待打包,完全符合docker load加载标准</p>
|
||||||
|
|
||||||
<div class="features">
|
<div class="features">
|
||||||
<div class="feature">
|
<div class="feature">
|
||||||
@@ -582,7 +582,7 @@
|
|||||||
|
|
||||||
<form id="batchForm">
|
<form id="batchForm">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label" for="imagesTextarea">镜像列表,每行一个,会将多个镜像合并为tar格式,完全兼容docker load</label>
|
<label class="form-label" for="imagesTextarea">镜像列表,每行一个,会将多个镜像自动合并,符合官方标准,完全兼容docker load</label>
|
||||||
<textarea
|
<textarea
|
||||||
id="imagesTextarea"
|
id="imagesTextarea"
|
||||||
class="textarea"
|
class="textarea"
|
||||||
@@ -756,7 +756,7 @@
|
|||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
// 获取文件名
|
// 获取文件名
|
||||||
const contentDisposition = response.headers.get('Content-Disposition');
|
const contentDisposition = response.headers.get('Content-Disposition');
|
||||||
let filename = `batch_${images.length}_images.docker.gz`;
|
let filename = `batch_${images.length}_images.tar`;
|
||||||
|
|
||||||
if (contentDisposition) {
|
if (contentDisposition) {
|
||||||
const matches = contentDisposition.match(/filename="(.+)"/);
|
const matches = contentDisposition.match(/filename="(.+)"/);
|
||||||
|
|||||||
Reference in New Issue
Block a user