Files
reg/cmd/image_push.go
2026-11-05 10:18:35 +03:00

123 lines
3.9 KiB
Go

package cmd
import (
"context"
"flag"
"io"
"os"
reg "reg/internal/reg"
)
// RunPush реализует команду `reg push <local-path> <image>:<tag> [options]`.
//
// Опции:
// -mount-from <src> кросс-репозиторное монтирование blob'ов из <src>
// (при наличии — сначала пробуем mount, потом загрузку)
// -chunk-size <bytes> размер чанка для chunked-загрузки (0 = монолитная)
func RunPush(ctx context.Context, client *reg.RegistryClient, args []string) int {
if len(args) < 2 {
reg.PrintError(MsgPushRequiresTwoArgs.String())
return 1
}
localPath := args[0]
remoteArg := args[1]
cmdFlags := flag.NewFlagSet("push", flag.ContinueOnError)
cmdFlags.SetOutput(io.Discard)
mountFromFlag := cmdFlags.String("mount-from", "", "source repository for cross-repo blob mount")
chunkSizeFlag := cmdFlags.Int64("chunk-size", 0, "chunk size in bytes for chunked upload (0 = monolithic)")
cmdFlags.Usage = func() {}
if code, ok := parseFlags(cmdFlags, args[2:]); !ok {
return code
}
if *chunkSizeFlag < 0 {
reg.PrintError(MsgChunkSizeNonNeg.String())
return 1
}
remoteRef, err := reg.ParseImageRef(remoteArg)
if err != nil {
reg.PrintError(err.Error())
return 1
}
if remoteRef.Tag == "" {
reg.PrintError(MsgPushTargetNoTag.String())
return 1
}
layout, cleanup, err := reg.LoadOCILayout(localPath)
if err != nil {
reg.PrintError(MsgLoadLayoutError.F(err))
return 1
}
defer cleanup()
type blob struct {
digest string
size int64
path string
}
blobs := []blob{
{layout.ConfigDigest, layout.ConfigSize, layout.BlobPath(layout.ConfigDigest)},
}
for _, layer := range layout.Layers {
blobs = append(blobs, blob{layer.Digest, layer.Size, layout.BlobPath(layer.Digest)})
}
for _, b := range blobs {
// 1. Проверяем, существует ли blob.
exists, err := client.BlobExists(ctx, remoteRef.Repository, b.digest)
if err != nil {
reg.PrintError(MsgBlobCheckError.F(b.digest, err))
return 1
}
if exists {
continue
}
// 2. Пытаемся смонтировать из другого репозитория (если задан -mount-from).
if *mountFromFlag != "" {
mounted, err := client.MountBlob(ctx, remoteRef.Repository, b.digest, *mountFromFlag)
if err == nil && mounted {
continue
}
// при ошибке или 202 Accepted — обычная загрузка
}
// 3. Загружаем: монолитно или чанками.
f, err := os.Open(b.path)
if err != nil {
reg.PrintError(MsgBlobOpenError.F(b.digest, err))
return 1
}
if *chunkSizeFlag > 0 {
err = client.UploadBlobChunked(ctx, remoteRef.Repository, b.digest, b.size, f, *chunkSizeFlag)
} else {
err = client.UploadBlob(ctx, remoteRef.Repository, b.digest, b.size, f)
}
f.Close()
if err != nil {
reg.PrintError(MsgBlobUploadError.F(b.digest, err))
return 1
}
}
// 4. Загружаем манифест.
if err := client.PutManifest(ctx, remoteRef.Repository, remoteRef.Tag, layout.ManifestBody, layout.ManifestMediaType); err != nil {
reg.PrintError(MsgManifestUploadError.F(err))
return 1
}
result := map[string]interface{}{
"repository": remoteRef.Repository,
"tag": remoteRef.Tag,
"digest": layout.ManifestDigest,
"pushed": true,
}
reg.PrintJSON(result)
return 0
}