working commit

This commit is contained in:
2026-02-07 14:10:54 +02:00
parent 90905ace89
commit cd274d614a
12 changed files with 124 additions and 276 deletions
+33 -10
View File
@@ -1,3 +1,13 @@
/*
* 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 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"`
}
+8 -8
View File
@@ -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
+1
View File
@@ -7,6 +7,7 @@
* Distribution of this work is permitted, but commercial use and
* modifications are strictly prohibited.
*/
package handler
import (
+9 -9
View File
@@ -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 {
+7 -7
View File
@@ -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 {
+3 -3
View File
@@ -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`
+4 -4
View File
@@ -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)
+2 -2
View File
@@ -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
+11 -11
View File
@@ -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,
+6
View File
@@ -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()
-193
View File
@@ -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
}
+40 -29
View File
@@ -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))
}
}