dotcloud/docker

View on GitHub
image/fs.go

Summary

Maintainability
A
0 mins
Test Coverage
package image // import "github.com/docker/docker/image"

import (
    "context"
    "fmt"
    "os"
    "path/filepath"
    "sync"

    "github.com/containerd/log"
    "github.com/docker/docker/pkg/ioutils"
    "github.com/opencontainers/go-digest"
    "github.com/pkg/errors"
)

// DigestWalkFunc is function called by StoreBackend.Walk
type DigestWalkFunc func(id digest.Digest) error

// StoreBackend provides interface for image.Store persistence
type StoreBackend interface {
    Walk(f DigestWalkFunc) error
    Get(id digest.Digest) ([]byte, error)
    Set(data []byte) (digest.Digest, error)
    Delete(id digest.Digest) error
    SetMetadata(id digest.Digest, key string, data []byte) error
    GetMetadata(id digest.Digest, key string) ([]byte, error)
    DeleteMetadata(id digest.Digest, key string) error
}

// fs implements StoreBackend using the filesystem.
type fs struct {
    sync.RWMutex
    root string
}

const (
    contentDirName  = "content"
    metadataDirName = "metadata"
)

// NewFSStoreBackend returns new filesystem based backend for image.Store
func NewFSStoreBackend(root string) (StoreBackend, error) {
    return newFSStore(root)
}

func newFSStore(root string) (*fs, error) {
    s := &fs{
        root: root,
    }
    if err := os.MkdirAll(filepath.Join(root, contentDirName, string(digest.Canonical)), 0o700); err != nil {
        return nil, errors.Wrap(err, "failed to create storage backend")
    }
    if err := os.MkdirAll(filepath.Join(root, metadataDirName, string(digest.Canonical)), 0o700); err != nil {
        return nil, errors.Wrap(err, "failed to create storage backend")
    }
    return s, nil
}

func (s *fs) contentFile(dgst digest.Digest) string {
    return filepath.Join(s.root, contentDirName, string(dgst.Algorithm()), dgst.Encoded())
}

func (s *fs) metadataDir(dgst digest.Digest) string {
    return filepath.Join(s.root, metadataDirName, string(dgst.Algorithm()), dgst.Encoded())
}

// Walk calls the supplied callback for each image ID in the storage backend.
func (s *fs) Walk(f DigestWalkFunc) error {
    // Only Canonical digest (sha256) is currently supported
    s.RLock()
    dir, err := os.ReadDir(filepath.Join(s.root, contentDirName, string(digest.Canonical)))
    s.RUnlock()
    if err != nil {
        return err
    }
    for _, v := range dir {
        dgst := digest.NewDigestFromEncoded(digest.Canonical, v.Name())
        if err := dgst.Validate(); err != nil {
            log.G(context.TODO()).Debugf("skipping invalid digest %s: %s", dgst, err)
            continue
        }
        if err := f(dgst); err != nil {
            return err
        }
    }
    return nil
}

// Get returns the content stored under a given digest.
func (s *fs) Get(dgst digest.Digest) ([]byte, error) {
    s.RLock()
    defer s.RUnlock()

    return s.get(dgst)
}

func (s *fs) get(dgst digest.Digest) ([]byte, error) {
    content, err := os.ReadFile(s.contentFile(dgst))
    if err != nil {
        return nil, errors.Wrapf(err, "failed to get digest %s", dgst)
    }

    // todo: maybe optional
    if digest.FromBytes(content) != dgst {
        return nil, fmt.Errorf("failed to verify: %v", dgst)
    }

    return content, nil
}

// Set stores content by checksum.
func (s *fs) Set(data []byte) (digest.Digest, error) {
    s.Lock()
    defer s.Unlock()

    if len(data) == 0 {
        return "", fmt.Errorf("invalid empty data")
    }

    dgst := digest.FromBytes(data)
    if err := ioutils.AtomicWriteFile(s.contentFile(dgst), data, 0o600); err != nil {
        return "", errors.Wrap(err, "failed to write digest data")
    }

    return dgst, nil
}

// Delete removes content and metadata files associated with the digest.
func (s *fs) Delete(dgst digest.Digest) error {
    s.Lock()
    defer s.Unlock()

    if err := os.RemoveAll(s.metadataDir(dgst)); err != nil {
        return err
    }
    return os.Remove(s.contentFile(dgst))
}

// SetMetadata sets metadata for a given ID. It fails if there's no base file.
func (s *fs) SetMetadata(dgst digest.Digest, key string, data []byte) error {
    s.Lock()
    defer s.Unlock()
    if _, err := s.get(dgst); err != nil {
        return err
    }

    baseDir := filepath.Join(s.metadataDir(dgst))
    if err := os.MkdirAll(baseDir, 0o700); err != nil {
        return err
    }
    return ioutils.AtomicWriteFile(filepath.Join(s.metadataDir(dgst), key), data, 0o600)
}

// GetMetadata returns metadata for a given digest.
func (s *fs) GetMetadata(dgst digest.Digest, key string) ([]byte, error) {
    s.RLock()
    defer s.RUnlock()

    if _, err := s.get(dgst); err != nil {
        return nil, err
    }
    bytes, err := os.ReadFile(filepath.Join(s.metadataDir(dgst), key))
    if err != nil {
        return nil, errors.Wrap(err, "failed to read metadata")
    }
    return bytes, nil
}

// DeleteMetadata removes the metadata associated with a digest.
func (s *fs) DeleteMetadata(dgst digest.Digest, key string) error {
    s.Lock()
    defer s.Unlock()

    return os.RemoveAll(filepath.Join(s.metadataDir(dgst), key))
}