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