From 19b173357a3e834b9f9bb03aec6b82fba31023d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9E=D0=BB=D0=B5=D0=B3=20=D0=91=D0=BE=D1=80=D0=BE=D0=B4?= =?UTF-8?q?=D0=B8=D0=BD?= Date: Thu, 26 Feb 2026 18:15:10 +0200 Subject: [PATCH 1/6] working commit --- authbas.go | 24 +++++++++++++++++++ client.go | 32 ++++++++++++++++++++++++++ digest.go | 50 ++++++++++++++++++++++++++++++++++++++++ getman.go | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++ getman_test.go | 34 +++++++++++++++++++++++++++ go.mod | 11 +++++++++ go.sum | 10 ++++++++ putman.go | 50 ++++++++++++++++++++++++++++++++++++++++ refer.go | 55 ++++++++++++++++++++++++++++++++++++++++++++ 9 files changed, 328 insertions(+) create mode 100644 authbas.go create mode 100644 client.go create mode 100644 digest.go create mode 100644 getman.go create mode 100644 getman_test.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 putman.go create mode 100644 refer.go diff --git a/authbas.go b/authbas.go new file mode 100644 index 0000000..c26dfb0 --- /dev/null +++ b/authbas.go @@ -0,0 +1,24 @@ +package client + +import ( + "encoding/base64" +) + + +type Authenticator interface { + MakeHeader(user, pass string) (key, value string, err error) +} + +type BasicAuthenticator struct {} + +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 +} + + + diff --git a/client.go b/client.go new file mode 100644 index 0000000..78e7779 --- /dev/null +++ b/client.go @@ -0,0 +1,32 @@ +package client + +import ( + "crypto/tls" + "net/http" +) + + +type Client struct { + httpClient *http.Client + authenticator Authenticator + userAgent string +} + +func NewClient(skipTLSVerify bool) *Client { + transport := &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: skipTLSVerify, + }, + } + httpClient := &http.Client{ + Transport: transport, + } + return &Client{ + httpClient: httpClient, + userAgent: "ociClient/1.0", + } +} + +func (cli *Client) SetAuthenticator(auth Authenticator) { + cli.authenticator = auth +} diff --git a/digest.go b/digest.go new file mode 100644 index 0000000..becbe6e --- /dev/null +++ b/digest.go @@ -0,0 +1,50 @@ +package client + +import ( + "crypto/sha256" + "crypto/sha512" + "encoding/hex" + "strings" +) + +func SHA256Digest(src []byte) string { + hasher := sha256.New() + hasher.Write(src) + sum := hasher.Sum(nil) + return "sha256:" + hex.EncodeToString(sum) +} + +func SHA512Digest(src []byte) string { + hasher := sha512.New() + hasher.Write(src) + sum := hasher.Sum(nil) + return "sha512:" + hex.EncodeToString(sum) +} + +const ( + 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 +} diff --git a/getman.go b/getman.go new file mode 100644 index 0000000..cf66473 --- /dev/null +++ b/getman.go @@ -0,0 +1,62 @@ +package client + +import ( + "bytes" + "context" + "fmt" + "io" + "net/http" + "strconv" +) + +func (cli *Client) GetManifest(ctx context.Context, rawref string) (bool, string, []byte, error) { + var err error + var exist bool + var mime string + var man []byte + + ref, err := NewReference(rawref) + if err != nil { + return exist, mime, man, err + } + uri := ref.Manifest() + + 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", "*/*") + resp, err := cli.httpClient.Do(req) + if err != nil { + return exist, mime, man, err + } + defer resp.Body.Close() + + if resp.StatusCode == http.StatusNotFound { + return exist, mime, man, err + } + if resp.StatusCode != http.StatusOK { + err := fmt.Errorf("Unxected response code %s", resp.Status) + return exist, mime, man, err + } + contentLength := resp.Header.Get("Content-Length") + manSize, err := strconv.ParseInt(contentLength, 10, 64) + if err != nil { + return exist, mime, man, err + } + mime = resp.Header.Get("Content-Type") + if mime == "" { + err := fmt.Errorf("Empty MIME type declaration") + return exist, mime, man, err + } + buffer := bytes.NewBuffer(nil) + recSize, err := io.Copy(buffer, resp.Body) + if manSize != recSize { + err := fmt.Errorf("Mismatch declared and actual body size") + return exist, mime, man, err + } + man = buffer.Bytes() + exist = true + return exist, mime, man, err +} diff --git a/getman_test.go b/getman_test.go new file mode 100644 index 0000000..4fab84c --- /dev/null +++ b/getman_test.go @@ -0,0 +1,34 @@ +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/go.mod b/go.mod new file mode 100644 index 0000000..e473a6b --- /dev/null +++ b/go.mod @@ -0,0 +1,11 @@ +module client + +go 1.25.0 + +require github.com/stretchr/testify v1.11.1 + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..c4c1710 --- /dev/null +++ b/go.sum @@ -0,0 +1,10 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/putman.go b/putman.go new file mode 100644 index 0000000..6552d6a --- /dev/null +++ b/putman.go @@ -0,0 +1,50 @@ +package client + +import ( + "bytes" + "context" + "fmt" + "net/http" +) + +func (cli *Client) PutManifest(ctx context.Context, rawref string, man []byte, mime string) error { + var err error + + ref, err := NewReference(rawref) + if err != nil { + return err + } + uri := ref.Manifest() + user, pass := ref.Userinfo() + + buffer := bytes.NewBuffer(man) + req, err := http.NewRequestWithContext(ctx, http.MethodPut, uri, buffer) + if err != nil { + return err + } + 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) + } + resp, err := cli.httpClient.Do(req) + if err != nil { + return err + } + resp.Body.Close() + if resp.StatusCode != http.StatusAccepted { + err = fmt.Errorf("Manifest not accepted, code %d", resp.StatusCode) + return err + } + loc := resp.Header.Get("Location") + if loc == "" { + err := fmt.Errorf("Empty manifest location declaration") + return err + } + return err +} diff --git a/refer.go b/refer.go new file mode 100644 index 0000000..ae9dfc1 --- /dev/null +++ b/refer.go @@ -0,0 +1,55 @@ +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 +} From 7de22e3816fad44b7793f50b36e2ad516958a776 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9E=D0=BB=D0=B5=D0=B3=20=D0=91=D0=BE=D1=80=D0=BE=D0=B4?= =?UTF-8?q?=D0=B8=D0=BD?= Date: Fri, 27 Feb 2026 03:14:24 +0200 Subject: [PATCH 2/6] working commit --- authbas.go | 20 +++----- blobexist.go | 59 +++++++++++++++++++++ client.go | 55 +++++++++++++++----- client_test.go | 134 ++++++++++++++++++++++++++++++++++++++++++++++++ copyctx.go | 39 ++++++++++++++ copyctx_test.go | 28 ++++++++++ delman.go | 49 ++++++++++++++++++ digest.go | 45 ++++++++-------- getblob.go | 63 +++++++++++++++++++++++ getman.go | 43 ++++++++++++++-- getman_test.go | 34 ------------ getupload.go | 51 ++++++++++++++++++ manexist.go | 70 +++++++++++++++++++++++++ mimetyp.go | 6 +++ pathupload.go | 54 +++++++++++++++++++ putman.go | 18 +++---- putupload.go | 54 +++++++++++++++++++ refer.go | 55 -------------------- referer.go | 100 ++++++++++++++++++++++++++++++++++++ 19 files changed, 827 insertions(+), 150 deletions(-) create mode 100644 blobexist.go create mode 100644 client_test.go create mode 100644 copyctx.go create mode 100644 copyctx_test.go create mode 100644 delman.go create mode 100644 getblob.go delete mode 100644 getman_test.go create mode 100644 getupload.go create mode 100644 manexist.go create mode 100644 mimetyp.go create mode 100644 pathupload.go create mode 100644 putupload.go delete mode 100644 refer.go create mode 100644 referer.go 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 +} From 9fa5a00421d0e7a8a5c5b05a5b6838a96206dfbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9E=D0=BB=D0=B5=D0=B3=20=D0=91=D0=BE=D1=80=D0=BE=D0=B4?= =?UTF-8?q?=D0=B8=D0=BD?= Date: Fri, 27 Feb 2026 19:04:57 +0200 Subject: [PATCH 3/6] working commit --- blobexist.go | 15 ++---- client.go | 88 +++++++++++++++++++++++++++++------ client_test.go | 123 +++++++++++++++++++++++++++++-------------------- delman.go | 17 ++----- getblob.go | 16 ++----- getman.go | 16 ++----- getupload.go | 17 ++----- manexist.go | 16 ++----- pathupload.go | 13 +----- putman.go | 14 ++---- putupload.go | 13 +----- referer.go | 75 ++++++++++++++---------------- 12 files changed, 210 insertions(+), 213 deletions(-) diff --git a/blobexist.go b/blobexist.go index bc69013..1656680 100644 --- a/blobexist.go +++ b/blobexist.go @@ -7,17 +7,16 @@ import ( "strconv" ) -func (cli *Client) BlobExists(ctx context.Context, rawref string) (bool, int64, error) { +func (cli *Client) BlobExists(ctx context.Context, rawrepo string, digest string) (bool, int64, error) { var err error var exist bool var size int64 - ref, err := NewReference(rawref) + ref, err := NewRepository(rawrepo) if err != nil { return exist, size, err } - uri := ref.Blob() - user, pass := ref.Userinfo() + uri := ref.Blob(digest) fmt.Println(uri) req, err := http.NewRequestWithContext(ctx, http.MethodHead, uri, nil) @@ -27,14 +26,6 @@ func (cli *Client) BlobExists(ctx context.Context, rawref string) (bool, int64, 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 diff --git a/client.go b/client.go index 5439c48..70f792c 100644 --- a/client.go +++ b/client.go @@ -2,13 +2,13 @@ package client import ( "crypto/tls" + "encoding/base64" "net/http" ) type Client struct { - httpClient *http.Client - authenticator Authenticator - userAgent string + httpClient *http.Client + userAgent string } func NewClient() *Client { @@ -22,28 +22,88 @@ func NewClient() *Client { } } -func (cli *Client) SetAuthenticator(auth Authenticator) { - cli.authenticator = auth -} - func (cli *Client) SetTransport(transport http.RoundTripper) { cli.httpClient.Transport = transport } -type WrapTransport struct { - transport http.RoundTripper +type MiddlewareFunc func(next http.RoundTripper) http.RoundTripper + +func (cli *Client) UseMiddleware(mwFunc MiddlewareFunc) { + cli.httpClient.Transport = mwFunc(cli.httpClient.Transport) } -func NewWrapTransport(transport http.RoundTripper) *WrapTransport { - return &WrapTransport{ - transport: transport, +// ExampleMiddleware +func NewExampleMiddleware() MiddlewareFunc { + return func(next http.RoundTripper) http.RoundTripper { + return newExampleTransport(next) } } -func (wrap *WrapTransport) RoundTrip(req *http.Request) (*http.Response, error) { - return wrap.transport.RoundTrip(req) +type ExampleTransport struct { + next http.RoundTripper } +func newExampleTransport(next http.RoundTripper) *ExampleTransport { + return &ExampleTransport{ + next: next, + } +} + +func (tran ExampleTransport) RoundTrip(req *http.Request) (*http.Response, error) { + return tran.next.RoundTrip(req) +} + +// BasicAuthMiddleware +func NewBasicAuthMiddleware(user, pass string) MiddlewareFunc { + return func(next http.RoundTripper) http.RoundTripper { + return newBasicAuthMW(next, user, pass) + } +} + +type BasicAuthMW struct { + user, pass string + next http.RoundTripper +} + +func newBasicAuthMW(next http.RoundTripper, user, pass string) *BasicAuthMW { + return &BasicAuthMW{ + user: user, + pass: pass, + next: next, + } +} + +func (tran BasicAuthMW) RoundTrip(req *http.Request) (*http.Response, error) { + pair := base64.StdEncoding.EncodeToString([]byte(tran.user + ":" + tran.pass)) + req.Header.Set("Authorization", "Basic "+pair) + return tran.next.RoundTrip(req) +} + +// BearerAuthMiddleware +func NewBearerAuthMiddleware(token string) MiddlewareFunc { + return func(next http.RoundTripper) http.RoundTripper { + return newBearerAuthMW(next, token) + } +} + +type BearerAuthMW struct { + token string + next http.RoundTripper +} + +func newBearerAuthMW(next http.RoundTripper, token string) *BearerAuthMW { + return &BearerAuthMW{ + token: token, + next: next, + } +} + +func (tran BearerAuthMW) RoundTrip(req *http.Request) (*http.Response, error) { + req.Header.Set("Authorization", "Bearer "+tran.token) + return tran.next.RoundTrip(req) +} + +// DefaultTransport type DefaultTransport struct { transport http.RoundTripper } diff --git a/client_test.go b/client_test.go index 4e40da7..00f4fe2 100644 --- a/client_test.go +++ b/client_test.go @@ -12,15 +12,16 @@ import ( "time" ) -func TestClientGetManifest(t *testing.T) { - rawrefs := []string{ - "mirror.gcr.io/alpine:3.20.0", - "mirror.gcr.io/alpine:sha256:29e5ba63e79337818e6c63cfcc68e2ab4e9ca483853b2de303bfbfba9372426c", +func xxxTestClientGetManifest(t *testing.T) { + rawrepo := "mirror.gcr.io/alpine" + tags := []string{ + "3.20.0", + "sha256:29e5ba63e79337818e6c63cfcc68e2ab4e9ca483853b2de303bfbfba9372426c", } - for _, rawref := range rawrefs { + for _, tag := range tags { cli := NewClient() ctx, _ := context.WithTimeout(context.Background(), 10*time.Second) - exist, mime, man, err := cli.GetManifest(ctx, rawref) + exist, mime, man, err := cli.GetManifest(ctx, rawrepo, tag) require.NoError(t, err) require.True(t, exist) @@ -32,15 +33,17 @@ func TestClientGetManifest(t *testing.T) { } } -func xxxTestClientManifestExists(t *testing.T) { - rawrefs := []string{ - "mirror.gcr.io/alpine:3.20.0", - "mirror.gcr.io/alpine:sha256:29e5ba63e79337818e6c63cfcc68e2ab4e9ca483853b2de303bfbfba9372426c", +func TestClientManifestExists(t *testing.T) { + rawrepo := "mirror.gcr.io/alpine" + tags := []string{ + "3.20.0", + "sha256:29e5ba63e79337818e6c63cfcc68e2ab4e9ca483853b2de303bfbfba9372426c", } - for _, rawref := range rawrefs { + for _, tag := range tags { + cli := NewClient() ctx, _ := context.WithTimeout(context.Background(), 10*time.Second) - exist, mime, size, csum, err := cli.ManifestExists(ctx, rawref) + exist, mime, size, csum, err := cli.ManifestExists(ctx, rawrepo, tag) require.NoError(t, err) require.True(t, exist) @@ -52,13 +55,14 @@ func xxxTestClientManifestExists(t *testing.T) { } func xxxTestClientBlobExists(t *testing.T) { - rawrefs := []string{ - "mirror.gcr.io/alpine:sha256:3b8747b05489980f63da1d2b8e5a444c55777f69540394397b0bc1c76c3e41f2", + rawrepos := []string{ + "mirror.gcr.io/alpine", } - for _, rawref := range rawrefs { + for _, rawrepo := range rawrepos { cli := NewClient() ctx, _ := context.WithTimeout(context.Background(), 10*time.Second) - exist, size, err := cli.BlobExists(ctx, rawref) + digest := "sha256:3b8747b05489980f63da1d2b8e5a444c55777f69540394397b0bc1c76c3e41f2" + exist, size, err := cli.BlobExists(ctx, rawrepo, digest) require.NoError(t, err) require.True(t, exist) @@ -66,52 +70,37 @@ func xxxTestClientBlobExists(t *testing.T) { } } -func xxxTestClientGetBlob(t *testing.T) { - rawrefs := []string{ - "mirror.gcr.io/alpine:sha256:3b8747b05489980f63da1d2b8e5a444c55777f69540394397b0bc1c76c3e41f2", +func TestClientGetBlob(t *testing.T) { + rawrepos := []string{ + "mirror.gcr.io/alpine", } - for _, rawref := range rawrefs { + for _, rawrepo := range rawrepos { cli := NewClient() ctx, _ := context.WithTimeout(context.Background(), 10*time.Second) buffer := bytes.NewBuffer(nil) - exist, err := cli.GetBlob(ctx, rawref, buffer) + digest := "sha256:3b8747b05489980f63da1d2b8e5a444c55777f69540394397b0bc1c76c3e41f2" + exist, err := cli.GetBlob(ctx, rawrepo, buffer, digest) require.NoError(t, err) require.True(t, exist) fmt.Printf("Size: %d\n", len(buffer.Bytes())) } } -func TestClientGetUpload(t *testing.T) { - rawrefs := []string{ +func xxxxTestClientGetUpload(t *testing.T) { + rawrepos := []string{ "mstore:mstore@localhost:1025/alpine:3.20.0", } - for _, rawref := range rawrefs { + cli := NewClient() + cli.UseMiddleware(NewBasicAuthMiddleware("mstore", "mstore")) + + for _, rawrepo := range rawrepos { 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) + loc, err = cli.GetUpload(ctx, rawrepo) 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) + fmt.Printf("Upload Location: %s\n", loc) } { srcsize := 1024 + 145 @@ -122,13 +111,49 @@ func TestClientGetUpload(t *testing.T) { 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))) + bloc, err := cli.PatchUpload(ctx, rawrepo, src, loc, int64(len(srcdata))) require.NoError(t, err) - fmt.Printf("Location: %s\n", bloc) + fmt.Printf("Path 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) + + ctx, _ := context.WithTimeout(context.Background(), 10*time.Second) + bloc, err := cli.PutUpload(ctx, rawrepo, src, loc, digest, int64(len(srcdata))) + require.NoError(t, err) + fmt.Printf("Put blob Location: %s\n", bloc) } } } + +func xxxxTestClientGetToken(t *testing.T) { + var token string + var err error + { + cli := NewClient() + cli.UseMiddleware(NewBasicAuthMiddleware("onborodin", "2Albert334")) + ctx, _ := context.WithTimeout(context.Background(), 10*time.Second) + token, err = cli.GetToken(ctx, "https://auth.docker.io/token") + require.NoError(t, err) + + } + fmt.Printf("Token: %s\n", token) + { + rawrepo := "docker.io/onborodin/toolbox:0.18" + cli := NewClient() + cli.UseMiddleware(NewBearerAuthMiddleware(token)) + ctx, _ := context.WithTimeout(context.Background(), 10*time.Second) + loc, err := cli.GetUpload(ctx, rawrepo) + require.NoError(t, err) + fmt.Printf("Upload Location: %s\n", loc) + } + +} diff --git a/delman.go b/delman.go index f5b3c61..979666d 100644 --- a/delman.go +++ b/delman.go @@ -6,32 +6,21 @@ import ( "net/http" ) -func (cli *Client) DeleteManifest(ctx context.Context, rawref string) (bool, error) { +func (cli *Client) DeleteManifest(ctx context.Context, rawrepo, tag string) (bool, error) { var err error var exist bool - ref, err := NewReference(rawref) + ref, err := NewRepository(rawrepo) if err != nil { return exist, err } - uri := ref.Manifest() - user, pass := ref.Userinfo() - + uri := ref.Manifest(tag) 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 diff --git a/getblob.go b/getblob.go index a314331..9d6617e 100644 --- a/getblob.go +++ b/getblob.go @@ -8,16 +8,15 @@ import ( "strconv" ) -func (cli *Client) GetBlob(ctx context.Context, rawref string, writer io.Writer) (bool, error) { +func (cli *Client) GetBlob(ctx context.Context, rawrepo string, writer io.Writer, digest string) (bool, error) { var err error var exist bool - ref, err := NewReference(rawref) + ref, err := NewRepository(rawrepo) if err != nil { return exist, err } - uri := ref.Blob() - user, pass := ref.Userinfo() + uri := ref.Blob(digest) req, err := http.NewRequestWithContext(ctx, http.MethodGet, uri, nil) if err != nil { @@ -25,15 +24,6 @@ func (cli *Client) GetBlob(ctx context.Context, rawref string, writer io.Writer) } 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 diff --git a/getman.go b/getman.go index c4a05cd..89fdfba 100644 --- a/getman.go +++ b/getman.go @@ -9,18 +9,17 @@ import ( "strings" ) -func (cli *Client) GetManifest(ctx context.Context, rawref string) (bool, string, []byte, error) { +func (cli *Client) GetManifest(ctx context.Context, rawrepo, tag string) (bool, string, []byte, error) { var err error var exist bool var mime string var man []byte - ref, err := NewReference(rawref) + ref, err := NewRepository(rawrepo) if err != nil { return exist, mime, man, err } - uri := ref.Manifest() - user, pass := ref.Userinfo() + uri := ref.Manifest(tag) req, err := http.NewRequestWithContext(ctx, http.MethodGet, uri, nil) if err != nil { @@ -28,15 +27,6 @@ func (cli *Client) GetManifest(ctx context.Context, rawref string) (bool, string } 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, man, err - } - req.Header.Set(authHeader, authKey) - } - resp, err := cli.httpClient.Do(req) if err != nil { return exist, mime, man, err diff --git a/getupload.go b/getupload.go index 7903ac6..05634d5 100644 --- a/getupload.go +++ b/getupload.go @@ -6,16 +6,15 @@ import ( "net/http" ) -func (cli *Client) GetUpload(ctx context.Context, rawref string) (string, error) { +func (cli *Client) GetUpload(ctx context.Context, rawrepo string) (string, error) { var err error var loc string - ref, err := NewReference(rawref) + ref, err := NewRepository(rawrepo) 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 { @@ -23,26 +22,20 @@ func (cli *Client) GetUpload(ctx context.Context, rawref string) (string, error) } 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() + fmt.Printf("=== %++v\n", resp.Header) + 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 diff --git a/manexist.go b/manexist.go index 9292e3c..25d2a23 100644 --- a/manexist.go +++ b/manexist.go @@ -7,19 +7,18 @@ import ( "strconv" ) -func (cli *Client) ManifestExists(ctx context.Context, rawref string) (bool, string, int64, string, error) { +func (cli *Client) ManifestExists(ctx context.Context, rawrepo, tag 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) + ref, err := NewRepository(rawrepo) if err != nil { return exist, mime, size, csum, err } - uri := ref.Manifest() - user, pass := ref.Userinfo() + uri := ref.Manifest(tag) req, err := http.NewRequestWithContext(ctx, http.MethodHead, uri, nil) if err != nil { @@ -27,15 +26,6 @@ func (cli *Client) ManifestExists(ctx context.Context, rawref string) (bool, str } 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 diff --git a/pathupload.go b/pathupload.go index cf665bb..11238cd 100644 --- a/pathupload.go +++ b/pathupload.go @@ -8,11 +8,11 @@ import ( "strconv" ) -func (cli *Client) PatchUpload(ctx context.Context, rawref string, src io.Reader, uploc string, size int64) (string, error) { +func (cli *Client) PatchUpload(ctx context.Context, rawrepo string, src io.Reader, uploc string, size int64) (string, error) { var err error var ouloc string - ref, err := NewReference(rawref) + ref, err := NewRepository(rawrepo) if err != nil { return ouloc, err } @@ -20,8 +20,6 @@ func (cli *Client) PatchUpload(ctx context.Context, rawref string, src io.Reader 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 @@ -29,13 +27,6 @@ func (cli *Client) PatchUpload(ctx context.Context, rawref string, src io.Reader 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 diff --git a/putman.go b/putman.go index 6dcb1a7..8d1e73a 100644 --- a/putman.go +++ b/putman.go @@ -7,15 +7,14 @@ import ( "net/http" ) -func (cli *Client) PutManifest(ctx context.Context, rawref string, man []byte, mime string) error { +func (cli *Client) PutManifest(ctx context.Context, rawrepo, tag string, man []byte, mime string) error { var err error - ref, err := NewReference(rawref) + ref, err := NewRepository(rawrepo) if err != nil { return err } - uri := ref.Manifest() - user, pass := ref.Userinfo() + uri := ref.Manifest(tag) buffer := bytes.NewBuffer(man) req, err := http.NewRequestWithContext(ctx, http.MethodPut, uri, buffer) @@ -25,13 +24,6 @@ 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) - } resp, err := cli.httpClient.Do(req) if err != nil { return err diff --git a/putupload.go b/putupload.go index 8578457..0e6768a 100644 --- a/putupload.go +++ b/putupload.go @@ -8,11 +8,11 @@ import ( "strconv" ) -func (cli *Client) PutUpload(ctx context.Context, rawref string, src io.Reader, uploc, digest string, size int64) (string, error) { +func (cli *Client) PutUpload(ctx context.Context, rawrepo string, src io.Reader, uploc, digest string, size int64) (string, error) { var err error var bloc string - ref, err := NewReference(rawref) + ref, err := NewRepository(rawrepo) if err != nil { return bloc, err } @@ -20,8 +20,6 @@ func (cli *Client) PutUpload(ctx context.Context, rawref string, src io.Reader, 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 @@ -29,13 +27,6 @@ func (cli *Client) PutUpload(ctx context.Context, rawref string, src io.Reader, 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 diff --git a/referer.go b/referer.go index 8756e12..43bf861 100644 --- a/referer.go +++ b/referer.go @@ -1,88 +1,87 @@ package client import ( - "errors" "net/url" "strings" ) -type Reference struct { +type Repository struct { urlobj *url.URL user, pass string - repo, tag string + base string } -func NewReference(rawref string) (*Reference, error) { - ref := &Reference{} - if !strings.Contains(rawref, "://") { - rawref = "https://" + rawref +func NewRepository(rawrepo string) (*Repository, error) { + repo := &Repository{} + if !strings.Contains(rawrepo, "://") { + rawrepo = "https://" + rawrepo } - urlobj, err := url.Parse(rawref) + urlobj, err := url.Parse(rawrepo) if err != nil { - return ref, err + return repo, err } if urlobj.User != nil { - ref.user = urlobj.User.Username() - ref.pass, _ = urlobj.User.Password() + repo.user = urlobj.User.Username() + repo.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 + repo.urlobj = urlobj + repo.base = repo.urlobj.Path + repo.urlobj.Path = "/" + repo.urlobj = urlobj - return ref, err + return repo, err } -func (ref *Reference) Manifest() string { - curl := ref.urlobj.JoinPath("/v2", ref.repo, "/manifests", ref.tag) +func (repo *Repository) Manifest(tag string) string { + curl := repo.urlobj.JoinPath("/v2", repo.base, "/manifests", tag) return curl.String() } -func (ref *Reference) Blob() string { - curl := ref.urlobj.JoinPath("/v2", ref.repo, "/blobs", ref.tag) +func (repo *Repository) Blob(digest string) string { + curl := repo.urlobj.JoinPath("/v2", repo.base, "/blobs", digest) return curl.String() } -func (ref *Reference) Upload() string { - curl := ref.urlobj.JoinPath("/v2", ref.repo, "/blobs/uploads/") +func (repo *Repository) Upload() string { + curl := repo.urlobj.JoinPath("/v2", repo.base, "/blobs/uploads/") return curl.String() } -func (ref *Reference) Patch(loc string) (string, error) { +func (repo *Repository) Patch(loc string) (string, error) { var curl *url.URL var out string var err error - + if isUUID(loc) { + curl = repo.urlobj.JoinPath("/v2/", repo.base, "/blobs/uploads/", loc) + return curl.String(), nil + } if strings.Contains(loc, "://") { curl, err = url.Parse(loc) if err != nil { return out, err } } else { - curl = ref.urlobj.JoinPath(loc) + curl = repo.urlobj.JoinPath(loc) } out = curl.String() return out, err } -func (ref *Reference) Put(loc, digest string) (string, error) { +func (repo *Repository) Put(loc, digest string) (string, error) { var curl *url.URL var out string var err error - if strings.Contains(loc, "://") { + + if isUUID(loc) { + curl = repo.urlobj.JoinPath("/v2/", repo.base, "/blobs/uploads/", loc) + } else if strings.Contains(loc, "://") { curl, err = url.Parse(loc) if err != nil { return out, err } } else { - curl = ref.urlobj.JoinPath(loc) + curl = repo.urlobj.JoinPath(loc) } query := curl.Query() query.Set("digest", digest) @@ -91,10 +90,6 @@ func (ref *Reference) Put(loc, digest string) (string, error) { return out, err } -func (ref *Reference) Tag() string { - return ref.tag -} - -func (ref *Reference) Userinfo() (string, string) { - return ref.user, ref.pass +func (repo *Repository) Userinfo() (string, string) { + return repo.user, repo.pass } From 86c7831cd8698a7ab1d58841c57be40adf09ddbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9E=D0=BB=D0=B5=D0=B3=20=D0=91=D0=BE=D1=80=D0=BE=D0=B4?= =?UTF-8?q?=D0=B8=D0=BD?= Date: Sun, 1 Mar 2026 17:07:20 +0200 Subject: [PATCH 4/6] working commit --- authbas.go | 2 +- blobexist.go | 2 +- client.go | 2 +- client_test.go | 2 +- copyctx.go | 2 +- copyctx_test.go | 2 +- delman.go | 2 +- digest.go | 2 +- getblob.go | 2 +- getman.go | 2 +- getupload.go | 2 +- go.mod | 6 +++- go.sum | 6 ++++ manexist.go | 2 +- mimetyp.go | 2 +- pathupload.go | 2 +- putman.go | 2 +- putupload.go | 2 +- referer.go | 95 ------------------------------------------------- 19 files changed, 27 insertions(+), 112 deletions(-) delete mode 100644 referer.go diff --git a/authbas.go b/authbas.go index 705eee9..1b555eb 100644 --- a/authbas.go +++ b/authbas.go @@ -1,4 +1,4 @@ -package client +package repocli import ( "encoding/base64" diff --git a/blobexist.go b/blobexist.go index 1656680..3f4ab09 100644 --- a/blobexist.go +++ b/blobexist.go @@ -1,4 +1,4 @@ -package client +package repocli import ( "context" diff --git a/client.go b/client.go index 70f792c..404fece 100644 --- a/client.go +++ b/client.go @@ -1,4 +1,4 @@ -package client +package repocli import ( "crypto/tls" diff --git a/client_test.go b/client_test.go index 00f4fe2..af759ee 100644 --- a/client_test.go +++ b/client_test.go @@ -1,4 +1,4 @@ -package client +package repocli import ( "github.com/stretchr/testify/require" diff --git a/copyctx.go b/copyctx.go index 0dc3091..b9a8acf 100644 --- a/copyctx.go +++ b/copyctx.go @@ -1,4 +1,4 @@ -package client +package repocli import ( "context" diff --git a/copyctx_test.go b/copyctx_test.go index f289cc1..b6c991e 100644 --- a/copyctx_test.go +++ b/copyctx_test.go @@ -1,4 +1,4 @@ -package client +package repocli import ( "github.com/stretchr/testify/require" diff --git a/delman.go b/delman.go index 979666d..0050a8f 100644 --- a/delman.go +++ b/delman.go @@ -1,4 +1,4 @@ -package client +package repocli import ( "context" diff --git a/digest.go b/digest.go index 8874348..2eab175 100644 --- a/digest.go +++ b/digest.go @@ -1,4 +1,4 @@ -package client +package repocli import ( "crypto/sha256" diff --git a/getblob.go b/getblob.go index 9d6617e..ee92aab 100644 --- a/getblob.go +++ b/getblob.go @@ -1,4 +1,4 @@ -package client +package repocli import ( "context" diff --git a/getman.go b/getman.go index 89fdfba..ba727b6 100644 --- a/getman.go +++ b/getman.go @@ -1,4 +1,4 @@ -package client +package repocli import ( "bytes" diff --git a/getupload.go b/getupload.go index 05634d5..e4c3cd6 100644 --- a/getupload.go +++ b/getupload.go @@ -1,4 +1,4 @@ -package client +package repocli import ( "context" diff --git a/go.mod b/go.mod index e473a6b..e1c0b83 100644 --- a/go.mod +++ b/go.mod @@ -2,10 +2,14 @@ module client go 1.25.0 -require github.com/stretchr/testify v1.11.1 +require ( + github.com/opencontainers/image-spec v1.1.0 + github.com/stretchr/testify v1.11.1 +) require ( github.com/davecgh/go-spew v1.1.1 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index c4c1710..e4da8cc 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,11 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= diff --git a/manexist.go b/manexist.go index 25d2a23..f4855ac 100644 --- a/manexist.go +++ b/manexist.go @@ -1,4 +1,4 @@ -package client +package repocli import ( "context" diff --git a/mimetyp.go b/mimetyp.go index 4f84603..c5da23f 100644 --- a/mimetyp.go +++ b/mimetyp.go @@ -1,4 +1,4 @@ -package client +package repocli const ( MediaTypeDDMLv2 = "application/vnd.docker.distribution.manifest.list.v2+json" diff --git a/pathupload.go b/pathupload.go index 11238cd..f1bc1b6 100644 --- a/pathupload.go +++ b/pathupload.go @@ -1,4 +1,4 @@ -package client +package repocli import ( "context" diff --git a/putman.go b/putman.go index 8d1e73a..e199cc2 100644 --- a/putman.go +++ b/putman.go @@ -1,4 +1,4 @@ -package client +package repocli import ( "bytes" diff --git a/putupload.go b/putupload.go index 0e6768a..31065ab 100644 --- a/putupload.go +++ b/putupload.go @@ -1,4 +1,4 @@ -package client +package repocli import ( "context" diff --git a/referer.go b/referer.go deleted file mode 100644 index 43bf861..0000000 --- a/referer.go +++ /dev/null @@ -1,95 +0,0 @@ -package client - -import ( - "net/url" - "strings" -) - -type Repository struct { - urlobj *url.URL - user, pass string - base string -} - -func NewRepository(rawrepo string) (*Repository, error) { - repo := &Repository{} - if !strings.Contains(rawrepo, "://") { - rawrepo = "https://" + rawrepo - } - urlobj, err := url.Parse(rawrepo) - if err != nil { - return repo, err - } - if urlobj.User != nil { - repo.user = urlobj.User.Username() - repo.pass, _ = urlobj.User.Password() - urlobj.User = nil - } - repo.urlobj = urlobj - repo.base = repo.urlobj.Path - repo.urlobj.Path = "/" - repo.urlobj = urlobj - - return repo, err -} - -func (repo *Repository) Manifest(tag string) string { - curl := repo.urlobj.JoinPath("/v2", repo.base, "/manifests", tag) - return curl.String() -} - -func (repo *Repository) Blob(digest string) string { - curl := repo.urlobj.JoinPath("/v2", repo.base, "/blobs", digest) - return curl.String() -} - -func (repo *Repository) Upload() string { - curl := repo.urlobj.JoinPath("/v2", repo.base, "/blobs/uploads/") - return curl.String() -} - -func (repo *Repository) Patch(loc string) (string, error) { - var curl *url.URL - var out string - var err error - if isUUID(loc) { - curl = repo.urlobj.JoinPath("/v2/", repo.base, "/blobs/uploads/", loc) - return curl.String(), nil - } - if strings.Contains(loc, "://") { - curl, err = url.Parse(loc) - if err != nil { - return out, err - } - } else { - curl = repo.urlobj.JoinPath(loc) - } - out = curl.String() - return out, err -} - -func (repo *Repository) Put(loc, digest string) (string, error) { - var curl *url.URL - var out string - var err error - - if isUUID(loc) { - curl = repo.urlobj.JoinPath("/v2/", repo.base, "/blobs/uploads/", loc) - } else if strings.Contains(loc, "://") { - curl, err = url.Parse(loc) - if err != nil { - return out, err - } - } else { - curl = repo.urlobj.JoinPath(loc) - } - query := curl.Query() - query.Set("digest", digest) - curl.RawQuery = query.Encode() - out = curl.String() - return out, err -} - -func (repo *Repository) Userinfo() (string, string) { - return repo.user, repo.pass -} From 32c9cc26fc885d40e3e1de64b2490dade305f92f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9E=D0=BB=D0=B5=D0=B3=20=D0=91=D0=BE=D1=80=D0=BE=D0=B4?= =?UTF-8?q?=D0=B8=D0=BD?= Date: Sun, 1 Mar 2026 21:35:40 +0200 Subject: [PATCH 5/6] working commit --- client.go | 13 +++++++++++++ client_test.go | 4 ++-- getman.go | 5 +++++ 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/client.go b/client.go index 404fece..0983196 100644 --- a/client.go +++ b/client.go @@ -22,6 +22,19 @@ func NewClient() *Client { } } +func NewClientWithTransport(transport http.RoundTripper, mwFunc ...MiddlewareFunc) *Client { + if transport == nil { + transport = NewDefaultTransport() + } + httpClient := &http.Client{ + Transport: transport, + } + return &Client{ + httpClient: httpClient, + userAgent: "ociClient/1.0", + } +} + func (cli *Client) SetTransport(transport http.RoundTripper) { cli.httpClient.Transport = transport } diff --git a/client_test.go b/client_test.go index af759ee..efcb220 100644 --- a/client_test.go +++ b/client_test.go @@ -33,7 +33,7 @@ func xxxTestClientGetManifest(t *testing.T) { } } -func TestClientManifestExists(t *testing.T) { +func xxxTestClientManifestExists(t *testing.T) { rawrepo := "mirror.gcr.io/alpine" tags := []string{ "3.20.0", @@ -70,7 +70,7 @@ func xxxTestClientBlobExists(t *testing.T) { } } -func TestClientGetBlob(t *testing.T) { +func xxxTestClientGetBlob(t *testing.T) { rawrepos := []string{ "mirror.gcr.io/alpine", } diff --git a/getman.go b/getman.go index ba727b6..a51dc84 100644 --- a/getman.go +++ b/getman.go @@ -3,6 +3,7 @@ package repocli import ( "bytes" "context" + "errors" "fmt" "net/http" "strconv" @@ -41,6 +42,10 @@ func (cli *Client) GetManifest(ctx context.Context, rawrepo, tag string) (bool, return exist, mime, man, err } contentLength := resp.Header.Get("Content-Length") + if contentLength == "" { + err = errors.New("Empty Content-Length header") + return exist, mime, man, err + } manSize, err := strconv.ParseInt(contentLength, 10, 64) if err != nil { return exist, mime, man, err From 7bc972ba76305c400012dda79d27974f2203c269 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9E=D0=BB=D0=B5=D0=B3=20=D0=91=D0=BE=D1=80=D0=BE=D0=B4?= =?UTF-8?q?=D0=B8=D0=BD?= Date: Sun, 1 Mar 2026 21:35:52 +0200 Subject: [PATCH 6/6] working commit --- jwt.go | 63 ++++++++++++++++++++++++++++ pullimage.go | 104 ++++++++++++++++++++++++++++++++++++++++++++++ pullimage_test.go | 25 +++++++++++ reference.go | 56 +++++++++++++++++++++++++ repo.go | 100 ++++++++++++++++++++++++++++++++++++++++++++ repo_test.go | 25 +++++++++++ uuid.go | 12 ++++++ 7 files changed, 385 insertions(+) create mode 100644 jwt.go create mode 100644 pullimage.go create mode 100644 pullimage_test.go create mode 100644 reference.go create mode 100644 repo.go create mode 100644 repo_test.go create mode 100644 uuid.go diff --git a/jwt.go b/jwt.go new file mode 100644 index 0000000..6c1b218 --- /dev/null +++ b/jwt.go @@ -0,0 +1,63 @@ +package repocli + +import ( + "bytes" + "context" + "fmt" + "net/http" + //"strconv" + //"strings" + "encoding/json" + "time" +) + +type JWT struct { + Token string `json:"token"` + AccessToken string `json:"access_token"` + ExpiresIn int `json:"expires_in"` + IssuedAt time.Time `json:"issued_at"` +} + +func (cli *Client) GetToken(ctx context.Context, uri string) (string, error) { + var err error + var token string + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, uri, nil) + if err != nil { + return token, err + } + req.Header.Set("User-Agent", cli.userAgent) + req.Header.Set("Accept", "*/*") + resp, err := cli.httpClient.Do(req) + if err != nil { + return token, err + } + defer resp.Body.Close() + + if resp.StatusCode == http.StatusNotFound { + return token, err + } + if resp.StatusCode != http.StatusOK { + err := fmt.Errorf("Unxected response code %s", resp.Status) + return token, err + } + mime := resp.Header.Get("Content-Type") + if mime != "application/json" { + err := fmt.Errorf("Empty MIME type declaration") + return token, err + } + buffer := bytes.NewBuffer(nil) + recSize, err := Copy(ctx, buffer, resp.Body) + if recSize == 0 { + err := fmt.Errorf("Zero actual body size") + return token, err + } + tokenJson := buffer.Bytes() + jwt := &JWT{} + err = json.Unmarshal(tokenJson, jwt) + if err != nil { + return token, err + } + token = jwt.Token + return token, err +} diff --git a/pullimage.go b/pullimage.go new file mode 100644 index 0000000..c29e78b --- /dev/null +++ b/pullimage.go @@ -0,0 +1,104 @@ +package repocli + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + + ocispec "github.com/opencontainers/image-spec/specs-go/v1" +) + +const ( + MediaTypeOIIv1 = "application/vnd.oci.image.index.v1+json" + MediatypeDDMLv2 = "application/vnd.docker.distribution.manifest.list.v2+json" + + MediatypeDDMv2 = "application/vnd.docker.distribution.manifest.v2+json" + MediaTypeOIMv1 = "application/vnd.oci.image.manifest.v1+json" +) + +type Downloader struct { + cli *Client +} + +func NewDownloader(client *Client) *Downloader { + return &Downloader{ + cli: client, + } +} + +func (down *Downloader) Pull(ctx context.Context, rawref, dir, os, arch string) error { + var err error + ref, err := NewReference(rawref) + if err != nil { + return err + } + rawrepo := ref.Repo() + tag := ref.Tag() + + exist, mime, man, err := down.cli.GetManifest(ctx, rawrepo, tag) + if err != nil { + return err + } + if !exist { + err = errors.New("Manifest not found") + return err + } + + if mime == MediaTypeOIIv1 || mime == MediatypeDDMLv2 { + var index ocispec.Index + err = json.Unmarshal(man, &index) + if err != nil { + return err + } + for _, descr := range index.Manifests { + if descr.Platform != nil { + cond := descr.Platform.Architecture == arch + cond = cond && descr.Platform.OS == os + if cond { + tag = descr.Digest.String() + } + } + } + } + fmt.Printf("Tag: %s\n", tag) + exist, mime, man, err = down.cli.GetManifest(ctx, rawrepo, tag) + if err != nil { + return err + } + fmt.Printf("Mime: %s\n", mime) + if !exist { + err = errors.New("Manifest not found") + return err + } + if mime != MediaTypeOIMv1 && mime != MediatypeDDMv2 { + err = errors.New("Unknown manifest media type") + return err + } + var manifest ocispec.Manifest + err = json.Unmarshal(man, &manifest) + if err != nil { + return err + } + "oci-layout" + "index.json" + + layers := make([]ocispec.Descriptor, 0) + layers = append(layers, manifest.Config) + layers = append(layers, manifest.Layers...) + for _, layer := range layers { + digest := layer.Digest.String() + exist, err := down.cli.GetBlob(ctx, rawrepo, io.Discard, digest) + if err != nil { + return err + } + if !exist { + err = errors.New("Layer not found") + return err + } + fmt.Printf("Layer type: %s\n", layer.MediaType) + + } + return err +} diff --git a/pullimage_test.go b/pullimage_test.go new file mode 100644 index 0000000..41aeb1f --- /dev/null +++ b/pullimage_test.go @@ -0,0 +1,25 @@ +package repocli + +import ( + "github.com/stretchr/testify/require" + + //"fmt" + "context" + "testing" + "time" +) + +func TestPullImage(t *testing.T) { + + ctx, _ := context.WithTimeout(context.Background(), 10*time.Second) + cli := NewClientWithTransport(nil, nil) + require.NotNil(t, cli) + + down := NewDownloader(cli) + require.NotNil(t, down) + + rawref := "mirror.gcr.io/alpine:3.20.0" + destdir := "qwert" + err := down.Pull(ctx, rawref, destdir, "linux", "amd64") + require.NoError(t, err) +} diff --git a/reference.go b/reference.go new file mode 100644 index 0000000..a08a749 --- /dev/null +++ b/reference.go @@ -0,0 +1,56 @@ +package repocli + +import ( + "errors" + "net/url" + "path" + "strings" +) + +type Reference struct { + urlobj *url.URL + user, pass string + base, 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 + } + ref.urlobj = urlobj + + repotag := strings.SplitN(ref.urlobj.Path, ":", 2) + if len(repotag) != 2 { + err = errors.New("Incorrect reference format") + return ref, err + } + ref.base = repotag[0] + ref.tag = repotag[1] + + ref.urlobj.Path = "/" + ref.urlobj = urlobj + + return ref, err +} + +func (ref *Reference) String() string { + return path.Join(ref.urlobj.Host, ref.base+":"+ref.tag) +} + +func (ref *Reference) Repo() string { + return path.Join(ref.urlobj.Host, ref.base) +} + +func (ref *Reference) Tag() string { + return ref.tag +} diff --git a/repo.go b/repo.go new file mode 100644 index 0000000..17d4cfa --- /dev/null +++ b/repo.go @@ -0,0 +1,100 @@ +package repocli + +import ( + "net/url" + "strings" +) + +type Repository struct { + urlobj *url.URL + user, pass string + base string +} + +func NewRepository(rawrepo string) (*Repository, error) { + repo := &Repository{} + if !strings.Contains(rawrepo, "://") { + rawrepo = "https://" + rawrepo + } + urlobj, err := url.Parse(rawrepo) + if err != nil { + return repo, err + } + if urlobj.User != nil { + repo.user = urlobj.User.Username() + repo.pass, _ = urlobj.User.Password() + urlobj.User = nil + } + repo.urlobj = urlobj + repo.base = repo.urlobj.Path + repo.urlobj.Path = "/" + repo.urlobj = urlobj + + return repo, err +} + +func (repo *Repository) String() string { + curl := repo.urlobj.JoinPath(repo.base) + return curl.String() +} + +func (repo *Repository) Manifest(tag string) string { + curl := repo.urlobj.JoinPath("/v2", repo.base, "/manifests", tag) + return curl.String() +} + +func (repo *Repository) Blob(digest string) string { + curl := repo.urlobj.JoinPath("/v2", repo.base, "/blobs", digest) + return curl.String() +} + +func (repo *Repository) Upload() string { + curl := repo.urlobj.JoinPath("/v2", repo.base, "/blobs/uploads/") + return curl.String() +} + +func (repo *Repository) Patch(loc string) (string, error) { + var curl *url.URL + var out string + var err error + if isUUID(loc) { + curl = repo.urlobj.JoinPath("/v2/", repo.base, "/blobs/uploads/", loc) + return curl.String(), nil + } + if strings.Contains(loc, "://") { + curl, err = url.Parse(loc) + if err != nil { + return out, err + } + } else { + curl = repo.urlobj.JoinPath(loc) + } + out = curl.String() + return out, err +} + +func (repo *Repository) Put(loc, digest string) (string, error) { + var curl *url.URL + var out string + var err error + + if isUUID(loc) { + curl = repo.urlobj.JoinPath("/v2/", repo.base, "/blobs/uploads/", loc) + } else if strings.Contains(loc, "://") { + curl, err = url.Parse(loc) + if err != nil { + return out, err + } + } else { + curl = repo.urlobj.JoinPath(loc) + } + query := curl.Query() + query.Set("digest", digest) + curl.RawQuery = query.Encode() + out = curl.String() + return out, err +} + +func (repo *Repository) Userinfo() (string, string) { + return repo.user, repo.pass +} diff --git a/repo_test.go b/repo_test.go new file mode 100644 index 0000000..9d930a3 --- /dev/null +++ b/repo_test.go @@ -0,0 +1,25 @@ +package repocli + +import ( + "github.com/stretchr/testify/require" + + "fmt" + "testing" +) + +func xxxTestResrerer(t *testing.T) { + ref, err := NewRepository("registry.example.com/lib/alpine") + require.NoError(t, err) + + fmt.Printf("Manifest:\t%s\n", ref.Manifest("3.30.0")) + + digest := SHA256Digest([]byte("qwerty")) + fmt.Printf("Blob:\t\t%s\n", ref.Blob(digest)) + fmt.Printf("POST:\t\t%s\n", ref.Upload()) + uuid := "8be4df61-93ca-11d2-aa0d-00e098032b8c" + rawurl, err := ref.Patch(uuid) + require.NoError(t, err) + fmt.Printf("PATH:\t\t%s\n", rawurl) + rawurl, err = ref.Put(uuid, digest) + fmt.Printf("PUT:\t\t%s\n", rawurl) +} diff --git a/uuid.go b/uuid.go new file mode 100644 index 0000000..d79621f --- /dev/null +++ b/uuid.go @@ -0,0 +1,12 @@ +package repocli + +import ( + "regexp" +) + +const uuidRegex = `^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$` + +func isUUID(src string) bool { + re := regexp.MustCompile(uuidRegex) + return re.MatchString(src) +}