From efdabf3efc7b6af9d4fadec1c2e9be959cf07c5c 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: Sat, 7 Mar 2026 19:11:20 +0200 Subject: [PATCH] add accntcli --- app/imageoper/service.go | 7 +- pkg/accntcli/account.go | 235 ++++++++++++++++++ pkg/{rpccli => accntcli}/client.go | 2 +- pkg/accntcli/grant.go | 216 ++++++++++++++++ pkg/accntcli/referer.go | 54 ++++ pkg/accntcli/service.go | 93 +++++++ pkg/filecli/getfile.go | 6 +- pkg/filecli/listcoll.go | 6 +- pkg/filecli/listfiles.go | 6 +- pkg/filecli/{repo.go => referer.go} | 28 +-- pkg/repocli/blobexist.go | 2 +- pkg/repocli/delblob.go | 2 +- pkg/repocli/delman.go | 2 +- pkg/repocli/getblob.go | 2 +- pkg/repocli/getman.go | 2 +- pkg/repocli/getupload.go | 2 +- pkg/repocli/manexist.go | 2 +- pkg/repocli/patchupload.go | 45 ++++ pkg/repocli/putman.go | 2 +- pkg/repocli/putupload.go | 2 +- pkg/repocli/{repo.go => referer.go} | 20 +- pkg/repocli/{repo_test.go => referer_test.go} | 2 +- pkg/rpccli/repo.go | 56 ----- 23 files changed, 698 insertions(+), 96 deletions(-) create mode 100644 pkg/accntcli/account.go rename pkg/{rpccli => accntcli}/client.go (99%) create mode 100644 pkg/accntcli/grant.go create mode 100644 pkg/accntcli/referer.go create mode 100644 pkg/accntcli/service.go rename pkg/filecli/{repo.go => referer.go} (70%) create mode 100644 pkg/repocli/patchupload.go rename pkg/repocli/{repo.go => referer.go} (77%) rename pkg/repocli/{repo_test.go => referer_test.go} (90%) delete mode 100644 pkg/rpccli/repo.go diff --git a/app/imageoper/service.go b/app/imageoper/service.go index 7a183ee..32c12ee 100644 --- a/app/imageoper/service.go +++ b/app/imageoper/service.go @@ -13,11 +13,14 @@ type SendHelloParams struct{} type SendHelloResult struct { Message string `json:"message"` + Alive bool `json:"alive"` } func (oper *Operator) SendHello(param *SendHelloParams) (*SendHelloResult, error) { var err error - res := &SendHelloResult{} - res.Message = "hello" + res := &SendHelloResult{ + Alive: true, + Message: "hello", + } return res, err } diff --git a/pkg/accntcli/account.go b/pkg/accntcli/account.go new file mode 100644 index 0000000..fe304e0 --- /dev/null +++ b/pkg/accntcli/account.go @@ -0,0 +1,235 @@ +/* + * Copyright 2026 Oleg Borodin + * + * This work is published and licensed under a Creative Commons + * Attribution-NonCommercial-NoDerivatives 4.0 International License. + * + * Distribution of this work is permitted, but commercial use and + * modifications are strictly prohibited. + */ +package accntcli + +import ( + "context" + "encoding/json" + "errors" + + "mstore/app/accoper" + "mstore/app/handler" + "mstore/pkg/descr" +) + +func (cli *Client) CreateAccount(ctx context.Context, host, user, pass string) (string, error) { + var err error + var res string + params := accoper.CreateAccountParams{ + Username: user, + Password: pass, + } + reqdata, err := json.Marshal(params) + if err != nil { + return res, err + } + respdata, err := cli.doHTTPCall(ctx, host, "accont", "create", reqdata) + if err != nil { + return res, err + } + response := handler.NewResponse[accoper.CreateAccountResult]() + err = json.Unmarshal(respdata, response) + if err != nil { + return res, err + } + if response.Error { + err = errors.New(response.Message) + return res, err + } + res = response.Result.AccountID + return res, err +} + +func (cli *Client) GetAccountByID(ctx context.Context, host, accountID string) (*descr.AccountShort, error) { + var err error + res := &descr.AccountShort{} + + params := accoper.GetAccountParams{ + AccountID: accountID, + } + reqdata, err := json.Marshal(params) + if err != nil { + return res, err + } + resdata, err := cli.doHTTPCall(ctx, host, "account", "get", reqdata) + if err != nil { + return res, err + } + + response := handler.NewResponse[accoper.GetAccountResult]() + err = json.Unmarshal(resdata, response) + if err != nil { + return res, err + } + if response.Error { + err = errors.New(response.Message) + return res, err + } + res = response.Result.Account + return res, err +} + +func (cli *Client) GetAccountByName(ctx context.Context, host, username string) (*descr.AccountShort, error) { + var err error + res := &descr.AccountShort{} + params := accoper.GetAccountParams{ + Username: username, + } + reqdata, err := json.Marshal(params) + if err != nil { + return res, err + } + resdata, err := cli.doHTTPCall(ctx, host, "account", "get", reqdata) + if err != nil { + return res, err + } + response := handler.NewResponse[accoper.GetAccountResult]() + err = json.Unmarshal(resdata, response) + if err != nil { + return res, err + } + if response.Error { + err = errors.New(response.Message) + return res, err + } + res = response.Result.Account + return res, err +} + +func (cli *Client) UpdateAccountByID(ctx context.Context, host string, accountID, newUsername, newPassword string) error { + var err error + params := accoper.UpdateAccountParams{ + AccountID: accountID, + NewUsername: newUsername, + NewPassword: newPassword, + } + reqdata, err := json.Marshal(params) + if err != nil { + return err + } + resdata, err := cli.doHTTPCall(ctx, host, "account", "update", reqdata) + if err != nil { + return err + } + response := handler.NewResponse[accoper.UpdateAccountResult]() + err = json.Unmarshal(resdata, response) + if err != nil { + return err + } + if response.Error { + err = errors.New(response.Message) + return err + } + return err +} + +func (cli *Client) UpdateAccountByName(ctx context.Context, host, username, newUsername, newPassword string) error { + var err error + params := accoper.UpdateAccountParams{ + Username: username, + NewUsername: newUsername, + NewPassword: newPassword, + } + reqdata, err := json.Marshal(params) + if err != nil { + return err + } + resdata, err := cli.doHTTPCall(ctx, host, "account", "update", reqdata) + if err != nil { + return err + } + response := handler.NewResponse[accoper.UpdateAccountResult]() + err = json.Unmarshal(resdata, response) + if err != nil { + return err + } + if response.Error { + err = errors.New(response.Message) + return err + } + return err +} + +func (cli *Client) DeleteAccountByName(ctx context.Context, host, username string) error { + var err error + + params := accoper.DeleteAccountParams{ + Username: username, + } + reqdata, err := json.Marshal(params) + if err != nil { + return err + } + resdata, err := cli.doHTTPCall(ctx, host, "account", "delete", reqdata) + if err != nil { + return err + } + response := handler.NewResponse[accoper.DeleteAccountResult]() + err = json.Unmarshal(resdata, response) + if err != nil { + return err + } + if response.Error { + err = errors.New(response.Message) + return err + } + return err +} + +func (cli *Client) DeleteAccountByID(ctx context.Context, host, accountID string) error { + var err error + params := accoper.DeleteAccountParams{ + AccountID: accountID, + } + reqdata, err := json.Marshal(params) + if err != nil { + return err + } + resdata, err := cli.doHTTPCall(ctx, host, "account", "delete", reqdata) + if err != nil { + return err + } + response := handler.NewResponse[accoper.DeleteAccountResult]() + err = json.Unmarshal(resdata, response) + if err != nil { + return err + } + if response.Error { + err = errors.New(response.Message) + return err + } + return err +} + +func (cli *Client) ListAccounts(ctx context.Context, host string) ([]descr.AccountShort, error) { + var err error + res := make([]descr.AccountShort, 0) + + params := accoper.ListAccountsParams{} + reqdata, err := json.Marshal(params) + if err != nil { + return res, err + } + resdata, err := cli.doHTTPCall(ctx, host, "accounts", "list", reqdata) + if err != nil { + return res, err + } + response := handler.NewResponse[accoper.ListAccountsResult]() + err = json.Unmarshal(resdata, response) + if err != nil { + return res, err + } + if response.Error { + err = errors.New(response.Message) + return res, err + } + res = response.Result.Accounts + return res, err +} diff --git a/pkg/rpccli/client.go b/pkg/accntcli/client.go similarity index 99% rename from pkg/rpccli/client.go rename to pkg/accntcli/client.go index a054b00..2cf8f48 100644 --- a/pkg/rpccli/client.go +++ b/pkg/accntcli/client.go @@ -1,4 +1,4 @@ -package rpccli +package accntcli import ( "crypto/tls" diff --git a/pkg/accntcli/grant.go b/pkg/accntcli/grant.go new file mode 100644 index 0000000..09c48f9 --- /dev/null +++ b/pkg/accntcli/grant.go @@ -0,0 +1,216 @@ +/* + * Copyright 2026 Oleg Borodin + * + * This work is published and licensed under a Creative Commons + * Attribution-NonCommercial-NoDerivatives 4.0 International License. + * + * Distribution of this work is permitted, but commercial use and + * modifications are strictly prohibited. + */ +package accntcli + +import ( + "context" + "encoding/json" + "errors" + + "mstore/app/accoper" + "mstore/app/handler" + "mstore/pkg/descr" +) + +func (cli *Client) CreateGrantByAccountID(ctx context.Context, host string, accountID, right, pattern string) (string, error) { + var err error + var res string + + params := accoper.CreateGrantParams{ + AccountID: accountID, + Right: right, + Pattern: pattern, + } + reqdata, err := json.Marshal(params) + if err != nil { + return res, err + } + respdata, err := cli.doHTTPCall(ctx, host, "grant", "create", reqdata) + if err != nil { + return res, err + } + + response := handler.NewResponse[accoper.CreateGrantResult]() + err = json.Unmarshal(respdata, response) + if err != nil { + return res, err + } + if response.Error { + err = errors.New(response.Message) + return res, err + } + res = response.Result.GrantID + return res, err +} + +func (cli *Client) CreateGrantByUsername(ctx context.Context, host, username string, right string, pattern string) (string, error) { + var err error + var res string + params := accoper.CreateGrantParams{ + Username: username, + Right: right, + Pattern: pattern, + } + reqdata, err := json.Marshal(params) + if err != nil { + return res, err + } + respdata, err := cli.doHTTPCall(ctx, host, "grant", "create", reqdata) + if err != nil { + return res, err + } + + response := handler.NewResponse[accoper.CreateGrantResult]() + err = json.Unmarshal(respdata, response) + if err != nil { + return res, err + } + if response.Error { + err = errors.New(response.Message) + return res, err + } + res = response.Result.GrantID + return res, err +} + +func (cli *Client) GetGrant(ctx context.Context, host, grantID string) (*descr.Grant, error) { + var err error + res := &descr.Grant{} + params := accoper.GetGrantParams{ + GrantID: grantID, + } + reqdata, err := json.Marshal(params) + if err != nil { + return res, err + } + respdata, err := cli.doHTTPCall(ctx, host, "grant", "get", reqdata) + if err != nil { + return res, err + } + + response := handler.NewResponse[accoper.GetGrantResult]() + err = json.Unmarshal(respdata, response) + if err != nil { + return res, err + } + if response.Error { + err = errors.New(response.Message) + return res, err + } + res = response.Result.Grant + return res, err +} + +func (cli *Client) UpdateGrant(ctx context.Context, host, grantID, newPattern string) error { + var err error + params := accoper.UpdateGrantParams{ + GrantID: grantID, + NewPattern: newPattern, + } + reqdata, err := json.Marshal(params) + if err != nil { + return err + } + respdata, err := cli.doHTTPCall(ctx, host, "grant", "update", reqdata) + if err != nil { + return err + } + response := handler.NewResponse[accoper.UpdateGrantResult]() + err = json.Unmarshal(respdata, response) + if err != nil { + return err + } + if response.Error { + err = errors.New(response.Message) + return err + } + return err +} + +func (cli *Client) DeleteGrant(ctx context.Context, host, grantID string) error { + var err error + params := accoper.DeleteGrantParams{ + GrantID: grantID, + } + reqdata, err := json.Marshal(params) + if err != nil { + return err + } + respdata, err := cli.doHTTPCall(ctx, host, "grant", "delete", reqdata) + if err != nil { + return err + } + + response := handler.NewResponse[accoper.DeleteGrantResult]() + err = json.Unmarshal(respdata, response) + if err != nil { + return err + } + if response.Error { + err = errors.New(response.Message) + return err + } + return err +} + +func (cli *Client) ListGrantsByAccountID(ctx context.Context, host, accountID string) ([]descr.Grant, error) { + var err error + res := make([]descr.Grant, 0) + params := accoper.ListGrantsParams{ + AccountID: accountID, + } + reqdata, err := json.Marshal(params) + if err != nil { + return res, err + } + respdata, err := cli.doHTTPCall(ctx, host, "grants", "list", reqdata) + if err != nil { + return res, err + } + response := handler.NewResponse[accoper.ListGrantsResult]() + err = json.Unmarshal(respdata, response) + if err != nil { + return res, err + } + if response.Error { + err = errors.New(response.Message) + return res, err + } + res = response.Result.Grants + return res, err +} + +func (cli *Client) ListGrantsByUsername(ctx context.Context, host, username string) ([]descr.Grant, error) { + var err error + res := make([]descr.Grant, 0) + params := accoper.ListGrantsParams{ + Username: username, + } + reqdata, err := json.Marshal(params) + if err != nil { + return res, err + } + respdata, err := cli.doHTTPCall(ctx, host, "grants", "list", reqdata) + if err != nil { + return res, err + } + + response := handler.NewResponse[accoper.ListGrantsResult]() + err = json.Unmarshal(respdata, response) + if err != nil { + return res, err + } + if response.Error { + err = errors.New(response.Message) + return res, err + } + res = response.Result.Grants + return res, err +} diff --git a/pkg/accntcli/referer.go b/pkg/accntcli/referer.go new file mode 100644 index 0000000..36e637f --- /dev/null +++ b/pkg/accntcli/referer.go @@ -0,0 +1,54 @@ +package accntcli + +import ( + "net/url" + "path" + "strings" +) + +type Referer struct { + urlobj *url.URL + user, pass string + obj, oper string +} + +func NewReferer(hostname, object, operation string) (*Referer, error) { + ref := &Referer{ + obj: object, + oper: operation, + } + if !strings.Contains(hostname, "://") { + hostname = "https://" + hostname + } + urlobj, err := url.Parse(hostname) + if err != nil { + return ref, err + } + if urlobj.User != nil { + ref.user = urlobj.User.Username() + ref.pass, _ = urlobj.User.Password() + urlobj.User = nil + } + urlobj.Path = "/" + ref.urlobj = urlobj + return ref, err +} + +func (ref *Referer) Raw() string { + return path.Join(ref.urlobj.Host, "/v3/api/", ref.obj, ref.oper) +} + +func (ref *Referer) Point() string { + curl := ref.urlobj.JoinPath("/v3/api/", ref.obj, ref.oper) + return curl.String() +} + +func (ref *Referer) Userinfo() (string, string) { + return ref.user, ref.pass +} + +func (ref *Referer) SetUserinfo(user, pass string) { + if user != "" && pass != "" { + ref.user, ref.pass = user, pass + } +} diff --git a/pkg/accntcli/service.go b/pkg/accntcli/service.go new file mode 100644 index 0000000..fb0270d --- /dev/null +++ b/pkg/accntcli/service.go @@ -0,0 +1,93 @@ +/* + * Copyright 2026 Oleg Borodin + * + * This work is published and licensed under a Creative Commons + * Attribution-NonCommercial-NoDerivatives 4.0 International License. + * + * Distribution of this work is permitted, but commercial use and + * modifications are strictly prohibited. + */ +package accntcli + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "strconv" + + "mstore/app/handler" + "mstore/app/imageoper" +) + +func (cli *Client) ServiceHello(ctx context.Context, host string) (bool, error) { + var res bool + var err error + + params := imageoper.SendHelloParams{} + reqdata, err := json.Marshal(params) + if err != nil { + return res, err + } + resdata, err := cli.doHTTPCall(ctx, host, "service", "hello", reqdata) + if err != nil { + return res, err + } + response := handler.Response[imageoper.SendHelloResult]{} + err = json.Unmarshal(resdata, &response) + if err != nil { + return res, err + } + if response.Error { + err = errors.New(response.Message) + return res, err + } + res = response.Result.Alive + return res, err +} + +func (cli *Client) doHTTPCall(ctx context.Context, host, obj, oper string, req []byte) ([]byte, error) { + var err error + var res []byte + + reader := bytes.NewReader(req) + ref, err := NewReferer(host, obj, oper) + if err != nil { + return res, err + } + httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, ref.Point(), reader) + if err != nil { + return res, err + } + httpReq.Header.Set("User-Agent", cli.userAgent) + httpReq.Header.Set("Accept", "*/*") + httpResp, err := cli.httpClient.Do(httpReq) + if err != nil { + return res, err + } + defer httpResp.Body.Close() + if httpResp.StatusCode != http.StatusOK { + err := fmt.Errorf("Unexpected response code: %s", httpResp.Status) + return res, err + } + contentLength := httpResp.Header.Get("Content-Length") + if contentLength == "" { + err := fmt.Errorf("Content-Length header is missing") + return res, err + } + blobSize, err := strconv.ParseInt(contentLength, 10, 64) + if err != nil { + return res, err + } + buffer := bytes.NewBuffer(nil) + recSize, err := io.Copy(buffer, httpResp.Body) + if blobSize != recSize { + err := fmt.Errorf("Mismatch declared and actual body size, %d and %d", blobSize, recSize) + return res, err + } + res = buffer.Bytes() + return res, err +} diff --git a/pkg/filecli/getfile.go b/pkg/filecli/getfile.go index 3826cf3..18c8391 100644 --- a/pkg/filecli/getfile.go +++ b/pkg/filecli/getfile.go @@ -38,6 +38,10 @@ func (cli *Client) GetFile(ctx context.Context, rawpath string, writer io.Writer return exist, err } contentLength := resp.Header.Get("Content-Length") + if contentLength == "" { + err := fmt.Errorf("Content-Length header is missing") + return exist, err + } blobSize, err := strconv.ParseInt(contentLength, 10, 64) if err != nil { return exist, err @@ -45,7 +49,7 @@ func (cli *Client) GetFile(ctx context.Context, rawpath string, writer io.Writer recSize, err := Copy(ctx, writer, resp.Body) if blobSize != recSize { - err := fmt.Errorf("Mismatch declared and actual body size, %d and %d", blobSize, recSize) + err := fmt.Errorf("Mismatch declared and actual body size: %d and %d", blobSize, recSize) return exist, err } exist = true diff --git a/pkg/filecli/listcoll.go b/pkg/filecli/listcoll.go index 9046af5..133180d 100644 --- a/pkg/filecli/listcoll.go +++ b/pkg/filecli/listcoll.go @@ -38,6 +38,10 @@ func (cli *Client) ListCollections(ctx context.Context, rawpath string) ([]byte, return list, err } contentLength := resp.Header.Get("Content-Length") + if contentLength == "" { + err := fmt.Errorf("Content-Length header is missing") + return list, err + } blobSize, err := strconv.ParseInt(contentLength, 10, 64) if err != nil { return list, err @@ -45,7 +49,7 @@ func (cli *Client) ListCollections(ctx context.Context, rawpath string) ([]byte, buffer := bytes.NewBuffer(nil) recSize, err := Copy(ctx, buffer, resp.Body) if blobSize != recSize { - err := fmt.Errorf("Mismatch declared and actual body size, %d and %d", blobSize, recSize) + err := fmt.Errorf("Mismatch declared and actual body size: %d and %d", blobSize, recSize) return list, err } list = buffer.Bytes() diff --git a/pkg/filecli/listfiles.go b/pkg/filecli/listfiles.go index b215d73..125411e 100644 --- a/pkg/filecli/listfiles.go +++ b/pkg/filecli/listfiles.go @@ -38,6 +38,10 @@ func (cli *Client) ListFiles(ctx context.Context, rawpath string) ([]byte, error return list, err } contentLength := resp.Header.Get("Content-Length") + if contentLength == "" { + err := fmt.Errorf("Content-Length header is missing") + return list, err + } blobSize, err := strconv.ParseInt(contentLength, 10, 64) if err != nil { return list, err @@ -45,7 +49,7 @@ func (cli *Client) ListFiles(ctx context.Context, rawpath string) ([]byte, error buffer := bytes.NewBuffer(nil) recSize, err := Copy(ctx, buffer, resp.Body) if blobSize != recSize { - err := fmt.Errorf("Mismatch declared and actual body size, %d and %d", blobSize, recSize) + err := fmt.Errorf("Mismatch declared and actual body size: %d and %d", blobSize, recSize) return list, err } list = buffer.Bytes() diff --git a/pkg/filecli/repo.go b/pkg/filecli/referer.go similarity index 70% rename from pkg/filecli/repo.go rename to pkg/filecli/referer.go index e2235ee..b762ef5 100644 --- a/pkg/filecli/repo.go +++ b/pkg/filecli/referer.go @@ -13,15 +13,15 @@ const ( PathTypeRegexp = "regexp" ) -type Repository struct { +type Referer struct { urlobj *url.URL user, pass string resource string values url.Values } -func ParsePath(rawpath string) (*Repository, error) { - repo := &Repository{ +func ParsePath(rawpath string) (*Referer, error) { + repo := &Referer{ values: url.Values{}, } if !strings.Contains(rawpath, "://") { @@ -43,7 +43,7 @@ func ParsePath(rawpath string) (*Repository, error) { return repo, err } -func (repo *Repository) Raw() string { +func (repo *Referer) Raw() string { res := path.Join(repo.urlobj.Host, repo.resource) query := repo.values.Encode() if query != "" { @@ -52,47 +52,47 @@ func (repo *Repository) Raw() string { return res } -func (repo *Repository) SetResource(resource string) { +func (repo *Referer) SetResource(resource string) { repo.resource = path.Join("/", resource) } -func (repo *Repository) JoinResource(resource string) { +func (repo *Referer) JoinResource(resource string) { repo.resource = path.Join("/", repo.resource, resource) } -func (repo *Repository) PathType(typ string) { +func (repo *Referer) PathType(typ string) { repo.values.Set("pathType", typ) } -func (repo *Repository) DryRun(yesno bool) { +func (repo *Referer) DryRun(yesno bool) { repo.values.Set("dryRun", strconv.FormatBool(yesno)) } -func (repo *Repository) File() string { +func (repo *Referer) File() string { curl := repo.urlobj.JoinPath("/v3/api/file/", repo.resource) return curl.String() } -func (repo *Repository) Files() string { +func (repo *Referer) Files() string { curl := repo.urlobj.JoinPath("/v3/api/files/", repo.resource) return curl.String() } -func (repo *Repository) Collection() string { +func (repo *Referer) Collection() string { curl := repo.urlobj.JoinPath("/v3/api/collection/", repo.resource) return curl.String() } -func (repo *Repository) Collections() string { +func (repo *Referer) Collections() string { curl := repo.urlobj.JoinPath("/v3/api/collections/", repo.resource) return curl.String() } -func (repo *Repository) Userinfo() (string, string) { +func (repo *Referer) Userinfo() (string, string) { return repo.user, repo.pass } -func (repo *Repository) SetUserinfo(user, pass string) { +func (repo *Referer) SetUserinfo(user, pass string) { if user != "" && pass != "" { repo.user, repo.pass = user, pass } diff --git a/pkg/repocli/blobexist.go b/pkg/repocli/blobexist.go index 3f4ab09..292193c 100644 --- a/pkg/repocli/blobexist.go +++ b/pkg/repocli/blobexist.go @@ -12,7 +12,7 @@ func (cli *Client) BlobExists(ctx context.Context, rawrepo string, digest string var exist bool var size int64 - ref, err := NewRepository(rawrepo) + ref, err := NewReferer(rawrepo) if err != nil { return exist, size, err } diff --git a/pkg/repocli/delblob.go b/pkg/repocli/delblob.go index bb6c652..58b5656 100644 --- a/pkg/repocli/delblob.go +++ b/pkg/repocli/delblob.go @@ -10,7 +10,7 @@ func (cli *Client) DeleteBlob(ctx context.Context, rawrepo, digest string) (bool var err error var exist bool - ref, err := NewRepository(rawrepo) + ref, err := NewReferer(rawrepo) if err != nil { return exist, err } diff --git a/pkg/repocli/delman.go b/pkg/repocli/delman.go index 0050a8f..c985b75 100644 --- a/pkg/repocli/delman.go +++ b/pkg/repocli/delman.go @@ -10,7 +10,7 @@ func (cli *Client) DeleteManifest(ctx context.Context, rawrepo, tag string) (boo var err error var exist bool - ref, err := NewRepository(rawrepo) + ref, err := NewReferer(rawrepo) if err != nil { return exist, err } diff --git a/pkg/repocli/getblob.go b/pkg/repocli/getblob.go index ee92aab..b1c6ace 100644 --- a/pkg/repocli/getblob.go +++ b/pkg/repocli/getblob.go @@ -12,7 +12,7 @@ func (cli *Client) GetBlob(ctx context.Context, rawrepo string, writer io.Writer var err error var exist bool - ref, err := NewRepository(rawrepo) + ref, err := NewReferer(rawrepo) if err != nil { return exist, err } diff --git a/pkg/repocli/getman.go b/pkg/repocli/getman.go index 986d894..2731234 100644 --- a/pkg/repocli/getman.go +++ b/pkg/repocli/getman.go @@ -19,7 +19,7 @@ func (cli *Client) GetManifest(ctx context.Context, rawrepo, tag string) (bool, var mime string var man []byte - ref, err := NewRepository(rawrepo) + ref, err := NewReferer(rawrepo) if err != nil { return exist, mime, man, err } diff --git a/pkg/repocli/getupload.go b/pkg/repocli/getupload.go index 3caf8e3..837d2e1 100644 --- a/pkg/repocli/getupload.go +++ b/pkg/repocli/getupload.go @@ -10,7 +10,7 @@ func (cli *Client) GetUpload(ctx context.Context, rawrepo string) (string, error var err error var loc string - ref, err := NewRepository(rawrepo) + ref, err := NewReferer(rawrepo) if err != nil { return loc, err } diff --git a/pkg/repocli/manexist.go b/pkg/repocli/manexist.go index f4855ac..5098e81 100644 --- a/pkg/repocli/manexist.go +++ b/pkg/repocli/manexist.go @@ -14,7 +14,7 @@ func (cli *Client) ManifestExists(ctx context.Context, rawrepo, tag string) (boo var size int64 var csum string - ref, err := NewRepository(rawrepo) + ref, err := NewReferer(rawrepo) if err != nil { return exist, mime, size, csum, err } diff --git a/pkg/repocli/patchupload.go b/pkg/repocli/patchupload.go new file mode 100644 index 0000000..cefb5f5 --- /dev/null +++ b/pkg/repocli/patchupload.go @@ -0,0 +1,45 @@ +package repocli + +import ( + "context" + "fmt" + "io" + "net/http" + "strconv" +) + +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 := NewReferer(rawrepo) + if err != nil { + return ouloc, err + } + uri, err := ref.Patch(uploc) + if err != nil { + return ouloc, err + } + 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)) + 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/pkg/repocli/putman.go b/pkg/repocli/putman.go index a3da22e..6b4e254 100644 --- a/pkg/repocli/putman.go +++ b/pkg/repocli/putman.go @@ -12,7 +12,7 @@ import ( func (cli *Client) PutManifest(ctx context.Context, rawrepo, tag string, man []byte, mime string) error { var err error - ref, err := NewRepository(rawrepo) + ref, err := NewReferer(rawrepo) if err != nil { return err } diff --git a/pkg/repocli/putupload.go b/pkg/repocli/putupload.go index 31065ab..77bdd07 100644 --- a/pkg/repocli/putupload.go +++ b/pkg/repocli/putupload.go @@ -12,7 +12,7 @@ func (cli *Client) PutUpload(ctx context.Context, rawrepo string, src io.Reader, var err error var bloc string - ref, err := NewRepository(rawrepo) + ref, err := NewReferer(rawrepo) if err != nil { return bloc, err } diff --git a/pkg/repocli/repo.go b/pkg/repocli/referer.go similarity index 77% rename from pkg/repocli/repo.go rename to pkg/repocli/referer.go index 17d4cfa..39f1517 100644 --- a/pkg/repocli/repo.go +++ b/pkg/repocli/referer.go @@ -5,14 +5,14 @@ import ( "strings" ) -type Repository struct { +type Referer struct { urlobj *url.URL user, pass string base string } -func NewRepository(rawrepo string) (*Repository, error) { - repo := &Repository{} +func NewReferer(rawrepo string) (*Referer, error) { + repo := &Referer{} if !strings.Contains(rawrepo, "://") { rawrepo = "https://" + rawrepo } @@ -33,27 +33,27 @@ func NewRepository(rawrepo string) (*Repository, error) { return repo, err } -func (repo *Repository) String() string { +func (repo *Referer) String() string { curl := repo.urlobj.JoinPath(repo.base) return curl.String() } -func (repo *Repository) Manifest(tag string) string { +func (repo *Referer) Manifest(tag string) string { curl := repo.urlobj.JoinPath("/v2", repo.base, "/manifests", tag) return curl.String() } -func (repo *Repository) Blob(digest string) string { +func (repo *Referer) Blob(digest string) string { curl := repo.urlobj.JoinPath("/v2", repo.base, "/blobs", digest) return curl.String() } -func (repo *Repository) Upload() string { +func (repo *Referer) Upload() string { curl := repo.urlobj.JoinPath("/v2", repo.base, "/blobs/uploads/") return curl.String() } -func (repo *Repository) Patch(loc string) (string, error) { +func (repo *Referer) Patch(loc string) (string, error) { var curl *url.URL var out string var err error @@ -73,7 +73,7 @@ func (repo *Repository) Patch(loc string) (string, error) { return out, err } -func (repo *Repository) Put(loc, digest string) (string, error) { +func (repo *Referer) Put(loc, digest string) (string, error) { var curl *url.URL var out string var err error @@ -95,6 +95,6 @@ func (repo *Repository) Put(loc, digest string) (string, error) { return out, err } -func (repo *Repository) Userinfo() (string, string) { +func (repo *Referer) Userinfo() (string, string) { return repo.user, repo.pass } diff --git a/pkg/repocli/repo_test.go b/pkg/repocli/referer_test.go similarity index 90% rename from pkg/repocli/repo_test.go rename to pkg/repocli/referer_test.go index 5599629..0ef1e6c 100644 --- a/pkg/repocli/repo_test.go +++ b/pkg/repocli/referer_test.go @@ -10,7 +10,7 @@ import ( ) func xxxTestResrerer(t *testing.T) { - ref, err := NewRepository("registry.example.com/lib/alpine") + ref, err := NewReferer("registry.example.com/lib/alpine") require.NoError(t, err) fmt.Printf("Manifest:\t%s\n", ref.Manifest("3.30.0")) diff --git a/pkg/rpccli/repo.go b/pkg/rpccli/repo.go deleted file mode 100644 index 9d29906..0000000 --- a/pkg/rpccli/repo.go +++ /dev/null @@ -1,56 +0,0 @@ -package rpccli - -import ( - "net/url" - "path" - "strconv" - "strings" -) - -type Referer struct { - urlobj *url.URL - user, pass string - obj, oper string -} - -func NewReferer(hostname, object, operation string) (*Referer, error) { - repo := &Referer{ - obj: object, - oper: operation, - } - if !strings.Contains(hostname, "://") { - hostname = "https://" + hostname - } - urlobj, err := url.Parse(hostname) - if err != nil { - return repo, err - } - if urlobj.User != nil { - repo.user = urlobj.User.Username() - repo.pass, _ = urlobj.User.Password() - urlobj.User = nil - } - repo.resource = path.Join("/", urlobj.Path) - urlobj.Path = "/" - repo.urlobj = urlobj - return repo, err -} - -func (repo *Referer) Raw() string { - return path.Join(repo.urlobj.Host, "/v3/api/", repo.obj, repo.oper) -} - -func (repo *Referer) URI(object, operation) string { - curl := repo.urlobj.JoinPath("/v3/api/", object, operation) - return curl.String() -} - -func (repo *Referer) Userinfo() (string, string) { - return repo.user, repo.pass -} - -func (repo *Referer) SetUserinfo(user, pass string) { - if user != "" && pass != "" { - repo.user, repo.pass = user, pass - } -}