diff --git a/authbas.go b/authbas.go index c26dfb0..705eee9 100644 --- a/authbas.go +++ b/authbas.go @@ -1,24 +1,20 @@ package client import ( - "encoding/base64" + "encoding/base64" ) - type Authenticator interface { - MakeHeader(user, pass string) (key, value string, err error) + MakeHeader(user, pass string) (key, value string, err error) } -type BasicAuthenticator struct {} +type BasicAuthenticator struct{} -func NewBasicAuthenticator() *BasicAuthenticator{ - return &BasicAuthenticator{} +func NewBasicAuthenticator() *BasicAuthenticator { + return &BasicAuthenticator{} } -func (auth *BasicAuthenticator) MakeHeader(user, pass string) (string, string, error){ - pair := base64.StdEncoding.EncodeToString([]byte(user + ":" + pass)) - return "Autentification", "Basic " + pair, nil +func (auth *BasicAuthenticator) MakeHeader(user, pass string) (string, string, error) { + pair := base64.StdEncoding.EncodeToString([]byte(user + ":" + pass)) + return "Authorization", "Basic " + pair, nil } - - - diff --git a/blobexist.go b/blobexist.go new file mode 100644 index 0000000..bc69013 --- /dev/null +++ b/blobexist.go @@ -0,0 +1,59 @@ +package client + +import ( + "context" + "fmt" + "net/http" + "strconv" +) + +func (cli *Client) BlobExists(ctx context.Context, rawref string) (bool, int64, error) { + var err error + var exist bool + var size int64 + + ref, err := NewReference(rawref) + if err != nil { + return exist, size, err + } + uri := ref.Blob() + user, pass := ref.Userinfo() + + fmt.Println(uri) + req, err := http.NewRequestWithContext(ctx, http.MethodHead, uri, nil) + if err != nil { + return exist, size, err + } + req.Header.Set("User-Agent", cli.userAgent) + req.Header.Set("Accept", "*/*") + + if cli.authenticator != nil { + authHeader, authKey, err := cli.authenticator.MakeHeader(user, pass) + if err != nil { + return exist, size, err + } + req.Header.Set(authHeader, authKey) + } + + resp, err := cli.httpClient.Do(req) + if err != nil { + return exist, size, err + } + defer resp.Body.Close() + + if resp.StatusCode == http.StatusNotFound { + return exist, size, err + } + if resp.StatusCode != http.StatusOK { + err := fmt.Errorf("Unxected response code %s", resp.Status) + return exist, size, err + } + contentLength := resp.Header.Get("Content-Length") + size, err = strconv.ParseInt(contentLength, 10, 64) + if err != nil { + return exist, size, err + } + + exist = true + return exist, size, err +} diff --git a/client.go b/client.go index 78e7779..5439c48 100644 --- a/client.go +++ b/client.go @@ -5,28 +5,59 @@ import ( "net/http" ) - type Client struct { httpClient *http.Client - authenticator Authenticator - userAgent string + authenticator Authenticator + userAgent string } -func NewClient(skipTLSVerify bool) *Client { - transport := &http.Transport{ - TLSClientConfig: &tls.Config{ - InsecureSkipVerify: skipTLSVerify, - }, - } +func NewClient() *Client { + defaultTripper := NewDefaultTransport() httpClient := &http.Client{ - Transport: transport, + Transport: defaultTripper, } return &Client{ httpClient: httpClient, - userAgent: "ociClient/1.0", + userAgent: "ociClient/1.0", } } func (cli *Client) SetAuthenticator(auth Authenticator) { - cli.authenticator = auth + cli.authenticator = auth +} + +func (cli *Client) SetTransport(transport http.RoundTripper) { + cli.httpClient.Transport = transport +} + +type WrapTransport struct { + transport http.RoundTripper +} + +func NewWrapTransport(transport http.RoundTripper) *WrapTransport { + return &WrapTransport{ + transport: transport, + } +} + +func (wrap *WrapTransport) RoundTrip(req *http.Request) (*http.Response, error) { + return wrap.transport.RoundTrip(req) +} + +type DefaultTransport struct { + transport http.RoundTripper +} + +func NewDefaultTransport() *DefaultTransport { + return &DefaultTransport{ + transport: &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + }, + }, + } +} + +func (wrap *DefaultTransport) RoundTrip(req *http.Request) (*http.Response, error) { + return wrap.transport.RoundTrip(req) } diff --git a/client_test.go b/client_test.go new file mode 100644 index 0000000..4e40da7 --- /dev/null +++ b/client_test.go @@ -0,0 +1,134 @@ +package client + +import ( + "github.com/stretchr/testify/require" + + "bytes" + "context" + "encoding/json" + "fmt" + "math/rand" + "testing" + "time" +) + +func TestClientGetManifest(t *testing.T) { + rawrefs := []string{ + "mirror.gcr.io/alpine:3.20.0", + "mirror.gcr.io/alpine:sha256:29e5ba63e79337818e6c63cfcc68e2ab4e9ca483853b2de303bfbfba9372426c", + } + for _, rawref := range rawrefs { + cli := NewClient() + ctx, _ := context.WithTimeout(context.Background(), 10*time.Second) + exist, mime, man, err := cli.GetManifest(ctx, rawref) + require.NoError(t, err) + require.True(t, exist) + + fmt.Printf("Type: %s\n", mime) + buffer := bytes.NewBuffer(nil) + err = json.Indent(buffer, man, " ", " ") + require.NoError(t, err) + //fmt.Printf("%s\n", buffer.String()) + } +} + +func xxxTestClientManifestExists(t *testing.T) { + rawrefs := []string{ + "mirror.gcr.io/alpine:3.20.0", + "mirror.gcr.io/alpine:sha256:29e5ba63e79337818e6c63cfcc68e2ab4e9ca483853b2de303bfbfba9372426c", + } + for _, rawref := range rawrefs { + cli := NewClient() + ctx, _ := context.WithTimeout(context.Background(), 10*time.Second) + exist, mime, size, csum, err := cli.ManifestExists(ctx, rawref) + require.NoError(t, err) + require.True(t, exist) + + fmt.Printf("MIME: %s\n", mime) + fmt.Printf("Size: %d\n", size) + fmt.Printf("Sum: %s\n", csum) + fmt.Printf("Typ: %d\n", DigestType(csum)) + } +} + +func xxxTestClientBlobExists(t *testing.T) { + rawrefs := []string{ + "mirror.gcr.io/alpine:sha256:3b8747b05489980f63da1d2b8e5a444c55777f69540394397b0bc1c76c3e41f2", + } + for _, rawref := range rawrefs { + cli := NewClient() + ctx, _ := context.WithTimeout(context.Background(), 10*time.Second) + exist, size, err := cli.BlobExists(ctx, rawref) + require.NoError(t, err) + require.True(t, exist) + + fmt.Printf("Size: %d\n", size) + } +} + +func xxxTestClientGetBlob(t *testing.T) { + rawrefs := []string{ + "mirror.gcr.io/alpine:sha256:3b8747b05489980f63da1d2b8e5a444c55777f69540394397b0bc1c76c3e41f2", + } + for _, rawref := range rawrefs { + cli := NewClient() + ctx, _ := context.WithTimeout(context.Background(), 10*time.Second) + buffer := bytes.NewBuffer(nil) + exist, err := cli.GetBlob(ctx, rawref, buffer) + require.NoError(t, err) + require.True(t, exist) + fmt.Printf("Size: %d\n", len(buffer.Bytes())) + } +} + +func TestClientGetUpload(t *testing.T) { + rawrefs := []string{ + "mstore:mstore@localhost:1025/alpine:3.20.0", + } + for _, rawref := range rawrefs { + var err error + var loc string + { + cli := NewClient() + cli.SetAuthenticator(NewBasicAuthenticator()) + ctx, _ := context.WithTimeout(context.Background(), 10*time.Second) + loc, err = cli.GetUpload(ctx, rawref) + require.NoError(t, err) + fmt.Printf("Location: %s\n", loc) + } + { + + srcsize := 1024 + 145 + srcdata := make([]byte, srcsize) + _, err = rand.Read(srcdata) + require.NoError(t, err) + + src := bytes.NewReader(srcdata) + digest := SHA256Digest(srcdata) + + cli := NewClient() + cli.SetAuthenticator(NewBasicAuthenticator()) + ctx, _ := context.WithTimeout(context.Background(), 10*time.Second) + bloc, err := cli.PutUpload(ctx, rawref, src, loc, digest, int64(len(srcdata))) + require.NoError(t, err) + fmt.Printf("Location: %s\n", bloc) + } + { + srcsize := 1024 + 145 + srcdata := make([]byte, srcsize) + _, err = rand.Read(srcdata) + require.NoError(t, err) + + src := bytes.NewReader(srcdata) + //digest := SHA256Digest(srcdata) + + cli := NewClient() + cli.SetAuthenticator(NewBasicAuthenticator()) + ctx, _ := context.WithTimeout(context.Background(), 10*time.Second) + bloc, err := cli.PatchUpload(ctx, rawref, src, loc, int64(len(srcdata))) + require.NoError(t, err) + fmt.Printf("Location: %s\n", bloc) + } + + } +} diff --git a/copyctx.go b/copyctx.go new file mode 100644 index 0000000..0dc3091 --- /dev/null +++ b/copyctx.go @@ -0,0 +1,39 @@ +package client + +import ( + "context" + "errors" + "io" +) + +func Copy(ctx context.Context, writer io.Writer, reader io.Reader) (int64, error) { + var err error + var size int64 + var halt bool + buffer := make([]byte, 1024*4) + for { + select { + case <-ctx.Done(): + err = errors.New("Break copy by context") + break + default: + } + rsize, err := reader.Read(buffer) + if err == io.EOF { + err = nil + halt = true + } + if err != nil { + return size, err + } + wsize, err := writer.Write(buffer[0:rsize]) + size += int64(wsize) + if err != nil { + return size, err + } + if halt { + break + } + } + return size, err +} diff --git a/copyctx_test.go b/copyctx_test.go new file mode 100644 index 0000000..f289cc1 --- /dev/null +++ b/copyctx_test.go @@ -0,0 +1,28 @@ +package client + +import ( + "github.com/stretchr/testify/require" + + "bytes" + "context" + "fmt" + "math/rand" + "testing" +) + +func TestCopy(t *testing.T) { + srcsize := 1024 + 145 + srcdata := make([]byte, srcsize) + _, err := rand.Read(srcdata) + require.NoError(t, err) + + src := bytes.NewReader(srcdata) + dst := bytes.NewBuffer(nil) + + ctx := context.Background() + recsize, err := Copy(ctx, dst, src) + require.NoError(t, err) + + fmt.Printf("Size: %d %d\n", recsize, srcsize) + require.Equal(t, int64(srcsize), recsize) +} diff --git a/delman.go b/delman.go new file mode 100644 index 0000000..f5b3c61 --- /dev/null +++ b/delman.go @@ -0,0 +1,49 @@ +package client + +import ( + "context" + "fmt" + "net/http" +) + +func (cli *Client) DeleteManifest(ctx context.Context, rawref string) (bool, error) { + var err error + var exist bool + + ref, err := NewReference(rawref) + if err != nil { + return exist, err + } + uri := ref.Manifest() + user, pass := ref.Userinfo() + + req, err := http.NewRequestWithContext(ctx, http.MethodDelete, uri, nil) + if err != nil { + return exist, err + } + req.Header.Set("User-Agent", cli.userAgent) + req.Header.Set("Accept", "*/*") + + if cli.authenticator != nil { + authHeader, authKey, err := cli.authenticator.MakeHeader(user, pass) + if err != nil { + return exist, err + } + req.Header.Set(authHeader, authKey) + } + + resp, err := cli.httpClient.Do(req) + if err != nil { + return exist, err + } + defer resp.Body.Close() + if resp.StatusCode == http.StatusNotFound { + return exist, err + } + if resp.StatusCode != http.StatusOK { + err := fmt.Errorf("Unxected response code %s", resp.Status) + return exist, err + } + exist = true + return exist, err +} diff --git a/digest.go b/digest.go index becbe6e..8874348 100644 --- a/digest.go +++ b/digest.go @@ -4,7 +4,7 @@ import ( "crypto/sha256" "crypto/sha512" "encoding/hex" - "strings" + "strings" ) func SHA256Digest(src []byte) string { @@ -22,29 +22,28 @@ func SHA512Digest(src []byte) string { } const ( - Undefined int = iota - SHA256 - SHA512 + Undefined int = iota + SHA256 + SHA512 ) - func DigestType(digest string) int { - var err error - var typ int - digest = strings.ToLower(digest) - digest = strings.TrimPrefix(digest, "sha256:") - digest = strings.TrimPrefix(digest, "sha512:") - decoded, err := hex.DecodeString(digest) - if err != nil { - return Undefined - } - switch (len(decoded)) { - case 64: - typ = SHA256 - case 128: - typ = SHA512 - default: - typ = Undefined - } - return typ + var err error + var typ int + digest = strings.ToLower(digest) + digest = strings.TrimPrefix(digest, "sha256:") + digest = strings.TrimPrefix(digest, "sha512:") + decoded, err := hex.DecodeString(digest) + if err != nil { + return Undefined + } + switch len(decoded) { + case 32: + typ = SHA256 + case 64: + typ = SHA512 + default: + typ = Undefined + } + return typ } diff --git a/getblob.go b/getblob.go new file mode 100644 index 0000000..a314331 --- /dev/null +++ b/getblob.go @@ -0,0 +1,63 @@ +package client + +import ( + "context" + "fmt" + "io" + "net/http" + "strconv" +) + +func (cli *Client) GetBlob(ctx context.Context, rawref string, writer io.Writer) (bool, error) { + var err error + var exist bool + + ref, err := NewReference(rawref) + if err != nil { + return exist, err + } + uri := ref.Blob() + user, pass := ref.Userinfo() + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, uri, nil) + if err != nil { + return exist, err + } + req.Header.Set("User-Agent", cli.userAgent) + req.Header.Set("Accept", "*/*") + + if cli.authenticator != nil { + authHeader, authKey, err := cli.authenticator.MakeHeader(user, pass) + if err != nil { + return exist, err + } + req.Header.Set(authHeader, authKey) + } + + resp, err := cli.httpClient.Do(req) + if err != nil { + return exist, err + } + defer resp.Body.Close() + + if resp.StatusCode == http.StatusNotFound { + return exist, err + } + if resp.StatusCode != http.StatusOK { + err := fmt.Errorf("Unexpected response code %s", resp.Status) + return exist, err + } + contentLength := resp.Header.Get("Content-Length") + blobSize, err := strconv.ParseInt(contentLength, 10, 64) + if err != nil { + return exist, err + } + + recSize, err := Copy(ctx, writer, resp.Body) + if blobSize != recSize { + err := fmt.Errorf("Mismatch declared and actual body size, %d and %d", blobSize, recSize) + return exist, err + } + exist = true + return exist, err +} diff --git a/getman.go b/getman.go index cf66473..c4a05cd 100644 --- a/getman.go +++ b/getman.go @@ -4,9 +4,9 @@ import ( "bytes" "context" "fmt" - "io" "net/http" "strconv" + "strings" ) func (cli *Client) GetManifest(ctx context.Context, rawref string) (bool, string, []byte, error) { @@ -20,13 +20,23 @@ func (cli *Client) GetManifest(ctx context.Context, rawref string) (bool, string return exist, mime, man, err } uri := ref.Manifest() + user, pass := ref.Userinfo() req, err := http.NewRequestWithContext(ctx, http.MethodGet, uri, nil) if err != nil { return exist, mime, man, err } req.Header.Set("User-Agent", cli.userAgent) - req.Header.Set("Accept", "*/*") + req.Header.Set("Accept", "*/*") + + if cli.authenticator != nil { + authHeader, authKey, err := cli.authenticator.MakeHeader(user, pass) + if err != nil { + return exist, mime, man, err + } + req.Header.Set(authHeader, authKey) + } + resp, err := cli.httpClient.Do(req) if err != nil { return exist, mime, man, err @@ -51,12 +61,35 @@ func (cli *Client) GetManifest(ctx context.Context, rawref string) (bool, string return exist, mime, man, err } buffer := bytes.NewBuffer(nil) - recSize, err := io.Copy(buffer, resp.Body) + recSize, err := Copy(ctx, buffer, resp.Body) if manSize != recSize { - err := fmt.Errorf("Mismatch declared and actual body size") + err := fmt.Errorf("Mismatch declared and actual body size, %d and %d", manSize, recSize) return exist, mime, man, err } - man = buffer.Bytes() + man = buffer.Bytes() + + csum := resp.Header.Get("Docker-Content-Digest") + if csum == "" { + err := fmt.Errorf("Empty digest declaration") + return exist, mime, man, err + } + csum = strings.ToLower(csum) + switch DigestType(csum) { + case SHA256: + if csum != SHA256Digest(man) { + err := fmt.Errorf("Mismatch digest and actual declaration") + return exist, mime, man, err + } + case SHA512: + if csum != SHA256Digest(man) { + err := fmt.Errorf("Mismatch digest and actual declaration") + return exist, mime, man, err + } + default: + err := fmt.Errorf("Unknown digest type: %s", csum) + return exist, mime, man, err + } + exist = true return exist, mime, man, err } diff --git a/getman_test.go b/getman_test.go deleted file mode 100644 index 4fab84c..0000000 --- a/getman_test.go +++ /dev/null @@ -1,34 +0,0 @@ -package client - -import ( - "github.com/stretchr/testify/require" - - "testing" - "fmt" - "time" - "context" - "encoding/json" - "bytes" -) - - -func TestClientGetManigest(t *testing.T) { - rawrefs := []string{ - "mirror.gcr.io/alpine:3.20.0", - "mirror.gcr.io/alpine:sha256:29e5ba63e79337818e6c63cfcc68e2ab4e9ca483853b2de303bfbfba9372426c", - } - for _, rawref := range rawrefs { - cli := NewClient(true) - ctx, _ := context.WithTimeout(context.Background(), 10 * time.Second) - exist, mime, man, err := cli.GetManifest(ctx, rawref) - require.NoError(t, err) - require.True(t, exist) - - fmt.Printf("MIME: %s\n", mime) - buffer := bytes.NewBuffer(nil) - err = json.Indent(buffer, man, " ", " ") - require.NoError(t, err) - fmt.Printf("%s\n", buffer.String()) - } -} - diff --git a/getupload.go b/getupload.go new file mode 100644 index 0000000..7903ac6 --- /dev/null +++ b/getupload.go @@ -0,0 +1,51 @@ +package client + +import ( + "context" + "fmt" + "net/http" +) + +func (cli *Client) GetUpload(ctx context.Context, rawref string) (string, error) { + var err error + var loc string + + ref, err := NewReference(rawref) + if err != nil { + return loc, err + } + uri := ref.Upload() + user, pass := ref.Userinfo() + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, uri, nil) + if err != nil { + return loc, err + } + req.Header.Set("User-Agent", cli.userAgent) + req.Header.Set("Accept", "*/*") + + if cli.authenticator != nil { + authHeader, authKey, err := cli.authenticator.MakeHeader(user, pass) + if err != nil { + return loc, err + } + req.Header.Set(authHeader, authKey) + } + + resp, err := cli.httpClient.Do(req) + if err != nil { + return loc, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusAccepted { + err := fmt.Errorf("Unxected response code %s", resp.Status) + return loc, err + } + loc = resp.Header.Get("Location") + if loc == "" { + err := fmt.Errorf("Empty location declaration") + return loc, err + } + return loc, err +} diff --git a/manexist.go b/manexist.go new file mode 100644 index 0000000..9292e3c --- /dev/null +++ b/manexist.go @@ -0,0 +1,70 @@ +package client + +import ( + "context" + "fmt" + "net/http" + "strconv" +) + +func (cli *Client) ManifestExists(ctx context.Context, rawref string) (bool, string, int64, string, error) { + var err error + var exist bool + var mime string + var size int64 + var csum string + + ref, err := NewReference(rawref) + if err != nil { + return exist, mime, size, csum, err + } + uri := ref.Manifest() + user, pass := ref.Userinfo() + + req, err := http.NewRequestWithContext(ctx, http.MethodHead, uri, nil) + if err != nil { + return exist, mime, size, csum, err + } + req.Header.Set("User-Agent", cli.userAgent) + req.Header.Set("Accept", "*/*") + + if cli.authenticator != nil { + authHeader, authKey, err := cli.authenticator.MakeHeader(user, pass) + if err != nil { + return exist, mime, size, csum, err + } + req.Header.Set(authHeader, authKey) + } + + resp, err := cli.httpClient.Do(req) + if err != nil { + return exist, mime, size, csum, err + } + defer resp.Body.Close() + + if resp.StatusCode == http.StatusNotFound { + return exist, mime, size, csum, err + } + if resp.StatusCode != http.StatusOK { + err := fmt.Errorf("Unxected response code %s", resp.Status) + return exist, mime, size, csum, err + } + contentLength := resp.Header.Get("Content-Length") + size, err = strconv.ParseInt(contentLength, 10, 64) + if err != nil { + return exist, mime, size, csum, err + } + mime = resp.Header.Get("Content-Type") + if mime == "" { + err := fmt.Errorf("Empty MIME type declaration") + return exist, mime, size, csum, err + } + csum = resp.Header.Get("Docker-Content-Digest") + if csum == "" { + err := fmt.Errorf("Empty digest declaration") + return exist, mime, size, csum, err + } + + exist = true + return exist, mime, size, csum, err +} diff --git a/mimetyp.go b/mimetyp.go new file mode 100644 index 0000000..4f84603 --- /dev/null +++ b/mimetyp.go @@ -0,0 +1,6 @@ +package client + +const ( + MediaTypeDDMLv2 = "application/vnd.docker.distribution.manifest.list.v2+json" + MediaTypeDDMv2 = "application/vnd.docker.distribution.manifest.v2+json" +) diff --git a/pathupload.go b/pathupload.go new file mode 100644 index 0000000..cf665bb --- /dev/null +++ b/pathupload.go @@ -0,0 +1,54 @@ +package client + +import ( + "context" + "fmt" + "io" + "net/http" + "strconv" +) + +func (cli *Client) PatchUpload(ctx context.Context, rawref string, src io.Reader, uploc string, size int64) (string, error) { + var err error + var ouloc string + + ref, err := NewReference(rawref) + if err != nil { + return ouloc, err + } + uri, err := ref.Patch(uploc) + if err != nil { + return ouloc, err + } + user, pass := ref.Userinfo() + + req, err := http.NewRequestWithContext(ctx, http.MethodPatch, uri, src) + if err != nil { + return ouloc, err + } + req.Header.Set("User-Agent", cli.userAgent) + req.Header.Set("Content-Type", "application/octet-stream") + req.Header.Set("Content-Length", strconv.FormatInt(size, 10)) + if cli.authenticator != nil { + authHeader, authKey, err := cli.authenticator.MakeHeader(user, pass) + if err != nil { + return ouloc, err + } + req.Header.Set(authHeader, authKey) + } + resp, err := cli.httpClient.Do(req) + if err != nil { + return ouloc, err + } + resp.Body.Close() + if resp.StatusCode != http.StatusAccepted { + err = fmt.Errorf("Upload not accepted, code %d", resp.StatusCode) + return ouloc, err + } + ouloc = resp.Header.Get("Location") + if ouloc == "" { + err := fmt.Errorf("Empty blob location declaration") + return ouloc, err + } + return ouloc, err +} diff --git a/putman.go b/putman.go index 6552d6a..6dcb1a7 100644 --- a/putman.go +++ b/putman.go @@ -15,7 +15,7 @@ func (cli *Client) PutManifest(ctx context.Context, rawref string, man []byte, m return err } uri := ref.Manifest() - user, pass := ref.Userinfo() + user, pass := ref.Userinfo() buffer := bytes.NewBuffer(man) req, err := http.NewRequestWithContext(ctx, http.MethodPut, uri, buffer) @@ -25,19 +25,19 @@ func (cli *Client) PutManifest(ctx context.Context, rawref string, man []byte, m req.Header.Set("User-Agent", cli.userAgent) req.Header.Set("Docker-Content-Digest", SHA256Digest(man)) req.Header.Set("Content-Type", mime) - if cli.authenticator != nil { - authHeader, authKey, err := cli.authenticator.MakeHeader(user, pass) - if err != nil { - return err - } - req.Header.Set(authHeader, authKey) - } + if cli.authenticator != nil { + authHeader, authKey, err := cli.authenticator.MakeHeader(user, pass) + if err != nil { + return err + } + req.Header.Set(authHeader, authKey) + } resp, err := cli.httpClient.Do(req) if err != nil { return err } resp.Body.Close() - if resp.StatusCode != http.StatusAccepted { + if resp.StatusCode != http.StatusAccepted { err = fmt.Errorf("Manifest not accepted, code %d", resp.StatusCode) return err } diff --git a/putupload.go b/putupload.go new file mode 100644 index 0000000..8578457 --- /dev/null +++ b/putupload.go @@ -0,0 +1,54 @@ +package client + +import ( + "context" + "fmt" + "io" + "net/http" + "strconv" +) + +func (cli *Client) PutUpload(ctx context.Context, rawref string, src io.Reader, uploc, digest string, size int64) (string, error) { + var err error + var bloc string + + ref, err := NewReference(rawref) + if err != nil { + return bloc, err + } + uri, err := ref.Put(uploc, digest) + if err != nil { + return bloc, err + } + user, pass := ref.Userinfo() + + req, err := http.NewRequestWithContext(ctx, http.MethodPut, uri, src) + if err != nil { + return bloc, err + } + req.Header.Set("User-Agent", cli.userAgent) + req.Header.Set("Content-Type", "application/octet-stream") + req.Header.Set("Content-Length", strconv.FormatInt(size, 10)) + if cli.authenticator != nil { + authHeader, authKey, err := cli.authenticator.MakeHeader(user, pass) + if err != nil { + return bloc, err + } + req.Header.Set(authHeader, authKey) + } + resp, err := cli.httpClient.Do(req) + if err != nil { + return bloc, err + } + resp.Body.Close() + if resp.StatusCode != http.StatusCreated { + err = fmt.Errorf("Upload not accepted, code %d", resp.StatusCode) + return bloc, err + } + bloc = resp.Header.Get("Location") + if bloc == "" { + err := fmt.Errorf("Empty blob location declaration") + return bloc, err + } + return bloc, err +} diff --git a/refer.go b/refer.go deleted file mode 100644 index ae9dfc1..0000000 --- a/refer.go +++ /dev/null @@ -1,55 +0,0 @@ -package client - -import ( - "errors" - "net/url" - "strings" -) - -type Reference struct { - urlobj *url.URL - user, pass string - repo, tag string -} - -func NewReference(rawref string) (*Reference, error) { - ref := &Reference{} - if !strings.Contains(rawref, "://") { - rawref = "https://" + rawref - } - urlobj, err := url.Parse(rawref) - if err != nil { - return ref, err - } - if urlobj.User != nil { - ref.user = urlobj.User.Username() - ref.pass, _ = urlobj.User.Password() - urlobj.User = nil - } - repotag := strings.SplitN(urlobj.Path, ":", 2) - if len(repotag) != 2 { - err = errors.New("Incorrect repo") - return ref, err - } - ref.urlobj = urlobj - ref.urlobj.Path = "" - ref.repo = repotag[0] - ref.tag = repotag[1] - ref.urlobj = urlobj - - return ref, err -} - -func (ref *Reference) Manifest() string { - curl := ref.urlobj.JoinPath("/v2", ref.repo, "/manifests", ref.tag) - return curl.String() -} - -func (ref *Reference) Blob(digest string) string { - curl := ref.urlobj.JoinPath("/v2", ref.repo, "/blobs", digest) - return curl.String() -} - -func (ref *Reference) Userinfo() (string, string) { - return ref.user, ref.pass -} diff --git a/referer.go b/referer.go new file mode 100644 index 0000000..8756e12 --- /dev/null +++ b/referer.go @@ -0,0 +1,100 @@ +package client + +import ( + "errors" + "net/url" + "strings" +) + +type Reference struct { + urlobj *url.URL + user, pass string + repo, tag string +} + +func NewReference(rawref string) (*Reference, error) { + ref := &Reference{} + if !strings.Contains(rawref, "://") { + rawref = "https://" + rawref + } + urlobj, err := url.Parse(rawref) + if err != nil { + return ref, err + } + if urlobj.User != nil { + ref.user = urlobj.User.Username() + ref.pass, _ = urlobj.User.Password() + urlobj.User = nil + } + repotag := strings.SplitN(urlobj.Path, ":", 2) + if len(repotag) != 2 { + err = errors.New("Incorrect repo") + return ref, err + } + ref.urlobj = urlobj + ref.urlobj.Path = "/" + ref.repo = repotag[0] + ref.tag = repotag[1] + ref.urlobj = urlobj + + return ref, err +} + +func (ref *Reference) Manifest() string { + curl := ref.urlobj.JoinPath("/v2", ref.repo, "/manifests", ref.tag) + return curl.String() +} + +func (ref *Reference) Blob() string { + curl := ref.urlobj.JoinPath("/v2", ref.repo, "/blobs", ref.tag) + return curl.String() +} + +func (ref *Reference) Upload() string { + curl := ref.urlobj.JoinPath("/v2", ref.repo, "/blobs/uploads/") + return curl.String() +} + +func (ref *Reference) Patch(loc string) (string, error) { + var curl *url.URL + var out string + var err error + + if strings.Contains(loc, "://") { + curl, err = url.Parse(loc) + if err != nil { + return out, err + } + } else { + curl = ref.urlobj.JoinPath(loc) + } + out = curl.String() + return out, err +} + +func (ref *Reference) Put(loc, digest string) (string, error) { + var curl *url.URL + var out string + var err error + if strings.Contains(loc, "://") { + curl, err = url.Parse(loc) + if err != nil { + return out, err + } + } else { + curl = ref.urlobj.JoinPath(loc) + } + query := curl.Query() + query.Set("digest", digest) + curl.RawQuery = query.Encode() + out = curl.String() + return out, err +} + +func (ref *Reference) Tag() string { + return ref.tag +} + +func (ref *Reference) Userinfo() (string, string) { + return ref.user, ref.pass +}