feat: proxy download image

This commit is contained in:
loveuer
2024-04-15 18:02:54 +08:00
parent c5d0b8e45b
commit 410a4c0d8d
57 changed files with 10913 additions and 316 deletions

View File

@ -21,5 +21,13 @@ func NewApi(
api.Any("/*path", handler.Root(bh, uh, mh))
}
{
api := app.Group("/api/repo")
api.Get("/settings", handler.RepoSettings)
api.Get("/list", handler.RepoList(mh))
api.Get("/tag/list", handler.TagList(mh))
api.Post("/proxy", handler.ProxyDownloadImage(mh, bh))
}
return app
}

View File

@ -29,7 +29,7 @@ func handleCatalog(ctx *nf.Ctx, m interfaces.ManifestHandler) error {
list *model.Catalog
)
if list, re = m.Catelog(ctx.Request.Context(), n, 0); re != nil {
if list, re = m.Catalog(ctx.Request.Context(), n, 0, ""); re != nil {
return rerr.Error(ctx, re)
}

View File

@ -86,13 +86,24 @@ func handleManifest(ctx *nf.Ctx, m interfaces.ManifestHandler) error {
return ctx.SendStatus(http.StatusOK)
case http.MethodPut:
b := &bytes.Buffer{}
io.Copy(b, ctx.Request.Body)
h, _, _ := model.SHA256(bytes.NewReader(b.Bytes()))
digest := h.String()
var (
err error
buf = &bytes.Buffer{}
hash model.Hash
)
mf := model.Manifest{
Blob: b.Bytes(),
if _, err = io.Copy(buf, ctx.Request.Body); err != nil {
return rerr.Error(ctx, rerr.ErrInternal(err))
}
if hash, _, err = model.SHA256(bytes.NewReader(buf.Bytes())); err != nil {
return rerr.Error(ctx, rerr.ErrInternal(err))
}
digest := hash.String()
mf := model.RepoSimpleManifest{
Blob: buf.Bytes(),
ContentType: ctx.Get("Content-Type"),
}
@ -104,7 +115,7 @@ func handleManifest(ctx *nf.Ctx, m interfaces.ManifestHandler) error {
WithField("target", target).
WithField("digest", digest).
WithField("content-type", ctx.Get("Content-Type")).
WithField("content", b.String()).
WithField("content", buf.String()).
Debug()
if err := m.Put(ctx.Request.Context(), repo, target, digest, &mf); err != nil {

307
internal/handler/repo.go Normal file
View File

@ -0,0 +1,307 @@
package handler
import (
"bytes"
"encoding/json"
"fmt"
"github.com/google/go-containerregistry/pkg/name"
"github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/remote"
"github.com/loveuer/nf"
"github.com/sirupsen/logrus"
"io"
"net/http"
"net/url"
"nf-repo/internal/interfaces"
"nf-repo/internal/model"
"nf-repo/internal/opt"
"nf-repo/internal/util/r"
"nf-repo/internal/util/rerr"
"strings"
)
func RepoSettings(c *nf.Ctx) error {
return r.Resp200(c, nf.Map{
"base_address": opt.BaseAddress,
})
}
func RepoList(mh interfaces.ManifestHandler) nf.HandlerFunc {
return func(c *nf.Ctx) error {
type Req struct {
Keyword string `json:"keyword" query:"keyword"`
N int `json:"n" query:"n"`
Last int `json:"last" query:"last"`
}
var (
err error
re *rerr.RepositoryError
req = new(Req)
catalog *model.Catalog
)
if err = c.QueryParser(req); err != nil {
return r.Resp400(c, err.Error())
}
if req.N == 0 {
req.N = 20
}
if req.N > 1000 {
return r.Resp400(c, "limit invalid: too big")
}
if catalog, re = mh.Catalog(c.Request.Context(), req.N, req.Last, req.Keyword); re != nil {
return r.Resp(c, uint32(re.Status), "", re.Code, re.Message)
}
return r.Resp200(c, nf.Map{"list": catalog.Repos, "total": 0})
}
}
func TagList(mh interfaces.ManifestHandler) nf.HandlerFunc {
return func(c *nf.Ctx) error {
type Req struct {
Repo string `json:"repo" query:"repo"`
Keyword string `json:"keyword" query:"keyword"`
N int `json:"n" query:"n"`
Last int `json:"last" query:"last"`
}
var (
err error
req = new(Req)
tag *model.Tag
re *rerr.RepositoryError
)
if err = c.QueryParser(req); err != nil {
return r.Resp400(c, err.Error())
}
if req.Repo == "" {
return r.Resp400(c, "repo invalid")
}
if req.N <= 0 {
req.N = 20
}
if req.N > 1000 {
return r.Resp400(c, "limit invalid: too big")
}
if tag, re = mh.Tags(c.Request.Context(), req.Repo, req.N, req.Last, req.Keyword); re != nil {
return r.Resp(c, uint32(re.Status), "", re.Code, re.Message)
}
return r.Resp200(c, nf.Map{"list": tag.RepoTags, "total": tag.Tags})
}
}
func ProxyDownloadImage(mh interfaces.ManifestHandler, bh interfaces.BlobHandler) nf.HandlerFunc {
return func(c *nf.Ctx) error {
type Req struct {
Source string `json:"source"`
Target string `json:"target"`
Proxy string `json:"proxy"`
}
type Msg struct {
Target string `json:"target"`
Action string `json:"action"`
Msg string `json:"msg"`
Data any `json:"data"`
}
var (
err error
req = new(Req)
puller *remote.Puller
tn name.Tag
des *remote.Descriptor
transport = &http.Transport{}
img v1.Image
manifest *v1.Manifest
bs []byte
mhash model.Hash
//progressCh = make(chan v1.Update, 16)
)
if err = c.BodyParser(req); err != nil {
return r.Resp400(c, err.Error())
}
if req.Target == "" {
return r.Resp400(c, "target invalid")
}
repoTags := strings.Split(req.Target, ":")
if len(repoTags) != 2 {
return r.Resp400(c, "target invalid: should only contain one ':'")
}
repo, tag := repoTags[0], repoTags[1]
repo = strings.TrimPrefix(repo, opt.BaseAddress)
repo = strings.Trim(repo, "/")
if req.Source == "" {
return r.Resp400(c, "source invalid")
}
imageName := fmt.Sprintf("%s/%s:%s", opt.BaseAddress, repo, tag)
_ = imageName
logrus.
WithField("path", "handler.ProxyDownloadImage").
WithField("req", *req).
WithField("repo", repo).
Debug()
if req.Proxy != "" {
var pu *url.URL
if pu, err = url.Parse(req.Proxy); err != nil {
return r.Resp400(c, err.Error())
}
transport.Proxy = http.ProxyURL(pu)
}
if puller, err = remote.NewPuller(
remote.WithTransport(transport),
); err != nil {
return r.Resp500(c, err.Error())
}
if tn, err = name.NewTag(req.Source); err != nil {
return r.Resp400(c, err.Error())
}
_ = c.SSEvent("pull", Msg{
Target: req.Target,
Action: "get",
Msg: "开始获取镜像信息",
})
_ = c.Flush()
if des, err = puller.Get(c.Request.Context(), tn); err != nil {
return r.Resp500(c, err.Error())
}
if img, err = des.Image(); err != nil {
return r.Resp500(c, err.Error())
}
if manifest, err = img.Manifest(); err != nil {
return r.Resp500(c, err.Error())
}
total := model.ManifestCountSize(manifest)
size := 0
_ = c.SSEvent("pull", Msg{
Target: req.Target,
Action: "layer",
Msg: "正在获取镜像数据: 0.00%",
Data: map[string]any{"total": total, "size": size, "index_total": len(manifest.Layers) + 1, "index_size": 0},
})
_ = c.Flush()
var (
tly v1.Layer
tdigest v1.Hash
treader io.ReadCloser
)
if tly, err = img.LayerByDigest(manifest.Config.Digest); err != nil {
return r.Resp500(c, err.Error())
}
if tdigest, err = tly.Digest(); err != nil {
return r.Resp500(c, err.Error())
}
if treader, err = tly.Uncompressed(); err != nil {
return r.Resp500(c, err.Error())
}
defer treader.Close()
if err = bh.Put(
c.Request.Context(),
repo,
model.Hash{Algorithm: tdigest.Algorithm, Hex: tdigest.Hex},
treader,
); err != nil {
return r.Resp500(c, err.Error())
}
size = size + int(manifest.Config.Size)
_ = c.SSEvent("pull", Msg{
Target: req.Target,
Action: "layer",
Msg: fmt.Sprintf("正在获取镜像数据: %.2f%%", float64(size)/float64(total)*100),
Data: map[string]any{"total": model.ManifestCountSize(manifest), "size": size, "index_total": len(manifest.Layers) + 1, "index_size": 1},
})
_ = c.Flush()
if bs, err = json.Marshal(manifest); err != nil {
return r.Resp500(c, err.Error())
}
if mhash, _, err = model.SHA256(bytes.NewReader(bs)); err != nil {
return r.Resp500(c, err.Error())
}
for idx := range manifest.Layers {
var (
reader io.ReadCloser
lyHash v1.Hash
ly v1.Layer
)
lyHash = manifest.Layers[idx].Digest
if ly, err = img.LayerByDigest(manifest.Layers[idx].Digest); err != nil {
return r.Resp500(c, err.Error())
}
if reader, err = ly.Compressed(); err != nil {
return r.Resp500(c, err.Error())
}
defer reader.Close()
if err = bh.Put(c.Request.Context(), repo, model.Hash{Algorithm: lyHash.Algorithm, Hex: lyHash.Hex}, reader); err != nil {
return r.Resp500(c, err.Error())
}
size = size + int(manifest.Layers[idx].Size)
_ = c.SSEvent("pull", Msg{
Target: req.Target,
Action: "layer",
Msg: fmt.Sprintf("正在获取镜像数据: %.2f%%", float64(size)/float64(total)*100),
Data: map[string]any{"total": model.ManifestCountSize(manifest), "size": size, "index_total": len(manifest.Layers) + 1, "index_size": 2 + idx},
})
_ = c.Flush()
}
var re *rerr.RepositoryError
if re = mh.Put(c.Request.Context(), repo, tag, mhash.String(), &model.RepoSimpleManifest{
ContentType: string(manifest.MediaType),
Blob: bs,
}); re != nil {
return r.Resp(c, uint32(re.Status), re.Message, re.Code, nil)
}
_ = c.SSEvent("pull", Msg{
Target: req.Target,
Action: "done",
Msg: "获取成功",
Data: manifest,
})
return c.Flush()
}
}

View File

@ -42,7 +42,11 @@ func handleTags(ctx *nf.Ctx, m interfaces.ManifestHandler) error {
})
}
if list, re = m.Tags(ctx.Request.Context(), repo, req.N, req.Last); err != nil {
if req.N <= 0 {
req.N = 100
}
if list, re = m.Tags(ctx.Request.Context(), repo, req.N, req.Last, ""); err != nil {
return rerr.Error(ctx, re)
}

View File

@ -7,6 +7,7 @@ import (
"nf-repo/internal/interfaces"
"nf-repo/internal/model"
"nf-repo/internal/opt"
"nf-repo/internal/verify"
"os"
"path"
"sync"
@ -48,12 +49,13 @@ func (l *localHandler) Stat(ctx context.Context, repo string, hash model.Hash) (
var (
err error
info os.FileInfo
sp = l.path(hash)
)
l.Lock()
defer l.Unlock()
if info, err = os.Stat(l.path(hash)); err != nil {
if info, err = os.Stat(sp); err != nil {
if errors.Is(err, os.ErrNotExist) {
return 0, opt.ErrNotFound
}
@ -68,11 +70,17 @@ func (l *localHandler) Put(ctx context.Context, repo string, hash model.Hash, rc
var (
err error
f *os.File
nrc io.ReadCloser
)
l.Lock()
defer l.Unlock()
if nrc, err = verify.ReadCloser(rc, verify.SizeUnknown, hash); err != nil {
return err
}
defer nrc.Close()
if f, err = os.OpenFile(l.path(hash), os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0644); err != nil {
return err
}

View File

@ -0,0 +1,11 @@
package interfaces
type Enum interface {
Value() int64
Code() string
Label() string
MarshalJSON() ([]byte, error)
All() []Enum
}

View File

@ -9,10 +9,10 @@ import (
type ManifestHandler interface {
Get(ctx context.Context, repo string, target string) (io.ReadCloser, string, *rerr.RepositoryError)
Put(ctx context.Context, repo string, target string, digest string, mf *model.Manifest) *rerr.RepositoryError
Put(ctx context.Context, repo string, target string, digest string, mf *model.RepoSimpleManifest) *rerr.RepositoryError
Delete(ctx context.Context, repo string, target string) *rerr.RepositoryError
Catelog(ctx context.Context, limit int, last int) (*model.Catalog, *rerr.RepositoryError)
Tags(ctx context.Context, repo string, limit, last int) (*model.Tag, *rerr.RepositoryError)
Catalog(ctx context.Context, limit int, last int, keyword string) (*model.Catalog, *rerr.RepositoryError)
Tags(ctx context.Context, repo string, limit, last int, keyword string) (*model.Tag, *rerr.RepositoryError)
Referrers(ctx context.Context, repo string, target string) (*model.IndexManifest, *rerr.RepositoryError)
}

View File

@ -9,6 +9,7 @@ import (
"github.com/samber/lo"
"github.com/sirupsen/logrus"
"gorm.io/gorm"
"gorm.io/gorm/clause"
"io"
"net/http"
"nf-repo/internal/interfaces"
@ -17,16 +18,10 @@ import (
"nf-repo/internal/util/tools"
)
type PackageManifest struct {
Id uint64 `json:"id" gorm:"primaryKey;column:id"`
CreatedAt int64 `json:"created_at" gorm:"column:created_at;autoCreateTime:milli"`
UpdatedAt int64 `json:"updated_at" gorm:"column:updated_at;autoUpdateTime:milli"`
Repo string `json:"repo" gorm:"uniqueIndex:repo_tag_idx;column:repo"`
Target string `json:"target" gorm:"uniqueIndex:repo_tag_idx;column:target"`
Digest string `json:"digest" gorm:"unique;column:digest"`
ContentType string `json:"content_type" gorm:"column:content_type"`
Content []byte `json:"content" gorm:"column:content;type:bytes"`
type pm struct {
model.PackageManifest
Digest string `json:"digest" gorm:"column:digest"`
Size int `json:"size" gorm:"column:size"`
}
type dbManifests struct {
@ -35,20 +30,38 @@ type dbManifests struct {
func (m *dbManifests) Get(ctx context.Context, repo string, target string) (io.ReadCloser, string, *rerr.RepositoryError) {
var (
err error
pm = new(PackageManifest)
h model.Hash
tx = m.db.TX(tools.Timeout(5)).Model(pm)
err error
hash model.Hash
pd = new(model.PackageDigest)
)
if h, err = model.NewHash(target); err == nil {
tx = tx.Where("digest", h.String())
} else {
tx = tx.Where("repo", repo).
Where("target", target)
if hash, err = model.NewHash(target); err == nil {
if err = m.db.TX(tools.Timeout(5)).
Model(&model.PackageDigest{}).
Where("digest", hash.String()).
Take(pd).
Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, "", &rerr.RepositoryError{
Status: http.StatusNotFound,
Code: "NAME_UNKNOWN",
Message: fmt.Sprintf("Unknown name: %s@%s", repo, target),
}
}
return nil, "", rerr.ErrInternal(err)
}
return io.NopCloser(bytes.NewReader(pd.Content)), pd.ContentType, nil
}
if err = tx.
var pm = new(model.PackageManifest)
if err = m.db.TX(tools.Timeout(5)).
Model(&model.PackageManifest{}).
Where("repo", repo).
Where("tag", target).
Take(pm).
Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
@ -62,39 +75,85 @@ func (m *dbManifests) Get(ctx context.Context, repo string, target string) (io.R
return nil, "", rerr.ErrInternal(err)
}
return io.NopCloser(bytes.NewReader(pm.Content)), pm.ContentType, nil
if err = m.db.TX(tools.Timeout(5)).
Model(&model.PackageDigest{}).
Where("id", pm.DigestId).
Take(pd).
Error; err != nil {
logrus.
WithField("path", "dbManifests.Get").
WithField("digest", pm.DigestId).
WithField("err", err.Error()).
Error()
return nil, "", rerr.ErrInternal(err)
}
return io.NopCloser(bytes.NewReader(pd.Content)), pd.ContentType, nil
}
func (m *dbManifests) Put(ctx context.Context, repo string, target string, digest string, mf *model.Manifest) *rerr.RepositoryError {
func (m *dbManifests) Put(ctx context.Context, repo string, tag string, digest string, mf *model.RepoSimpleManifest) *rerr.RepositoryError {
var (
err error
pm = &PackageManifest{
Repo: repo,
Target: target,
pm = &model.PackageManifest{
Repo: repo,
Tag: tag,
}
pd = &model.PackageDigest{
Digest: digest,
ContentType: mf.ContentType,
Content: mf.Blob,
}
blob = new(model.RepoSimpleManifestBlob)
)
// todo on conflict
if err = json.Unmarshal(mf.Blob, blob); err != nil {
return rerr.ErrInternal(err)
}
pd.Size = blob.CountSize()
if err = m.db.TX(tools.Timeout(5)).
Clauses(clause.Returning{}).
Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "digest"}},
DoUpdates: clause.Set{
{
Column: clause.Column{Name: "content_type"},
Value: mf.ContentType,
},
{
Column: clause.Column{Name: "content"},
Value: mf.Blob,
},
{
Column: clause.Column{Name: "size"},
Value: pd.Size,
},
}}).
Create(pd).
Error; err != nil {
logrus.
WithField("path", "dbManifests.Put.Create").
WithField("err", err.Error()).
Error()
return rerr.ErrInternal(err)
}
pm.DigestId = pd.Id
if err = m.db.TX(tools.Timeout(5)).Create(pm).Error; err == nil {
return nil
}
logrus.
WithField("path", "dbManifests.Put.Create").
WithField("err", err.Error()).
Trace()
if err = m.db.TX(tools.Timeout(5)).Model(&PackageManifest{}).
Where("(repo = ? AND target = ?) OR (digest = ?)", repo, target, digest).
if err = m.db.TX(tools.Timeout(5)).Model(&model.PackageManifest{}).
Where("(repo = ? AND tag = ?)", repo, tag).
Updates(map[string]any{
"repo": repo,
"target": target,
"digest": digest,
"content_type": mf.ContentType,
"content": mf.Blob,
"repo": repo,
"tag": tag,
"digest_id": pm.DigestId,
}).
Error; err != nil {
logrus.
@ -115,7 +174,7 @@ func (m *dbManifests) Delete(ctx context.Context, repo string, target string) *r
if err = m.db.TX(tools.Timeout(5)).
Where("repo", repo).
Where("target", target).
Delete(&PackageManifest{}).
Delete(&model.PackageManifest{}).
Error; err != nil {
return rerr.ErrInternal(err)
}
@ -123,14 +182,23 @@ func (m *dbManifests) Delete(ctx context.Context, repo string, target string) *r
return nil
}
func (m *dbManifests) Catelog(ctx context.Context, limit int, last int) (*model.Catalog, *rerr.RepositoryError) {
func (m *dbManifests) Catalog(ctx context.Context, limit int, last int, keyword string) (*model.Catalog, *rerr.RepositoryError) {
var (
err error
list = make([]*PackageManifest, 0)
list = make([]*pm, 0)
tx = m.db.TX(tools.Timeout(5))
)
if err = m.db.TX(tools.Timeout(5)).Model(&PackageManifest{}).
Order("updated_at").
tx = tx.Model(&model.PackageManifest{}).
Select("\"package_manifests\".*", "\"pd\".\"digest\" as digest", "\"pd\".\"size\" as size")
if keyword != "" {
k := fmt.Sprintf("%%%s%%", keyword)
tx = tx.Where("package_manifests.repo like ?", k)
}
if err = tx.Group("\"package_manifests\".\"repo\"").
Joins("LEFT JOIN package_digests pd on \"pd\".\"id\" = \"package_manifests\".\"digest_id\"").
Order("updated_at DESC, tag = 'latest' DESC").
Offset(last).
Limit(limit).
Find(&list).
@ -139,20 +207,39 @@ func (m *dbManifests) Catelog(ctx context.Context, limit int, last int) (*model.
}
return &model.Catalog{
Repos: lo.Map(list, func(item *PackageManifest, index int) string {
Repositories: lo.Map(list, func(item *pm, index int) string {
return item.Repo
}),
Repos: lo.Map(list, func(item *pm, index int) *model.PackageManifest {
item.PackageManifest.Digest = item.Digest
item.PackageManifest.Size = item.Size
return &item.PackageManifest
}),
}, nil
}
func (m *dbManifests) Tags(ctx context.Context, repo string, limit, last int) (*model.Tag, *rerr.RepositoryError) {
func (m *dbManifests) Tags(ctx context.Context, repo string, limit, last int, keyword string) (*model.Tag, *rerr.RepositoryError) {
var (
err error
list = make([]*PackageManifest, 0)
err error
list = make([]*pm, 0)
tx = m.db.TX(tools.Timeout(5)).Model(&model.PackageManifest{}).Select("\"package_manifests\".*", "\"pd\".\"digest\" as digest")
txc = m.db.TX(tools.Timeout(5)).Model(&model.PackageManifest{}).Select("COUNT(id)")
total int
)
if err = m.db.TX(tools.Timeout(5)).Model(&PackageManifest{}).
Where("repo", repo).
if keyword != "" {
k := fmt.Sprintf("%%%s%%", keyword)
tx = tx.Where("\"package_manifests\".\"tag\" like ?", k)
txc = txc.Where("tag like ?", k)
}
if err = txc.Find(&total).Error; err != nil {
return nil, rerr.ErrInternal(err)
}
if err = tx.
Where("package_manifests.repo", repo).
Joins("LEFT JOIN package_digests pd on \"pd\".\"id\" = \"package_manifests\".\"digest_id\"").
Order("updated_at").
Offset(last).
Limit(limit).
@ -163,49 +250,90 @@ func (m *dbManifests) Tags(ctx context.Context, repo string, limit, last int) (*
return &model.Tag{
Name: repo,
Tags: lo.Map(list, func(item *PackageManifest, index int) string {
return item.Target
Tags: lo.Map(list, func(item *pm, index int) string {
return item.Tag
}),
RepoTags: lo.Map(list, func(item *pm, index int) *model.PackageManifest {
item.PackageManifest.Digest = item.Digest
return &item.PackageManifest
}),
Total: total,
}, nil
}
func (m *dbManifests) Referrers(ctx context.Context, repo string, target string) (*model.IndexManifest, *rerr.RepositoryError) {
var (
err error
pm = new(PackageManifest)
pm = new(model.PackageManifest)
pd = new(model.PackageDigest)
manifest = &model.IndexManifest{}
tx = m.db.TX(tools.Timeout(5)).Model(pm)
tx = m.db.TX(tools.Timeout(5))
hash model.Hash
)
h, err := model.NewHash(target)
if err != nil {
tx = tx.Where("repo", repo).Where("digest", h.String())
} else {
tx = tx.Where("repo", repo).Where("target", target)
}
if hash, err = model.NewHash(target); err == nil {
if err = tx.Model(&model.PackageDigest{}).
Where("repo", repo).
Where("digest", hash.String()).
Take(pd).
Error; err != nil {
if err = m.db.TX(tools.Timeout(5)).Model(pm).
Take(pm).
Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, &rerr.RepositoryError{
Status: http.StatusNotFound,
Code: "NAME_UNKNOWN",
Message: fmt.Sprintf("Unknown name: %s@%s", repo, target),
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, &rerr.RepositoryError{
Status: http.StatusNotFound,
Code: "NAME_UNKNOWN",
Message: fmt.Sprintf("Unknown name: %s@%s", repo, target),
}
}
logrus.
WithField("path", "dbManifests.Referrers.Take").
WithField("repo", repo).
WithField("target", target).
WithField("err", err.Error()).
Debug()
return nil, rerr.ErrInternal(err)
}
} else {
if err = tx.Model(&model.PackageManifest{}).
Where("repo", repo).
Where("tag", target).
Take(pd).
Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, &rerr.RepositoryError{
Status: http.StatusNotFound,
Code: "NAME_UNKNOWN",
Message: fmt.Sprintf("Unknown name: %s@%s", repo, target),
}
}
logrus.
WithField("path", "dbManifests.Referrers.Take").
WithField("repo", repo).
WithField("target", target).
Error()
return nil, rerr.ErrInternal(err)
}
logrus.
WithField("path", "dbManifests.Referrers.Take").
WithField("repo", repo).
WithField("target", target).
WithField("err", err.Error()).
Debug()
if err = tx.Model(&model.PackageDigest{}).
Where("digest_id", pm.DigestId).
Take(pd).
Error; err != nil {
return nil, rerr.ErrInternal(err)
logrus.
WithField("path", "dbManifests.Referrers.Take").
WithField("repo", repo).
WithField("target", target).
Error()
return nil, rerr.ErrInternal(err)
}
}
if err = json.Unmarshal(pm.Content, manifest); err != nil {
if err = json.Unmarshal(pd.Content, manifest); err != nil {
logrus.
WithField("path", "dbManifests.Referrers.Unmarshal").
WithField("repo", repo).
@ -223,7 +351,10 @@ func NewManifestDBHandler(tx interfaces.Database) interfaces.ManifestHandler {
err error
)
if err = tx.TX(tools.Timeout(5)).AutoMigrate(&PackageManifest{}); err != nil {
if err = tx.TX(tools.Timeout(5)).AutoMigrate(
&model.PackageManifest{},
&model.PackageDigest{},
); err != nil {
logrus.
WithField("path", "NewManifestDBHandler").
WithField("method", "AutoMigrate").

View File

@ -19,7 +19,7 @@ import (
type memManifest struct {
sync.RWMutex
m map[string]map[string]*model.Manifest
m map[string]map[string]*model.RepoSimpleManifest
}
func (m *memManifest) Referrers(ctx context.Context, repo string, target string) (*model.IndexManifest, *rerr.RepositoryError) {
@ -81,7 +81,7 @@ func (m *memManifest) Referrers(ctx context.Context, repo string, target string)
return im, nil
}
func (m *memManifest) Tags(ctx context.Context, repo string, limit int, last int) (*model.Tag, *rerr.RepositoryError) {
func (m *memManifest) Tags(ctx context.Context, repo string, limit int, last int, keyword string) (*model.Tag, *rerr.RepositoryError) {
m.RLock()
defer m.RUnlock()
@ -96,10 +96,20 @@ func (m *memManifest) Tags(ctx context.Context, repo string, limit int, last int
var tags []string
for tag := range c {
if !strings.Contains(tag, "sha256:") {
if strings.Contains(tag, "sha256:") {
continue
}
if keyword == "" {
tags = append(tags, tag)
continue
}
if strings.Contains(tag, keyword) {
tags = append(tags, tag)
}
}
sort.Strings(tags)
// https://github.com/opencontainers/distribution-spec/blob/b505e9cc53ec499edbd9c1be32298388921bb705/detail.md#tags-paginated
@ -134,7 +144,7 @@ func (m *memManifest) Tags(ctx context.Context, repo string, limit int, last int
return tagsToList, nil
}
func (m *memManifest) Catelog(ctx context.Context, limit, last int) (*model.Catalog, *rerr.RepositoryError) {
func (m *memManifest) Catalog(ctx context.Context, limit, last int, keyword string) (*model.Catalog, *rerr.RepositoryError) {
m.RLock()
defer m.RUnlock()
@ -146,23 +156,24 @@ func (m *memManifest) Catelog(ctx context.Context, limit, last int) (*model.Cata
if countRepos >= limit {
break
}
if keyword != "" && !strings.Contains(key, keyword) {
continue
}
countRepos++
repos = append(repos, key)
}
repositoriesToList := &model.Catalog{
Repos: repos,
Repositories: repos,
}
return repositoriesToList, nil
}
func (m *memManifest) Put(ctx context.Context, repo string, target string, digest string, mf *model.Manifest) *rerr.RepositoryError {
// If the manifest
// list's constituent manifests are already uploaded.
// This isn't strictly required by the registry API, but some
// registries require this.
func (m *memManifest) Put(ctx context.Context, repo string, target string, digest string, mf *model.RepoSimpleManifest) *rerr.RepositoryError {
if types.MediaType(mf.ContentType).IsIndex() {
if err := func() *rerr.RepositoryError {
m.RLock()
@ -205,7 +216,7 @@ func (m *memManifest) Put(ctx context.Context, repo string, target string, diges
defer m.Unlock()
if _, ok := m.m[repo]; !ok {
m.m[repo] = make(map[string]*model.Manifest, 2)
m.m[repo] = make(map[string]*model.RepoSimpleManifest, 2)
}
// Allow future references by target (tag) and immutable digest.
@ -274,5 +285,5 @@ func (m *memManifest) Get(ctx context.Context, repo string, target string) (io.R
}
func NewManifestMemHandler() interfaces.ManifestHandler {
return &memManifest{m: make(map[string]map[string]*model.Manifest)}
return &memManifest{m: make(map[string]map[string]*model.RepoSimpleManifest)}
}

View File

@ -6,6 +6,9 @@ import (
"github.com/sirupsen/logrus"
"gorm.io/gorm"
"nf-repo/internal/interfaces"
"nf-repo/internal/opt"
"os"
"path"
)
type tx struct {
@ -13,7 +16,13 @@ type tx struct {
}
func (t *tx) TX(ctx context.Context) *gorm.DB {
return t.db.Session(&gorm.Session{}).WithContext(ctx)
db := t.db.Session(&gorm.Session{}).WithContext(ctx)
if opt.Debug {
db = db.Debug()
}
return db
}
func newTX(db *gorm.DB) interfaces.Database {
@ -39,6 +48,10 @@ func Must(database interfaces.Database, err error) interfaces.Database {
}
func NewSqliteTX(filepath string) (interfaces.Database, error) {
if err := os.MkdirAll(path.Dir(filepath), 0755); err != nil {
return nil, err
}
db, err := gorm.Open(sqlite.Open(filepath), &gorm.Config{})
return newTX(db), err
}

View File

@ -96,6 +96,12 @@ func (l *localUploader) Done(ctx context.Context, bh interfaces.BlobHandler, id
}
defer vrc.Close()
logrus.
WithField("path", "localUploader.Done").
WithField("id", id).
WithField("size", size).
Error()
if err := bh.Put(ctx, repo, hash, vrc); err != nil {
if errors.As(err, &verify.Error{}) {
logrus.

View File

@ -1,5 +1,12 @@
package model
type Catalog struct {
Repos []string `json:"repositories"`
type Repo struct {
Name string
CreatedAt int64
UpdatedAt int64
}
type Catalog struct {
Repositories []string `json:"repositories"`
Repos []*PackageManifest `json:"repos"`
Total int `json:"total"`
}

View File

@ -2,15 +2,55 @@ package model
import (
"encoding/json"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/samber/lo"
"io"
"nf-repo/internal/model/types"
)
type Manifest struct {
type RepoSimpleManifestLayer struct {
MediaType string `json:"mediaType"`
Size int `json:"size"`
Digest string `json:"digest"`
}
type RepoSimpleManifestBlob struct {
SchemaVersion int `json:"schemaVersion"`
MediaType string `json:"mediaType"`
Config struct {
MediaType string `json:"mediaType"`
Size int `json:"size"`
Digest string `json:"digest"`
} `json:"config"`
Layers []RepoSimpleManifestLayer `json:"layers"`
}
func (b *RepoSimpleManifestBlob) CountSize() int {
return b.Config.Size + lo.Sum(lo.Map(b.Layers, func(item RepoSimpleManifestLayer, _ int) int {
return item.Size
}))
}
func ManifestCountSize(m *v1.Manifest) int {
return int(m.Config.Size) + lo.Sum(lo.Map(m.Layers, func(item v1.Descriptor, _ int) int {
return int(item.Size)
}))
}
type RepoSimpleManifest struct {
Blob []byte `json:"blob"`
ContentType string `json:"content_type"`
}
type Manifest struct {
SchemaVersion int64 `json:"schemaVersion"`
MediaType types.MediaType `json:"mediaType,omitempty"`
Config Descriptor `json:"config"`
Layers []Descriptor `json:"layers"`
Annotations map[string]string `json:"annotations,omitempty"`
Subject *Descriptor `json:"subject,omitempty"`
}
type Descriptor struct {
MediaType types.MediaType `json:"mediaType"`
Size int64 `json:"size"`
@ -37,3 +77,34 @@ func ParseIndexManifest(r io.Reader) (*IndexManifest, error) {
}
return &im, nil
}
type PackageManifest struct {
Id uint64 `json:"id" gorm:"primaryKey;column:id"`
CreatedAt int64 `json:"created_at" gorm:"column:created_at;autoCreateTime:milli"`
UpdatedAt int64 `json:"updated_at" gorm:"column:updated_at;autoUpdateTime:milli"`
DigestId uint64 `json:"digest_id" gorm:"column:digest_id"`
Digest string `json:"digest" gorm:"-"`
Size int `json:"size" gorm:"-"`
Repo string `json:"repo" gorm:"uniqueIndex:repo_tag_idx;column:repo"`
Tag string `json:"tag" gorm:"uniqueIndex:repo_tag_idx;column:tag"`
}
type PackageDigest struct {
Id uint64 `json:"id" gorm:"primaryKey;column:id"`
CreatedAt int64 `json:"created_at" gorm:"column:created_at;autoCreateTime:milli"`
UpdatedAt int64 `json:"updated_at" gorm:"column:updated_at;autoUpdateTime:milli"`
Digest string `json:"digest" gorm:"uniqueIndex;column:digest"`
Size int `json:"size" gorm:"column:size;default:0"`
ContentType string `json:"content_type" gorm:"column:content_type"`
Content []byte `json:"content" gorm:"column:content;type:bytes"`
}
func ParseManifest(r io.Reader) (*Manifest, error) {
m := Manifest{}
if err := json.NewDecoder(r).Decode(&m); err != nil {
return nil, err
}
return &m, nil
}

View File

@ -1,6 +1,8 @@
package model
type Tag struct {
Name string `json:"name"`
Tags []string `json:"tags"`
Name string `json:"name"`
Tags []string `json:"tags"`
RepoTags []*PackageManifest `json:"repo_tags"`
Total int `json:"total"`
}

View File

@ -5,8 +5,11 @@ import "errors"
const (
DefaultMaxSize = 32 * 1024 * 1024
ReferrersEnabled = true
BaseAddress = "repo.me"
)
var (
Debug = false
ErrNotFound = errors.New("not found")
)

136
internal/util/r/resp.go Normal file
View File

@ -0,0 +1,136 @@
package r
import (
"fmt"
"github.com/loveuer/nf"
"strings"
)
const (
MSG200 = "请求成功"
MSG202 = "请求成功, 请稍后..."
MSG400 = "请求参数错误"
MSG401 = "登录已过期, 请重新登录"
MSG403 = "请求权限不足"
MSG404 = "请求资源未找到"
MSG429 = "请求过于频繁, 请稍后再试"
MSG500 = "服务器开小差了, 请稍后再试"
MSG501 = "功能开发中, 尽情期待"
)
func handleEmptyMsg(status uint32, msg string) string {
if msg == "" {
switch status {
case 200:
msg = MSG200
case 202:
msg = MSG202
case 400:
msg = MSG400
case 401:
msg = MSG401
case 403:
msg = MSG403
case 404:
msg = MSG404
case 429:
msg = MSG429
case 500:
msg = MSG500
case 501:
msg = MSG501
}
}
return msg
}
func Resp(c *nf.Ctx, status uint32, msg string, err string, data any) error {
msg = handleEmptyMsg(status, msg)
if data == nil {
return c.Status(int(status)).JSON(nf.Map{"status": status, "msg": msg, "err": err})
}
return c.Status(int(status)).JSON(nf.Map{"status": status, "msg": msg, "err": err, "data": data})
}
func Resp200(c *nf.Ctx, data any, msgs ...string) error {
msg := MSG200
if len(msgs) > 0 && msgs[0] != "" {
msg = fmt.Sprintf("%s: %s", msg, strings.Join(msgs, "; "))
}
return Resp(c, 200, msg, "", data)
}
func Resp202(c *nf.Ctx, data any, msgs ...string) error {
msg := MSG202
if len(msgs) > 0 && msgs[0] != "" {
msg = fmt.Sprintf("%s: %s", msg, strings.Join(msgs, "; "))
}
return Resp(c, 202, msg, "", data)
}
func Resp400(c *nf.Ctx, data any, msgs ...string) error {
msg := MSG400
err := ""
if len(msgs) > 0 && msgs[0] != "" {
msg = fmt.Sprintf("%s: %s", msg, strings.Join(msgs, "; "))
err = msg
}
return Resp(c, 400, msg, err, data)
}
func Resp401(c *nf.Ctx, data any, msgs ...string) error {
msg := MSG401
err := ""
if len(msgs) > 0 && msgs[0] != "" {
msg = fmt.Sprintf("%s: %s", msg, strings.Join(msgs, "; "))
err = msg
}
return Resp(c, 401, msg, err, data)
}
func Resp403(c *nf.Ctx, data any, msgs ...string) error {
msg := MSG403
err := ""
if len(msgs) > 0 && msgs[0] != "" {
msg = fmt.Sprintf("%s: %s", msg, strings.Join(msgs, "; "))
err = msg
}
return Resp(c, 403, msg, err, data)
}
func Resp429(c *nf.Ctx, data any, msgs ...string) error {
msg := MSG429
err := ""
if len(msgs) > 0 && msgs[0] != "" {
msg = fmt.Sprintf("%s: %s", msg, strings.Join(msgs, "; "))
err = ""
}
return Resp(c, 429, msg, err, data)
}
func Resp500(c *nf.Ctx, data any, msgs ...string) error {
msg := MSG500
err := ""
if len(msgs) > 0 && msgs[0] != "" {
msg = fmt.Sprintf("%s: %s", msg, strings.Join(msgs, "; "))
err = msg
}
return Resp(c, 500, msg, err, data)
}

View File

@ -1,147 +1,69 @@
// Copyright 2020 Google LLC All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package verify
import (
"bytes"
"errors"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"strings"
"nf-repo/internal/model"
"os"
"testing"
v1 "github.com/google/go-containerregistry/pkg/v1"
)
func mustHash(s string, t *testing.T) v1.Hash {
h, _, err := v1.SHA256(strings.NewReader(s))
func TestVerify(t *testing.T) {
bs := []byte(`{"architecture":"amd64","config":{"Hostname":"","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],"Cmd":["/bin/sh"],"Image":"sha256:9a5ce069f40cfe0f2270eafbff0a0f2fa08f1add73571af9f78209e96bb8a5e9","Volumes":null,"WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":null},"container":"4189cbc534955765760c227f328ec1cdd52e8550681c2bf9f8f990b27b644f9c","container_config":{"Hostname":"4189cbc53495","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],"Cmd":["/bin/sh","-c","#(nop) ","CMD [\"/bin/sh\"]"],"Image":"sha256:9a5ce069f40cfe0f2270eafbff0a0f2fa08f1add73571af9f78209e96bb8a5e9","Volumes":null,"WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":{}},"created":"2024-01-27T00:30:48.743965523Z","docker_version":"20.10.23","history":[{"created":"2024-01-27T00:30:48.624602109Z","created_by":"/bin/sh -c #(nop) ADD file:37a76ec18f9887751cd8473744917d08b7431fc4085097bb6a09d81b41775473 in / "},{"created":"2024-01-27T00:30:48.743965523Z","created_by":"/bin/sh -c #(nop) CMD [\"/bin/sh\"]","empty_layer":true}],"os":"linux","rootfs":{"type":"layers","diff_ids":["sha256:d4fc045c9e3a848011de66f34b81f052d4f2c15a17bb196d637e526349601820"]}}`)
//hash, _ := model.NewHash("sha256:05455a08881ea9cf0e752bc48e61bbd71a34c029bb13df01e40e3e70e0d007bd")
badHash, _ := model.NewHash("sha256:12455a08881ea9cf0e7f2bc48e61bbd71a34c029bb13df01e40e3e70e0d007bd")
nrc, err := ReadCloser(io.NopCloser(bytes.NewReader(bs)), -1, badHash)
if err != nil {
t.Fatalf("v1.SHA256(%s) = %v", s, err)
t.Error(1, err)
return
}
t.Logf("Hashed: %q -> %q", s, h)
return h
}
func TestVerificationFailure(t *testing.T) {
want := "This is the input string."
buf := bytes.NewBufferString(want)
verified, err := ReadCloser(io.NopCloser(buf), int64(len(want)), mustHash("not the same", t))
nbs, err := io.ReadAll(nrc)
if err != nil {
t.Fatal("ReadCloser() =", err)
t.Error(2, err)
}
if b, err := io.ReadAll(verified); err == nil {
t.Errorf("ReadAll() = %q; want verification error", string(b))
if string(nbs) != string(bs) {
t.Error(3, "not same")
}
t.Log("new bytes:", string(nbs))
}
func TestVerification(t *testing.T) {
want := "This is the input string."
buf := bytes.NewBufferString(want)
verified, err := ReadCloser(io.NopCloser(buf), int64(len(want)), mustHash(want, t))
func TestVerify2(t *testing.T) {
name := "4a666f159bd07f62aa19ce8eceac018931223de67c4bd19ded87b83eb7b103ca"
f, err := os.Open(name)
if err != nil {
t.Fatal("ReadCloser() =", err)
t.Error(1, err)
return
}
if _, err := io.ReadAll(verified); err != nil {
t.Error("ReadAll() =", err)
}
}
func TestVerificationSizeUnknown(t *testing.T) {
want := "This is the input string."
buf := bytes.NewBufferString(want)
verified, err := ReadCloser(io.NopCloser(buf), SizeUnknown, mustHash(want, t))
bs, err := io.ReadAll(f)
if err != nil {
t.Fatal("ReadCloser() =", err)
t.Error(2, err)
}
if _, err := io.ReadAll(verified); err != nil {
t.Error("ReadAll() =", err)
}
}
func TestBadHash(t *testing.T) {
h := v1.Hash{
Algorithm: "fake256",
Hex: "whatever",
}
_, err := ReadCloser(io.NopCloser(strings.NewReader("hi")), 0, h)
if err == nil {
t.Errorf("ReadCloser() = %v, wanted err", err)
}
}
func TestBadSize(t *testing.T) {
want := "This is the input string."
// having too much content or expecting too much content returns an error.
for _, size := range []int64{3, 100} {
t.Run(fmt.Sprintf("expecting size %d", size), func(t *testing.T) {
buf := bytes.NewBufferString(want)
rc, err := ReadCloser(io.NopCloser(buf), size, mustHash(want, t))
if err != nil {
t.Fatal("ReadCloser() =", err)
}
if b, err := io.ReadAll(rc); err == nil {
t.Errorf("ReadAll() = %q; want verification error", string(b))
}
})
}
}
func TestDescriptor(t *testing.T) {
for _, tc := range []struct {
err error
desc v1.Descriptor
}{{
err: errors.New("error verifying descriptor; Data == nil"),
}, {
err: errors.New(`error verifying Digest; got "sha256:ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad", want ":"`),
desc: v1.Descriptor{
Data: []byte("abc"),
},
}, {
err: errors.New("error verifying Size; got 3, want 0"),
desc: v1.Descriptor{
Data: []byte("abc"),
Digest: v1.Hash{
Algorithm: "sha256",
Hex: "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
},
},
}, {
desc: v1.Descriptor{
Data: []byte("abc"),
Size: 3,
Digest: v1.Hash{
Algorithm: "sha256",
Hex: "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
},
},
}} {
got, want := Descriptor(tc.desc), tc.err
if got == nil {
if want != nil {
t.Errorf("Descriptor(): got nil, want %v", want)
}
} else if want == nil {
t.Errorf("Descriptor(): got %v, want nil", got)
} else if got, want := got.Error(), want.Error(); got != want {
t.Errorf("Descriptor(): got %q, want %q", got, want)
}
shaer := sha256.New()
shaer.Write(bs)
sum := hex.EncodeToString(shaer.Sum(nil))
r, err := ReadCloser(io.NopCloser(bytes.NewReader(bs)), int64(len(bs)), model.Hash{Algorithm: "sha256", Hex: name})
if err != nil {
t.Error(4, err)
return
}
if _, err = io.ReadAll(r); err != nil {
t.Error(5, err)
return
}
if sum != name {
t.Error(3, fmt.Sprintf("want: %s got: %s", name, sum))
}
_ = r
}

View File

@ -1,57 +0,0 @@
package x
import "testing"
func TestMinPathSum(t *testing.T) {
grid := [][]int{
{3, 8, 6, 0, 5, 9, 9, 6, 3, 4, 0, 5, 7, 3, 9, 3},
{0, 9, 2, 5, 5, 4, 9, 1, 4, 6, 9, 5, 6, 7, 3, 2},
{8, 2, 2, 3, 3, 3, 1, 6, 9, 1, 1, 6, 6, 2, 1, 9},
{1, 3, 6, 9, 9, 5, 0, 3, 4, 9, 1, 0, 9, 6, 2, 7},
{8, 6, 2, 2, 1, 3, 0, 0, 7, 2, 7, 5, 4, 8, 4, 8},
{4, 1, 9, 5, 8, 9, 9, 2, 0, 2, 5, 1, 8, 7, 0, 9},
{6, 2, 1, 7, 8, 1, 8, 5, 5, 7, 0, 2, 5, 7, 2, 1},
{8, 1, 7, 6, 2, 8, 1, 2, 2, 6, 4, 0, 5, 4, 1, 3},
{9, 2, 1, 7, 6, 1, 4, 3, 8, 6, 5, 5, 3, 9, 7, 3},
{0, 6, 0, 2, 4, 3, 7, 6, 1, 3, 8, 6, 9, 0, 0, 8},
{4, 3, 7, 2, 4, 3, 6, 4, 0, 3, 9, 5, 3, 6, 9, 3},
{2, 1, 8, 8, 4, 5, 6, 5, 8, 7, 3, 7, 7, 5, 8, 3},
{0, 7, 6, 6, 1, 2, 0, 3, 5, 0, 8, 0, 8, 7, 4, 3},
{0, 4, 3, 4, 9, 0, 1, 9, 7, 7, 8, 6, 4, 6, 9, 5},
{6, 5, 1, 9, 9, 2, 2, 7, 4, 2, 7, 2, 2, 3, 7, 2},
{7, 1, 9, 6, 1, 2, 7, 0, 9, 6, 6, 4, 4, 5, 1, 0},
{3, 4, 9, 2, 8, 3, 1, 2, 6, 9, 7, 0, 2, 4, 2, 0},
{5, 1, 8, 8, 4, 6, 8, 5, 2, 4, 1, 6, 2, 2, 9, 7},
}
result := minPathSum(grid)
t.Log("result:", result)
}
func minPathSum(grid [][]int) int {
return sum(grid, len(grid)-1, len(grid[0])-1)
}
func sum(grid [][]int, x, y int) int {
if x == 0 && y == 0 {
return grid[x][y]
}
if x == 0 {
return grid[x][y] + sum(grid, x, y-1)
}
if y == 0 {
return grid[x][y] + sum(grid, x-1, y)
}
return grid[x][y] + _min(sum(grid, x-1, y), sum(grid, x, y-1))
}
func _min(a, b int) int {
if a < b {
return a
}
return b
}