2016-04-27 11:59:43 +02:00
|
|
|
/*
|
|
|
|
Copyright (c) 2016 VMware, Inc. All Rights Reserved.
|
|
|
|
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 registry
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
2016-06-21 10:39:03 +02:00
|
|
|
"crypto/tls"
|
2016-04-27 11:59:43 +02:00
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
2016-05-17 12:23:45 +02:00
|
|
|
"io"
|
2016-04-27 11:59:43 +02:00
|
|
|
"io/ioutil"
|
|
|
|
"net/http"
|
|
|
|
"net/url"
|
|
|
|
"strconv"
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
"github.com/docker/distribution/manifest/schema1"
|
|
|
|
"github.com/docker/distribution/manifest/schema2"
|
2016-06-21 10:39:03 +02:00
|
|
|
|
2016-06-27 08:37:26 +02:00
|
|
|
"github.com/vmware/harbor/utils"
|
2016-05-24 08:59:36 +02:00
|
|
|
registry_error "github.com/vmware/harbor/utils/registry/error"
|
2016-04-27 11:59:43 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
// Repository holds information of a repository entity
|
|
|
|
type Repository struct {
|
|
|
|
Name string
|
|
|
|
Endpoint *url.URL
|
|
|
|
client *http.Client
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewRepository returns an instance of Repository
|
|
|
|
func NewRepository(name, endpoint string, client *http.Client) (*Repository, error) {
|
|
|
|
name = strings.TrimSpace(name)
|
2016-06-20 16:55:36 +02:00
|
|
|
|
2016-06-21 10:39:03 +02:00
|
|
|
u, err := utils.ParseEndpoint(endpoint)
|
2016-04-27 11:59:43 +02:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
repository := &Repository{
|
|
|
|
Name: name,
|
|
|
|
Endpoint: u,
|
|
|
|
client: client,
|
|
|
|
}
|
|
|
|
|
|
|
|
return repository, nil
|
|
|
|
}
|
|
|
|
|
2016-06-21 10:39:03 +02:00
|
|
|
// NewRepositoryWithModifiers returns an instance of Repository according to the modifiers
|
|
|
|
func NewRepositoryWithModifiers(name, endpoint string, insecure bool, modifiers ...Modifier) (*Repository, error) {
|
|
|
|
t := &http.Transport{
|
|
|
|
TLSClientConfig: &tls.Config{
|
|
|
|
InsecureSkipVerify: insecure,
|
|
|
|
},
|
2016-04-27 11:59:43 +02:00
|
|
|
}
|
|
|
|
|
2016-06-21 10:39:03 +02:00
|
|
|
transport := NewTransport(t, modifiers...)
|
2016-04-27 11:59:43 +02:00
|
|
|
|
2016-08-16 07:45:59 +02:00
|
|
|
return NewRepository(name, endpoint, &http.Client{
|
|
|
|
Transport: transport,
|
|
|
|
})
|
2016-04-27 11:59:43 +02:00
|
|
|
}
|
|
|
|
|
2016-06-01 09:09:10 +02:00
|
|
|
func parseError(err error) error {
|
|
|
|
if urlErr, ok := err.(*url.Error); ok {
|
|
|
|
if regErr, ok := urlErr.Err.(*registry_error.Error); ok {
|
|
|
|
return regErr
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2016-04-27 11:59:43 +02:00
|
|
|
// ListTag ...
|
|
|
|
func (r *Repository) ListTag() ([]string, error) {
|
|
|
|
tags := []string{}
|
|
|
|
req, err := http.NewRequest("GET", buildTagListURL(r.Endpoint.String(), r.Name), nil)
|
|
|
|
if err != nil {
|
|
|
|
return tags, err
|
|
|
|
}
|
|
|
|
|
|
|
|
resp, err := r.client.Do(req)
|
|
|
|
if err != nil {
|
2016-06-01 09:09:10 +02:00
|
|
|
return tags, parseError(err)
|
2016-04-27 11:59:43 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
|
|
|
b, err := ioutil.ReadAll(resp.Body)
|
|
|
|
if err != nil {
|
|
|
|
return tags, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if resp.StatusCode == http.StatusOK {
|
|
|
|
tagsResp := struct {
|
|
|
|
Tags []string `json:"tags"`
|
|
|
|
}{}
|
|
|
|
|
|
|
|
if err := json.Unmarshal(b, &tagsResp); err != nil {
|
|
|
|
return tags, err
|
|
|
|
}
|
|
|
|
|
|
|
|
tags = tagsResp.Tags
|
|
|
|
|
|
|
|
return tags, nil
|
|
|
|
}
|
2016-05-24 08:59:36 +02:00
|
|
|
return tags, ®istry_error.Error{
|
2016-04-27 11:59:43 +02:00
|
|
|
StatusCode: resp.StatusCode,
|
2016-05-24 08:59:36 +02:00
|
|
|
Detail: string(b),
|
2016-04-27 11:59:43 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
// ManifestExist ...
|
|
|
|
func (r *Repository) ManifestExist(reference string) (digest string, exist bool, err error) {
|
|
|
|
req, err := http.NewRequest("HEAD", buildManifestURL(r.Endpoint.String(), r.Name, reference), nil)
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
req.Header.Add(http.CanonicalHeaderKey("Accept"), schema1.MediaTypeManifest)
|
|
|
|
req.Header.Add(http.CanonicalHeaderKey("Accept"), schema2.MediaTypeManifest)
|
|
|
|
|
|
|
|
resp, err := r.client.Do(req)
|
|
|
|
if err != nil {
|
2016-06-01 09:09:10 +02:00
|
|
|
err = parseError(err)
|
2016-04-27 11:59:43 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2016-07-14 11:50:25 +02:00
|
|
|
defer resp.Body.Close()
|
|
|
|
|
2016-04-27 11:59:43 +02:00
|
|
|
if resp.StatusCode == http.StatusOK {
|
|
|
|
exist = true
|
|
|
|
digest = resp.Header.Get(http.CanonicalHeaderKey("Docker-Content-Digest"))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if resp.StatusCode == http.StatusNotFound {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
b, err := ioutil.ReadAll(resp.Body)
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2016-05-24 08:59:36 +02:00
|
|
|
err = ®istry_error.Error{
|
2016-04-27 11:59:43 +02:00
|
|
|
StatusCode: resp.StatusCode,
|
2016-05-24 08:59:36 +02:00
|
|
|
Detail: string(b),
|
2016-04-27 11:59:43 +02:00
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// PullManifest ...
|
|
|
|
func (r *Repository) PullManifest(reference string, acceptMediaTypes []string) (digest, mediaType string, payload []byte, err error) {
|
|
|
|
req, err := http.NewRequest("GET", buildManifestURL(r.Endpoint.String(), r.Name, reference), nil)
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, mediaType := range acceptMediaTypes {
|
|
|
|
req.Header.Add(http.CanonicalHeaderKey("Accept"), mediaType)
|
|
|
|
}
|
|
|
|
|
|
|
|
resp, err := r.client.Do(req)
|
|
|
|
if err != nil {
|
2016-06-01 09:09:10 +02:00
|
|
|
err = parseError(err)
|
2016-04-27 11:59:43 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
defer resp.Body.Close()
|
|
|
|
b, err := ioutil.ReadAll(resp.Body)
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if resp.StatusCode == http.StatusOK {
|
|
|
|
digest = resp.Header.Get(http.CanonicalHeaderKey("Docker-Content-Digest"))
|
|
|
|
mediaType = resp.Header.Get(http.CanonicalHeaderKey("Content-Type"))
|
|
|
|
payload = b
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2016-05-24 08:59:36 +02:00
|
|
|
err = ®istry_error.Error{
|
2016-04-27 11:59:43 +02:00
|
|
|
StatusCode: resp.StatusCode,
|
2016-05-24 08:59:36 +02:00
|
|
|
Detail: string(b),
|
2016-04-27 11:59:43 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// PushManifest ...
|
|
|
|
func (r *Repository) PushManifest(reference, mediaType string, payload []byte) (digest string, err error) {
|
|
|
|
req, err := http.NewRequest("PUT", buildManifestURL(r.Endpoint.String(), r.Name, reference),
|
|
|
|
bytes.NewReader(payload))
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
req.Header.Set(http.CanonicalHeaderKey("Content-Type"), mediaType)
|
|
|
|
|
|
|
|
resp, err := r.client.Do(req)
|
|
|
|
if err != nil {
|
2016-06-01 09:09:10 +02:00
|
|
|
err = parseError(err)
|
2016-04-27 11:59:43 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2016-07-14 11:50:25 +02:00
|
|
|
defer resp.Body.Close()
|
|
|
|
|
2016-04-27 11:59:43 +02:00
|
|
|
if resp.StatusCode == http.StatusCreated {
|
|
|
|
digest = resp.Header.Get(http.CanonicalHeaderKey("Docker-Content-Digest"))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
b, err := ioutil.ReadAll(resp.Body)
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2016-05-24 08:59:36 +02:00
|
|
|
err = ®istry_error.Error{
|
2016-04-27 11:59:43 +02:00
|
|
|
StatusCode: resp.StatusCode,
|
2016-05-24 08:59:36 +02:00
|
|
|
Detail: string(b),
|
2016-04-27 11:59:43 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// DeleteManifest ...
|
|
|
|
func (r *Repository) DeleteManifest(digest string) error {
|
|
|
|
req, err := http.NewRequest("DELETE", buildManifestURL(r.Endpoint.String(), r.Name, digest), nil)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
resp, err := r.client.Do(req)
|
|
|
|
if err != nil {
|
2016-06-01 09:09:10 +02:00
|
|
|
return parseError(err)
|
2016-04-27 11:59:43 +02:00
|
|
|
}
|
|
|
|
|
2016-07-14 11:50:25 +02:00
|
|
|
defer resp.Body.Close()
|
|
|
|
|
2016-04-27 11:59:43 +02:00
|
|
|
if resp.StatusCode == http.StatusAccepted {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
b, err := ioutil.ReadAll(resp.Body)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2016-05-24 08:59:36 +02:00
|
|
|
return ®istry_error.Error{
|
2016-04-27 11:59:43 +02:00
|
|
|
StatusCode: resp.StatusCode,
|
2016-05-24 08:59:36 +02:00
|
|
|
Detail: string(b),
|
2016-04-27 11:59:43 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// DeleteTag ...
|
|
|
|
func (r *Repository) DeleteTag(tag string) error {
|
|
|
|
digest, exist, err := r.ManifestExist(tag)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if !exist {
|
2016-05-24 08:59:36 +02:00
|
|
|
return ®istry_error.Error{
|
2016-04-27 11:59:43 +02:00
|
|
|
StatusCode: http.StatusNotFound,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return r.DeleteManifest(digest)
|
|
|
|
}
|
|
|
|
|
|
|
|
// BlobExist ...
|
|
|
|
func (r *Repository) BlobExist(digest string) (bool, error) {
|
|
|
|
req, err := http.NewRequest("HEAD", buildBlobURL(r.Endpoint.String(), r.Name, digest), nil)
|
|
|
|
if err != nil {
|
|
|
|
return false, err
|
|
|
|
}
|
|
|
|
|
|
|
|
resp, err := r.client.Do(req)
|
|
|
|
if err != nil {
|
2016-06-01 09:09:10 +02:00
|
|
|
return false, parseError(err)
|
2016-04-27 11:59:43 +02:00
|
|
|
}
|
|
|
|
|
2016-07-14 11:50:25 +02:00
|
|
|
defer resp.Body.Close()
|
|
|
|
|
2016-04-27 11:59:43 +02:00
|
|
|
if resp.StatusCode == http.StatusOK {
|
|
|
|
return true, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
if resp.StatusCode == http.StatusNotFound {
|
|
|
|
return false, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
b, err := ioutil.ReadAll(resp.Body)
|
|
|
|
if err != nil {
|
|
|
|
return false, err
|
|
|
|
}
|
|
|
|
|
2016-05-24 08:59:36 +02:00
|
|
|
return false, ®istry_error.Error{
|
2016-04-27 11:59:43 +02:00
|
|
|
StatusCode: resp.StatusCode,
|
2016-05-24 08:59:36 +02:00
|
|
|
Detail: string(b),
|
2016-04-27 11:59:43 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-07-14 11:50:25 +02:00
|
|
|
// PullBlob : client must close data if it is not nil
|
2016-05-17 12:23:45 +02:00
|
|
|
func (r *Repository) PullBlob(digest string) (size int64, data io.ReadCloser, err error) {
|
2016-04-27 11:59:43 +02:00
|
|
|
req, err := http.NewRequest("GET", buildBlobURL(r.Endpoint.String(), r.Name, digest), nil)
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
resp, err := r.client.Do(req)
|
|
|
|
if err != nil {
|
2016-06-01 09:09:10 +02:00
|
|
|
err = parseError(err)
|
2016-04-27 11:59:43 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if resp.StatusCode == http.StatusOK {
|
|
|
|
contengLength := resp.Header.Get(http.CanonicalHeaderKey("Content-Length"))
|
|
|
|
size, err = strconv.ParseInt(contengLength, 10, 64)
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
2016-05-17 12:23:45 +02:00
|
|
|
data = resp.Body
|
|
|
|
return
|
|
|
|
}
|
2016-08-16 07:45:59 +02:00
|
|
|
// can not close the connect if the status code is 200
|
2016-05-17 12:23:45 +02:00
|
|
|
defer resp.Body.Close()
|
2016-07-14 11:50:25 +02:00
|
|
|
|
2016-05-17 12:23:45 +02:00
|
|
|
b, err := ioutil.ReadAll(resp.Body)
|
|
|
|
if err != nil {
|
2016-04-27 11:59:43 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2016-05-24 08:59:36 +02:00
|
|
|
err = ®istry_error.Error{
|
2016-04-27 11:59:43 +02:00
|
|
|
StatusCode: resp.StatusCode,
|
2016-05-24 08:59:36 +02:00
|
|
|
Detail: string(b),
|
2016-04-27 11:59:43 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r *Repository) initiateBlobUpload(name string) (location, uploadUUID string, err error) {
|
|
|
|
req, err := http.NewRequest("POST", buildInitiateBlobUploadURL(r.Endpoint.String(), r.Name), nil)
|
|
|
|
req.Header.Set(http.CanonicalHeaderKey("Content-Length"), "0")
|
|
|
|
|
|
|
|
resp, err := r.client.Do(req)
|
|
|
|
if err != nil {
|
2016-06-01 09:09:10 +02:00
|
|
|
err = parseError(err)
|
2016-04-27 11:59:43 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2016-07-14 11:50:25 +02:00
|
|
|
defer resp.Body.Close()
|
|
|
|
|
2016-04-27 11:59:43 +02:00
|
|
|
if resp.StatusCode == http.StatusAccepted {
|
|
|
|
location = resp.Header.Get(http.CanonicalHeaderKey("Location"))
|
|
|
|
uploadUUID = resp.Header.Get(http.CanonicalHeaderKey("Docker-Upload-UUID"))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
b, err := ioutil.ReadAll(resp.Body)
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2016-05-24 08:59:36 +02:00
|
|
|
err = ®istry_error.Error{
|
2016-04-27 11:59:43 +02:00
|
|
|
StatusCode: resp.StatusCode,
|
2016-05-24 08:59:36 +02:00
|
|
|
Detail: string(b),
|
2016-04-27 11:59:43 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2016-05-17 12:23:45 +02:00
|
|
|
func (r *Repository) monolithicBlobUpload(location, digest string, size int64, data io.Reader) error {
|
|
|
|
req, err := http.NewRequest("PUT", buildMonolithicBlobUploadURL(location, digest), data)
|
2016-04-27 11:59:43 +02:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
resp, err := r.client.Do(req)
|
|
|
|
if err != nil {
|
2016-06-01 09:09:10 +02:00
|
|
|
return parseError(err)
|
2016-04-27 11:59:43 +02:00
|
|
|
}
|
|
|
|
|
2016-07-14 11:50:25 +02:00
|
|
|
defer resp.Body.Close()
|
|
|
|
|
2016-04-27 11:59:43 +02:00
|
|
|
if resp.StatusCode == http.StatusCreated {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
b, err := ioutil.ReadAll(resp.Body)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2016-05-24 08:59:36 +02:00
|
|
|
return ®istry_error.Error{
|
2016-04-27 11:59:43 +02:00
|
|
|
StatusCode: resp.StatusCode,
|
2016-05-24 08:59:36 +02:00
|
|
|
Detail: string(b),
|
2016-04-27 11:59:43 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// PushBlob ...
|
2016-05-17 12:23:45 +02:00
|
|
|
func (r *Repository) PushBlob(digest string, size int64, data io.Reader) error {
|
2016-04-27 11:59:43 +02:00
|
|
|
location, _, err := r.initiateBlobUpload(r.Name)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return r.monolithicBlobUpload(location, digest, size, data)
|
|
|
|
}
|
|
|
|
|
|
|
|
// DeleteBlob ...
|
|
|
|
func (r *Repository) DeleteBlob(digest string) error {
|
|
|
|
req, err := http.NewRequest("DELETE", buildBlobURL(r.Endpoint.String(), r.Name, digest), nil)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
resp, err := r.client.Do(req)
|
|
|
|
if err != nil {
|
2016-06-01 09:09:10 +02:00
|
|
|
return parseError(err)
|
2016-04-27 11:59:43 +02:00
|
|
|
}
|
|
|
|
|
2016-07-14 11:50:25 +02:00
|
|
|
defer resp.Body.Close()
|
|
|
|
|
2016-04-27 11:59:43 +02:00
|
|
|
if resp.StatusCode == http.StatusAccepted {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
b, err := ioutil.ReadAll(resp.Body)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2016-05-24 08:59:36 +02:00
|
|
|
return ®istry_error.Error{
|
2016-04-27 11:59:43 +02:00
|
|
|
StatusCode: resp.StatusCode,
|
2016-05-24 08:59:36 +02:00
|
|
|
Detail: string(b),
|
2016-04-27 11:59:43 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func buildPingURL(endpoint string) string {
|
|
|
|
return fmt.Sprintf("%s/v2/", endpoint)
|
|
|
|
}
|
|
|
|
|
|
|
|
func buildTagListURL(endpoint, repoName string) string {
|
|
|
|
return fmt.Sprintf("%s/v2/%s/tags/list", endpoint, repoName)
|
|
|
|
}
|
|
|
|
|
|
|
|
func buildManifestURL(endpoint, repoName, reference string) string {
|
|
|
|
return fmt.Sprintf("%s/v2/%s/manifests/%s", endpoint, repoName, reference)
|
|
|
|
}
|
|
|
|
|
|
|
|
func buildBlobURL(endpoint, repoName, reference string) string {
|
|
|
|
return fmt.Sprintf("%s/v2/%s/blobs/%s", endpoint, repoName, reference)
|
|
|
|
}
|
|
|
|
|
|
|
|
func buildInitiateBlobUploadURL(endpoint, repoName string) string {
|
|
|
|
return fmt.Sprintf("%s/v2/%s/blobs/uploads/", endpoint, repoName)
|
|
|
|
}
|
|
|
|
|
|
|
|
func buildMonolithicBlobUploadURL(location, digest string) string {
|
2016-08-16 07:45:59 +02:00
|
|
|
query := ""
|
|
|
|
if strings.ContainsRune(location, '?') {
|
|
|
|
query = "&"
|
|
|
|
} else {
|
|
|
|
query = "?"
|
|
|
|
}
|
|
|
|
query += fmt.Sprintf("digest=%s", digest)
|
|
|
|
return fmt.Sprintf("%s%s", location, query)
|
2016-04-27 11:59:43 +02:00
|
|
|
}
|