working commit

This commit is contained in:
2026-02-11 12:53:31 +02:00
parent 517c518df2
commit 6fe7f7c15e
12 changed files with 392 additions and 186 deletions
+131
View File
@@ -0,0 +1,131 @@
/*
* Copyright 2026 Oleg Borodin <onborodin@gmail.com>
*
* 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 client
import (
"bytes"
"context"
"crypto/tls"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"mstore/app/handler"
"mstore/app/operator"
"mstore/pkg/auxhttp"
)
func (cli *Client) CreateAccount(ctx context.Context, hosturi, username, password string) error {
var err error
apiuri, err := url.JoinPath(hosturi, "/v3/api/account/get")
if err != nil {
return err
}
operParams := operator.CreateAccountParams{
Username: username,
Password: password,
}
paramsJson, err := json.Marshal(operParams)
if err != nil {
return err
}
respBytes, err := doHTTPCall(ctx, apiuri, paramsJson)
if err != nil {
return err
}
operRes := handler.NewResponse[operator.CreateAccountResult]()
err = json.Unmarshal(respBytes, operRes)
if err != nil {
return err
}
if !operRes.Error {
err = fmt.Errorf("%s", operRes.Message)
return err
}
return err
}
func (cli *Client) GetAccount(ctx context.Context, hosturi, id, username string) error {
var err error
apipath, err := url.JoinPath(hosturi, "/v3/api/account/get")
if err != nil {
return err
}
operParams := operator.GetAccountParams{
Username: username,
AccountID: id,
}
paramsJson, err := json.Marshal(operParams)
if err != nil {
return err
}
respBytes, err := doHTTPCall(ctx, apipath, paramsJson)
if err != nil {
return err
}
operRes := handler.NewResponse[operator.CreateAccountResult]()
err = json.Unmarshal(respBytes, operRes)
if err != nil {
return err
}
if !operRes.Error {
err = fmt.Errorf("%s", operRes.Message)
return err
}
return err
}
func doHTTPCall(ctx context.Context, apiuri string, reqBytes []byte) ([]byte, error) {
var err error
respBytes := make([]byte, 0)
apiuri, username, password, err := repackServiceURI(apiuri)
if err != nil {
return respBytes, err
}
reqBuffer := bytes.NewBuffer(reqBytes)
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, apiuri, reqBuffer)
if err != nil {
return respBytes, err
}
httpReq.Header.Set("Content-Type", "application/json")
if username != "" && password != "" {
basicHeader := auxhttp.EncodeBasicAuth(username, password)
httpReq.Header.Add("Authorization", basicHeader)
}
transport := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
httpClient := &http.Client{
Transport: transport,
}
httpResp, err := httpClient.Do(httpReq)
if err != nil {
return respBytes, err
}
defer httpResp.Body.Close()
if httpResp.StatusCode != http.StatusOK {
err := fmt.Errorf("Wrong StatusCode header: %s", httpResp.Status)
return respBytes, err
}
respBuffer := bytes.NewBuffer(nil)
_, err = io.Copy(respBuffer, httpResp.Body)
if err != nil {
return respBytes, err
}
respBytes = respBuffer.Bytes()
return respBytes, err
}
-79
View File
@@ -1,79 +0,0 @@
/*
* Copyright 2026 Oleg Borodin <onborodin@gmail.com>
*
* 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 client
import (
"encoding/base64"
"net/url"
"path"
"strings"
)
const (
serviceAPI = "/v3/api/service/"
fileAPI = "/v3/api/file/"
filesAPI = "/v3/api/files/"
)
func encodeBasicAuth(username, password string) string {
auth := username + ":" + password
return "Basic " + base64.StdEncoding.EncodeToString([]byte(auth))
}
func convertServiceRefer(ref string) (string, error) {
var err error
var res string
if !strings.Contains(ref, "://") {
ref = "https://" + ref
}
url, err := url.Parse(ref)
if err != nil {
return res, err
}
url.Path = path.Clean(url.Path)
url.Path = path.Join(serviceAPI, url.Path)
url.User = nil
res = url.String()
return res, err
}
func convertFileRefer(ref string) (string, error) {
var err error
var res string
if !strings.Contains(ref, "://") {
ref = "https://" + ref
}
url, err := url.Parse(ref)
if err != nil {
return res, err
}
url.Path = path.Clean(url.Path)
url.Path = path.Join(fileAPI, url.Path)
url.User = nil
res = url.String()
return res, err
}
func convertFilesRefer(ref string) (string, error) {
var err error
var res string
if !strings.Contains(ref, "://") {
ref = "https://" + ref
}
url, err := url.Parse(ref)
if err != nil {
return res, err
}
url.Path = path.Clean(url.Path)
url.Path = path.Join(filesAPI, url.Path)
url.User = nil
res = url.String()
return res, err
}
+21 -14
View File
@@ -13,40 +13,47 @@ import (
"context"
"crypto/tls"
"net/http"
"net/url"
"time"
)
type Client struct {
username string
password string
}
type Client struct{}
func NewClient() *Client {
return &Client{}
}
func NewClientWithAuth(username, password string) *Client {
return &Client{
username: username,
password: password,
func convertServiceURI(ref string) (string, error) {
var err error
var res string
const serviceAPI = "/v3/api/service/"
const serviceScheme = "https"
uri, err := url.Parse(ref)
if err != nil {
return res, err
}
uri.Path = serviceAPI
uri.Scheme = serviceScheme
res = uri.String()
return res, err
}
func (cli *Client) ServiceHello(ctx context.Context, ref string, timeout time.Duration) (bool, error) {
func (cli *Client) ServiceHello(ctx context.Context, serviceuri string, timeout time.Duration) (bool, error) {
var res bool
var err error
ctx, _ = context.WithTimeout(ctx, timeout)
ref, err = convertServiceRefer(ref)
serviceuri, _, _, err = repackServiceURI(serviceuri)
if err != nil {
return res, err
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, ref, nil)
serviceuri, err = convertServiceURI(serviceuri)
if err != nil {
return res, err
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, serviceuri, nil)
if err != nil {
return res, err
}
transport := &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
+120 -61
View File
@@ -15,28 +15,17 @@ import (
"fmt"
"io"
"net/http"
"net/url"
"os"
"path"
"path/filepath"
"strconv"
"strings"
"mstore/pkg/auxhttp"
)
func (cli *Client) FileExists(ctx context.Context, ref string) (bool, error) {
var res bool
var err error
ref, err = convertFileRefer(ref)
if err != nil {
return res, err
}
req, err := http.NewRequestWithContext(ctx, http.MethodHead, ref, nil)
if err != nil {
return res, err
}
if cli.username != "" && cli.password != "" {
req.Header.Add("Authorization", encodeBasicAuth(cli.username, cli.password))
}
func makeHTTPClient() *http.Client {
transport := &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
@@ -45,6 +34,80 @@ func (cli *Client) FileExists(ctx context.Context, ref string) (bool, error) {
client := &http.Client{
Transport: transport,
}
return client
}
func convertFileURI(fileuri string) (string, error) {
var err error
var res string
uri, err := url.Parse(fileuri)
if err != nil {
return res, err
}
const fileAPI = "/v3/api/file/"
uri.Path, err = url.JoinPath(fileAPI, uri.Path)
if err != nil {
return res, err
}
res = uri.String()
return res, err
}
func convertFilesURI(fileuri string) (string, error) {
var err error
var res string
uri, err := url.Parse(fileuri)
const filesAPI = "/v3/api/files/"
uri.Path, err = url.JoinPath(filesAPI, uri.Path)
if err != nil {
return res, err
}
res = uri.String()
return res, err
}
func repackServiceURI(fileuri string) (string, string, string, error) {
var err error
var res, username, password string
if !strings.Contains(fileuri, "://") {
fileuri = "https://" + fileuri
}
uri, err := url.Parse(fileuri)
if err != nil {
return res, username, password, err
}
uri.Path = path.Clean(uri.Path)
if uri.User != nil {
username = uri.User.Username()
password, _ = uri.User.Password()
}
uri.User = nil
uri.Scheme = "https"
res = uri.String()
return res, username, password, err
}
func (cli *Client) FileExists(ctx context.Context, fileuri string) (bool, error) {
var res bool
var err error
fileuri, username, password, err := repackServiceURI(fileuri)
if err != nil {
return res, err
}
fileuri, err = convertFileURI(fileuri)
if err != nil {
return res, err
}
req, err := http.NewRequestWithContext(ctx, http.MethodHead, fileuri, nil)
if err != nil {
return res, err
}
if username != "" && password != "" {
basic := auxhttp.EncodeBasicAuth(username, password)
req.Header.Add("Authorization", basic)
}
client := makeHTTPClient()
resp, err := client.Do(req)
if err != nil {
return res, err
@@ -56,9 +119,14 @@ func (cli *Client) FileExists(ctx context.Context, ref string) (bool, error) {
return res, err
}
func (cli *Client) PutFile(ctx context.Context, filename, ref string) error {
func (cli *Client) PutFile(ctx context.Context, filename, fileuri string) error {
var err error
ref, err = convertFileRefer(ref)
fileuri, username, password, err := repackServiceURI(fileuri)
if err != nil {
return err
}
fileuri, err = convertFileURI(fileuri)
if err != nil {
return err
}
@@ -68,23 +136,10 @@ func (cli *Client) PutFile(ctx context.Context, filename, ref string) error {
}
defer file.Close()
req, err := http.NewRequestWithContext(ctx, http.MethodPut, ref, file)
req, err := http.NewRequestWithContext(ctx, http.MethodPut, fileuri, file)
if err != nil {
return err
}
if cli.username != "" && cli.password != "" {
req.Header.Add("Authorization", encodeBasicAuth(cli.username, cli.password))
}
transport := &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
}
client := &http.Client{
Transport: transport,
}
fileinfo, err := os.Stat(filename)
if err != nil {
return err
@@ -93,6 +148,11 @@ func (cli *Client) PutFile(ctx context.Context, filename, ref string) error {
req.ContentLength = filesize
req.Header.Set("Content-Type", "application/octet-stream")
if username != "" && password != "" {
basic := auxhttp.EncodeBasicAuth(username, password)
req.Header.Add("Authorization", basic)
}
client := makeHTTPClient()
resp, err := client.Do(req)
if err != nil {
@@ -107,31 +167,31 @@ func (cli *Client) PutFile(ctx context.Context, filename, ref string) error {
return err
}
func (cli *Client) GetFile(ctx context.Context, ref, filename string) (int64, error) {
func (cli *Client) GetFile(ctx context.Context, fileuri, filename string) (int64, error) {
var err error
var size int64
ref, err = convertFileRefer(ref)
fileuri, username, password, err := repackServiceURI(fileuri)
if err != nil {
return size, err
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, ref, nil)
fileuri, err = convertFileURI(fileuri)
if err != nil {
return size, err
}
if cli.username != "" && cli.password != "" {
req.Header.Add("Authorization", encodeBasicAuth(cli.username, cli.password))
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fileuri, nil)
if err != nil {
return size, err
}
transport := &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
}
client := &http.Client{
Transport: transport,
if username != "" && password != "" {
basic := auxhttp.EncodeBasicAuth(username, password)
req.Header.Add("Authorization", basic)
}
client := makeHTTPClient()
resp, err := client.Do(req)
if err != nil {
return size, err
@@ -172,29 +232,28 @@ func (cli *Client) GetFile(ctx context.Context, ref, filename string) (int64, er
return size, err
}
func (cli *Client) DeleteFile(ctx context.Context, ref, filename string) error {
func (cli *Client) DeleteFile(ctx context.Context, fileuri, filename string) error {
var err error
ref, err = convertFileRefer(ref)
if err != nil {
return err
}
req, err := http.NewRequestWithContext(ctx, http.MethodDelete, ref, nil)
fileuri, username, password, err := repackServiceURI(fileuri)
if err != nil {
return err
}
if cli.username != "" && cli.password != "" {
req.Header.Add("Authorization", encodeBasicAuth(cli.username, cli.password))
fileuri, err = convertFileURI(fileuri)
if err != nil {
return err
}
req, err := http.NewRequestWithContext(ctx, http.MethodDelete, fileuri, nil)
if err != nil {
return err
}
if username != "" && password != "" {
basic := auxhttp.EncodeBasicAuth(username, password)
req.Header.Add("Authorization", basic)
}
transport := &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
}
client := &http.Client{
Transport: transport,
}
client := makeHTTPClient()
resp, err := client.Do(req)
if err != nil {
return err
-23
View File
@@ -1,23 +0,0 @@
/*
* Copyright 2026 Oleg Borodin <onborodin@gmail.com>
*
* 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 client
import (
"encoding/hex"
"fmt"
"math/rand"
)
func makeTmpFileName(prefix string) string {
randBytes := make([]byte, 6)
rand.Read(randBytes)
suffix := hex.EncodeToString(randBytes)
return fmt.Sprintf("%s.tmp.%s", prefix, suffix)
}
+25
View File
@@ -12,6 +12,8 @@ package client
import (
"crypto/tls"
"net/http"
"net/url"
"strings"
)
type roundTripper struct{}
@@ -25,3 +27,26 @@ func (t *roundTripper) RoundTrip(r *http.Request) (*http.Response, error) {
}
return httpTransport.RoundTrip(r)
}
func repackReference(ref string) (string, string, string, error) {
var err error
var username string
var password string
if !strings.Contains(ref, `://`) {
ref = "https://" + ref
}
uri, err := url.Parse(ref)
if err != nil {
return ref, username, password, err
}
username = uri.User.Username()
password, _ = uri.User.Password()
uri.User = nil
uri.Path, _ = url.JoinPath("/", uri.Path)
if err != nil {
return ref, username, password, err
}
ref = uri.Host + uri.Path
return ref, username, password, err
}
+10 -7
View File
@@ -31,8 +31,11 @@ func (cli *Client) ImageInfo(ctx context.Context, imagepath string, timeout time
var err error
res := &ImageDescr{}
imagepath, username, password, err := repackReference(imagepath)
if err != nil {
return res, err
}
ctx, _ = context.WithTimeout(ctx, timeout)
options := make([]crane.Option, 0)
options = append(options, crane.WithContext(ctx))
@@ -45,7 +48,7 @@ func (cli *Client) ImageInfo(ctx context.Context, imagepath string, timeout time
return res, err
}
if cli.username != "" && cli.password != "" {
if username != "" && password != "" {
defaultTransport := &roundTripper{}
scopes := []string{repo.Scope(transport.PullScope)}
@@ -55,8 +58,8 @@ func (cli *Client) ImageInfo(ctx context.Context, imagepath string, timeout time
return res, err
}
basicAuth := &authn.Basic{
Username: cli.username,
Password: cli.password,
Username: username,
Password: password,
}
authTransport, err := transport.NewWithContext(ctx, reg, basicAuth, defaultTransport, scopes)
if err != nil {
@@ -90,7 +93,7 @@ func (cli *Client) ImageInfo(ctx context.Context, imagepath string, timeout time
remoteOptions := make([]remote.Option, 0)
remoteOptions = append(remoteOptions, remote.WithContext(ctx))
if cli.username != "" && cli.password != "" {
if username != "" && password != "" {
defaultTransport := &roundTripper{}
scopes := []string{repo.Scope(transport.PullScope)}
@@ -100,8 +103,8 @@ func (cli *Client) ImageInfo(ctx context.Context, imagepath string, timeout time
return res, err
}
basicAuth := &authn.Basic{
Username: cli.username,
Password: cli.password,
Username: username,
Password: password,
}
authTransport, err := transport.NewWithContext(ctx, reg, basicAuth, defaultTransport, scopes)
if err != nil {
+10 -4
View File
@@ -14,6 +14,7 @@ import (
"os"
"time"
"mstore/pkg/auxtool"
"mstore/pkg/auxutar"
"github.com/google/go-containerregistry/pkg/authn"
@@ -26,10 +27,15 @@ func (cli *Client) PullImage(ctx context.Context, filepath, imagepath string, ti
var err error
ctx, _ = context.WithTimeout(ctx, timeout)
imagepath, username, password, err := repackReference(imagepath)
if err != nil {
return err
}
options := make([]crane.Option, 0)
options = append(options, crane.WithContext(ctx))
if cli.username != "" && cli.password != "" {
if username != "" && password != "" {
ref, err := name.ParseReference(imagepath)
if err != nil {
return err
@@ -48,8 +54,8 @@ func (cli *Client) PullImage(ctx context.Context, filepath, imagepath string, ti
return err
}
basicAuth := &authn.Basic{
Username: cli.username,
Password: cli.password,
Username: username,
Password: password,
}
authTransport, err := transport.NewWithContext(ctx, reg, basicAuth, defaultTransport, scopes)
if err != nil {
@@ -65,7 +71,7 @@ func (cli *Client) PullImage(ctx context.Context, filepath, imagepath string, ti
if err != nil {
return err
}
dstdir := makeTmpFileName(filepath)
dstdir := auxtool.MakeTmpFilename(filepath)
err = os.MkdirAll(dstdir, 0750)
if err != nil {
return err
+10 -4
View File
@@ -15,6 +15,7 @@ import (
"os"
"time"
"mstore/pkg/auxtool"
"mstore/pkg/auxutar"
"github.com/google/go-containerregistry/pkg/authn"
@@ -29,9 +30,14 @@ func (cli *Client) PushImage(ctx context.Context, filepath, imagepath string, ti
var err error
ctx, _ = context.WithTimeout(ctx, timeout)
imagepath, username, password, err := repackReference(imagepath)
if err != nil {
return err
}
options := make([]crane.Option, 0)
options = append(options, crane.WithContext(ctx))
if cli.username != "" && cli.password != "" {
if username != "" && password != "" {
ref, err := name.ParseReference(imagepath)
if err != nil {
return err
@@ -53,8 +59,8 @@ func (cli *Client) PushImage(ctx context.Context, filepath, imagepath string, ti
return err
}
basicAuth := &authn.Basic{
Username: cli.username,
Password: cli.password,
Username: username,
Password: password,
}
authTransport, err := transport.NewWithContext(ctx, reg, basicAuth, defaultTransport, scopes)
@@ -67,7 +73,7 @@ func (cli *Client) PushImage(ctx context.Context, filepath, imagepath string, ti
options = append(options, crane.WithTransport(defaultTransport))
}
dstdir := makeTmpFileName(filepath)
dstdir := auxtool.MakeTmpFilename(filepath)
err = auxutar.Unarchive(filepath, dstdir)
if err != nil {
os.RemoveAll(dstdir)