342 lines
7.9 KiB
Go
342 lines
7.9 KiB
Go
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)
|
|
}
|