...

Source file src/github.com/docker/distribution/registry/client/repository.go

Documentation: github.com/docker/distribution/registry/client

     1  package client
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/json"
     7  	"errors"
     8  	"fmt"
     9  	"io"
    10  	"io/ioutil"
    11  	"net/http"
    12  	"net/url"
    13  	"strconv"
    14  	"strings"
    15  	"time"
    16  
    17  	"github.com/distribution/reference"
    18  	"github.com/docker/distribution"
    19  	v2 "github.com/docker/distribution/registry/api/v2"
    20  	"github.com/docker/distribution/registry/client/transport"
    21  	"github.com/docker/distribution/registry/storage/cache"
    22  	"github.com/docker/distribution/registry/storage/cache/memory"
    23  	"github.com/opencontainers/go-digest"
    24  )
    25  
    26  // Registry provides an interface for calling Repositories, which returns a catalog of repositories.
    27  type Registry interface {
    28  	Repositories(ctx context.Context, repos []string, last string) (n int, err error)
    29  }
    30  
    31  // checkHTTPRedirect is a callback that can manipulate redirected HTTP
    32  // requests. It is used to preserve Accept and Range headers.
    33  func checkHTTPRedirect(req *http.Request, via []*http.Request) error {
    34  	if len(via) >= 10 {
    35  		return errors.New("stopped after 10 redirects")
    36  	}
    37  
    38  	if len(via) > 0 {
    39  		for headerName, headerVals := range via[0].Header {
    40  			if headerName != "Accept" && headerName != "Range" {
    41  				continue
    42  			}
    43  			for _, val := range headerVals {
    44  				// Don't add to redirected request if redirected
    45  				// request already has a header with the same
    46  				// name and value.
    47  				hasValue := false
    48  				for _, existingVal := range req.Header[headerName] {
    49  					if existingVal == val {
    50  						hasValue = true
    51  						break
    52  					}
    53  				}
    54  				if !hasValue {
    55  					req.Header.Add(headerName, val)
    56  				}
    57  			}
    58  		}
    59  	}
    60  
    61  	return nil
    62  }
    63  
    64  // NewRegistry creates a registry namespace which can be used to get a listing of repositories
    65  func NewRegistry(baseURL string, transport http.RoundTripper) (Registry, error) {
    66  	ub, err := v2.NewURLBuilderFromString(baseURL, false)
    67  	if err != nil {
    68  		return nil, err
    69  	}
    70  
    71  	client := &http.Client{
    72  		Transport:     transport,
    73  		Timeout:       1 * time.Minute,
    74  		CheckRedirect: checkHTTPRedirect,
    75  	}
    76  
    77  	return &registry{
    78  		client: client,
    79  		ub:     ub,
    80  	}, nil
    81  }
    82  
    83  type registry struct {
    84  	client *http.Client
    85  	ub     *v2.URLBuilder
    86  }
    87  
    88  // Repositories returns a lexigraphically sorted catalog given a base URL.  The 'entries' slice will be filled up to the size
    89  // of the slice, starting at the value provided in 'last'.  The number of entries will be returned along with io.EOF if there
    90  // are no more entries
    91  func (r *registry) Repositories(ctx context.Context, entries []string, last string) (int, error) {
    92  	var numFilled int
    93  	var returnErr error
    94  
    95  	values := buildCatalogValues(len(entries), last)
    96  	u, err := r.ub.BuildCatalogURL(values)
    97  	if err != nil {
    98  		return 0, err
    99  	}
   100  
   101  	resp, err := r.client.Get(u)
   102  	if err != nil {
   103  		return 0, err
   104  	}
   105  	defer resp.Body.Close()
   106  
   107  	if SuccessStatus(resp.StatusCode) {
   108  		var ctlg struct {
   109  			Repositories []string `json:"repositories"`
   110  		}
   111  		decoder := json.NewDecoder(resp.Body)
   112  
   113  		if err := decoder.Decode(&ctlg); err != nil {
   114  			return 0, err
   115  		}
   116  
   117  		copy(entries, ctlg.Repositories)
   118  		numFilled = len(ctlg.Repositories)
   119  
   120  		link := resp.Header.Get("Link")
   121  		if link == "" {
   122  			returnErr = io.EOF
   123  		}
   124  	} else {
   125  		return 0, HandleErrorResponse(resp)
   126  	}
   127  
   128  	return numFilled, returnErr
   129  }
   130  
   131  // NewRepository creates a new Repository for the given repository name and base URL.
   132  func NewRepository(name reference.Named, baseURL string, transport http.RoundTripper) (distribution.Repository, error) {
   133  	ub, err := v2.NewURLBuilderFromString(baseURL, false)
   134  	if err != nil {
   135  		return nil, err
   136  	}
   137  
   138  	client := &http.Client{
   139  		Transport:     transport,
   140  		CheckRedirect: checkHTTPRedirect,
   141  		// TODO(dmcgowan): create cookie jar
   142  	}
   143  
   144  	return &repository{
   145  		client: client,
   146  		ub:     ub,
   147  		name:   name,
   148  	}, nil
   149  }
   150  
   151  type repository struct {
   152  	client *http.Client
   153  	ub     *v2.URLBuilder
   154  	name   reference.Named
   155  }
   156  
   157  func (r *repository) Named() reference.Named {
   158  	return r.name
   159  }
   160  
   161  func (r *repository) Blobs(ctx context.Context) distribution.BlobStore {
   162  	statter := &blobStatter{
   163  		name:   r.name,
   164  		ub:     r.ub,
   165  		client: r.client,
   166  	}
   167  	return &blobs{
   168  		name:    r.name,
   169  		ub:      r.ub,
   170  		client:  r.client,
   171  		statter: cache.NewCachedBlobStatter(memory.NewInMemoryBlobDescriptorCacheProvider(), statter),
   172  	}
   173  }
   174  
   175  func (r *repository) Manifests(ctx context.Context, options ...distribution.ManifestServiceOption) (distribution.ManifestService, error) {
   176  	// todo(richardscothern): options should be sent over the wire
   177  	return &manifests{
   178  		name:   r.name,
   179  		ub:     r.ub,
   180  		client: r.client,
   181  		etags:  make(map[string]string),
   182  	}, nil
   183  }
   184  
   185  func (r *repository) Tags(ctx context.Context) distribution.TagService {
   186  	return &tags{
   187  		client: r.client,
   188  		ub:     r.ub,
   189  		name:   r.Named(),
   190  	}
   191  }
   192  
   193  // tags implements remote tagging operations.
   194  type tags struct {
   195  	client *http.Client
   196  	ub     *v2.URLBuilder
   197  	name   reference.Named
   198  }
   199  
   200  // All returns all tags
   201  func (t *tags) All(ctx context.Context) ([]string, error) {
   202  	var tags []string
   203  
   204  	listURLStr, err := t.ub.BuildTagsURL(t.name)
   205  	if err != nil {
   206  		return tags, err
   207  	}
   208  
   209  	listURL, err := url.Parse(listURLStr)
   210  	if err != nil {
   211  		return tags, err
   212  	}
   213  
   214  	for {
   215  		resp, err := t.client.Get(listURL.String())
   216  		if err != nil {
   217  			return tags, err
   218  		}
   219  		defer resp.Body.Close()
   220  
   221  		if SuccessStatus(resp.StatusCode) {
   222  			b, err := ioutil.ReadAll(resp.Body)
   223  			if err != nil {
   224  				return tags, err
   225  			}
   226  
   227  			tagsResponse := struct {
   228  				Tags []string `json:"tags"`
   229  			}{}
   230  			if err := json.Unmarshal(b, &tagsResponse); err != nil {
   231  				return tags, err
   232  			}
   233  			tags = append(tags, tagsResponse.Tags...)
   234  			if link := resp.Header.Get("Link"); link != "" {
   235  				linkURLStr := strings.Trim(strings.Split(link, ";")[0], "<>")
   236  				linkURL, err := url.Parse(linkURLStr)
   237  				if err != nil {
   238  					return tags, err
   239  				}
   240  
   241  				listURL = listURL.ResolveReference(linkURL)
   242  			} else {
   243  				return tags, nil
   244  			}
   245  		} else {
   246  			return tags, HandleErrorResponse(resp)
   247  		}
   248  	}
   249  }
   250  
   251  func descriptorFromResponse(response *http.Response) (distribution.Descriptor, error) {
   252  	desc := distribution.Descriptor{}
   253  	headers := response.Header
   254  
   255  	ctHeader := headers.Get("Content-Type")
   256  	if ctHeader == "" {
   257  		return distribution.Descriptor{}, errors.New("missing or empty Content-Type header")
   258  	}
   259  	desc.MediaType = ctHeader
   260  
   261  	digestHeader := headers.Get("Docker-Content-Digest")
   262  	if digestHeader == "" {
   263  		bytes, err := ioutil.ReadAll(response.Body)
   264  		if err != nil {
   265  			return distribution.Descriptor{}, err
   266  		}
   267  		_, desc, err := distribution.UnmarshalManifest(ctHeader, bytes)
   268  		if err != nil {
   269  			return distribution.Descriptor{}, err
   270  		}
   271  		return desc, nil
   272  	}
   273  
   274  	dgst, err := digest.Parse(digestHeader)
   275  	if err != nil {
   276  		return distribution.Descriptor{}, err
   277  	}
   278  	desc.Digest = dgst
   279  
   280  	lengthHeader := headers.Get("Content-Length")
   281  	if lengthHeader == "" {
   282  		return distribution.Descriptor{}, errors.New("missing or empty Content-Length header")
   283  	}
   284  	length, err := strconv.ParseInt(lengthHeader, 10, 64)
   285  	if err != nil {
   286  		return distribution.Descriptor{}, err
   287  	}
   288  	desc.Size = length
   289  
   290  	return desc, nil
   291  
   292  }
   293  
   294  // Get issues a HEAD request for a Manifest against its named endpoint in order
   295  // to construct a descriptor for the tag.  If the registry doesn't support HEADing
   296  // a manifest, fallback to GET.
   297  func (t *tags) Get(ctx context.Context, tag string) (distribution.Descriptor, error) {
   298  	ref, err := reference.WithTag(t.name, tag)
   299  	if err != nil {
   300  		return distribution.Descriptor{}, err
   301  	}
   302  	u, err := t.ub.BuildManifestURL(ref)
   303  	if err != nil {
   304  		return distribution.Descriptor{}, err
   305  	}
   306  
   307  	newRequest := func(method string) (*http.Response, error) {
   308  		req, err := http.NewRequest(method, u, nil)
   309  		if err != nil {
   310  			return nil, err
   311  		}
   312  
   313  		for _, t := range distribution.ManifestMediaTypes() {
   314  			req.Header.Add("Accept", t)
   315  		}
   316  		resp, err := t.client.Do(req)
   317  		return resp, err
   318  	}
   319  
   320  	resp, err := newRequest("HEAD")
   321  	if err != nil {
   322  		return distribution.Descriptor{}, err
   323  	}
   324  	defer resp.Body.Close()
   325  
   326  	switch {
   327  	case resp.StatusCode >= 200 && resp.StatusCode < 400 && len(resp.Header.Get("Docker-Content-Digest")) > 0:
   328  		// if the response is a success AND a Docker-Content-Digest can be retrieved from the headers
   329  		return descriptorFromResponse(resp)
   330  	default:
   331  		// if the response is an error - there will be no body to decode.
   332  		// Issue a GET request:
   333  		//   - for data from a server that does not handle HEAD
   334  		//   - to get error details in case of a failure
   335  		resp, err = newRequest("GET")
   336  		if err != nil {
   337  			return distribution.Descriptor{}, err
   338  		}
   339  		defer resp.Body.Close()
   340  
   341  		if resp.StatusCode >= 200 && resp.StatusCode < 400 {
   342  			return descriptorFromResponse(resp)
   343  		}
   344  		return distribution.Descriptor{}, HandleErrorResponse(resp)
   345  	}
   346  }
   347  
   348  func (t *tags) Lookup(ctx context.Context, digest distribution.Descriptor) ([]string, error) {
   349  	panic("not implemented")
   350  }
   351  
   352  func (t *tags) Tag(ctx context.Context, tag string, desc distribution.Descriptor) error {
   353  	panic("not implemented")
   354  }
   355  
   356  func (t *tags) Untag(ctx context.Context, tag string) error {
   357  	panic("not implemented")
   358  }
   359  
   360  type manifests struct {
   361  	name   reference.Named
   362  	ub     *v2.URLBuilder
   363  	client *http.Client
   364  	etags  map[string]string
   365  }
   366  
   367  func (ms *manifests) Exists(ctx context.Context, dgst digest.Digest) (bool, error) {
   368  	ref, err := reference.WithDigest(ms.name, dgst)
   369  	if err != nil {
   370  		return false, err
   371  	}
   372  	u, err := ms.ub.BuildManifestURL(ref)
   373  	if err != nil {
   374  		return false, err
   375  	}
   376  
   377  	resp, err := ms.client.Head(u)
   378  	if err != nil {
   379  		return false, err
   380  	}
   381  
   382  	if SuccessStatus(resp.StatusCode) {
   383  		return true, nil
   384  	} else if resp.StatusCode == http.StatusNotFound {
   385  		return false, nil
   386  	}
   387  	return false, HandleErrorResponse(resp)
   388  }
   389  
   390  // AddEtagToTag allows a client to supply an eTag to Get which will be
   391  // used for a conditional HTTP request.  If the eTag matches, a nil manifest
   392  // and ErrManifestNotModified error will be returned. etag is automatically
   393  // quoted when added to this map.
   394  func AddEtagToTag(tag, etag string) distribution.ManifestServiceOption {
   395  	return etagOption{tag, etag}
   396  }
   397  
   398  type etagOption struct{ tag, etag string }
   399  
   400  func (o etagOption) Apply(ms distribution.ManifestService) error {
   401  	if ms, ok := ms.(*manifests); ok {
   402  		ms.etags[o.tag] = fmt.Sprintf(`"%s"`, o.etag)
   403  		return nil
   404  	}
   405  	return fmt.Errorf("etag options is a client-only option")
   406  }
   407  
   408  // ReturnContentDigest allows a client to set a the content digest on
   409  // a successful request from the 'Docker-Content-Digest' header. This
   410  // returned digest is represents the digest which the registry uses
   411  // to refer to the content and can be used to delete the content.
   412  func ReturnContentDigest(dgst *digest.Digest) distribution.ManifestServiceOption {
   413  	return contentDigestOption{dgst}
   414  }
   415  
   416  type contentDigestOption struct{ digest *digest.Digest }
   417  
   418  func (o contentDigestOption) Apply(ms distribution.ManifestService) error {
   419  	return nil
   420  }
   421  
   422  func (ms *manifests) Get(ctx context.Context, dgst digest.Digest, options ...distribution.ManifestServiceOption) (distribution.Manifest, error) {
   423  	var (
   424  		digestOrTag string
   425  		ref         reference.Named
   426  		err         error
   427  		contentDgst *digest.Digest
   428  		mediaTypes  []string
   429  	)
   430  
   431  	for _, option := range options {
   432  		switch opt := option.(type) {
   433  		case distribution.WithTagOption:
   434  			digestOrTag = opt.Tag
   435  			ref, err = reference.WithTag(ms.name, opt.Tag)
   436  			if err != nil {
   437  				return nil, err
   438  			}
   439  		case contentDigestOption:
   440  			contentDgst = opt.digest
   441  		case distribution.WithManifestMediaTypesOption:
   442  			mediaTypes = opt.MediaTypes
   443  		default:
   444  			err := option.Apply(ms)
   445  			if err != nil {
   446  				return nil, err
   447  			}
   448  		}
   449  	}
   450  
   451  	if digestOrTag == "" {
   452  		digestOrTag = dgst.String()
   453  		ref, err = reference.WithDigest(ms.name, dgst)
   454  		if err != nil {
   455  			return nil, err
   456  		}
   457  	}
   458  
   459  	if len(mediaTypes) == 0 {
   460  		mediaTypes = distribution.ManifestMediaTypes()
   461  	}
   462  
   463  	u, err := ms.ub.BuildManifestURL(ref)
   464  	if err != nil {
   465  		return nil, err
   466  	}
   467  
   468  	req, err := http.NewRequest("GET", u, nil)
   469  	if err != nil {
   470  		return nil, err
   471  	}
   472  
   473  	for _, t := range mediaTypes {
   474  		req.Header.Add("Accept", t)
   475  	}
   476  
   477  	if _, ok := ms.etags[digestOrTag]; ok {
   478  		req.Header.Set("If-None-Match", ms.etags[digestOrTag])
   479  	}
   480  
   481  	resp, err := ms.client.Do(req)
   482  	if err != nil {
   483  		return nil, err
   484  	}
   485  	defer resp.Body.Close()
   486  	if resp.StatusCode == http.StatusNotModified {
   487  		return nil, distribution.ErrManifestNotModified
   488  	} else if SuccessStatus(resp.StatusCode) {
   489  		if contentDgst != nil {
   490  			dgst, err := digest.Parse(resp.Header.Get("Docker-Content-Digest"))
   491  			if err == nil {
   492  				*contentDgst = dgst
   493  			}
   494  		}
   495  		mt := resp.Header.Get("Content-Type")
   496  		body, err := ioutil.ReadAll(resp.Body)
   497  
   498  		if err != nil {
   499  			return nil, err
   500  		}
   501  		m, _, err := distribution.UnmarshalManifest(mt, body)
   502  		if err != nil {
   503  			return nil, err
   504  		}
   505  		return m, nil
   506  	}
   507  	return nil, HandleErrorResponse(resp)
   508  }
   509  
   510  // Put puts a manifest.  A tag can be specified using an options parameter which uses some shared state to hold the
   511  // tag name in order to build the correct upload URL.
   512  func (ms *manifests) Put(ctx context.Context, m distribution.Manifest, options ...distribution.ManifestServiceOption) (digest.Digest, error) {
   513  	ref := ms.name
   514  	var tagged bool
   515  
   516  	for _, option := range options {
   517  		if opt, ok := option.(distribution.WithTagOption); ok {
   518  			var err error
   519  			ref, err = reference.WithTag(ref, opt.Tag)
   520  			if err != nil {
   521  				return "", err
   522  			}
   523  			tagged = true
   524  		} else {
   525  			err := option.Apply(ms)
   526  			if err != nil {
   527  				return "", err
   528  			}
   529  		}
   530  	}
   531  	mediaType, p, err := m.Payload()
   532  	if err != nil {
   533  		return "", err
   534  	}
   535  
   536  	if !tagged {
   537  		// generate a canonical digest and Put by digest
   538  		_, d, err := distribution.UnmarshalManifest(mediaType, p)
   539  		if err != nil {
   540  			return "", err
   541  		}
   542  		ref, err = reference.WithDigest(ref, d.Digest)
   543  		if err != nil {
   544  			return "", err
   545  		}
   546  	}
   547  
   548  	manifestURL, err := ms.ub.BuildManifestURL(ref)
   549  	if err != nil {
   550  		return "", err
   551  	}
   552  
   553  	putRequest, err := http.NewRequest("PUT", manifestURL, bytes.NewReader(p))
   554  	if err != nil {
   555  		return "", err
   556  	}
   557  
   558  	putRequest.Header.Set("Content-Type", mediaType)
   559  
   560  	resp, err := ms.client.Do(putRequest)
   561  	if err != nil {
   562  		return "", err
   563  	}
   564  	defer resp.Body.Close()
   565  
   566  	if SuccessStatus(resp.StatusCode) {
   567  		dgstHeader := resp.Header.Get("Docker-Content-Digest")
   568  		dgst, err := digest.Parse(dgstHeader)
   569  		if err != nil {
   570  			return "", err
   571  		}
   572  
   573  		return dgst, nil
   574  	}
   575  
   576  	return "", HandleErrorResponse(resp)
   577  }
   578  
   579  func (ms *manifests) Delete(ctx context.Context, dgst digest.Digest) error {
   580  	ref, err := reference.WithDigest(ms.name, dgst)
   581  	if err != nil {
   582  		return err
   583  	}
   584  	u, err := ms.ub.BuildManifestURL(ref)
   585  	if err != nil {
   586  		return err
   587  	}
   588  	req, err := http.NewRequest("DELETE", u, nil)
   589  	if err != nil {
   590  		return err
   591  	}
   592  
   593  	resp, err := ms.client.Do(req)
   594  	if err != nil {
   595  		return err
   596  	}
   597  	defer resp.Body.Close()
   598  
   599  	if SuccessStatus(resp.StatusCode) {
   600  		return nil
   601  	}
   602  	return HandleErrorResponse(resp)
   603  }
   604  
   605  // todo(richardscothern): Restore interface and implementation with merge of #1050
   606  /*func (ms *manifests) Enumerate(ctx context.Context, manifests []distribution.Manifest, last distribution.Manifest) (n int, err error) {
   607  	panic("not supported")
   608  }*/
   609  
   610  type blobs struct {
   611  	name   reference.Named
   612  	ub     *v2.URLBuilder
   613  	client *http.Client
   614  
   615  	statter distribution.BlobDescriptorService
   616  	distribution.BlobDeleter
   617  }
   618  
   619  func sanitizeLocation(location, base string) (string, error) {
   620  	baseURL, err := url.Parse(base)
   621  	if err != nil {
   622  		return "", err
   623  	}
   624  
   625  	locationURL, err := url.Parse(location)
   626  	if err != nil {
   627  		return "", err
   628  	}
   629  
   630  	return baseURL.ResolveReference(locationURL).String(), nil
   631  }
   632  
   633  func (bs *blobs) Stat(ctx context.Context, dgst digest.Digest) (distribution.Descriptor, error) {
   634  	return bs.statter.Stat(ctx, dgst)
   635  
   636  }
   637  
   638  func (bs *blobs) Get(ctx context.Context, dgst digest.Digest) ([]byte, error) {
   639  	reader, err := bs.Open(ctx, dgst)
   640  	if err != nil {
   641  		return nil, err
   642  	}
   643  	defer reader.Close()
   644  
   645  	return ioutil.ReadAll(reader)
   646  }
   647  
   648  func (bs *blobs) Open(ctx context.Context, dgst digest.Digest) (distribution.ReadSeekCloser, error) {
   649  	ref, err := reference.WithDigest(bs.name, dgst)
   650  	if err != nil {
   651  		return nil, err
   652  	}
   653  	blobURL, err := bs.ub.BuildBlobURL(ref)
   654  	if err != nil {
   655  		return nil, err
   656  	}
   657  
   658  	return transport.NewHTTPReadSeeker(bs.client, blobURL,
   659  		func(resp *http.Response) error {
   660  			if resp.StatusCode == http.StatusNotFound {
   661  				return distribution.ErrBlobUnknown
   662  			}
   663  			return HandleErrorResponse(resp)
   664  		}), nil
   665  }
   666  
   667  func (bs *blobs) ServeBlob(ctx context.Context, w http.ResponseWriter, r *http.Request, dgst digest.Digest) error {
   668  	panic("not implemented")
   669  }
   670  
   671  func (bs *blobs) Put(ctx context.Context, mediaType string, p []byte) (distribution.Descriptor, error) {
   672  	writer, err := bs.Create(ctx)
   673  	if err != nil {
   674  		return distribution.Descriptor{}, err
   675  	}
   676  	dgstr := digest.Canonical.Digester()
   677  	n, err := io.Copy(writer, io.TeeReader(bytes.NewReader(p), dgstr.Hash()))
   678  	if err != nil {
   679  		return distribution.Descriptor{}, err
   680  	}
   681  	if n < int64(len(p)) {
   682  		return distribution.Descriptor{}, fmt.Errorf("short copy: wrote %d of %d", n, len(p))
   683  	}
   684  
   685  	desc := distribution.Descriptor{
   686  		MediaType: mediaType,
   687  		Size:      int64(len(p)),
   688  		Digest:    dgstr.Digest(),
   689  	}
   690  
   691  	return writer.Commit(ctx, desc)
   692  }
   693  
   694  type optionFunc func(interface{}) error
   695  
   696  func (f optionFunc) Apply(v interface{}) error {
   697  	return f(v)
   698  }
   699  
   700  // WithMountFrom returns a BlobCreateOption which designates that the blob should be
   701  // mounted from the given canonical reference.
   702  func WithMountFrom(ref reference.Canonical) distribution.BlobCreateOption {
   703  	return optionFunc(func(v interface{}) error {
   704  		opts, ok := v.(*distribution.CreateOptions)
   705  		if !ok {
   706  			return fmt.Errorf("unexpected options type: %T", v)
   707  		}
   708  
   709  		opts.Mount.ShouldMount = true
   710  		opts.Mount.From = ref
   711  
   712  		return nil
   713  	})
   714  }
   715  
   716  func (bs *blobs) Create(ctx context.Context, options ...distribution.BlobCreateOption) (distribution.BlobWriter, error) {
   717  	var opts distribution.CreateOptions
   718  
   719  	for _, option := range options {
   720  		err := option.Apply(&opts)
   721  		if err != nil {
   722  			return nil, err
   723  		}
   724  	}
   725  
   726  	var values []url.Values
   727  
   728  	if opts.Mount.ShouldMount {
   729  		values = append(values, url.Values{"from": {opts.Mount.From.Name()}, "mount": {opts.Mount.From.Digest().String()}})
   730  	}
   731  
   732  	u, err := bs.ub.BuildBlobUploadURL(bs.name, values...)
   733  	if err != nil {
   734  		return nil, err
   735  	}
   736  
   737  	req, err := http.NewRequest("POST", u, nil)
   738  	if err != nil {
   739  		return nil, err
   740  	}
   741  
   742  	resp, err := bs.client.Do(req)
   743  	if err != nil {
   744  		return nil, err
   745  	}
   746  	defer resp.Body.Close()
   747  
   748  	switch resp.StatusCode {
   749  	case http.StatusCreated:
   750  		desc, err := bs.statter.Stat(ctx, opts.Mount.From.Digest())
   751  		if err != nil {
   752  			return nil, err
   753  		}
   754  		return nil, distribution.ErrBlobMounted{From: opts.Mount.From, Descriptor: desc}
   755  	case http.StatusAccepted:
   756  		// TODO(dmcgowan): Check for invalid UUID
   757  		uuid := resp.Header.Get("Docker-Upload-UUID")
   758  		location, err := sanitizeLocation(resp.Header.Get("Location"), u)
   759  		if err != nil {
   760  			return nil, err
   761  		}
   762  
   763  		return &httpBlobUpload{
   764  			statter:   bs.statter,
   765  			client:    bs.client,
   766  			uuid:      uuid,
   767  			startedAt: time.Now(),
   768  			location:  location,
   769  		}, nil
   770  	default:
   771  		return nil, HandleErrorResponse(resp)
   772  	}
   773  }
   774  
   775  func (bs *blobs) Resume(ctx context.Context, id string) (distribution.BlobWriter, error) {
   776  	panic("not implemented")
   777  }
   778  
   779  func (bs *blobs) Delete(ctx context.Context, dgst digest.Digest) error {
   780  	return bs.statter.Clear(ctx, dgst)
   781  }
   782  
   783  type blobStatter struct {
   784  	name   reference.Named
   785  	ub     *v2.URLBuilder
   786  	client *http.Client
   787  }
   788  
   789  func (bs *blobStatter) Stat(ctx context.Context, dgst digest.Digest) (distribution.Descriptor, error) {
   790  	ref, err := reference.WithDigest(bs.name, dgst)
   791  	if err != nil {
   792  		return distribution.Descriptor{}, err
   793  	}
   794  	u, err := bs.ub.BuildBlobURL(ref)
   795  	if err != nil {
   796  		return distribution.Descriptor{}, err
   797  	}
   798  
   799  	resp, err := bs.client.Head(u)
   800  	if err != nil {
   801  		return distribution.Descriptor{}, err
   802  	}
   803  	defer resp.Body.Close()
   804  
   805  	if SuccessStatus(resp.StatusCode) {
   806  		lengthHeader := resp.Header.Get("Content-Length")
   807  		if lengthHeader == "" {
   808  			return distribution.Descriptor{}, fmt.Errorf("missing content-length header for request: %s", u)
   809  		}
   810  
   811  		length, err := strconv.ParseInt(lengthHeader, 10, 64)
   812  		if err != nil {
   813  			return distribution.Descriptor{}, fmt.Errorf("error parsing content-length: %v", err)
   814  		}
   815  
   816  		return distribution.Descriptor{
   817  			MediaType: resp.Header.Get("Content-Type"),
   818  			Size:      length,
   819  			Digest:    dgst,
   820  		}, nil
   821  	} else if resp.StatusCode == http.StatusNotFound {
   822  		return distribution.Descriptor{}, distribution.ErrBlobUnknown
   823  	}
   824  	return distribution.Descriptor{}, HandleErrorResponse(resp)
   825  }
   826  
   827  func buildCatalogValues(maxEntries int, last string) url.Values {
   828  	values := url.Values{}
   829  
   830  	if maxEntries > 0 {
   831  		values.Add("n", strconv.Itoa(maxEntries))
   832  	}
   833  
   834  	if last != "" {
   835  		values.Add("last", last)
   836  	}
   837  
   838  	return values
   839  }
   840  
   841  func (bs *blobStatter) Clear(ctx context.Context, dgst digest.Digest) error {
   842  	ref, err := reference.WithDigest(bs.name, dgst)
   843  	if err != nil {
   844  		return err
   845  	}
   846  	blobURL, err := bs.ub.BuildBlobURL(ref)
   847  	if err != nil {
   848  		return err
   849  	}
   850  
   851  	req, err := http.NewRequest("DELETE", blobURL, nil)
   852  	if err != nil {
   853  		return err
   854  	}
   855  
   856  	resp, err := bs.client.Do(req)
   857  	if err != nil {
   858  		return err
   859  	}
   860  	defer resp.Body.Close()
   861  
   862  	if SuccessStatus(resp.StatusCode) {
   863  		return nil
   864  	}
   865  	return HandleErrorResponse(resp)
   866  }
   867  
   868  func (bs *blobStatter) SetDescriptor(ctx context.Context, dgst digest.Digest, desc distribution.Descriptor) error {
   869  	return nil
   870  }
   871  

View as plain text