working commit
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
Copyright The ORAS Authors.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package cas
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"sync"
|
||||
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
contentpkg "oras.land/oras-go/v2/content"
|
||||
"oras.land/oras-go/v2/errdef"
|
||||
"oras.land/oras-go/v2/internal/descriptor"
|
||||
)
|
||||
|
||||
// Memory is a memory based CAS.
|
||||
type Memory struct {
|
||||
content sync.Map // map[descriptor.Descriptor][]byte
|
||||
}
|
||||
|
||||
// NewMemory creates a new Memory CAS.
|
||||
func NewMemory() *Memory {
|
||||
return &Memory{}
|
||||
}
|
||||
|
||||
// Fetch fetches the content identified by the descriptor.
|
||||
func (m *Memory) Fetch(_ context.Context, target ocispec.Descriptor) (io.ReadCloser, error) {
|
||||
key := descriptor.FromOCI(target)
|
||||
content, exists := m.content.Load(key)
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("%s: %s: %w", key.Digest, key.MediaType, errdef.ErrNotFound)
|
||||
}
|
||||
return io.NopCloser(bytes.NewReader(content.([]byte))), nil
|
||||
}
|
||||
|
||||
// Push pushes the content, matching the expected descriptor.
|
||||
func (m *Memory) Push(_ context.Context, expected ocispec.Descriptor, content io.Reader) error {
|
||||
key := descriptor.FromOCI(expected)
|
||||
|
||||
// check if the content exists in advance to avoid reading from the content.
|
||||
if _, exists := m.content.Load(key); exists {
|
||||
return fmt.Errorf("%s: %s: %w", key.Digest, key.MediaType, errdef.ErrAlreadyExists)
|
||||
}
|
||||
|
||||
// read and try to store the content.
|
||||
value, err := contentpkg.ReadAll(content, expected)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, exists := m.content.LoadOrStore(key, value); exists {
|
||||
return fmt.Errorf("%s: %s: %w", key.Digest, key.MediaType, errdef.ErrAlreadyExists)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Exists returns true if the described content exists.
|
||||
func (m *Memory) Exists(_ context.Context, target ocispec.Descriptor) (bool, error) {
|
||||
key := descriptor.FromOCI(target)
|
||||
_, exists := m.content.Load(key)
|
||||
return exists, nil
|
||||
}
|
||||
|
||||
// Map dumps the memory into a built-in map structure.
|
||||
// Like other operations, calling Map() is go-routine safe. However, it does not
|
||||
// necessarily correspond to any consistent snapshot of the storage contents.
|
||||
func (m *Memory) Map() map[descriptor.Descriptor][]byte {
|
||||
res := make(map[descriptor.Descriptor][]byte)
|
||||
m.content.Range(func(key, value interface{}) bool {
|
||||
res[key.(descriptor.Descriptor)] = value.([]byte)
|
||||
return true
|
||||
})
|
||||
return res
|
||||
}
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
/*
|
||||
Copyright The ORAS Authors.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package cas
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"sync"
|
||||
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"oras.land/oras-go/v2/content"
|
||||
"oras.land/oras-go/v2/internal/ioutil"
|
||||
)
|
||||
|
||||
// Proxy is a caching proxy for the storage.
|
||||
// The first fetch call of a described content will read from the remote and
|
||||
// cache the fetched content.
|
||||
// The subsequent fetch call will read from the local cache.
|
||||
type Proxy struct {
|
||||
content.ReadOnlyStorage
|
||||
Cache content.Storage
|
||||
StopCaching bool
|
||||
}
|
||||
|
||||
// NewProxy creates a proxy for the `base` storage, using the `cache` storage as
|
||||
// the cache.
|
||||
func NewProxy(base content.ReadOnlyStorage, cache content.Storage) *Proxy {
|
||||
return &Proxy{
|
||||
ReadOnlyStorage: base,
|
||||
Cache: cache,
|
||||
}
|
||||
}
|
||||
|
||||
// NewProxyWithLimit creates a proxy for the `base` storage, using the `cache`
|
||||
// storage with a push size limit as the cache.
|
||||
func NewProxyWithLimit(base content.ReadOnlyStorage, cache content.Storage, pushLimit int64) *Proxy {
|
||||
limitedCache := content.LimitStorage(cache, pushLimit)
|
||||
return &Proxy{
|
||||
ReadOnlyStorage: base,
|
||||
Cache: limitedCache,
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch fetches the content identified by the descriptor.
|
||||
func (p *Proxy) Fetch(ctx context.Context, target ocispec.Descriptor) (io.ReadCloser, error) {
|
||||
if p.StopCaching {
|
||||
return p.FetchCached(ctx, target)
|
||||
}
|
||||
|
||||
rc, err := p.Cache.Fetch(ctx, target)
|
||||
if err == nil {
|
||||
return rc, nil
|
||||
}
|
||||
|
||||
rc, err = p.ReadOnlyStorage.Fetch(ctx, target)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pr, pw := io.Pipe()
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
var pushErr error
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
pushErr = p.Cache.Push(ctx, target, pr)
|
||||
if pushErr != nil {
|
||||
pr.CloseWithError(pushErr)
|
||||
}
|
||||
}()
|
||||
closer := ioutil.CloserFunc(func() error {
|
||||
rcErr := rc.Close()
|
||||
if err := pw.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
wg.Wait()
|
||||
if pushErr != nil {
|
||||
return pushErr
|
||||
}
|
||||
return rcErr
|
||||
})
|
||||
|
||||
return struct {
|
||||
io.Reader
|
||||
io.Closer
|
||||
}{
|
||||
Reader: io.TeeReader(rc, pw),
|
||||
Closer: closer,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// FetchCached fetches the content identified by the descriptor.
|
||||
// If the content is not cached, it will be fetched from the remote without
|
||||
// caching.
|
||||
func (p *Proxy) FetchCached(ctx context.Context, target ocispec.Descriptor) (io.ReadCloser, error) {
|
||||
exists, err := p.Cache.Exists(ctx, target)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if exists {
|
||||
return p.Cache.Fetch(ctx, target)
|
||||
}
|
||||
return p.ReadOnlyStorage.Fetch(ctx, target)
|
||||
}
|
||||
|
||||
// Exists returns true if the described content exists.
|
||||
func (p *Proxy) Exists(ctx context.Context, target ocispec.Descriptor) (bool, error) {
|
||||
exists, err := p.Cache.Exists(ctx, target)
|
||||
if err == nil && exists {
|
||||
return true, nil
|
||||
}
|
||||
return p.ReadOnlyStorage.Exists(ctx, target)
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
Copyright The ORAS Authors.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package set
|
||||
|
||||
// Set represents a set data structure.
|
||||
type Set[T comparable] map[T]struct{}
|
||||
|
||||
// New returns an initialized set.
|
||||
func New[T comparable]() Set[T] {
|
||||
return make(Set[T])
|
||||
}
|
||||
|
||||
// Add adds item into the set s.
|
||||
func (s Set[T]) Add(item T) {
|
||||
s[item] = struct{}{}
|
||||
}
|
||||
|
||||
// Contains returns true if the set s contains item.
|
||||
func (s Set[T]) Contains(item T) bool {
|
||||
_, ok := s[item]
|
||||
return ok
|
||||
}
|
||||
|
||||
// Delete deletes an item from the set.
|
||||
func (s Set[T]) Delete(item T) {
|
||||
delete(s, item)
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
Copyright The ORAS Authors.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package copyutil
|
||||
|
||||
import (
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
)
|
||||
|
||||
// NodeInfo represents information of a node that is being visited in
|
||||
// ExtendedCopy.
|
||||
type NodeInfo struct {
|
||||
// Node represents a node in the graph.
|
||||
Node ocispec.Descriptor
|
||||
// Depth represents the depth of the node in the graph.
|
||||
Depth int
|
||||
}
|
||||
|
||||
// Stack represents a stack data structure that is used in ExtendedCopy for
|
||||
// storing node information.
|
||||
type Stack []NodeInfo
|
||||
|
||||
// IsEmpty returns true if the stack is empty, otherwise returns false.
|
||||
func (s *Stack) IsEmpty() bool {
|
||||
return len(*s) == 0
|
||||
}
|
||||
|
||||
// Push pushes an item to the stack.
|
||||
func (s *Stack) Push(i NodeInfo) {
|
||||
*s = append(*s, i)
|
||||
}
|
||||
|
||||
// Pop pops the top item out of the stack.
|
||||
func (s *Stack) Pop() (NodeInfo, bool) {
|
||||
if s.IsEmpty() {
|
||||
return NodeInfo{}, false
|
||||
}
|
||||
|
||||
last := len(*s) - 1
|
||||
top := (*s)[last]
|
||||
*s = (*s)[:last]
|
||||
return top, true
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
Copyright The ORAS Authors.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package descriptor
|
||||
|
||||
import (
|
||||
"github.com/opencontainers/go-digest"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"oras.land/oras-go/v2/internal/docker"
|
||||
"oras.land/oras-go/v2/internal/spec"
|
||||
)
|
||||
|
||||
// DefaultMediaType is the media type used when no media type is specified.
|
||||
const DefaultMediaType string = "application/octet-stream"
|
||||
|
||||
// Descriptor contains the minimun information to describe the disposition of
|
||||
// targeted content.
|
||||
// Since it only has strings and integers, Descriptor is a comparable struct.
|
||||
type Descriptor struct {
|
||||
// MediaType is the media type of the object this schema refers to.
|
||||
MediaType string `json:"mediaType,omitempty"`
|
||||
|
||||
// Digest is the digest of the targeted content.
|
||||
Digest digest.Digest `json:"digest"`
|
||||
|
||||
// Size specifies the size in bytes of the blob.
|
||||
Size int64 `json:"size"`
|
||||
}
|
||||
|
||||
// Empty is an empty descriptor
|
||||
var Empty Descriptor
|
||||
|
||||
// FromOCI shrinks the OCI descriptor to the minimum.
|
||||
func FromOCI(desc ocispec.Descriptor) Descriptor {
|
||||
return Descriptor{
|
||||
MediaType: desc.MediaType,
|
||||
Digest: desc.Digest,
|
||||
Size: desc.Size,
|
||||
}
|
||||
}
|
||||
|
||||
// IsForeignLayer checks if a descriptor describes a foreign layer.
|
||||
func IsForeignLayer(desc ocispec.Descriptor) bool {
|
||||
switch desc.MediaType {
|
||||
case ocispec.MediaTypeImageLayerNonDistributable,
|
||||
ocispec.MediaTypeImageLayerNonDistributableGzip,
|
||||
ocispec.MediaTypeImageLayerNonDistributableZstd,
|
||||
docker.MediaTypeForeignLayer:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// IsManifest checks if a descriptor describes a manifest.
|
||||
func IsManifest(desc ocispec.Descriptor) bool {
|
||||
switch desc.MediaType {
|
||||
case docker.MediaTypeManifest,
|
||||
docker.MediaTypeManifestList,
|
||||
ocispec.MediaTypeImageManifest,
|
||||
ocispec.MediaTypeImageIndex,
|
||||
spec.MediaTypeArtifactManifest:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Plain returns a plain descriptor that contains only MediaType, Digest and
|
||||
// Size.
|
||||
func Plain(desc ocispec.Descriptor) ocispec.Descriptor {
|
||||
return ocispec.Descriptor{
|
||||
MediaType: desc.MediaType,
|
||||
Digest: desc.Digest,
|
||||
Size: desc.Size,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
Copyright The ORAS Authors.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package docker
|
||||
|
||||
// docker media types
|
||||
const (
|
||||
MediaTypeConfig = "application/vnd.docker.container.image.v1+json"
|
||||
MediaTypeManifestList = "application/vnd.docker.distribution.manifest.list.v2+json"
|
||||
MediaTypeManifest = "application/vnd.docker.distribution.manifest.v2+json"
|
||||
MediaTypeForeignLayer = "application/vnd.docker.image.rootfs.foreign.diff.tar.gzip"
|
||||
)
|
||||
@@ -0,0 +1,201 @@
|
||||
/*
|
||||
Copyright The ORAS Authors.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package graph
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sync"
|
||||
|
||||
"github.com/opencontainers/go-digest"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"oras.land/oras-go/v2/content"
|
||||
"oras.land/oras-go/v2/errdef"
|
||||
"oras.land/oras-go/v2/internal/container/set"
|
||||
"oras.land/oras-go/v2/internal/descriptor"
|
||||
"oras.land/oras-go/v2/internal/status"
|
||||
"oras.land/oras-go/v2/internal/syncutil"
|
||||
)
|
||||
|
||||
// Memory is a memory based PredecessorFinder.
|
||||
type Memory struct {
|
||||
// nodes has the following properties and behaviors:
|
||||
// 1. a node exists in Memory.nodes if and only if it exists in the memory
|
||||
// 2. Memory.nodes saves the ocispec.Descriptor map keys, which are used by
|
||||
// the other fields.
|
||||
nodes map[descriptor.Descriptor]ocispec.Descriptor
|
||||
|
||||
// predecessors has the following properties and behaviors:
|
||||
// 1. a node exists in Memory.predecessors if it has at least one predecessor
|
||||
// in the memory, regardless of whether or not the node itself exists in
|
||||
// the memory.
|
||||
// 2. a node does not exist in Memory.predecessors, if it doesn't have any predecessors
|
||||
// in the memory.
|
||||
predecessors map[descriptor.Descriptor]set.Set[descriptor.Descriptor]
|
||||
|
||||
// successors has the following properties and behaviors:
|
||||
// 1. a node exists in Memory.successors if and only if it exists in the memory.
|
||||
// 2. a node's entry in Memory.successors is always consistent with the actual
|
||||
// content of the node, regardless of whether or not each successor exists
|
||||
// in the memory.
|
||||
successors map[descriptor.Descriptor]set.Set[descriptor.Descriptor]
|
||||
|
||||
lock sync.RWMutex
|
||||
}
|
||||
|
||||
// NewMemory creates a new memory PredecessorFinder.
|
||||
func NewMemory() *Memory {
|
||||
return &Memory{
|
||||
nodes: make(map[descriptor.Descriptor]ocispec.Descriptor),
|
||||
predecessors: make(map[descriptor.Descriptor]set.Set[descriptor.Descriptor]),
|
||||
successors: make(map[descriptor.Descriptor]set.Set[descriptor.Descriptor]),
|
||||
}
|
||||
}
|
||||
|
||||
// Index indexes predecessors for each direct successor of the given node.
|
||||
func (m *Memory) Index(ctx context.Context, fetcher content.Fetcher, node ocispec.Descriptor) error {
|
||||
_, err := m.index(ctx, fetcher, node)
|
||||
return err
|
||||
}
|
||||
|
||||
// Index indexes predecessors for all the successors of the given node.
|
||||
func (m *Memory) IndexAll(ctx context.Context, fetcher content.Fetcher, node ocispec.Descriptor) error {
|
||||
// track content status
|
||||
tracker := status.NewTracker()
|
||||
var fn syncutil.GoFunc[ocispec.Descriptor]
|
||||
fn = func(ctx context.Context, region *syncutil.LimitedRegion, desc ocispec.Descriptor) error {
|
||||
// skip the node if other go routine is working on it
|
||||
_, committed := tracker.TryCommit(desc)
|
||||
if !committed {
|
||||
return nil
|
||||
}
|
||||
successors, err := m.index(ctx, fetcher, desc)
|
||||
if err != nil {
|
||||
if errors.Is(err, errdef.ErrNotFound) {
|
||||
// skip the node if it does not exist
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
if len(successors) > 0 {
|
||||
// traverse and index successors
|
||||
return syncutil.Go(ctx, nil, fn, successors...)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return syncutil.Go(ctx, nil, fn, node)
|
||||
}
|
||||
|
||||
// Predecessors returns the nodes directly pointing to the current node.
|
||||
// Predecessors returns nil without error if the node does not exists in the
|
||||
// store. Like other operations, calling Predecessors() is go-routine safe.
|
||||
// However, it does not necessarily correspond to any consistent snapshot of
|
||||
// the stored contents.
|
||||
func (m *Memory) Predecessors(_ context.Context, node ocispec.Descriptor) ([]ocispec.Descriptor, error) {
|
||||
m.lock.RLock()
|
||||
defer m.lock.RUnlock()
|
||||
|
||||
key := descriptor.FromOCI(node)
|
||||
set, exists := m.predecessors[key]
|
||||
if !exists {
|
||||
return nil, nil
|
||||
}
|
||||
var res []ocispec.Descriptor
|
||||
for k := range set {
|
||||
res = append(res, m.nodes[k])
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// Remove removes the node from its predecessors and successors, and returns the
|
||||
// dangling root nodes caused by the deletion.
|
||||
func (m *Memory) Remove(node ocispec.Descriptor) []ocispec.Descriptor {
|
||||
m.lock.Lock()
|
||||
defer m.lock.Unlock()
|
||||
|
||||
nodeKey := descriptor.FromOCI(node)
|
||||
var danglings []ocispec.Descriptor
|
||||
// remove the node from its successors' predecessor list
|
||||
for successorKey := range m.successors[nodeKey] {
|
||||
predecessorEntry := m.predecessors[successorKey]
|
||||
predecessorEntry.Delete(nodeKey)
|
||||
|
||||
// if none of the predecessors of the node still exists, we remove the
|
||||
// predecessors entry and return it as a dangling node. Otherwise, we do
|
||||
// not remove the entry.
|
||||
if len(predecessorEntry) == 0 {
|
||||
delete(m.predecessors, successorKey)
|
||||
if _, exists := m.nodes[successorKey]; exists {
|
||||
danglings = append(danglings, m.nodes[successorKey])
|
||||
}
|
||||
}
|
||||
}
|
||||
delete(m.successors, nodeKey)
|
||||
delete(m.nodes, nodeKey)
|
||||
return danglings
|
||||
}
|
||||
|
||||
// DigestSet returns the set of node digest in memory.
|
||||
func (m *Memory) DigestSet() set.Set[digest.Digest] {
|
||||
m.lock.RLock()
|
||||
defer m.lock.RUnlock()
|
||||
|
||||
s := set.New[digest.Digest]()
|
||||
for desc := range m.nodes {
|
||||
s.Add(desc.Digest)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// index indexes predecessors for each direct successor of the given node.
|
||||
func (m *Memory) index(ctx context.Context, fetcher content.Fetcher, node ocispec.Descriptor) ([]ocispec.Descriptor, error) {
|
||||
successors, err := content.Successors(ctx, fetcher, node)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m.lock.Lock()
|
||||
defer m.lock.Unlock()
|
||||
|
||||
// index the node
|
||||
nodeKey := descriptor.FromOCI(node)
|
||||
m.nodes[nodeKey] = node
|
||||
|
||||
// for each successor, put it into the node's successors list, and
|
||||
// put node into the succeesor's predecessors list
|
||||
successorSet := set.New[descriptor.Descriptor]()
|
||||
m.successors[nodeKey] = successorSet
|
||||
for _, successor := range successors {
|
||||
successorKey := descriptor.FromOCI(successor)
|
||||
successorSet.Add(successorKey)
|
||||
predecessorSet, exists := m.predecessors[successorKey]
|
||||
if !exists {
|
||||
predecessorSet = set.New[descriptor.Descriptor]()
|
||||
m.predecessors[successorKey] = predecessorSet
|
||||
}
|
||||
predecessorSet.Add(nodeKey)
|
||||
}
|
||||
return successors, nil
|
||||
}
|
||||
|
||||
// Exists checks if the node exists in the graph
|
||||
func (m *Memory) Exists(node ocispec.Descriptor) bool {
|
||||
m.lock.RLock()
|
||||
defer m.lock.RUnlock()
|
||||
|
||||
nodeKey := descriptor.FromOCI(node)
|
||||
_, exists := m.nodes[nodeKey]
|
||||
return exists
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
Copyright The ORAS Authors.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package httputil
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Client is an interface for a HTTP client.
|
||||
// This interface is defined inside this package to prevent potential import
|
||||
// loop.
|
||||
type Client interface {
|
||||
// Do sends an HTTP request and returns an HTTP response.
|
||||
Do(*http.Request) (*http.Response, error)
|
||||
}
|
||||
|
||||
// readSeekCloser seeks http body by starting new connections.
|
||||
type readSeekCloser struct {
|
||||
client Client
|
||||
req *http.Request
|
||||
rc io.ReadCloser
|
||||
size int64
|
||||
offset int64
|
||||
closed bool
|
||||
}
|
||||
|
||||
// NewReadSeekCloser returns a seeker to make the HTTP response seekable.
|
||||
// Callers should ensure that the server supports Range request.
|
||||
func NewReadSeekCloser(client Client, req *http.Request, respBody io.ReadCloser, size int64) io.ReadSeekCloser {
|
||||
return &readSeekCloser{
|
||||
client: client,
|
||||
req: req,
|
||||
rc: respBody,
|
||||
size: size,
|
||||
}
|
||||
}
|
||||
|
||||
// Read reads the content body and counts offset.
|
||||
func (rsc *readSeekCloser) Read(p []byte) (n int, err error) {
|
||||
if rsc.closed {
|
||||
return 0, errors.New("read: already closed")
|
||||
}
|
||||
n, err = rsc.rc.Read(p)
|
||||
rsc.offset += int64(n)
|
||||
return
|
||||
}
|
||||
|
||||
// Seek starts a new connection to the remote for reading if position changes.
|
||||
func (rsc *readSeekCloser) Seek(offset int64, whence int) (int64, error) {
|
||||
if rsc.closed {
|
||||
return 0, errors.New("seek: already closed")
|
||||
}
|
||||
switch whence {
|
||||
case io.SeekCurrent:
|
||||
offset += rsc.offset
|
||||
case io.SeekStart:
|
||||
// no-op
|
||||
case io.SeekEnd:
|
||||
offset += rsc.size
|
||||
default:
|
||||
return 0, errors.New("seek: invalid whence")
|
||||
}
|
||||
if offset < 0 {
|
||||
return 0, errors.New("seek: an attempt was made to move the pointer before the beginning of the content")
|
||||
}
|
||||
if offset == rsc.offset {
|
||||
return offset, nil
|
||||
}
|
||||
if offset >= rsc.size {
|
||||
rsc.rc.Close()
|
||||
rsc.rc = http.NoBody
|
||||
rsc.offset = offset
|
||||
return offset, nil
|
||||
}
|
||||
|
||||
req := rsc.req.Clone(rsc.req.Context())
|
||||
req.Header.Set("Range", fmt.Sprintf("bytes=%d-%d", offset, rsc.size-1))
|
||||
resp, err := rsc.client.Do(req)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("seek: %s %q: %w", req.Method, req.URL, err)
|
||||
}
|
||||
if resp.StatusCode != http.StatusPartialContent {
|
||||
resp.Body.Close()
|
||||
return 0, fmt.Errorf("seek: %s %q: unexpected status code %d", resp.Request.Method, resp.Request.URL, resp.StatusCode)
|
||||
}
|
||||
|
||||
rsc.rc.Close()
|
||||
rsc.rc = resp.Body
|
||||
rsc.offset = offset
|
||||
return offset, nil
|
||||
}
|
||||
|
||||
// Close closes the content body.
|
||||
func (rsc *readSeekCloser) Close() error {
|
||||
if rsc.closed {
|
||||
return nil
|
||||
}
|
||||
rsc.closed = true
|
||||
return rsc.rc.Close()
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
Copyright The ORAS Authors.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package interfaces
|
||||
|
||||
import "oras.land/oras-go/v2/registry"
|
||||
|
||||
// ReferenceParser provides reference parsing.
|
||||
type ReferenceParser interface {
|
||||
// ParseReference parses a reference to a fully qualified reference.
|
||||
ParseReference(reference string) (registry.Reference, error)
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
Copyright The ORAS Authors.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package ioutil
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"oras.land/oras-go/v2/content"
|
||||
)
|
||||
|
||||
// CloserFunc is the basic Close method defined in io.Closer.
|
||||
type CloserFunc func() error
|
||||
|
||||
// Close performs close operation by the CloserFunc.
|
||||
func (fn CloserFunc) Close() error {
|
||||
return fn()
|
||||
}
|
||||
|
||||
// CopyBuffer copies from src to dst through the provided buffer
|
||||
// until either EOF is reached on src, or an error occurs.
|
||||
// The copied content is verified against the size and the digest.
|
||||
func CopyBuffer(dst io.Writer, src io.Reader, buf []byte, desc ocispec.Descriptor) error {
|
||||
// verify while copying
|
||||
vr := content.NewVerifyReader(src, desc)
|
||||
if _, err := io.CopyBuffer(dst, vr, buf); err != nil {
|
||||
return fmt.Errorf("copy failed: %w", err)
|
||||
}
|
||||
return vr.Verify()
|
||||
}
|
||||
|
||||
// Types returned by `io.NopCloser()`.
|
||||
var (
|
||||
nopCloserType = reflect.TypeOf(io.NopCloser(nil))
|
||||
nopCloserWriterToType = reflect.TypeOf(io.NopCloser(struct {
|
||||
io.Reader
|
||||
io.WriterTo
|
||||
}{}))
|
||||
)
|
||||
|
||||
// UnwrapNopCloser unwraps the reader wrapped by `io.NopCloser()`.
|
||||
// Similar implementation can be found in the built-in package `net/http`.
|
||||
// Reference: https://github.com/golang/go/blob/go1.22.1/src/net/http/transfer.go#L1090-L1105
|
||||
func UnwrapNopCloser(r io.Reader) io.Reader {
|
||||
switch reflect.TypeOf(r) {
|
||||
case nopCloserType, nopCloserWriterToType:
|
||||
return reflect.ValueOf(r).Field(0).Interface().(io.Reader)
|
||||
default:
|
||||
return r
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
Copyright The ORAS Authors.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package manifestutil
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"oras.land/oras-go/v2/content"
|
||||
"oras.land/oras-go/v2/internal/docker"
|
||||
"oras.land/oras-go/v2/internal/spec"
|
||||
)
|
||||
|
||||
// Config returns the config of desc, if present.
|
||||
func Config(ctx context.Context, fetcher content.Fetcher, desc ocispec.Descriptor) (*ocispec.Descriptor, error) {
|
||||
switch desc.MediaType {
|
||||
case docker.MediaTypeManifest, ocispec.MediaTypeImageManifest:
|
||||
content, err := content.FetchAll(ctx, fetcher, desc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// OCI manifest schema can be used to marshal docker manifest
|
||||
var manifest ocispec.Manifest
|
||||
if err := json.Unmarshal(content, &manifest); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &manifest.Config, nil
|
||||
default:
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Manifest returns the manifests of desc, if present.
|
||||
func Manifests(ctx context.Context, fetcher content.Fetcher, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
|
||||
switch desc.MediaType {
|
||||
case docker.MediaTypeManifestList, ocispec.MediaTypeImageIndex:
|
||||
content, err := content.FetchAll(ctx, fetcher, desc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// OCI manifest index schema can be used to marshal docker manifest list
|
||||
var index ocispec.Index
|
||||
if err := json.Unmarshal(content, &index); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return index.Manifests, nil
|
||||
default:
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Subject returns the subject of desc, if present.
|
||||
func Subject(ctx context.Context, fetcher content.Fetcher, desc ocispec.Descriptor) (*ocispec.Descriptor, error) {
|
||||
switch desc.MediaType {
|
||||
case ocispec.MediaTypeImageManifest, ocispec.MediaTypeImageIndex, spec.MediaTypeArtifactManifest:
|
||||
content, err := content.FetchAll(ctx, fetcher, desc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var manifest struct {
|
||||
Subject *ocispec.Descriptor `json:"subject,omitempty"`
|
||||
}
|
||||
if err := json.Unmarshal(content, &manifest); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return manifest.Subject, nil
|
||||
default:
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
/*
|
||||
Copyright The ORAS Authors.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package platform
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"oras.land/oras-go/v2/content"
|
||||
"oras.land/oras-go/v2/errdef"
|
||||
"oras.land/oras-go/v2/internal/docker"
|
||||
"oras.land/oras-go/v2/internal/manifestutil"
|
||||
)
|
||||
|
||||
// Match checks whether the current platform matches the target platform.
|
||||
// Match will return true if all of the following conditions are met.
|
||||
// - Architecture and OS exactly match.
|
||||
// - Variant and OSVersion exactly match if target platform provided.
|
||||
// - OSFeatures of the target platform are the subsets of the OSFeatures
|
||||
// array of the current platform.
|
||||
//
|
||||
// Note: Variant, OSVersion and OSFeatures are optional fields, will skip
|
||||
// the comparison if the target platform does not provide specific value.
|
||||
func Match(got *ocispec.Platform, want *ocispec.Platform) bool {
|
||||
if got == nil && want == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
if got == nil || want == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if got.Architecture != want.Architecture || got.OS != want.OS {
|
||||
return false
|
||||
}
|
||||
|
||||
if want.OSVersion != "" && got.OSVersion != want.OSVersion {
|
||||
return false
|
||||
}
|
||||
|
||||
if want.Variant != "" && got.Variant != want.Variant {
|
||||
return false
|
||||
}
|
||||
|
||||
if len(want.OSFeatures) != 0 && !isSubset(want.OSFeatures, got.OSFeatures) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// isSubset returns true if all items in slice A are present in slice B.
|
||||
func isSubset(a, b []string) bool {
|
||||
set := make(map[string]bool, len(b))
|
||||
for _, v := range b {
|
||||
set[v] = true
|
||||
}
|
||||
for _, v := range a {
|
||||
if _, ok := set[v]; !ok {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// SelectManifest implements platform filter and returns the descriptor of the
|
||||
// first matched manifest if the root is a manifest list. If the root is a
|
||||
// manifest, then return the root descriptor if platform matches.
|
||||
func SelectManifest(ctx context.Context, src content.ReadOnlyStorage, root ocispec.Descriptor, p *ocispec.Platform) (ocispec.Descriptor, error) {
|
||||
switch root.MediaType {
|
||||
case docker.MediaTypeManifestList, ocispec.MediaTypeImageIndex:
|
||||
manifests, err := manifestutil.Manifests(ctx, src, root)
|
||||
if err != nil {
|
||||
return ocispec.Descriptor{}, err
|
||||
}
|
||||
|
||||
// platform filter
|
||||
for _, m := range manifests {
|
||||
if Match(m.Platform, p) {
|
||||
return m, nil
|
||||
}
|
||||
}
|
||||
return ocispec.Descriptor{}, fmt.Errorf("%s: %w: no matching manifest was found in the manifest list", root.Digest, errdef.ErrNotFound)
|
||||
case docker.MediaTypeManifest, ocispec.MediaTypeImageManifest:
|
||||
// config will be non-nil for docker manifest and OCI image manifest
|
||||
config, err := manifestutil.Config(ctx, src, root)
|
||||
if err != nil {
|
||||
return ocispec.Descriptor{}, err
|
||||
}
|
||||
|
||||
configMediaType := docker.MediaTypeConfig
|
||||
if root.MediaType == ocispec.MediaTypeImageManifest {
|
||||
configMediaType = ocispec.MediaTypeImageConfig
|
||||
}
|
||||
cfgPlatform, err := getPlatformFromConfig(ctx, src, *config, configMediaType)
|
||||
if err != nil {
|
||||
return ocispec.Descriptor{}, err
|
||||
}
|
||||
|
||||
if Match(cfgPlatform, p) {
|
||||
return root, nil
|
||||
}
|
||||
return ocispec.Descriptor{}, fmt.Errorf("%s: %w: platform in manifest does not match target platform", root.Digest, errdef.ErrNotFound)
|
||||
default:
|
||||
return ocispec.Descriptor{}, fmt.Errorf("%s: %s: %w", root.Digest, root.MediaType, errdef.ErrUnsupported)
|
||||
}
|
||||
}
|
||||
|
||||
// getPlatformFromConfig returns a platform object which is made up from the
|
||||
// fields in config blob.
|
||||
func getPlatformFromConfig(ctx context.Context, src content.ReadOnlyStorage, desc ocispec.Descriptor, targetConfigMediaType string) (*ocispec.Platform, error) {
|
||||
if desc.MediaType != targetConfigMediaType {
|
||||
return nil, fmt.Errorf("fail to recognize platform from unknown config %s: expect %s: %w", desc.MediaType, targetConfigMediaType, errdef.ErrUnsupported)
|
||||
}
|
||||
|
||||
rc, err := src.Fetch(ctx, desc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rc.Close()
|
||||
|
||||
var platform ocispec.Platform
|
||||
if err = json.NewDecoder(rc).Decode(&platform); err != nil && err != io.EOF {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &platform, nil
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
Copyright The ORAS Authors.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package registryutil
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"sync"
|
||||
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"oras.land/oras-go/v2/content"
|
||||
"oras.land/oras-go/v2/internal/cas"
|
||||
"oras.land/oras-go/v2/internal/ioutil"
|
||||
"oras.land/oras-go/v2/registry"
|
||||
)
|
||||
|
||||
// ReferenceStorage represents a CAS that supports registry.ReferenceFetcher.
|
||||
type ReferenceStorage interface {
|
||||
content.ReadOnlyStorage
|
||||
registry.ReferenceFetcher
|
||||
}
|
||||
|
||||
// Proxy is a caching proxy dedicated for registry.ReferenceFetcher.
|
||||
// The first fetch call of a described content will read from the remote and
|
||||
// cache the fetched content.
|
||||
// The subsequent fetch call will read from the local cache.
|
||||
type Proxy struct {
|
||||
registry.ReferenceFetcher
|
||||
*cas.Proxy
|
||||
}
|
||||
|
||||
// NewProxy creates a proxy for the `base` ReferenceStorage, using the `cache`
|
||||
// storage as the cache.
|
||||
func NewProxy(base ReferenceStorage, cache content.Storage) *Proxy {
|
||||
return &Proxy{
|
||||
ReferenceFetcher: base,
|
||||
Proxy: cas.NewProxy(base, cache),
|
||||
}
|
||||
}
|
||||
|
||||
// FetchReference fetches the content identified by the reference from the
|
||||
// remote and cache the fetched content.
|
||||
func (p *Proxy) FetchReference(ctx context.Context, reference string) (ocispec.Descriptor, io.ReadCloser, error) {
|
||||
target, rc, err := p.ReferenceFetcher.FetchReference(ctx, reference)
|
||||
if err != nil {
|
||||
return ocispec.Descriptor{}, nil, err
|
||||
}
|
||||
|
||||
// skip caching if the content already exists in cache
|
||||
exists, err := p.Cache.Exists(ctx, target)
|
||||
if err != nil {
|
||||
return ocispec.Descriptor{}, nil, err
|
||||
}
|
||||
if exists {
|
||||
return target, rc, nil
|
||||
}
|
||||
|
||||
// cache content while reading
|
||||
pr, pw := io.Pipe()
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
var pushErr error
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
pushErr = p.Cache.Push(ctx, target, pr)
|
||||
if pushErr != nil {
|
||||
pr.CloseWithError(pushErr)
|
||||
}
|
||||
}()
|
||||
closer := ioutil.CloserFunc(func() error {
|
||||
rcErr := rc.Close()
|
||||
if err := pw.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
wg.Wait()
|
||||
if pushErr != nil {
|
||||
return pushErr
|
||||
}
|
||||
return rcErr
|
||||
})
|
||||
|
||||
return target, struct {
|
||||
io.Reader
|
||||
io.Closer
|
||||
}{
|
||||
Reader: io.TeeReader(rc, pw),
|
||||
Closer: closer,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
Copyright The ORAS Authors.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package resolver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"maps"
|
||||
"sync"
|
||||
|
||||
"github.com/opencontainers/go-digest"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"oras.land/oras-go/v2/errdef"
|
||||
"oras.land/oras-go/v2/internal/container/set"
|
||||
)
|
||||
|
||||
// Memory is a memory based resolver.
|
||||
type Memory struct {
|
||||
lock sync.RWMutex
|
||||
index map[string]ocispec.Descriptor
|
||||
tags map[digest.Digest]set.Set[string]
|
||||
}
|
||||
|
||||
// NewMemory creates a new Memory resolver.
|
||||
func NewMemory() *Memory {
|
||||
return &Memory{
|
||||
index: make(map[string]ocispec.Descriptor),
|
||||
tags: make(map[digest.Digest]set.Set[string]),
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve resolves a reference to a descriptor.
|
||||
func (m *Memory) Resolve(_ context.Context, reference string) (ocispec.Descriptor, error) {
|
||||
m.lock.RLock()
|
||||
defer m.lock.RUnlock()
|
||||
|
||||
desc, ok := m.index[reference]
|
||||
if !ok {
|
||||
return ocispec.Descriptor{}, fmt.Errorf("%s: %w", reference, errdef.ErrNotFound)
|
||||
}
|
||||
return desc, nil
|
||||
}
|
||||
|
||||
// Tag tags a descriptor with a reference string.
|
||||
func (m *Memory) Tag(_ context.Context, desc ocispec.Descriptor, reference string) error {
|
||||
m.lock.Lock()
|
||||
defer m.lock.Unlock()
|
||||
|
||||
m.index[reference] = desc
|
||||
tagSet, ok := m.tags[desc.Digest]
|
||||
if !ok {
|
||||
tagSet = set.New[string]()
|
||||
m.tags[desc.Digest] = tagSet
|
||||
}
|
||||
tagSet.Add(reference)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Untag removes a reference from index map.
|
||||
func (m *Memory) Untag(reference string) {
|
||||
m.lock.Lock()
|
||||
defer m.lock.Unlock()
|
||||
|
||||
desc, ok := m.index[reference]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
delete(m.index, reference)
|
||||
tagSet := m.tags[desc.Digest]
|
||||
tagSet.Delete(reference)
|
||||
if len(tagSet) == 0 {
|
||||
delete(m.tags, desc.Digest)
|
||||
}
|
||||
}
|
||||
|
||||
// Map dumps the memory into a built-in map structure.
|
||||
// Like other operations, calling Map() is go-routine safe.
|
||||
func (m *Memory) Map() map[string]ocispec.Descriptor {
|
||||
m.lock.RLock()
|
||||
defer m.lock.RUnlock()
|
||||
|
||||
return maps.Clone(m.index)
|
||||
}
|
||||
|
||||
// TagSet returns the set of tags of the descriptor.
|
||||
func (m *Memory) TagSet(desc ocispec.Descriptor) set.Set[string] {
|
||||
m.lock.RLock()
|
||||
defer m.lock.RUnlock()
|
||||
|
||||
tagSet := m.tags[desc.Digest]
|
||||
return maps.Clone(tagSet)
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
Copyright The ORAS Authors.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package spec
|
||||
|
||||
import ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
|
||||
const (
|
||||
// AnnotationArtifactCreated is the annotation key for the date and time on which the artifact was built, conforming to RFC 3339.
|
||||
AnnotationArtifactCreated = "org.opencontainers.artifact.created"
|
||||
|
||||
// AnnotationArtifactDescription is the annotation key for the human readable description for the artifact.
|
||||
AnnotationArtifactDescription = "org.opencontainers.artifact.description"
|
||||
|
||||
// AnnotationReferrersFiltersApplied is the annotation key for the comma separated list of filters applied by the registry in the referrers listing.
|
||||
AnnotationReferrersFiltersApplied = "org.opencontainers.referrers.filtersApplied"
|
||||
)
|
||||
|
||||
// MediaTypeArtifactManifest specifies the media type for a content descriptor.
|
||||
const MediaTypeArtifactManifest = "application/vnd.oci.artifact.manifest.v1+json"
|
||||
|
||||
// Artifact describes an artifact manifest.
|
||||
// This structure provides `application/vnd.oci.artifact.manifest.v1+json` mediatype when marshalled to JSON.
|
||||
//
|
||||
// This manifest type was introduced in image-spec v1.1.0-rc1 and was removed in
|
||||
// image-spec v1.1.0-rc3. It is not part of the current image-spec and is kept
|
||||
// here for Go compatibility.
|
||||
//
|
||||
// Reference: https://github.com/opencontainers/image-spec/pull/999
|
||||
type Artifact struct {
|
||||
// MediaType is the media type of the object this schema refers to.
|
||||
MediaType string `json:"mediaType"`
|
||||
|
||||
// ArtifactType is the IANA media type of the artifact this schema refers to.
|
||||
ArtifactType string `json:"artifactType"`
|
||||
|
||||
// Blobs is a collection of blobs referenced by this manifest.
|
||||
Blobs []ocispec.Descriptor `json:"blobs,omitempty"`
|
||||
|
||||
// Subject (reference) is an optional link from the artifact to another manifest forming an association between the artifact and the other manifest.
|
||||
Subject *ocispec.Descriptor `json:"subject,omitempty"`
|
||||
|
||||
// Annotations contains arbitrary metadata for the artifact manifest.
|
||||
Annotations map[string]string `json:"annotations,omitempty"`
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
Copyright The ORAS Authors.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package status
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"oras.land/oras-go/v2/internal/descriptor"
|
||||
)
|
||||
|
||||
// Tracker tracks content status described by a descriptor.
|
||||
type Tracker struct {
|
||||
status sync.Map // map[descriptor.Descriptor]chan struct{}
|
||||
}
|
||||
|
||||
// NewTracker creates a new content status tracker.
|
||||
func NewTracker() *Tracker {
|
||||
return &Tracker{}
|
||||
}
|
||||
|
||||
// TryCommit tries to commit the work for the target descriptor.
|
||||
// Returns true if committed. A channel is also returned for sending
|
||||
// notifications. Once the work is done, the channel should be closed.
|
||||
// Returns false if the work is done or still in progress.
|
||||
func (t *Tracker) TryCommit(target ocispec.Descriptor) (chan struct{}, bool) {
|
||||
key := descriptor.FromOCI(target)
|
||||
status, exists := t.status.LoadOrStore(key, make(chan struct{}))
|
||||
return status.(chan struct{}), !exists
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
Copyright The ORAS Authors.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package syncutil
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"golang.org/x/sync/errgroup"
|
||||
"golang.org/x/sync/semaphore"
|
||||
)
|
||||
|
||||
// LimitedRegion provides a way to bound concurrent access to a code block.
|
||||
type LimitedRegion struct {
|
||||
ctx context.Context
|
||||
limiter *semaphore.Weighted
|
||||
ended bool
|
||||
}
|
||||
|
||||
// LimitRegion creates a new LimitedRegion.
|
||||
func LimitRegion(ctx context.Context, limiter *semaphore.Weighted) *LimitedRegion {
|
||||
if limiter == nil {
|
||||
return nil
|
||||
}
|
||||
return &LimitedRegion{
|
||||
ctx: ctx,
|
||||
limiter: limiter,
|
||||
ended: true,
|
||||
}
|
||||
}
|
||||
|
||||
// Start starts the region with concurrency limit.
|
||||
func (lr *LimitedRegion) Start() error {
|
||||
if lr == nil || !lr.ended {
|
||||
return nil
|
||||
}
|
||||
if err := lr.limiter.Acquire(lr.ctx, 1); err != nil {
|
||||
return err
|
||||
}
|
||||
lr.ended = false
|
||||
return nil
|
||||
}
|
||||
|
||||
// End ends the region with concurrency limit.
|
||||
func (lr *LimitedRegion) End() {
|
||||
if lr == nil || lr.ended {
|
||||
return
|
||||
}
|
||||
lr.limiter.Release(1)
|
||||
lr.ended = true
|
||||
}
|
||||
|
||||
// GoFunc represents a function that can be invoked by Go.
|
||||
type GoFunc[T any] func(ctx context.Context, region *LimitedRegion, t T) error
|
||||
|
||||
// Go concurrently invokes fn on items.
|
||||
func Go[T any](ctx context.Context, limiter *semaphore.Weighted, fn GoFunc[T], items ...T) error {
|
||||
ctx, cancel := context.WithCancelCause(ctx)
|
||||
defer cancel(nil)
|
||||
|
||||
eg, egCtx := errgroup.WithContext(ctx)
|
||||
for _, item := range items {
|
||||
region := LimitRegion(egCtx, limiter)
|
||||
if err := region.Start(); err != nil {
|
||||
cancel(err)
|
||||
// break loop instead of returning to allow previously scheduled
|
||||
// goroutines to finish their deferred region.End() calls
|
||||
break
|
||||
}
|
||||
|
||||
eg.Go(func(t T, lr *LimitedRegion) func() error {
|
||||
return func() error {
|
||||
defer lr.End()
|
||||
|
||||
select {
|
||||
case <-egCtx.Done():
|
||||
// skip the task if the context is already cancelled
|
||||
return nil
|
||||
default:
|
||||
}
|
||||
|
||||
if err := fn(egCtx, lr, t); err != nil {
|
||||
cancel(err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}(item, region))
|
||||
}
|
||||
|
||||
if err := eg.Wait(); err != nil {
|
||||
cancel(err)
|
||||
}
|
||||
return context.Cause(ctx)
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
Copyright The ORAS Authors.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package syncutil
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
// LimitedGroup is a collection of goroutines working on subtasks that are part of
|
||||
// the same overall task.
|
||||
type LimitedGroup struct {
|
||||
grp *errgroup.Group
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
// LimitGroup returns a new LimitedGroup and an associated Context derived from ctx.
|
||||
//
|
||||
// The number of active goroutines in this group is limited to the given limit.
|
||||
// A negative value indicates no limit.
|
||||
//
|
||||
// The derived Context is canceled the first time a function passed to Go
|
||||
// returns a non-nil error or the first time Wait returns, whichever occurs
|
||||
// first.
|
||||
func LimitGroup(ctx context.Context, limit int) (*LimitedGroup, context.Context) {
|
||||
grp, ctx := errgroup.WithContext(ctx)
|
||||
grp.SetLimit(limit)
|
||||
return &LimitedGroup{grp: grp, ctx: ctx}, ctx
|
||||
}
|
||||
|
||||
// Go calls the given function in a new goroutine.
|
||||
// It blocks until the new goroutine can be added without the number of
|
||||
// active goroutines in the group exceeding the configured limit.
|
||||
//
|
||||
// The first call to return a non-nil error cancels the group's context.
|
||||
// After which, any subsequent calls to Go will not execute their given function.
|
||||
// The error will be returned by Wait.
|
||||
func (g *LimitedGroup) Go(f func() error) {
|
||||
g.grp.Go(func() error {
|
||||
select {
|
||||
case <-g.ctx.Done():
|
||||
return g.ctx.Err()
|
||||
default:
|
||||
return f()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Wait blocks until all function calls from the Go method have returned, then
|
||||
// returns the first non-nil error (if any) from them.
|
||||
func (g *LimitedGroup) Wait() error {
|
||||
return g.grp.Wait()
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
Copyright The ORAS Authors.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package syncutil
|
||||
|
||||
import "sync"
|
||||
|
||||
// mergeStatus represents the merge status of an item.
|
||||
type mergeStatus struct {
|
||||
// main indicates if items are being merged by the current go-routine.
|
||||
main bool
|
||||
// err represents the error of the merge operation.
|
||||
err error
|
||||
}
|
||||
|
||||
// Merge represents merge operations on items.
|
||||
// The state transfer is shown as below:
|
||||
//
|
||||
// +----------+
|
||||
// | Start +--------+-------------+
|
||||
// +----+-----+ | |
|
||||
// | | |
|
||||
// v v v
|
||||
// +----+-----+ +----+----+ +----+----+
|
||||
// +-------+ Prepare +<--+ Pending +-->+ Waiting |
|
||||
// | +----+-----+ +---------+ +----+----+
|
||||
// | | |
|
||||
// | v |
|
||||
// | + ---+---- + |
|
||||
// On Error | Resolve | |
|
||||
// | + ---+---- + |
|
||||
// | | |
|
||||
// | v |
|
||||
// | +----+-----+ |
|
||||
// +------>+ Complete +<---------------------+
|
||||
// +----+-----+
|
||||
// |
|
||||
// v
|
||||
// +----+-----+
|
||||
// | End |
|
||||
// +----------+
|
||||
type Merge[T any] struct {
|
||||
lock sync.Mutex
|
||||
committed bool
|
||||
items []T
|
||||
status chan mergeStatus
|
||||
pending []T
|
||||
pendingStatus chan mergeStatus
|
||||
}
|
||||
|
||||
// Do merges concurrent operations of items into a single call of prepare and
|
||||
// resolve.
|
||||
// If Do is called multiple times concurrently, only one of the calls will be
|
||||
// selected to invoke prepare and resolve.
|
||||
func (m *Merge[T]) Do(item T, prepare func() error, resolve func(items []T) error) error {
|
||||
status := <-m.assign(item)
|
||||
if status.main {
|
||||
err := prepare()
|
||||
items := m.commit()
|
||||
if err == nil {
|
||||
err = resolve(items)
|
||||
}
|
||||
m.complete(err)
|
||||
return err
|
||||
}
|
||||
return status.err
|
||||
}
|
||||
|
||||
// assign adds a new item into the item list.
|
||||
func (m *Merge[T]) assign(item T) <-chan mergeStatus {
|
||||
m.lock.Lock()
|
||||
defer m.lock.Unlock()
|
||||
|
||||
if m.committed {
|
||||
if m.pendingStatus == nil {
|
||||
m.pendingStatus = make(chan mergeStatus, 1)
|
||||
}
|
||||
m.pending = append(m.pending, item)
|
||||
return m.pendingStatus
|
||||
}
|
||||
|
||||
if m.status == nil {
|
||||
m.status = make(chan mergeStatus, 1)
|
||||
m.status <- mergeStatus{main: true}
|
||||
}
|
||||
m.items = append(m.items, item)
|
||||
return m.status
|
||||
}
|
||||
|
||||
// commit closes the assignment window, and the assigned items will be ready
|
||||
// for resolve.
|
||||
func (m *Merge[T]) commit() []T {
|
||||
m.lock.Lock()
|
||||
defer m.lock.Unlock()
|
||||
|
||||
m.committed = true
|
||||
return m.items
|
||||
}
|
||||
|
||||
// complete completes the previous merge, and moves the pending items to the
|
||||
// stage for the next merge.
|
||||
func (m *Merge[T]) complete(err error) {
|
||||
// notify results
|
||||
if err == nil {
|
||||
close(m.status)
|
||||
} else {
|
||||
remaining := len(m.items) - 1
|
||||
status := m.status
|
||||
for remaining > 0 {
|
||||
status <- mergeStatus{err: err}
|
||||
remaining--
|
||||
}
|
||||
}
|
||||
|
||||
// move pending items to the stage
|
||||
m.lock.Lock()
|
||||
defer m.lock.Unlock()
|
||||
|
||||
m.committed = false
|
||||
m.items = m.pending
|
||||
m.status = m.pendingStatus
|
||||
m.pending = nil
|
||||
m.pendingStatus = nil
|
||||
|
||||
if m.status != nil {
|
||||
m.status <- mergeStatus{main: true}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
Copyright The ORAS Authors.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package syncutil
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
// Once is an object that will perform exactly one action.
|
||||
// Unlike sync.Once, this Once allows the action to have return values.
|
||||
type Once struct {
|
||||
result interface{}
|
||||
err error
|
||||
status chan bool
|
||||
}
|
||||
|
||||
// NewOnce creates a new Once instance.
|
||||
func NewOnce() *Once {
|
||||
status := make(chan bool, 1)
|
||||
status <- true
|
||||
return &Once{
|
||||
status: status,
|
||||
}
|
||||
}
|
||||
|
||||
// Do calls the function f if and only if Do is being called first time or all
|
||||
// previous function calls are cancelled, deadline exceeded, or panicking.
|
||||
// When `once.Do(ctx, f)` is called multiple times, the return value of the
|
||||
// first call of the function f is stored, and is directly returned for other
|
||||
// calls.
|
||||
// Besides the return value of the function f, including the error, Do returns
|
||||
// true if the function f passed is called first and is not cancelled, deadline
|
||||
// exceeded, or panicking. Otherwise, returns false.
|
||||
func (o *Once) Do(ctx context.Context, f func() (interface{}, error)) (bool, interface{}, error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
o.status <- true
|
||||
panic(r)
|
||||
}
|
||||
}()
|
||||
for {
|
||||
select {
|
||||
case inProgress := <-o.status:
|
||||
if !inProgress {
|
||||
return false, o.result, o.err
|
||||
}
|
||||
result, err := f()
|
||||
if err == context.Canceled || err == context.DeadlineExceeded {
|
||||
o.status <- true
|
||||
return false, nil, err
|
||||
}
|
||||
o.result, o.err = result, err
|
||||
close(o.status)
|
||||
return true, result, err
|
||||
case <-ctx.Done():
|
||||
return false, nil, ctx.Err()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// OnceOrRetry is an object that will perform exactly one success action.
|
||||
type OnceOrRetry struct {
|
||||
done atomic.Bool
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
// OnceOrRetry calls the function f if and only if Do is being called for the
|
||||
// first time for this instance of Once or all previous calls to Do are failed.
|
||||
func (o *OnceOrRetry) Do(f func() error) error {
|
||||
// fast path
|
||||
if o.done.Load() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// slow path
|
||||
o.lock.Lock()
|
||||
defer o.lock.Unlock()
|
||||
|
||||
if o.done.Load() {
|
||||
return nil
|
||||
}
|
||||
if err := f(); err != nil {
|
||||
return err
|
||||
}
|
||||
o.done.Store(true)
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
Copyright The ORAS Authors.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package syncutil
|
||||
|
||||
import "sync"
|
||||
|
||||
// poolItem represents an item in Pool.
|
||||
type poolItem[T any] struct {
|
||||
value T
|
||||
refCount int
|
||||
}
|
||||
|
||||
// Pool is a scalable pool with items identified by keys.
|
||||
type Pool[T any] struct {
|
||||
// New optionally specifies a function to generate a value when Get would
|
||||
// otherwise return nil.
|
||||
// It may not be changed concurrently with calls to Get.
|
||||
New func() T
|
||||
|
||||
lock sync.Mutex
|
||||
items map[any]*poolItem[T]
|
||||
}
|
||||
|
||||
// Get gets the value identified by key.
|
||||
// The caller should invoke the returned function after using the returned item.
|
||||
func (p *Pool[T]) Get(key any) (*T, func()) {
|
||||
p.lock.Lock()
|
||||
defer p.lock.Unlock()
|
||||
|
||||
item, ok := p.items[key]
|
||||
if !ok {
|
||||
if p.items == nil {
|
||||
p.items = make(map[any]*poolItem[T])
|
||||
}
|
||||
item = &poolItem[T]{}
|
||||
if p.New != nil {
|
||||
item.value = p.New()
|
||||
}
|
||||
p.items[key] = item
|
||||
}
|
||||
item.refCount++
|
||||
|
||||
return &item.value, func() {
|
||||
p.lock.Lock()
|
||||
defer p.lock.Unlock()
|
||||
item.refCount--
|
||||
if item.refCount <= 0 {
|
||||
delete(p.items, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user