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)) }