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:
341
pkg/store/store.go
Normal file
341
pkg/store/store.go
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user