From 957e655694a6e0c84abc486fe8f2d5293d64a8dc 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: Mon, 2 Mar 2026 11:05:29 +0200 Subject: [PATCH] added pkg/filecli/ --- pkg/filecli/authbas.go | 20 +++++++ pkg/filecli/client.go | 125 +++++++++++++++++++++++++++++++++++++++ pkg/filecli/copyctx.go | 39 ++++++++++++ pkg/filecli/delfile.go | 38 ++++++++++++ pkg/filecli/fileexist.go | 50 ++++++++++++++++ pkg/filecli/getfile.go | 53 +++++++++++++++++ pkg/filecli/putfile.go | 35 +++++++++++ pkg/filecli/repo.go | 49 +++++++++++++++ 8 files changed, 409 insertions(+) create mode 100644 pkg/filecli/authbas.go create mode 100644 pkg/filecli/client.go create mode 100644 pkg/filecli/copyctx.go create mode 100644 pkg/filecli/delfile.go create mode 100644 pkg/filecli/fileexist.go create mode 100644 pkg/filecli/getfile.go create mode 100644 pkg/filecli/putfile.go create mode 100644 pkg/filecli/repo.go diff --git a/pkg/filecli/authbas.go b/pkg/filecli/authbas.go new file mode 100644 index 0000000..4e51400 --- /dev/null +++ b/pkg/filecli/authbas.go @@ -0,0 +1,20 @@ +package filecli + +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 "Authorization", "Basic " + pair, nil +} diff --git a/pkg/filecli/client.go b/pkg/filecli/client.go new file mode 100644 index 0000000..5670e4b --- /dev/null +++ b/pkg/filecli/client.go @@ -0,0 +1,125 @@ +package filecli + +import ( + "crypto/tls" + "encoding/base64" + "net/http" +) + +type Client struct { + httpClient *http.Client + userAgent string +} + +func NewClient(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 +} + +type MiddlewareFunc func(next http.RoundTripper) http.RoundTripper + +func (cli *Client) UseMiddleware(mwFunc MiddlewareFunc) { + cli.httpClient.Transport = mwFunc(cli.httpClient.Transport) +} + +// ExampleMiddleware +func NewExampleMiddleware() MiddlewareFunc { + return func(next http.RoundTripper) http.RoundTripper { + return newExampleTransport(next) + } +} + +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 +} + +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/pkg/filecli/copyctx.go b/pkg/filecli/copyctx.go new file mode 100644 index 0000000..9b3c077 --- /dev/null +++ b/pkg/filecli/copyctx.go @@ -0,0 +1,39 @@ +package filecli + +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/pkg/filecli/delfile.go b/pkg/filecli/delfile.go new file mode 100644 index 0000000..214d2d0 --- /dev/null +++ b/pkg/filecli/delfile.go @@ -0,0 +1,38 @@ +package filecli + +import ( + "context" + "fmt" + "net/http" +) + +func (cli *Client) DeleteFile(ctx context.Context, rawpath string) (bool, error) { + var err error + var exist bool + + ref, err := ParsePath(rawpath) + if err != nil { + return exist, err + } + uri := ref.File() + 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", "*/*") + 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/pkg/filecli/fileexist.go b/pkg/filecli/fileexist.go new file mode 100644 index 0000000..14632b3 --- /dev/null +++ b/pkg/filecli/fileexist.go @@ -0,0 +1,50 @@ +package filecli + +import ( + "context" + "fmt" + "net/http" + "strconv" +) + +func (cli *Client) FileExists(ctx context.Context, rawpath string) (bool, int64, error) { + var err error + var exist bool + var size int64 + + ref, err := ParsePath(rawpath) + if err != nil { + return exist, size, err + } + uri := ref.File() + + 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", "*/*") + + 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/pkg/filecli/getfile.go b/pkg/filecli/getfile.go new file mode 100644 index 0000000..6f854e5 --- /dev/null +++ b/pkg/filecli/getfile.go @@ -0,0 +1,53 @@ +package filecli + +import ( + "context" + "fmt" + "io" + "net/http" + "strconv" +) + +func (cli *Client) GetFile(ctx context.Context, rawpath string, writer io.Writer) (bool, error) { + var err error + var exist bool + + ref, err :=ParsePath(rawpath) + if err != nil { + return exist, err + } + uri := ref.File() + + 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", "*/*") + 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/pkg/filecli/putfile.go b/pkg/filecli/putfile.go new file mode 100644 index 0000000..c21cf51 --- /dev/null +++ b/pkg/filecli/putfile.go @@ -0,0 +1,35 @@ +package filecli + +import ( + "context" + "fmt" + "io" + "net/http" + "strconv" +) + +func (cli *Client) PutFile(ctx context.Context, rawpath string, src io.Reader, size int64) error { + var err error + ref, err := ParsePath(rawpath) + if err != nil { + return err + } + uri := ref.File() + req, err := http.NewRequestWithContext(ctx, http.MethodPut, uri, src) + if err != nil { + return 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 err + } + resp.Body.Close() + if resp.StatusCode != http.StatusOK { + err = fmt.Errorf("File not accepted, code %d", resp.StatusCode) + return err + } + return err +} diff --git a/pkg/filecli/repo.go b/pkg/filecli/repo.go new file mode 100644 index 0000000..0c436be --- /dev/null +++ b/pkg/filecli/repo.go @@ -0,0 +1,49 @@ +package filecli + +import ( + "net/url" + "strings" +) + +type Repository struct { + urlobj *url.URL + user, pass string + resource string +} + +func ParsePath(rawpath string) (*Repository, error) { + repo := &Repository{} + if !strings.Contains(rawpath, "://") { + rawpath = "https://" + rawpath + } + urlobj, err := url.Parse(rawpath) + 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 = repo.urlobj.Path + repo.urlobj = urlobj + repo.urlobj.Path = "/" + repo.urlobj = urlobj + + return repo, err +} + +func (repo *Repository) File() string { + curl := repo.urlobj.JoinPath("/v3/api/file", repo.resource) + return curl.String() +} + +func (repo *Repository) Files() string { + curl := repo.urlobj.JoinPath("/v3/api/files", repo.resource) + return curl.String() +} + + +func (repo *Repository) Userinfo() (string, string) { + return repo.user, repo.pass +}