package docker import "encoding/json" import "errors" import "io/fs" import "os" import "path/filepath" import "strings" type TagInfo struct { Tag string `json:"tag"` Digest string `json:"digest"` Size int64 `json:"size"` MediaType string `json:"media_type"` } type ManifestDetail struct { Reference string `json:"reference"` Digest string `json:"digest"` MediaType string `json:"media_type"` Size int64 `json:"size"` Config ImageConfig `json:"config"` Layers []ociDescriptor `json:"layers"` } type ImageConfig struct { Created string `json:"created"` Architecture string `json:"architecture"` OS string `json:"os"` } type manifestPayload struct { SchemaVersion int `json:"schemaVersion"` MediaType string `json:"mediaType"` Config ociDescriptor `json:"config"` Layers []ociDescriptor `json:"layers"` } func ListTags(repoPath string) ([]TagInfo, error) { var tags []string var err error var list []TagInfo var i int var tag string var desc ociDescriptor tags, err = listTags(repoPath) if err != nil { return nil, err } list = make([]TagInfo, 0, len(tags)) for i = 0; i < len(tags); i++ { tag = tags[i] desc, err = resolveTag(repoPath, tag) if err != nil { continue } list = append(list, TagInfo{ Tag: tag, Digest: desc.Digest, Size: desc.Size, MediaType: desc.MediaType, }) } return list, nil } func DeleteTag(repoPath string, tag string) error { var idx ociIndex var err error var updated []ociDescriptor var i int var desc ociDescriptor var keep bool idx, err = loadIndex(repoPath) if err != nil { return err } updated = make([]ociDescriptor, 0, len(idx.Manifests)) for i = 0; i < len(idx.Manifests); i++ { desc = idx.Manifests[i] keep = true if desc.Annotations != nil && desc.Annotations[ociTagAnnotation] == tag { keep = false } if keep { updated = append(updated, desc) } } idx.Manifests = updated return saveIndex(repoPath, idx) } func DeleteImage(repoPath string, image string) error { var imagePath string imagePath = ImagePath(repoPath, image) return os.RemoveAll(imagePath) } func RenameTag(repoPath string, from string, to string) error { var idx ociIndex var err error var i int var desc ociDescriptor var hasFrom bool var hasTo bool if from == "" || to == "" { return errors.New("tag required") } if from == to { return errors.New("tag unchanged") } idx, err = loadIndex(repoPath) if err != nil { return err } for i = 0; i < len(idx.Manifests); i++ { desc = idx.Manifests[i] if desc.Annotations == nil { continue } if desc.Annotations[ociTagAnnotation] == to { hasTo = true } } if hasTo { return errors.New("tag already exists") } for i = 0; i < len(idx.Manifests); i++ { desc = idx.Manifests[i] if desc.Annotations == nil { continue } if desc.Annotations[ociTagAnnotation] == from { desc.Annotations[ociTagAnnotation] = to idx.Manifests[i] = desc hasFrom = true break } } if !hasFrom { return ErrNotFound } return saveIndex(repoPath, idx) } func RenameImage(repoPath string, from string, to string) error { var srcPath string var destPath string var err error var info os.FileInfo var dir string if from == to { return errors.New("image unchanged") } if IsReservedImagePath(to) { return errors.New("invalid image name") } srcPath = ImagePath(repoPath, from) destPath = ImagePath(repoPath, to) info, err = os.Stat(srcPath) if err != nil { if os.IsNotExist(err) { return ErrNotFound } return err } if info == nil || !info.IsDir() { return errors.New("source is not a directory") } _, err = os.Stat(destPath) if err == nil { return errors.New("target already exists") } if err != nil && !os.IsNotExist(err) { return err } dir = filepath.Dir(destPath) err = os.MkdirAll(dir, 0o755) if err != nil { return err } return os.Rename(srcPath, destPath) } func ListImages(repoPath string) ([]string, error) { var images []string var err error var seen map[string]struct{} var root string var ok bool var dummy struct{} seen = map[string]struct{}{} root = filepath.Clean(repoPath) err = filepath.WalkDir(root, func(path string, entry fs.DirEntry, walkErr error) error { var base string var rel string var dir string if walkErr != nil { return walkErr } if entry.IsDir() { base = entry.Name() if base == "blobs" || base == "uploads" { return filepath.SkipDir } return nil } if entry.Name() != "oci-layout" { return nil } dir = filepath.Dir(path) rel, err = filepath.Rel(root, dir) if err != nil { return err } if rel == "." { rel = "" } rel = filepath.ToSlash(rel) if rel == ".root" { rel = "" } dummy, ok = seen[rel] _ = dummy if !ok { seen[rel] = struct{}{} images = append(images, rel) } return nil }) if err != nil && !os.IsNotExist(err) { return nil, err } return images, nil } func ImagePath(repoPath string, image string) string { var cleaned string var normalized string cleaned = strings.Trim(image, "/") if cleaned == "" { return filepath.Join(repoPath, ".root") } normalized = strings.ReplaceAll(cleaned, "\\", "/") if normalized == ".root" { return filepath.Join(repoPath, ".root") } return filepath.Join(repoPath, filepath.FromSlash(cleaned)) } func GetManifestDetail(repoPath string, reference string) (ManifestDetail, error) { var detail ManifestDetail var desc ociDescriptor var err error var data []byte var payload manifestPayload var configData []byte var config ImageConfig desc, err = resolveManifest(repoPath, reference) if err != nil { return detail, err } data, err = ReadBlob(repoPath, desc.Digest) if err != nil { return detail, err } err = json.Unmarshal(data, &payload) if err != nil { return detail, err } if payload.MediaType != "" { desc.MediaType = payload.MediaType } configData, err = ReadBlob(repoPath, payload.Config.Digest) if err == nil { _ = json.Unmarshal(configData, &config) } detail = ManifestDetail{ Reference: reference, Digest: desc.Digest, MediaType: desc.MediaType, Size: int64(len(data)), Config: config, Layers: payload.Layers, } return detail, nil }