added artifacts
This commit is contained in:
@@ -0,0 +1,374 @@
|
||||
// Package oci implements an OCI Distribution Specification v1.1 registry.
|
||||
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md
|
||||
//
|
||||
// Storage layout under ociRoot:
|
||||
//
|
||||
// blobs/sha256/<hex64> — content-addressable layer/config blobs
|
||||
// uploads/<uuid> — temporary files for in-progress chunked uploads
|
||||
package oci
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Registry manages the on-disk blob store and is used by the HTTP handler.
|
||||
type Registry struct {
|
||||
root string // absolute path to the OCI storage root
|
||||
}
|
||||
|
||||
// New creates a Registry rooted at ociRoot, creating the directory tree if needed.
|
||||
func New(ociRoot string) (*Registry, error) {
|
||||
for _, sub := range []string{"blobs/sha256", "uploads"} {
|
||||
if err := os.MkdirAll(filepath.Join(ociRoot, sub), 0755); err != nil {
|
||||
return nil, fmt.Errorf("oci: init storage %s: %w", sub, err)
|
||||
}
|
||||
}
|
||||
return &Registry{root: ociRoot}, nil
|
||||
}
|
||||
|
||||
// Root returns the storage root path.
|
||||
func (r *Registry) Root() string { return r.root }
|
||||
|
||||
// ─── Blob paths ───────────────────────────────────────────────────────────────
|
||||
|
||||
// BlobPath returns the filesystem path for a blob identified by its digest.
|
||||
// digest must be in the form "sha256:<hex>".
|
||||
func (r *Registry) BlobPath(digest string) (string, error) {
|
||||
hex, err := digestHex(digest)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.Join(r.root, "blobs", "sha256", hex), nil
|
||||
}
|
||||
|
||||
// UploadPath returns the filesystem path for a chunked upload session.
|
||||
func (r *Registry) UploadPath(uploadID string) string {
|
||||
return filepath.Join(r.root, "uploads", sanitiseID(uploadID))
|
||||
}
|
||||
|
||||
// BlobExists reports whether a blob with the given digest exists on disk.
|
||||
func (r *Registry) BlobExists(digest string) bool {
|
||||
p, err := r.BlobPath(digest)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
_, err = os.Stat(p)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// BlobSize returns the size of the blob in bytes, or -1 if it doesn't exist.
|
||||
func (r *Registry) BlobSize(digest string) int64 {
|
||||
p, err := r.BlobPath(digest)
|
||||
if err != nil {
|
||||
return -1
|
||||
}
|
||||
info, err := os.Stat(p)
|
||||
if err != nil {
|
||||
return -1
|
||||
}
|
||||
return info.Size()
|
||||
}
|
||||
|
||||
// ReadBlob opens a blob for streaming. Caller must close the returned file.
|
||||
func (r *Registry) ReadBlob(digest string) (*os.File, error) {
|
||||
p, err := r.BlobPath(digest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return os.Open(p)
|
||||
}
|
||||
|
||||
// WriteBlob writes src into the blob store, verifies the digest, and returns
|
||||
// the computed digest string ("sha256:<hex>") and size.
|
||||
// If a blob with the same digest already exists it is not overwritten.
|
||||
func (r *Registry) WriteBlob(src io.Reader) (digest string, size int64, err error) {
|
||||
tmp, err := os.CreateTemp(filepath.Join(r.root, "uploads"), "blob-*")
|
||||
if err != nil {
|
||||
return "", 0, fmt.Errorf("oci: create tmp blob: %w", err)
|
||||
}
|
||||
tmpPath := tmp.Name()
|
||||
defer func() {
|
||||
tmp.Close()
|
||||
if err != nil {
|
||||
os.Remove(tmpPath)
|
||||
}
|
||||
}()
|
||||
|
||||
h := sha256.New()
|
||||
mw := io.MultiWriter(tmp, h)
|
||||
size, err = io.Copy(mw, src)
|
||||
if err != nil {
|
||||
return "", 0, fmt.Errorf("oci: write blob: %w", err)
|
||||
}
|
||||
tmp.Close()
|
||||
|
||||
digest = "sha256:" + hex.EncodeToString(h.Sum(nil))
|
||||
dest, err2 := r.BlobPath(digest)
|
||||
if err2 != nil {
|
||||
os.Remove(tmpPath)
|
||||
return "", 0, err2
|
||||
}
|
||||
|
||||
if _, statErr := os.Stat(dest); statErr == nil {
|
||||
// Already exists — deduplication.
|
||||
os.Remove(tmpPath)
|
||||
return digest, size, nil
|
||||
}
|
||||
|
||||
if err = os.Rename(tmpPath, dest); err != nil {
|
||||
return "", 0, fmt.Errorf("oci: commit blob: %w", err)
|
||||
}
|
||||
return digest, size, nil
|
||||
}
|
||||
|
||||
// FinishUpload finalises a chunked upload: reads the temp file, verifies
|
||||
// clientDigest (if non-empty), atomically moves it to the blob store, and
|
||||
// returns the canonical digest and size.
|
||||
func (r *Registry) FinishUpload(uploadID, clientDigest string) (digest string, size int64, err error) {
|
||||
src := r.UploadPath(uploadID)
|
||||
f, err := os.Open(src)
|
||||
if err != nil {
|
||||
return "", 0, fmt.Errorf("oci: open upload: %w", err)
|
||||
}
|
||||
|
||||
h := sha256.New()
|
||||
size, err = io.Copy(h, f)
|
||||
f.Close()
|
||||
if err != nil {
|
||||
return "", 0, fmt.Errorf("oci: hash upload: %w", err)
|
||||
}
|
||||
|
||||
digest = "sha256:" + hex.EncodeToString(h.Sum(nil))
|
||||
|
||||
if clientDigest != "" && clientDigest != digest {
|
||||
os.Remove(src)
|
||||
return "", 0, &DigestMismatch{Expected: clientDigest, Actual: digest}
|
||||
}
|
||||
|
||||
dest, err := r.BlobPath(digest)
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
|
||||
if _, statErr := os.Stat(dest); statErr == nil {
|
||||
// Blob already exists — dedup.
|
||||
os.Remove(src)
|
||||
return digest, size, nil
|
||||
}
|
||||
|
||||
if err = os.Rename(src, dest); err != nil {
|
||||
return "", 0, fmt.Errorf("oci: commit upload: %w", err)
|
||||
}
|
||||
return digest, size, nil
|
||||
}
|
||||
|
||||
// AppendUpload appends src to an existing upload session file and returns the
|
||||
// new total offset.
|
||||
func (r *Registry) AppendUpload(uploadID string, src io.Reader) (newOffset int64, err error) {
|
||||
path := r.UploadPath(uploadID)
|
||||
f, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("oci: open upload for append: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
n, err := io.Copy(f, src)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("oci: append upload: %w", err)
|
||||
}
|
||||
|
||||
info, err := f.Stat()
|
||||
if err != nil {
|
||||
return n, nil
|
||||
}
|
||||
return info.Size(), nil
|
||||
}
|
||||
|
||||
// UploadOffset returns the number of bytes written to an upload session so far.
|
||||
func (r *Registry) UploadOffset(uploadID string) int64 {
|
||||
info, err := os.Stat(r.UploadPath(uploadID))
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return info.Size()
|
||||
}
|
||||
|
||||
// CancelUpload removes the temporary upload file.
|
||||
func (r *Registry) CancelUpload(uploadID string) {
|
||||
os.Remove(r.UploadPath(uploadID))
|
||||
}
|
||||
|
||||
// DeleteBlob removes a blob from disk.
|
||||
func (r *Registry) DeleteBlob(digest string) error {
|
||||
p, err := r.BlobPath(digest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Remove(p)
|
||||
}
|
||||
|
||||
// ─── Manifest helpers ─────────────────────────────────────────────────────────
|
||||
|
||||
// ManifestDescriptor extracts the digest and size from a raw manifest body.
|
||||
func ManifestDescriptor(body []byte) (digest string, size int64) {
|
||||
h := sha256.Sum256(body)
|
||||
return "sha256:" + hex.EncodeToString(h[:]), int64(len(body))
|
||||
}
|
||||
|
||||
// IsDigestRef returns true when ref looks like a digest ("sha256:<hex>").
|
||||
func IsDigestRef(ref string) bool {
|
||||
return strings.HasPrefix(ref, "sha256:")
|
||||
}
|
||||
|
||||
// ─── OCI error types ─────────────────────────────────────────────────────────
|
||||
|
||||
// ErrorCode is an OCI Distribution API error code.
|
||||
type ErrorCode string
|
||||
|
||||
const (
|
||||
ErrBlobUnknown ErrorCode = "BLOB_UNKNOWN"
|
||||
ErrBlobUploadInvalid ErrorCode = "BLOB_UPLOAD_INVALID"
|
||||
ErrBlobUploadUnknown ErrorCode = "BLOB_UPLOAD_UNKNOWN"
|
||||
ErrDigestInvalid ErrorCode = "DIGEST_INVALID"
|
||||
ErrManifestBlobUnknown ErrorCode = "MANIFEST_BLOB_UNKNOWN"
|
||||
ErrManifestInvalid ErrorCode = "MANIFEST_INVALID"
|
||||
ErrManifestUnknown ErrorCode = "MANIFEST_UNKNOWN"
|
||||
ErrNameInvalid ErrorCode = "NAME_INVALID"
|
||||
ErrNameUnknown ErrorCode = "NAME_UNKNOWN"
|
||||
ErrTagInvalid ErrorCode = "TAG_INVALID"
|
||||
ErrUnauthorized ErrorCode = "UNAUTHORIZED"
|
||||
ErrDenied ErrorCode = "DENIED"
|
||||
ErrUnsupported ErrorCode = "UNSUPPORTED"
|
||||
)
|
||||
|
||||
// APIError is a single OCI error entry.
|
||||
type APIError struct {
|
||||
Code ErrorCode `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Detail interface{} `json:"detail,omitempty"`
|
||||
}
|
||||
|
||||
// ErrorResponse is the top-level OCI error response body.
|
||||
type ErrorResponse struct {
|
||||
Errors []APIError `json:"errors"`
|
||||
}
|
||||
|
||||
// NewError builds an ErrorResponse JSON body.
|
||||
func NewError(code ErrorCode, msg string) []byte {
|
||||
b, _ := json.Marshal(ErrorResponse{Errors: []APIError{{Code: code, Message: msg}}})
|
||||
return b
|
||||
}
|
||||
|
||||
// DigestMismatch is returned when a client-provided digest doesn't match the computed one.
|
||||
type DigestMismatch struct {
|
||||
Expected string
|
||||
Actual string
|
||||
}
|
||||
|
||||
func (e *DigestMismatch) Error() string {
|
||||
return fmt.Sprintf("digest mismatch: expected %s, got %s", e.Expected, e.Actual)
|
||||
}
|
||||
|
||||
// ─── path helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
// digestHex validates a "sha256:<hex>" digest string and returns the hex part.
|
||||
func digestHex(digest string) (string, error) {
|
||||
if !strings.HasPrefix(digest, "sha256:") {
|
||||
return "", fmt.Errorf("oci: only sha256 digests are supported, got %q", digest)
|
||||
}
|
||||
h := strings.TrimPrefix(digest, "sha256:")
|
||||
if len(h) != 64 {
|
||||
return "", fmt.Errorf("oci: invalid sha256 digest length: %d", len(h))
|
||||
}
|
||||
return h, nil
|
||||
}
|
||||
|
||||
// sanitiseID strips any path separators from an upload ID.
|
||||
func sanitiseID(id string) string {
|
||||
return strings.NewReplacer("/", "", "\\", "", "..", "").Replace(id)
|
||||
}
|
||||
|
||||
// ParseOCIPath extracts the image name and the operation kind from a path
|
||||
// under /v2/. name may contain slashes (e.g. "alice/myapp").
|
||||
//
|
||||
// Returns: name, kind, ref where kind is one of:
|
||||
//
|
||||
// "tags" → ref = ""
|
||||
// "manifest" → ref = tag or digest
|
||||
// "blob" → ref = digest
|
||||
// "upload" → ref = uploadID (empty for new upload)
|
||||
// "" → unrecognised path
|
||||
func ParseOCIPath(rawPath string) (name, kind, ref string) {
|
||||
// Strip leading /v2/
|
||||
p := strings.TrimPrefix(rawPath, "/v2/")
|
||||
if p == "" || p == "/" {
|
||||
return "", "", ""
|
||||
}
|
||||
|
||||
// Try known suffixes from most to least specific.
|
||||
type suffix struct {
|
||||
needle string
|
||||
kind string
|
||||
}
|
||||
suffixes := []suffix{
|
||||
{"/blobs/uploads/", "upload"},
|
||||
{"/blobs/sha256:", "blob"},
|
||||
{"/blobs/", "blob"},
|
||||
{"/manifests/", "manifest"},
|
||||
{"/tags/list", "tags"},
|
||||
}
|
||||
|
||||
for _, s := range suffixes {
|
||||
idx := strings.Index(p, s.needle)
|
||||
if idx < 0 {
|
||||
continue
|
||||
}
|
||||
name = p[:idx]
|
||||
rest := p[idx+len(s.needle):]
|
||||
kind = s.kind
|
||||
switch s.kind {
|
||||
case "blob":
|
||||
// ref is digest: re-attach the sha256: prefix if needed
|
||||
if strings.HasSuffix(s.needle, ":") {
|
||||
ref = "sha256:" + rest
|
||||
} else {
|
||||
ref = rest
|
||||
}
|
||||
case "upload":
|
||||
ref = rest // upload UUID or empty for new session
|
||||
default:
|
||||
ref = rest
|
||||
}
|
||||
return name, kind, ref
|
||||
}
|
||||
return "", "", ""
|
||||
}
|
||||
|
||||
// ValidateName returns an error if the image name is empty or contains
|
||||
// invalid characters.
|
||||
func ValidateName(name string) error {
|
||||
if name == "" {
|
||||
return errors.New("empty image name")
|
||||
}
|
||||
for _, c := range name {
|
||||
if !isNameChar(c) {
|
||||
return fmt.Errorf("invalid character %q in image name", c)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func isNameChar(c rune) bool {
|
||||
return (c >= 'a' && c <= 'z') ||
|
||||
(c >= 'A' && c <= 'Z') ||
|
||||
(c >= '0' && c <= '9') ||
|
||||
c == '.' || c == '-' || c == '_' || c == '/'
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
package oci_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/forgeo/forgebucket/internal/domain/oci"
|
||||
)
|
||||
|
||||
func TestParseOCIPath(t *testing.T) {
|
||||
tests := []struct {
|
||||
path string
|
||||
wantName string
|
||||
wantKind string
|
||||
wantRef string
|
||||
}{
|
||||
{"/v2/", "", "", ""},
|
||||
{"/v2", "", "", ""},
|
||||
{"/v2/alice/myapp/tags/list", "alice/myapp", "tags", ""},
|
||||
{"/v2/alice/myapp/manifests/latest", "alice/myapp", "manifest", "latest"},
|
||||
{"/v2/alice/myapp/manifests/sha256:abc123", "alice/myapp", "manifest", "sha256:abc123"},
|
||||
{"/v2/alice/myapp/blobs/sha256:def456", "alice/myapp", "blob", "sha256:def456"},
|
||||
{"/v2/alice/myapp/blobs/uploads/", "alice/myapp", "upload", ""},
|
||||
{"/v2/alice/myapp/blobs/uploads/uuid123", "alice/myapp", "upload", "uuid123"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.path, func(t *testing.T) {
|
||||
name, kind, ref := oci.ParseOCIPath(tt.path)
|
||||
if name != tt.wantName {
|
||||
t.Errorf("name = %q, want %q", name, tt.wantName)
|
||||
}
|
||||
if kind != tt.wantKind {
|
||||
t.Errorf("kind = %q, want %q", kind, tt.wantKind)
|
||||
}
|
||||
if ref != tt.wantRef {
|
||||
t.Errorf("ref = %q, want %q", ref, tt.wantRef)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateName(t *testing.T) {
|
||||
if err := oci.ValidateName("alice/myapp"); err != nil {
|
||||
t.Errorf("valid name got error: %v", err)
|
||||
}
|
||||
if err := oci.ValidateName(""); err == nil {
|
||||
t.Error("empty name should error")
|
||||
}
|
||||
if err := oci.ValidateName("alice/my app"); err == nil {
|
||||
t.Error("name with spaces should error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBlobPath(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
reg, err := oci.New(dir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
p, err := reg.BlobPath("sha256:" + strings.Repeat("a", 64))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
expectedSuffix := filepath.Join("blobs", "sha256", strings.Repeat("a", 64))
|
||||
if !strings.HasSuffix(p, expectedSuffix) {
|
||||
t.Errorf("path %q does not end with %q", p, expectedSuffix)
|
||||
}
|
||||
|
||||
if _, err := reg.BlobPath("sha256:bad"); err == nil {
|
||||
t.Error("expected error for short hex")
|
||||
}
|
||||
if _, err := reg.BlobPath("md5:abc"); err == nil {
|
||||
t.Error("expected error for non-sha256 algorithm")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteAndReadBlob(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
reg, err := oci.New(dir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
content := []byte("hello oci blob")
|
||||
digest, size, err := reg.WriteBlob(bytes.NewReader(content))
|
||||
if err != nil {
|
||||
t.Fatalf("WriteBlob: %v", err)
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(digest, "sha256:") {
|
||||
t.Errorf("digest should start with sha256:, got %s", digest)
|
||||
}
|
||||
if size != int64(len(content)) {
|
||||
t.Errorf("size = %d, want %d", size, len(content))
|
||||
}
|
||||
|
||||
if !reg.BlobExists(digest) {
|
||||
t.Error("blob should exist after write")
|
||||
}
|
||||
|
||||
// Deduplication test: writing same content again should succeed without error.
|
||||
d2, s2, err := reg.WriteBlob(bytes.NewReader(content))
|
||||
if err != nil {
|
||||
t.Fatalf("WriteBlob duplicate: %v", err)
|
||||
}
|
||||
if d2 != digest {
|
||||
t.Errorf("digest mismatch: %s vs %s", d2, digest)
|
||||
}
|
||||
if s2 != size {
|
||||
t.Errorf("size mismatch: %d vs %d", s2, size)
|
||||
}
|
||||
|
||||
f, err := reg.ReadBlob(digest)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadBlob: %v", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
buf.ReadFrom(f)
|
||||
if buf.String() != string(content) {
|
||||
t.Errorf("content mismatch: got %s", buf.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestUploadSession(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
reg, _ := oci.New(dir)
|
||||
|
||||
uploadID := "test-upload-001"
|
||||
|
||||
// Append content in chunks.
|
||||
off, err := reg.AppendUpload(uploadID, strings.NewReader("chunk1"))
|
||||
if err != nil {
|
||||
t.Fatalf("AppendUpload: %v", err)
|
||||
}
|
||||
if off != 6 {
|
||||
t.Errorf("expected offset 6, got %d", off)
|
||||
}
|
||||
|
||||
off, err = reg.AppendUpload(uploadID, strings.NewReader("-chunk2"))
|
||||
if err != nil {
|
||||
t.Fatalf("AppendUpload second: %v", err)
|
||||
}
|
||||
if off != 13 {
|
||||
t.Errorf("expected offset 13 after chunk2, got %d", off)
|
||||
}
|
||||
|
||||
if reg.UploadOffset(uploadID) != 13 {
|
||||
t.Errorf("UploadOffset = %d, want 13", reg.UploadOffset(uploadID))
|
||||
}
|
||||
|
||||
// Finish upload with digest.
|
||||
digest, size, err := reg.FinishUpload(uploadID, "")
|
||||
if err != nil {
|
||||
t.Fatalf("FinishUpload: %v", err)
|
||||
}
|
||||
if !strings.HasPrefix(digest, "sha256:") {
|
||||
t.Errorf("expected sha256 digest, got %s", digest)
|
||||
}
|
||||
if size != 13 {
|
||||
t.Errorf("expected size 13, got %d", size)
|
||||
}
|
||||
|
||||
if !reg.BlobExists(digest) {
|
||||
t.Error("blob should exist after finish upload")
|
||||
}
|
||||
|
||||
// Verify content.
|
||||
f, _ := reg.ReadBlob(digest)
|
||||
buf := new(bytes.Buffer)
|
||||
buf.ReadFrom(f)
|
||||
f.Close()
|
||||
if buf.String() != "chunk1-chunk2" {
|
||||
t.Errorf("content = %q, want %q", buf.String(), "chunk1-chunk2")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFinishUploadDigestMismatch(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
reg, _ := oci.New(dir)
|
||||
|
||||
uploadID := "mismatch-upload"
|
||||
reg.AppendUpload(uploadID, strings.NewReader("some data"))
|
||||
|
||||
_, _, err := reg.FinishUpload(uploadID, "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
|
||||
if err == nil {
|
||||
t.Fatal("expected digest mismatch error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "digest mismatch") {
|
||||
t.Errorf("expected 'digest mismatch', got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestManifestDescriptor(t *testing.T) {
|
||||
body := []byte(`{"schemaVersion":2}`)
|
||||
digest, size := oci.ManifestDescriptor(body)
|
||||
if !strings.HasPrefix(digest, "sha256:") {
|
||||
t.Errorf("digest should be sha256, got %s", digest)
|
||||
}
|
||||
if size != int64(len(body)) {
|
||||
t.Errorf("size = %d, want %d", size, len(body))
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsDigestRef(t *testing.T) {
|
||||
if !oci.IsDigestRef("sha256:abc") {
|
||||
t.Error("sha256:abc should be a digest ref")
|
||||
}
|
||||
if oci.IsDigestRef("latest") {
|
||||
t.Error("latest should NOT be a digest ref")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteBlob(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
reg, _ := oci.New(dir)
|
||||
content := []byte("delete me")
|
||||
digest, _, _ := reg.WriteBlob(bytes.NewReader(content))
|
||||
|
||||
if !reg.BlobExists(digest) {
|
||||
t.Fatal("blob should exist after write")
|
||||
}
|
||||
|
||||
if err := reg.DeleteBlob(digest); err != nil {
|
||||
t.Fatalf("DeleteBlob: %v", err)
|
||||
}
|
||||
|
||||
if reg.BlobExists(digest) {
|
||||
t.Error("blob should not exist after delete")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewCreatesDirectories(t *testing.T) {
|
||||
dir := filepath.Join(t.TempDir(), "oci-storage")
|
||||
reg, err := oci.New(dir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for _, sub := range []string{"blobs/sha256", "uploads"} {
|
||||
p := filepath.Join(dir, sub)
|
||||
if _, err := os.Stat(p); os.IsNotExist(err) {
|
||||
t.Errorf("directory not created: %s", p)
|
||||
}
|
||||
}
|
||||
_ = reg
|
||||
}
|
||||
Reference in New Issue
Block a user