feat: complete OCI registry implementation with docker push/pull support

A lightweight OCI (Open Container Initiative) registry implementation written in Go.
This commit is contained in:
loveuer
2025-11-09 22:46:27 +08:00
commit 29088a6b54
45 changed files with 5629 additions and 0 deletions

341
pkg/store/store.go Normal file
View File

@@ -0,0 +1,341 @@
package store
import (
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"os"
"path/filepath"
"strings"
)
type Store interface {
CreatePartition(ctx context.Context, name string) error
// Blob ??
WriteBlob(ctx context.Context, digest string, r io.Reader) error
ReadBlob(ctx context.Context, digest string) (io.ReadCloser, error)
BlobExists(ctx context.Context, digest string) (bool, error)
GetBlobSize(ctx context.Context, digest string) (int64, error)
// Manifest ??
WriteManifest(ctx context.Context, digest string, content []byte) error
ReadManifest(ctx context.Context, digest string) ([]byte, error)
ManifestExists(ctx context.Context, digest string) (bool, error)
// Upload ??
CreateUpload(ctx context.Context, uuid string) (io.WriteCloser, error)
AppendUpload(ctx context.Context, uuid string, r io.Reader) (int64, error)
GetUploadSize(ctx context.Context, uuid string) (int64, error)
FinalizeUpload(ctx context.Context, uuid string, digest string) error
DeleteUpload(ctx context.Context, uuid string) error
}
type fileStore struct {
baseDir string
}
var (
Default Store
)
func Init(ctx context.Context, dataDir string) error {
Default = &fileStore{
baseDir: dataDir,
}
return nil
}
func (s *fileStore) CreatePartition(ctx context.Context, name string) error {
dirs := []string{
filepath.Join(s.baseDir, name, "blobs"),
filepath.Join(s.baseDir, name, "manifests"),
filepath.Join(s.baseDir, name, "uploads"),
}
for _, dir := range dirs {
if err := os.MkdirAll(dir, 0755); err != nil {
return fmt.Errorf("failed to create directory %s: %w", dir, err)
}
}
return nil
}
// blobPath ?? digest ?? blob ????
// ??: blobs/sha256/abc/def.../digest
func (s *fileStore) blobPath(digest string) (string, error) {
// ?? digest???: sha256:abc123...
parts := strings.SplitN(digest, ":", 2)
if len(parts) != 2 {
return "", fmt.Errorf("invalid digest format: %s", digest)
}
algo := parts[0]
hash := parts[1]
if algo != "sha256" {
return "", fmt.Errorf("unsupported digest algorithm: %s", algo)
}
// ??? 2 ????????????? 2 ?????????
if len(hash) < 4 {
return "", fmt.Errorf("invalid hash length: %s", hash)
}
path := filepath.Join(s.baseDir, "registry", "blobs", algo, hash[:2], hash[2:4], hash)
return path, nil
}
func (s *fileStore) WriteBlob(ctx context.Context, digest string, r io.Reader) error {
path, err := s.blobPath(digest)
if err != nil {
return err
}
// ???????
if _, err := os.Stat(path); err == nil {
return nil // ????????
}
// ????
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
return fmt.Errorf("failed to create blob directory: %w", err)
}
// ????
f, err := os.Create(path)
if err != nil {
return fmt.Errorf("failed to create blob file: %w", err)
}
defer f.Close()
// ???? digest ??
hasher := sha256.New()
tee := io.TeeReader(r, hasher)
if _, err := io.Copy(f, tee); err != nil {
os.Remove(path)
return fmt.Errorf("failed to write blob: %w", err)
}
// ?? digest
calculated := "sha256:" + hex.EncodeToString(hasher.Sum(nil))
if calculated != digest {
os.Remove(path)
return fmt.Errorf("digest mismatch: expected %s, got %s", digest, calculated)
}
return nil
}
func (s *fileStore) ReadBlob(ctx context.Context, digest string) (io.ReadCloser, error) {
path, err := s.blobPath(digest)
if err != nil {
return nil, err
}
f, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("blob not found: %w", err)
}
return f, nil
}
func (s *fileStore) BlobExists(ctx context.Context, digest string) (bool, error) {
path, err := s.blobPath(digest)
if err != nil {
return false, err
}
_, err = os.Stat(path)
if err == nil {
return true, nil
}
if os.IsNotExist(err) {
return false, nil
}
return false, err
}
func (s *fileStore) GetBlobSize(ctx context.Context, digest string) (int64, error) {
path, err := s.blobPath(digest)
if err != nil {
return 0, err
}
info, err := os.Stat(path)
if err != nil {
return 0, err
}
return info.Size(), nil
}
// manifestPath ?? digest ?? manifest ????
func (s *fileStore) manifestPath(digest string) (string, error) {
parts := strings.SplitN(digest, ":", 2)
if len(parts) != 2 {
return "", fmt.Errorf("invalid digest format: %s", digest)
}
algo := parts[0]
hash := parts[1]
if algo != "sha256" {
return "", fmt.Errorf("unsupported digest algorithm: %s", algo)
}
path := filepath.Join(s.baseDir, "registry", "manifests", algo, hash[:2], hash[2:4], hash)
return path, nil
}
func (s *fileStore) WriteManifest(ctx context.Context, digest string, content []byte) error {
path, err := s.manifestPath(digest)
if err != nil {
return err
}
// ????
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
return fmt.Errorf("failed to create manifest directory: %w", err)
}
// ?? digest
hasher := sha256.New()
hasher.Write(content)
calculated := "sha256:" + hex.EncodeToString(hasher.Sum(nil))
if calculated != digest {
return fmt.Errorf("digest mismatch: expected %s, got %s", digest, calculated)
}
// ????
if err := os.WriteFile(path, content, 0644); err != nil {
return fmt.Errorf("failed to write manifest: %w", err)
}
return nil
}
func (s *fileStore) ReadManifest(ctx context.Context, digest string) ([]byte, error) {
path, err := s.manifestPath(digest)
if err != nil {
return nil, err
}
content, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("manifest not found: %w", err)
}
return content, nil
}
func (s *fileStore) ManifestExists(ctx context.Context, digest string) (bool, error) {
path, err := s.manifestPath(digest)
if err != nil {
return false, err
}
_, err = os.Stat(path)
if err == nil {
return true, nil
}
if os.IsNotExist(err) {
return false, nil
}
return false, err
}
// uploadPath ??????????
func (s *fileStore) uploadPath(uuid string) string {
return filepath.Join(s.baseDir, "registry", "uploads", uuid)
}
func (s *fileStore) CreateUpload(ctx context.Context, uuid string) (io.WriteCloser, error) {
path := s.uploadPath(uuid)
// ????
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
return nil, fmt.Errorf("failed to create upload directory: %w", err)
}
f, err := os.Create(path)
if err != nil {
return nil, fmt.Errorf("failed to create upload file: %w", err)
}
return f, nil
}
func (s *fileStore) AppendUpload(ctx context.Context, uuid string, r io.Reader) (int64, error) {
path := s.uploadPath(uuid)
f, err := os.OpenFile(path, os.O_APPEND|os.O_WRONLY, 0644)
if err != nil {
return 0, fmt.Errorf("failed to open upload file: %w", err)
}
defer f.Close()
n, err := io.Copy(f, r)
if err != nil {
return 0, fmt.Errorf("failed to write to upload: %w", err)
}
return n, nil
}
func (s *fileStore) GetUploadSize(ctx context.Context, uuid string) (int64, error) {
path := s.uploadPath(uuid)
info, err := os.Stat(path)
if err != nil {
return 0, err
}
return info.Size(), nil
}
func (s *fileStore) FinalizeUpload(ctx context.Context, uuid string, digest string) error {
uploadPath := s.uploadPath(uuid)
blobPath, err := s.blobPath(digest)
if err != nil {
return err
}
// ???? blob ????????????
if _, err := os.Stat(blobPath); err == nil {
os.Remove(uploadPath)
return nil
}
// ??????
if err := os.MkdirAll(filepath.Dir(blobPath), 0755); err != nil {
return fmt.Errorf("failed to create blob directory: %w", err)
}
// ?? digest
f, err := os.Open(uploadPath)
if err != nil {
return fmt.Errorf("failed to open upload file: %w", err)
}
defer f.Close()
hasher := sha256.New()
if _, err := io.Copy(hasher, f); err != nil {
return fmt.Errorf("failed to calculate digest: %w", err)
}
calculated := "sha256:" + hex.EncodeToString(hasher.Sum(nil))
if calculated != digest {
return fmt.Errorf("digest mismatch: expected %s, got %s", digest, calculated)
}
// ????
if err := os.Rename(uploadPath, blobPath); err != nil {
return fmt.Errorf("failed to finalize upload: %w", err)
}
return nil
}
func (s *fileStore) DeleteUpload(ctx context.Context, uuid string) error {
path := s.uploadPath(uuid)
return os.Remove(path)
}