diff --git a/app/descr/account.go b/app/descr/account.go index 9607696..00df3b3 100644 --- a/app/descr/account.go +++ b/app/descr/account.go @@ -1,3 +1,13 @@ +/* + * 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 descr const ( @@ -8,17 +18,30 @@ const ( ) type Account struct { - ID int64 `json:"id" yaml:"id" db:"id"` - Username string `json:"username" yaml:"username" db:"username"` - Passhash string `json:"passhash" yaml:"passhash" db:"passhash"` - Disabled bool `json:"disabled" yaml:"disabled" db:"disabled"` - CreatedAt string `json:"createdAt" yaml:"createdAt" db:"created_at"` - UpdatedAt string `json:"updatedAt,omitempty" yaml:"updatedAt,omitempty" db:"updated_at"` + ID string `json:"id" db:"id"` + Username string `json:"username" db:"username"` + Passhash string `json:"passhash" db:"passhash"` + Disabled bool `json:"disabled" db:"disabled"` + CreatedAt string `json:"createdAt" db:"created_at"` + UpdatedAt string `json:"updatedAt,omitempty" db:"updated_at"` } type Grant struct { - ID int64 `json:"id" yaml:"id" db:"id"` - AccountID int64 `json:"accountID" yaml:"accountID" db:"account_id"` - Operation string `json:"operation" yaml:"operation" db:"operation"` - CreatedAt string `json:"createdAt" yaml:"createdAt" db:"created_at"` + ID string `json:"id" db:"id"` + AccountID string `json:"accountID" db:"account_id"` + Operation string `json:"operation" db:"operation"` + CreatedAt string `json:"createdAt" db:"created_at"` +} + +type GrantShortDescr struct { + Operation string `json:"operation"` + CreatedAt string `json:"createdAt"` +} + +type AccountShortDescr struct { + Username string `json:"username"` + Disabled bool `json:"disabled"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt,omitempty"` + Grants []GrantShortDescr `json:"grants"` } diff --git a/app/handler/authmw.go b/app/handler/authmw.go index c6934c6..11e33b3 100644 --- a/app/handler/authmw.go +++ b/app/handler/authmw.go @@ -36,14 +36,14 @@ func (hand *Handler) CheckAccess(rctx *router.Context) (bool, error) { var res bool authHeader := rctx.GetHeader("Authorization") - if authHeader != "" { - hand.logg.Debugf("Authorization header is %s", authHeader) - username, password, err := auxhttp.ParseBasicAuth(authHeader) - if err != nil { - return res, err - } - hand.logg.Debugf("Authorization username is %s:%s", username, password) - } + if authHeader != "" { + hand.logg.Debugf("Authorization header is %s", authHeader) + username, password, err := auxhttp.ParseBasicAuth(authHeader) + if err != nil { + return res, err + } + hand.logg.Debugf("Authorization username is %s:%s", username, password) + } res = true diff --git a/app/handler/blob.go b/app/handler/blob.go index 90bd89c..56afb33 100644 --- a/app/handler/blob.go +++ b/app/handler/blob.go @@ -7,6 +7,7 @@ * Distribution of this work is permitted, but commercial use and * modifications are strictly prohibited. */ + package handler import ( diff --git a/app/handler/file.go b/app/handler/file.go index f17aee7..2002076 100644 --- a/app/handler/file.go +++ b/app/handler/file.go @@ -31,24 +31,23 @@ func (hand *Handler) FileExists(rctx *router.Context) { rctx.SetStatus(code) return } - // TODO rctx.SetHeader("Content-Type", res.ContentType) - rctx.SetHeader("Content-Length", res.ContentLength) + rctx.SetHeader("Content-Size", res.ContentSize) rctx.SetHeader("Content-Digest", res.ContentDigest) - //rctx.SetHeader("Content-Length", zeroContentLength) + rctx.SetHeader("Content-Length", zeroContentLength) rctx.SetStatus(code) } func (hand *Handler) PutFile(rctx *router.Context) { - contentLength := rctx.GetHeader("Content-Length") + contentSize := rctx.GetHeader("Content-Size") contentType := rctx.GetHeader("Content-Type") filepath, _ := rctx.GetSubpath("filepath") params := &operator.PutFileParams{ - Filepath: filepath, - ContentLength: contentLength, - ContentType: contentType, - Source: rctx.Request.Body, + Filepath: filepath, + ContentType: contentType, + ContentSize: contentSize, + Source: rctx.Request.Body, } ctx := rctx.GetContext() @@ -76,8 +75,9 @@ func (hand *Handler) GetFile(rctx *router.Context) { } rctx.SetHeader("Content-Type", res.ContentType) - rctx.SetHeader("Content-Length", res.ContentLength) + rctx.SetHeader("Content-Size", res.ContentSize) rctx.SetHeader("Content-Digest", res.ContentDigest) + rctx.SetHeader("Content-Length", res.ContentSize) rctx.SetStatus(code) if res.Source != nil { diff --git a/app/handler/version.go b/app/handler/version.go index 0ac9623..840fa48 100644 --- a/app/handler/version.go +++ b/app/handler/version.go @@ -10,7 +10,7 @@ package handler import ( - "net/http" + "net/http" "mstore/app/operator" "mstore/app/router" @@ -21,12 +21,12 @@ func (hand *Handler) GetVersion(rctx *router.Context) { params := &operator.GetVersionParams{} hand.DumpHeaders("GetVersion", rctx) - authorization := rctx.GetHeader("Authorization") - if authorization == "" { - rctx.SetHeader("WWW-Authenticate", `Basic realm="mstore"`) - rctx.SetStatus(http.StatusUnauthorized) - return - } + authorization := rctx.GetHeader("Authorization") + if authorization == "" { + rctx.SetHeader("WWW-Authenticate", `Basic realm="mstore"`) + rctx.SetStatus(http.StatusUnauthorized) + return + } ctx := rctx.GetContext() _, code, err := hand.oper.GetVersion(ctx, params) if err != nil { diff --git a/app/maindb/account.go b/app/maindb/account.go index 6be6c84..4116c1e 100644 --- a/app/maindb/account.go +++ b/app/maindb/account.go @@ -19,7 +19,7 @@ func (db *Database) InsertAccount(ctx context.Context, account *descr.Account) e return err } -func (db *Database) UpdateAccountByID(ctx context.Context, accountID int64, account *descr.Account) error { +func (db *Database) UpdateAccountByID(ctx context.Context, accountID string, account *descr.Account) error { var err error request := `UPDATE accounts SET username = $1, passhash = $2, disabled = $3, updated_at = $4 WHERE id = $6` @@ -52,7 +52,7 @@ func (db *Database) CompletedListAccounts(ctx context.Context) ([]descr.Account, return res, err } -func (db *Database) GetAccountByID(ctx context.Context, accountID int64) (bool, *descr.Account, error) { +func (db *Database) GetAccountByID(ctx context.Context, accountID string) (bool, *descr.Account, error) { var err error var res *descr.Account var exists bool @@ -90,7 +90,7 @@ func (db *Database) GetAccountByUsername(ctx context.Context, username string) ( return exists, res, err } -func (db *Database) DeleteAccountByID(ctx context.Context, accountID int64) error { +func (db *Database) DeleteAccountByID(ctx context.Context, accountID string) error { var err error request := `DELETE FROM accounts WHERE id = $1` diff --git a/app/maindb/grant.go b/app/maindb/grant.go index 59f7448..0c1401a 100644 --- a/app/maindb/grant.go +++ b/app/maindb/grant.go @@ -17,7 +17,7 @@ func (db *Database) InsertGrant(ctx context.Context, grant *descr.Grant) error { return err } -func (db *Database) ListGrantsByAccountID(ctx context.Context, accountID int64) ([]descr.Grant, error) { +func (db *Database) ListGrantsByAccountID(ctx context.Context, accountID string) ([]descr.Grant, error) { var err error request := `SELECT * FROM grants WHERE account_id = $1` res := make([]descr.Grant, 0) @@ -39,7 +39,7 @@ func (db *Database) ListGrants(ctx context.Context) ([]descr.Grant, error) { return res, err } -func (db *Database) GetGrant(ctx context.Context, accountID int64, operation string) (bool, *descr.Grant, error) { +func (db *Database) GetGrant(ctx context.Context, accountID, operation string) (bool, *descr.Grant, error) { var err error res := &descr.Grant{} request := `SELECT * FROM grants WHERE account_id = $1 AND operation = $2 LIMIT 1` @@ -56,7 +56,7 @@ func (db *Database) GetGrant(ctx context.Context, accountID int64, operation str return true, res, err } -func (db *Database) DeleteGrantByAccountID(ctx context.Context, grantID int64, operation string) error { +func (db *Database) DeleteGrantByAccountID(ctx context.Context, grantID, operation string) error { var err error request := `DELETE FROM grants WHERE account_id = $1 AND operation = $2` _, err = db.db.Exec(request, grantID, operation) @@ -66,7 +66,7 @@ func (db *Database) DeleteGrantByAccountID(ctx context.Context, grantID int64, o return err } -func (db *Database) DeleteAllGrantsForAccountID(ctx context.Context, grantID int64) error { +func (db *Database) DeleteAllGrantsForAccountID(ctx context.Context, grantID string) error { var err error request := `DELETE FROM grants WHERE account_id = $1` _, err = db.db.Exec(request, grantID) diff --git a/app/maindb/schema.go b/app/maindb/schema.go index 5ec6275..d68cfec 100644 --- a/app/maindb/schema.go +++ b/app/maindb/schema.go @@ -58,7 +58,7 @@ const schema = ` --- DROP TABLE IF EXISTS accounts; CREATE TABLE IF NOT EXISTS accounts ( - id INT NOT NULL, + id TEXT NOT NULL, username TEXT NOT NULL, passhash TEXT NOT NULL, created_at TEXT NOT NULL, @@ -72,7 +72,7 @@ const schema = ` --- DROP TABLE IF EXISTS grants; CREATE TABLE IF NOT EXISTS grants ( - id INT NOT NULL, + id TEXT NOT NULL, account_id INT NOT NULL, operation TEXT NOT NULL, created_at TEXT NOT NULL diff --git a/app/operator/file.go b/app/operator/file.go index 9b1c8ca..30a6f97 100644 --- a/app/operator/file.go +++ b/app/operator/file.go @@ -31,7 +31,7 @@ type FileExistsParams struct { } type FileExistsResult struct { ContentType string - ContentLength string + ContentSize string ContentDigest string } @@ -64,7 +64,7 @@ func (oper *Operator) FileExists(ctx context.Context, param *FileExistsParams) ( return code, res, err } res = &FileExistsResult{ - ContentLength: strconv.FormatInt(fileDescr.Size, 10), + ContentSize: strconv.FormatInt(fileDescr.Size, 10), ContentType: fileDescr.Type, ContentDigest: fileDescr.Checksum, } @@ -73,10 +73,10 @@ func (oper *Operator) FileExists(ctx context.Context, param *FileExistsParams) ( // PutFile type PutFileParams struct { - ContentType string - ContentLength string - Filepath string - Source io.ReadCloser + ContentType string + ContentSize string + Filepath string + Source io.ReadCloser } type PutFileResult struct{} @@ -86,12 +86,12 @@ func (oper *Operator) PutFile(ctx context.Context, param *PutFileParams) (int, * var err error res := &PutFileResult{} - if param.ContentLength == "" { + if param.ContentSize == "" { code := http.StatusLengthRequired - err = fmt.Errorf("Content-Length is empty") + err = fmt.Errorf("Required Content-Size header is empty") return code, res, err } - size, err := strconv.ParseInt(param.ContentLength, 10, 64) + size, err := strconv.ParseInt(param.ContentSize, 10, 64) if err != nil { code := http.StatusLengthRequired return code, res, err @@ -166,7 +166,7 @@ type GetFileParams struct { } type GetFileResult struct { ContentType string - ContentLength string + ContentSize string ContentDigest string Source io.ReadCloser } @@ -200,7 +200,7 @@ func (oper *Operator) GetFile(ctx context.Context, param *GetFileParams) (int, * return code, res, err } res = &GetFileResult{ - ContentLength: strconv.FormatInt(fileDescr.Size, 10), + ContentSize: strconv.FormatInt(fileDescr.Size, 10), ContentType: fileDescr.Type, ContentDigest: fileDescr.Checksum, Source: reader, diff --git a/app/service/service.go b/app/service/service.go index 5afbec7..ce6b915 100644 --- a/app/service/service.go +++ b/app/service/service.go @@ -97,6 +97,12 @@ func (svc *Service) Build() error { svc.rout.Get(`/v2/{name}/tags/list`, svc.hand.GetTags) svc.rout.Get(`/v2/{name}/referrers/{digest}`, svc.hand.GetReferer) + svc.rout.Post(`/v3/account/create`, svc.hand.CreateAccount) + svc.rout.Post(`/v3/account/get`, svc.hand.GetAccount) + svc.rout.Post(`/v3/accounts/list`, svc.hand.ListAccounts) + svc.rout.Post(`/v3/account/update`, svc.hand.UpdateAccount) + svc.rout.Post(`/v3/account/delete`, svc.hand.DeleteAccount) + svc.rout.NotFound(svc.hand.NotFound) selector := svc.rout.Selector() diff --git a/pkg/client/client.go b/pkg/client/client.go index b1b2fe4..a752d07 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -12,12 +12,7 @@ package client import ( "context" "crypto/tls" - "fmt" - "io" "net/http" - "os" - "path/filepath" - "strconv" "time" ) @@ -70,191 +65,3 @@ func (cli *Client) ServiceHello(ctx context.Context, ref string, timeout time.Du } return res, err } - -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)) - } - - transport := &http.Transport{ - TLSClientConfig: &tls.Config{ - InsecureSkipVerify: true, - }, - } - client := &http.Client{ - Transport: transport, - } - resp, err := client.Do(req) - if err != nil { - return res, err - } - defer resp.Body.Close() - if resp.StatusCode == http.StatusOK { - res = true - } - return res, err -} - -func (cli *Client) PutFile(ctx context.Context, filename, ref string) error { - var err error - ref, err = convertFileRefer(ref) - if err != nil { - return err - } - file, err := os.Open(filename) - if err != nil { - return err - } - defer file.Close() - - req, err := http.NewRequestWithContext(ctx, http.MethodPut, ref, 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 - } - filesize := fileinfo.Size() - - req.ContentLength = filesize - req.Header.Set("Content-Type", "application/octet-stream") - - resp, err := client.Do(req) - if err != nil { - return err - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - err := fmt.Errorf("Received wrong status code: %s", resp.Status) - return err - } - return err -} - -func (cli *Client) GetFile(ctx context.Context, ref, filename string) (int64, error) { - var err error - var size int64 - ref, err = convertFileRefer(ref) - if err != nil { - return size, err - } - - req, err := http.NewRequestWithContext(ctx, http.MethodGet, ref, nil) - if err != nil { - return size, 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, - } - resp, err := client.Do(req) - if err != nil { - return size, err - } - defer resp.Body.Close() - - contentLength := resp.Header.Get("Content-Length") - if contentLength == "" { - err = fmt.Errorf("Empty Content-Length received") - return size, err - } - if resp.StatusCode != http.StatusOK { - err := fmt.Errorf("Received wrong status code: %s", resp.Status) - return size, err - } - declSize, err := strconv.ParseInt(contentLength, 10, 64) - if err != nil { - err = fmt.Errorf("Wrong Content-Length value: %v", err) - return size, err - } - dirname := filepath.Dir(filename) - err = os.MkdirAll(dirname, 0750) - if err != nil { - return size, err - } - file, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE, 0640) - if err != nil { - return size, err - } - size, err = io.Copy(file, resp.Body) - if err != nil { - return size, err - } - if size != declSize { - err := fmt.Errorf("Mismatch Content-Length and recorded filesize") - return size, err - } - return size, err -} - -func (cli *Client) DeleteFile(ctx context.Context, ref, filename string) error { - var err error - ref, err = convertFileRefer(ref) - if err != nil { - return err - } - req, err := http.NewRequestWithContext(ctx, http.MethodDelete, ref, nil) - 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, - } - resp, err := client.Do(req) - if err != nil { - return err - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - err := fmt.Errorf("Received wrong status code: %s", resp.Status) - return err - } - return err -} diff --git a/test/file_test.go b/test/file_test.go index 260c295..d1f73f4 100644 --- a/test/file_test.go +++ b/test/file_test.go @@ -20,6 +20,7 @@ import ( "net/http" "net/http/httptest" "path/filepath" + "strconv" "testing" "mstore/app/router" @@ -32,21 +33,54 @@ func TestFileOperations(t *testing.T) { srv, err := server.NewServer() require.NoError(t, err) + var srvport int64 = 10240 + rand.Int63n(1024) + srvdir := t.TempDir() + //srvaddr := fmt.Sprintf("127.0.0.1:%d", srvport) + filename := `bare.bin?abc=12` { err = srv.Configure() require.NoError(t, err) - //tmpdir := t.TempDir() - //srv.SetDatadir(tmpdir) - //srv.SetLogdir(tmpdir) - //srv.SetRundir(tmpdir) + err = srv.Configure() + require.NoError(t, err) + var tmpdir bool + tmpdir = true + if tmpdir { + srv.SetDatadir(srvdir) + srv.SetLogdir(srvdir) + srv.SetRundir(srvdir) + } + srv.SetPort(srvport) err = srv.Build() require.NoError(t, err) } + { + fmt.Printf("=== ServiceHello ===\n") + reqPath := "/service/hello" + routePath := "/service/hello" + rout := router.NewRouter() + hand := srv.Handler() + rout.Get(routePath, hand.SendHello) + + request, err := http.NewRequest("GET", reqPath, nil) + require.NoError(t, err) + + recorder := httptest.NewRecorder() + rout.ServeHTTP(recorder, request) + require.Equal(t, http.StatusOK, recorder.Code) + + fmt.Printf("Response code: %d\n", recorder.Code) + + bodyReader := recorder.Body + bodyBytes, err := io.ReadAll(bodyReader) + + fmt.Printf("Response body: %s\n", string(bodyBytes)) + } + //return { fmt.Printf("=== PutFile ===\n") reqPath := `/v3/api/file/` + filename @@ -71,8 +105,9 @@ func TestFileOperations(t *testing.T) { request, err := http.NewRequest("PUT", reqPath, source) require.NoError(t, err) - request.Header.Set("Content-Size", fmt.Sprintf("%d", datasize)) + request.ContentLength = int64(datasize) request.Header.Set("Content-Type", "application/octet-stream") + request.Header.Set("Content-Size", strconv.FormatInt(int64(datasize), 10)) recorder := httptest.NewRecorder() rout.ServeHTTP(recorder, request) @@ -136,7 +171,6 @@ func TestFileOperations(t *testing.T) { fmt.Printf("Response body: %s\n", string(bodyBytes)) } - return { fmt.Printf("=== DeleteFile ===\n") reqPath := filepath.Join(`/v3/api/file`, filename) @@ -187,29 +221,6 @@ func TestFileOperations(t *testing.T) { bodyReader := recorder.Body bodyBytes, err := io.ReadAll(bodyReader) - fmt.Printf("Response body: %s\n", string(bodyBytes)) - } - { - fmt.Printf("=== ServiceHello ===\n") - reqPath := "/service/hello" - routePath := "/service/hello" - - rout := router.NewRouter() - hand := srv.Handler() - rout.Get(routePath, hand.SendHello) - - request, err := http.NewRequest("GET", reqPath, nil) - require.NoError(t, err) - - recorder := httptest.NewRecorder() - rout.ServeHTTP(recorder, request) - require.Equal(t, http.StatusOK, recorder.Code) - - fmt.Printf("Response code: %d\n", recorder.Code) - - bodyReader := recorder.Body - bodyBytes, err := io.ReadAll(bodyReader) - fmt.Printf("Response body: %s\n", string(bodyBytes)) } }