feat: mem, local uploader
This commit is contained in:
		
							
								
								
									
										7
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | ||||
| .idea | ||||
| .vscode | ||||
| *.crt | ||||
| *.key | ||||
| *.sqlite | ||||
| *.db | ||||
| images | ||||
							
								
								
									
										28
									
								
								go.mod
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								go.mod
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | ||||
| module nf-repo | ||||
|  | ||||
| go 1.20 | ||||
|  | ||||
| require ( | ||||
| 	github.com/glebarez/sqlite v1.11.0 | ||||
| 	github.com/google/go-containerregistry v0.19.1 | ||||
| 	github.com/loveuer/nf v0.1.6 | ||||
| 	github.com/samber/lo v1.39.0 | ||||
| 	github.com/sirupsen/logrus v1.9.3 | ||||
| 	gorm.io/gorm v1.25.9 | ||||
| ) | ||||
|  | ||||
| require ( | ||||
| 	github.com/dustin/go-humanize v1.0.1 // indirect | ||||
| 	github.com/glebarez/go-sqlite v1.21.2 // indirect | ||||
| 	github.com/google/uuid v1.3.0 // indirect | ||||
| 	github.com/jinzhu/inflection v1.0.0 // indirect | ||||
| 	github.com/jinzhu/now v1.1.5 // indirect | ||||
| 	github.com/mattn/go-isatty v0.0.17 // indirect | ||||
| 	github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect | ||||
| 	golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect | ||||
| 	golang.org/x/sys v0.15.0 // indirect | ||||
| 	modernc.org/libc v1.22.5 // indirect | ||||
| 	modernc.org/mathutil v1.5.0 // indirect | ||||
| 	modernc.org/memory v1.5.0 // indirect | ||||
| 	modernc.org/sqlite v1.23.1 // indirect | ||||
| ) | ||||
							
								
								
									
										54
									
								
								go.sum
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								go.sum
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,54 @@ | ||||
| github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||||
| github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= | ||||
| github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||||
| github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= | ||||
| github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= | ||||
| github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo= | ||||
| github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k= | ||||
| github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw= | ||||
| github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ= | ||||
| github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= | ||||
| github.com/google/go-containerregistry v0.19.1 h1:yMQ62Al6/V0Z7CqIrrS1iYoA5/oQCm88DeNujc7C1KY= | ||||
| github.com/google/go-containerregistry v0.19.1/go.mod h1:YCMFNQeeXeLF+dnhhWkqDItx/JSkH01j1Kis4PsjzFI= | ||||
| github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= | ||||
| github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= | ||||
| github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= | ||||
| github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= | ||||
| github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= | ||||
| github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= | ||||
| github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= | ||||
| github.com/loveuer/nf v0.1.6 h1:YIhrQm1iIMkTqP6EOdQ7OsNoFXCD0/i7oi3yjaMtpCU= | ||||
| github.com/loveuer/nf v0.1.6/go.mod h1:uKsKYym27ravyTXSBSnxU86V7osxx9cM6DJ+dVBfJ1Q= | ||||
| github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= | ||||
| github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= | ||||
| github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | ||||
| github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | ||||
| github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= | ||||
| github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= | ||||
| github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= | ||||
| github.com/samber/lo v1.39.0 h1:4gTz1wUhNYLhFSKl6O+8peW0v2F4BCY034GRpU9WnuA= | ||||
| github.com/samber/lo v1.39.0/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= | ||||
| github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= | ||||
| github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= | ||||
| github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= | ||||
| github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= | ||||
| github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= | ||||
| golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 h1:3MTrJm4PyNL9NBqvYDSj3DHl46qQakyfqfWo4jgfaEM= | ||||
| golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE= | ||||
| golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= | ||||
| golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= | ||||
| gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | ||||
| gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | ||||
| gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= | ||||
| gorm.io/gorm v1.25.9 h1:wct0gxZIELDk8+ZqF/MVnHLkA1rvYlBWUMv2EdsK1g8= | ||||
| gorm.io/gorm v1.25.9/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= | ||||
| modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE= | ||||
| modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY= | ||||
| modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ= | ||||
| modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= | ||||
| modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds= | ||||
| modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= | ||||
| modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM= | ||||
| modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk= | ||||
							
								
								
									
										
											BIN
										
									
								
								internal/.DS_Store
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								internal/.DS_Store
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										31
									
								
								internal/and/and_closer.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								internal/and/and_closer.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | ||||
| package and | ||||
|  | ||||
| import ( | ||||
| 	"io" | ||||
| ) | ||||
|  | ||||
| type ReadCloser struct { | ||||
| 	io.Reader | ||||
| 	CloseFunc func() error | ||||
| } | ||||
|  | ||||
| var _ io.ReadCloser = (*ReadCloser)(nil) | ||||
|  | ||||
| // Close implements io.ReadCloser | ||||
| func (rac *ReadCloser) Close() error { | ||||
| 	return rac.CloseFunc() | ||||
| } | ||||
|  | ||||
| // WriteCloser implements io.WriteCloser by reading from a particular io.Writer | ||||
| // and then calling the provided "Close()" method. | ||||
| type WriteCloser struct { | ||||
| 	io.Writer | ||||
| 	CloseFunc func() error | ||||
| } | ||||
|  | ||||
| var _ io.WriteCloser = (*WriteCloser)(nil) | ||||
|  | ||||
| // Close implements io.WriteCloser | ||||
| func (wac *WriteCloser) Close() error { | ||||
| 	return wac.CloseFunc() | ||||
| } | ||||
							
								
								
									
										85
									
								
								internal/and/and_closer_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								internal/and/and_closer_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,85 @@ | ||||
| // 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 and | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"io" | ||||
| 	"testing" | ||||
| ) | ||||
|  | ||||
| func TestRead(t *testing.T) { | ||||
| 	want := "asdf" | ||||
| 	r := bytes.NewBufferString(want) | ||||
| 	called := false | ||||
|  | ||||
| 	rac := &ReadCloser{ | ||||
| 		Reader: r, | ||||
| 		CloseFunc: func() error { | ||||
| 			called = true | ||||
| 			return nil | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	data, err := io.ReadAll(rac) | ||||
| 	if err != nil { | ||||
| 		t.Errorf("ReadAll(rac) = %v", err) | ||||
| 	} | ||||
| 	if got := string(data); got != want { | ||||
| 		t.Errorf("ReadAll(rac); got %q, want %q", got, want) | ||||
| 	} | ||||
|  | ||||
| 	if called { | ||||
| 		t.Error("called before Close(); got true, wanted false") | ||||
| 	} | ||||
| 	if err := rac.Close(); err != nil { | ||||
| 		t.Errorf("Close() = %v", err) | ||||
| 	} | ||||
| 	if !called { | ||||
| 		t.Error("called after Close(); got false, wanted true") | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestWrite(t *testing.T) { | ||||
| 	w := bytes.NewBuffer([]byte{}) | ||||
| 	called := false | ||||
|  | ||||
| 	wac := &WriteCloser{ | ||||
| 		Writer: w, | ||||
| 		CloseFunc: func() error { | ||||
| 			called = true | ||||
| 			return nil | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	want := "asdf" | ||||
| 	if _, err := wac.Write([]byte(want)); err != nil { | ||||
| 		t.Errorf("Write(%q); = %v", want, err) | ||||
| 	} | ||||
|  | ||||
| 	if called { | ||||
| 		t.Error("called before Close(); got true, wanted false") | ||||
| 	} | ||||
| 	if err := wac.Close(); err != nil { | ||||
| 		t.Errorf("Close() = %v", err) | ||||
| 	} | ||||
| 	if !called { | ||||
| 		t.Error("called after Close(); got false, wanted true") | ||||
| 	} | ||||
|  | ||||
| 	if got := w.String(); got != want { | ||||
| 		t.Errorf("w.String(); got %q, want %q", got, want) | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										25
									
								
								internal/api/api.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								internal/api/api.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | ||||
| package api | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"github.com/loveuer/nf" | ||||
| 	"nf-repo/internal/handler" | ||||
| 	"nf-repo/internal/interfaces" | ||||
| 	"nf-repo/internal/opt" | ||||
| ) | ||||
|  | ||||
| func NewApi( | ||||
| 	ctx context.Context, | ||||
| 	bh interfaces.BlobHandler, | ||||
| 	uh interfaces.UploadHandler, | ||||
| 	mh interfaces.ManifestHandler, | ||||
| ) *nf.App { | ||||
| 	app := nf.New(nf.Config{BodyLimit: opt.DefaultMaxSize}) | ||||
|  | ||||
| 	{ | ||||
| 		api := app.Group("/v2") | ||||
| 		api.Any("/*path", handler.Root(bh, uh, mh)) | ||||
| 	} | ||||
|  | ||||
| 	return app | ||||
| } | ||||
							
								
								
									
										307
									
								
								internal/handler/blobs.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										307
									
								
								internal/handler/blobs.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,307 @@ | ||||
| package handler | ||||
|  | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"github.com/loveuer/nf" | ||||
| 	"github.com/sirupsen/logrus" | ||||
| 	"io" | ||||
| 	"log" | ||||
| 	"net/http" | ||||
| 	"nf-repo/internal/model" | ||||
| 	"nf-repo/internal/opt" | ||||
| 	"nf-repo/internal/util/rerr" | ||||
| 	"nf-repo/internal/verify" | ||||
| 	"path" | ||||
| 	"strings" | ||||
| ) | ||||
|  | ||||
| func handleBlobs(c *nf.Ctx) error { | ||||
| 	elem := strings.Split(c.Path(), "/") | ||||
| 	elem = elem[1:] | ||||
| 	if elem[len(elem)-1] == "" { | ||||
| 		elem = elem[:len(elem)-1] | ||||
| 	} | ||||
|  | ||||
| 	// Must have a path of form /v2/{name}/blobs/{upload,sha256:} | ||||
| 	if len(elem) < 4 { | ||||
| 		return rerr.Error(c, &rerr.RepositoryError{ | ||||
| 			Status:  http.StatusBadRequest, | ||||
| 			Code:    "NAME_INVALID", | ||||
| 			Message: "blobs must be attached to a repo", | ||||
| 		}) | ||||
| 	} | ||||
|  | ||||
| 	target := elem[len(elem)-1] | ||||
| 	service := elem[len(elem)-2] | ||||
| 	digest := c.Query("digest") | ||||
| 	contentRange := c.Get("Content-Range") | ||||
| 	rangeHeader := c.Get("Range") | ||||
| 	repo := c.Request.URL.Host + path.Join(elem[1:len(elem)-2]...) | ||||
|  | ||||
| 	logrus. | ||||
| 		WithField("handler", "handleBlob"). | ||||
| 		WithField("path", c.Path()). | ||||
| 		WithField("method", c.Method()). | ||||
| 		WithField("target", target). | ||||
| 		WithField("service", service). | ||||
| 		WithField("repo", repo). | ||||
| 		WithField("digest", digest). | ||||
| 		Debug() | ||||
|  | ||||
| 	switch c.Method() { | ||||
| 	case http.MethodHead: | ||||
| 		h, err := model.NewHash(target) | ||||
| 		if err != nil { | ||||
| 			return rerr.Error(c, &rerr.RepositoryError{ | ||||
| 				Status:  http.StatusBadRequest, | ||||
| 				Code:    "NAME_INVALID", | ||||
| 				Message: "invalid digest", | ||||
| 			}) | ||||
| 		} | ||||
|  | ||||
| 		var size int64 | ||||
|  | ||||
| 		size, err = b.blobHandler.Stat(c.Request.Context(), repo, h) | ||||
| 		if errors.Is(err, opt.ErrNotFound) { | ||||
| 			return rerr.Error(c, rerr.ErrBlobUnknown) | ||||
| 		} else if err != nil { | ||||
| 			var re model.RedirectError | ||||
| 			if errors.As(err, &re) { | ||||
| 				http.Redirect(c.RawWriter(), c.Request, re.Location, re.Code) | ||||
| 				return nil | ||||
| 			} | ||||
| 			return rerr.Error(c, rerr.ErrInternal(err)) | ||||
| 		} | ||||
|  | ||||
| 		c.Set("Content-Length", fmt.Sprint(size)) | ||||
| 		c.Set("Docker-Content-Digest", h.String()) | ||||
| 		return c.SendStatus(http.StatusOK) | ||||
|  | ||||
| 	case http.MethodGet: | ||||
| 		h, err := model.NewHash(target) | ||||
| 		if err != nil { | ||||
| 			return rerr.Error(c, &rerr.RepositoryError{ | ||||
| 				Status:  http.StatusBadRequest, | ||||
| 				Code:    "NAME_INVALID", | ||||
| 				Message: "invalid digest", | ||||
| 			}) | ||||
| 		} | ||||
|  | ||||
| 		var size int64 | ||||
| 		var r io.Reader | ||||
|  | ||||
| 		size, err = b.blobHandler.Stat(c.Request.Context(), repo, h) | ||||
| 		if errors.Is(err, opt.ErrNotFound) { | ||||
| 			return rerr.Error(c, rerr.ErrBlobUnknown) | ||||
| 		} else if err != nil { | ||||
| 			var re model.RedirectError | ||||
| 			if errors.As(err, &re) { | ||||
| 				http.Redirect(c.RawWriter(), c.Request, re.Location, re.Code) | ||||
| 				return nil | ||||
| 			} | ||||
|  | ||||
| 			return rerr.Error(c, rerr.ErrInternal(err)) | ||||
| 		} | ||||
|  | ||||
| 		rc, err := b.blobHandler.Get(c.Request.Context(), repo, h) | ||||
| 		if errors.Is(err, opt.ErrNotFound) { | ||||
| 			return rerr.Error(c, rerr.ErrBlobUnknown) | ||||
| 		} else if err != nil { | ||||
| 			var re model.RedirectError | ||||
| 			if errors.As(err, &re) { | ||||
| 				http.Redirect(c.RawWriter(), c.Request, re.Location, re.Code) | ||||
| 				return nil | ||||
| 			} | ||||
|  | ||||
| 			return rerr.Error(c, rerr.ErrInternal(err)) | ||||
| 		} | ||||
|  | ||||
| 		defer rc.Close() | ||||
| 		r = rc | ||||
|  | ||||
| 		if rangeHeader != "" { | ||||
| 			start, end := int64(0), int64(0) | ||||
| 			if _, err := fmt.Sscanf(rangeHeader, "bytes=%d-%d", &start, &end); err != nil { | ||||
| 				return rerr.Error(c, &rerr.RepositoryError{ | ||||
| 					Status:  http.StatusRequestedRangeNotSatisfiable, | ||||
| 					Code:    "BLOB_UNKNOWN", | ||||
| 					Message: "We don't understand your Range", | ||||
| 				}) | ||||
| 			} | ||||
|  | ||||
| 			n := (end + 1) - start | ||||
| 			if ra, ok := r.(io.ReaderAt); ok { | ||||
| 				if end+1 > size { | ||||
| 					return rerr.Error(c, &rerr.RepositoryError{ | ||||
| 						Status:  http.StatusRequestedRangeNotSatisfiable, | ||||
| 						Code:    "BLOB_UNKNOWN", | ||||
| 						Message: fmt.Sprintf("range end %d > %d size", end+1, size), | ||||
| 					}) | ||||
| 				} | ||||
| 				r = io.NewSectionReader(ra, start, n) | ||||
| 			} else { | ||||
| 				if _, err := io.CopyN(io.Discard, r, start); err != nil { | ||||
| 					return rerr.Error(c, &rerr.RepositoryError{ | ||||
| 						Status:  http.StatusRequestedRangeNotSatisfiable, | ||||
| 						Code:    "BLOB_UNKNOWN", | ||||
| 						Message: fmt.Sprintf("Failed to discard %d bytes", start), | ||||
| 					}) | ||||
| 				} | ||||
|  | ||||
| 				r = io.LimitReader(r, n) | ||||
| 			} | ||||
|  | ||||
| 			c.Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, size)) | ||||
| 			c.Set("Content-Length", fmt.Sprint(n)) | ||||
| 			c.Set("Docker-Content-Digest", h.String()) | ||||
| 			c.Status(http.StatusPartialContent) | ||||
| 		} else { | ||||
| 			c.Set("Content-Length", fmt.Sprint(size)) | ||||
| 			c.Set("Docker-Content-Digest", h.String()) | ||||
| 			c.Status(http.StatusOK) | ||||
| 		} | ||||
|  | ||||
| 		_, err = io.Copy(c.RawWriter(), r) | ||||
| 		return err | ||||
|  | ||||
| 	case http.MethodPost: | ||||
| 		// It is weird that this is "target" instead of "service", but | ||||
| 		// that's how the index math works out above. | ||||
| 		if target != "uploads" { | ||||
| 			return rerr.Error(c, &rerr.RepositoryError{ | ||||
| 				Status:  http.StatusBadRequest, | ||||
| 				Code:    "METHOD_UNKNOWN", | ||||
| 				Message: fmt.Sprintf("POST to /blobs must be followed by /uploads, got %s", target), | ||||
| 			}) | ||||
| 		} | ||||
|  | ||||
| 		if digest != "" { | ||||
| 			h, err := model.NewHash(digest) | ||||
| 			if err != nil { | ||||
| 				return rerr.Error(c, rerr.ErrDigestInvalid) | ||||
| 			} | ||||
|  | ||||
| 			vrc, err := verify.ReadCloser(c.Request.Body, c.Request.ContentLength, h) | ||||
| 			if err != nil { | ||||
| 				return rerr.Error(c, rerr.ErrInternal(err)) | ||||
| 			} | ||||
| 			defer vrc.Close() | ||||
|  | ||||
| 			if err = b.blobHandler.Put(c.Request.Context(), repo, h, vrc); err != nil { | ||||
| 				if errors.As(err, &verify.Error{}) { | ||||
| 					log.Printf("Digest mismatch: %v", err) | ||||
| 					return rerr.Error(c, rerr.ErrDigestMismatch) | ||||
| 				} | ||||
| 				return rerr.Error(c, rerr.ErrInternal(err)) | ||||
| 			} | ||||
| 			c.Set("Docker-Content-Digest", h.String()) | ||||
| 			return c.SendStatus(http.StatusCreated) | ||||
| 		} | ||||
|  | ||||
| 		id := b.uploadHandler.UploadId() | ||||
|  | ||||
| 		c.Set("Location", "/"+path.Join("v2", path.Join(elem[1:len(elem)-2]...), "blobs/uploads", id)) | ||||
| 		c.Set("Range", "0-0") | ||||
|  | ||||
| 		return c.SendStatus(http.StatusAccepted) | ||||
|  | ||||
| 	case http.MethodPatch: | ||||
| 		if service != "uploads" { | ||||
| 			return rerr.Error(c, &rerr.RepositoryError{ | ||||
| 				Status:  http.StatusBadRequest, | ||||
| 				Code:    "METHOD_UNKNOWN", | ||||
| 				Message: fmt.Sprintf("PATCH to /blobs must be followed by /uploads, got %s", service), | ||||
| 			}) | ||||
| 		} | ||||
|  | ||||
| 		start, end := 0, 0 | ||||
|  | ||||
| 		if contentRange != "" { | ||||
| 			if _, err := fmt.Sscanf(contentRange, "%d-%d", &start, &end); err != nil { | ||||
| 				return rerr.Error(c, &rerr.RepositoryError{ | ||||
| 					Status:  http.StatusRequestedRangeNotSatisfiable, | ||||
| 					Code:    "BLOB_UPLOAD_UNKNOWN", | ||||
| 					Message: "We don't understand your Content-Range", | ||||
| 				}) | ||||
| 			} | ||||
|  | ||||
| 			if end != start+int(c.Request.ContentLength) { | ||||
| 				return rerr.Error(c, &rerr.RepositoryError{ | ||||
| 					Status:  http.StatusRequestedRangeNotSatisfiable, | ||||
| 					Code:    "BLOB_UPLOAD_INVALID", | ||||
| 					Message: "blob upload content range mismatch content length", | ||||
| 				}) | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		end = start + int(c.Request.ContentLength) | ||||
|  | ||||
| 		var ( | ||||
| 			length int | ||||
| 			re     *rerr.RepositoryError | ||||
| 		) | ||||
| 		if length, re = b.uploadHandler.Write(c.Request.Context(), target, c.Request.Body, start, end); re != nil { | ||||
| 			return rerr.Error(c, re) | ||||
| 		} | ||||
|  | ||||
| 		c.Set("Location", "/"+path.Join("v2", path.Join(elem[1:len(elem)-3]...), "blobs/uploads", target)) | ||||
| 		c.Set("Range", fmt.Sprintf("0-%d", length-1)) | ||||
| 		return c.SendStatus(http.StatusNoContent) | ||||
|  | ||||
| 	case http.MethodPut: | ||||
| 		if service != "uploads" { | ||||
| 			return rerr.Error(c, &rerr.RepositoryError{ | ||||
| 				Status:  http.StatusBadRequest, | ||||
| 				Code:    "METHOD_UNKNOWN", | ||||
| 				Message: fmt.Sprintf("PUT to /blobs must be followed by /uploads, got %s", service), | ||||
| 			}) | ||||
| 		} | ||||
|  | ||||
| 		if digest == "" { | ||||
| 			return rerr.Error(c, &rerr.RepositoryError{ | ||||
| 				Status:  http.StatusBadRequest, | ||||
| 				Code:    "DIGEST_INVALID", | ||||
| 				Message: "digest not specified", | ||||
| 			}) | ||||
| 		} | ||||
|  | ||||
| 		hash, err := model.NewHash(digest) | ||||
| 		if err != nil { | ||||
| 			return rerr.Error(c, &rerr.RepositoryError{ | ||||
| 				Status:  http.StatusBadRequest, | ||||
| 				Code:    "NAME_INVALID", | ||||
| 				Message: "invalid digest", | ||||
| 			}) | ||||
| 		} | ||||
|  | ||||
| 		var re *rerr.RepositoryError | ||||
| 		if re = b.uploadHandler.Done(c.Request.Context(), b.blobHandler, target, c.Request.Body, int(c.Request.ContentLength), repo, hash); re != nil { | ||||
| 			return rerr.Error(c, re) | ||||
| 		} | ||||
|  | ||||
| 		c.Set("Docker-Content-Digest", hash.String()) | ||||
| 		return c.SendStatus(http.StatusCreated) | ||||
|  | ||||
| 	case http.MethodDelete: | ||||
| 		h, err := model.NewHash(target) | ||||
| 		if err != nil { | ||||
| 			return rerr.Error(c, &rerr.RepositoryError{ | ||||
| 				Status:  http.StatusBadRequest, | ||||
| 				Code:    "NAME_INVALID", | ||||
| 				Message: "invalid digest", | ||||
| 			}) | ||||
| 		} | ||||
| 		if err := b.blobHandler.Delete(c.Request.Context(), repo, h); err != nil { | ||||
| 			return rerr.Error(c, rerr.ErrInternal(err)) | ||||
| 		} | ||||
| 		return c.SendStatus(http.StatusAccepted) | ||||
|  | ||||
| 	default: | ||||
| 		return rerr.Error(c, &rerr.RepositoryError{ | ||||
| 			Status:  http.StatusBadRequest, | ||||
| 			Code:    "METHOD_UNKNOWN", | ||||
| 			Message: "We don't understand your method + url", | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										37
									
								
								internal/handler/catalog.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								internal/handler/catalog.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,37 @@ | ||||
| package handler | ||||
|  | ||||
| import ( | ||||
| 	"github.com/loveuer/nf" | ||||
| 	"net/http" | ||||
| 	"nf-repo/internal/interfaces" | ||||
| 	"nf-repo/internal/model" | ||||
| 	"nf-repo/internal/util/rerr" | ||||
| 	"strconv" | ||||
| ) | ||||
|  | ||||
| func handleCatalog(ctx *nf.Ctx, m interfaces.ManifestHandler) error { | ||||
| 	if ctx.Method() != "GET" { | ||||
| 		return rerr.Error(ctx, &rerr.RepositoryError{ | ||||
| 			Status:  http.StatusBadRequest, | ||||
| 			Code:    "METHOD_UNKNOWN", | ||||
| 			Message: "We don't understand your method + url", | ||||
| 		}) | ||||
| 	} | ||||
|  | ||||
| 	nStr := ctx.Query("n") | ||||
| 	n := 10000 | ||||
| 	if nStr != "" { | ||||
| 		n, _ = strconv.Atoi(nStr) | ||||
| 	} | ||||
|  | ||||
| 	var ( | ||||
| 		re   *rerr.RepositoryError | ||||
| 		list *model.Catalog | ||||
| 	) | ||||
|  | ||||
| 	if list, re = m.Catelog(ctx.Request.Context(), n, 0); re != nil { | ||||
| 		return rerr.Error(ctx, re) | ||||
| 	} | ||||
|  | ||||
| 	return ctx.JSON(list) | ||||
| } | ||||
							
								
								
									
										57
									
								
								internal/handler/judge.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								internal/handler/judge.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,57 @@ | ||||
| package handler | ||||
|  | ||||
| import ( | ||||
| 	"github.com/loveuer/nf" | ||||
| 	"strings" | ||||
| ) | ||||
|  | ||||
| func isBlob(c *nf.Ctx) bool { | ||||
| 	elem := strings.Split(c.Path(), "/") | ||||
| 	elem = elem[1:] | ||||
| 	if elem[len(elem)-1] == "" { | ||||
| 		elem = elem[:len(elem)-1] | ||||
| 	} | ||||
| 	if len(elem) < 3 { | ||||
| 		return false | ||||
| 	} | ||||
| 	return elem[len(elem)-2] == "blobs" || (elem[len(elem)-3] == "blobs" && | ||||
| 		elem[len(elem)-2] == "uploads") | ||||
| } | ||||
|  | ||||
| func isManifest(c *nf.Ctx) bool { | ||||
| 	elems := strings.Split(c.Path(), "/") | ||||
| 	elems = elems[1:] | ||||
| 	if len(elems) < 4 { | ||||
| 		return false | ||||
| 	} | ||||
| 	return elems[len(elems)-2] == "manifests" | ||||
| } | ||||
|  | ||||
| func isTags(c *nf.Ctx) bool { | ||||
| 	elems := strings.Split(c.Path(), "/") | ||||
| 	elems = elems[1:] | ||||
| 	if len(elems) < 4 { | ||||
| 		return false | ||||
| 	} | ||||
| 	return elems[len(elems)-2] == "tags" | ||||
| } | ||||
|  | ||||
| func isCatalog(c *nf.Ctx) bool { | ||||
| 	elems := strings.Split(c.Path(), "/") | ||||
| 	elems = elems[1:] | ||||
| 	if len(elems) < 2 { | ||||
| 		return false | ||||
| 	} | ||||
|  | ||||
| 	return elems[len(elems)-1] == "_catalog" | ||||
| } | ||||
|  | ||||
| func isReferrers(c *nf.Ctx) bool { | ||||
| 	elems := strings.Split(c.Path(), "/") | ||||
| 	elems = elems[1:] | ||||
| 	if len(elems) < 4 { | ||||
| 		return false | ||||
| 	} | ||||
|  | ||||
| 	return elems[len(elems)-2] == "referrers" | ||||
| } | ||||
							
								
								
									
										127
									
								
								internal/handler/manifest.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										127
									
								
								internal/handler/manifest.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,127 @@ | ||||
| package handler | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"fmt" | ||||
| 	"github.com/loveuer/nf" | ||||
| 	"github.com/sirupsen/logrus" | ||||
| 	"io" | ||||
| 	"net/http" | ||||
| 	"nf-repo/internal/interfaces" | ||||
| 	"nf-repo/internal/model" | ||||
| 	"nf-repo/internal/util/rerr" | ||||
| 	"strings" | ||||
| ) | ||||
|  | ||||
| func handleManifest(ctx *nf.Ctx, m interfaces.ManifestHandler) error { | ||||
| 	elem := strings.Split(ctx.Path(), "/") | ||||
| 	elem = elem[1:] | ||||
| 	target := elem[len(elem)-1] | ||||
| 	repo := strings.Join(elem[1:len(elem)-2], "/") | ||||
|  | ||||
| 	logrus. | ||||
| 		WithField("handler", "handleManifest"). | ||||
| 		WithField("path", ctx.Path()). | ||||
| 		WithField("method", ctx.Method()). | ||||
| 		WithField("repo", repo). | ||||
| 		WithField("target", target). | ||||
| 		Debug() | ||||
|  | ||||
| 	switch ctx.Method() { | ||||
| 	case http.MethodGet: | ||||
| 		var ( | ||||
| 			err         error | ||||
| 			reader      io.ReadCloser | ||||
| 			contentType string | ||||
| 			re          *rerr.RepositoryError | ||||
| 			bs          []byte | ||||
| 		) | ||||
|  | ||||
| 		if reader, contentType, re = m.Get(ctx.Request.Context(), repo, target); re != nil { | ||||
| 			return rerr.Error(ctx, re) | ||||
| 		} | ||||
|  | ||||
| 		if bs, err = io.ReadAll(reader); err != nil { | ||||
| 			return rerr.Error(ctx, &rerr.RepositoryError{ | ||||
| 				Status:  http.StatusInternalServerError, | ||||
| 				Code:    "INTERNAL_SERVER_ERROR", | ||||
| 				Message: err.Error(), | ||||
| 			}) | ||||
| 		} | ||||
|  | ||||
| 		h, _, _ := model.SHA256(bytes.NewReader(bs)) | ||||
| 		ctx.Set("Docker-Content-Digest", h.String()) | ||||
| 		ctx.Set("Content-Type", contentType) | ||||
| 		ctx.Set("Content-Length", fmt.Sprint(len(bs))) | ||||
| 		ctx.Status(http.StatusOK) | ||||
| 		_, err = ctx.Write(bs) | ||||
| 		return err | ||||
|  | ||||
| 	case http.MethodHead: | ||||
| 		var ( | ||||
| 			err         error | ||||
| 			reader      io.ReadCloser | ||||
| 			contentType string | ||||
| 			re          *rerr.RepositoryError | ||||
| 			bs          []byte | ||||
| 		) | ||||
|  | ||||
| 		if reader, contentType, re = m.Get(ctx.Request.Context(), repo, target); re != nil { | ||||
| 			return rerr.Error(ctx, re) | ||||
| 		} | ||||
|  | ||||
| 		if bs, err = io.ReadAll(reader); err != nil { | ||||
| 			return rerr.Error(ctx, &rerr.RepositoryError{ | ||||
| 				Status:  http.StatusInternalServerError, | ||||
| 				Code:    "INTERNAL_SERVER_ERROR", | ||||
| 				Message: err.Error(), | ||||
| 			}) | ||||
| 		} | ||||
|  | ||||
| 		h, _, _ := model.SHA256(bytes.NewReader(bs)) | ||||
| 		ctx.Set("Docker-Content-Digest", h.String()) | ||||
| 		ctx.Set("Content-Type", contentType) | ||||
| 		ctx.Set("Content-Length", fmt.Sprint(len(bs))) | ||||
|  | ||||
| 		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() | ||||
|  | ||||
| 		mf := model.Manifest{ | ||||
| 			Blob:        b.Bytes(), | ||||
| 			ContentType: ctx.Get("Content-Type"), | ||||
| 		} | ||||
|  | ||||
| 		logrus. | ||||
| 			WithField("handler", "handleManifest"). | ||||
| 			WithField("path", ctx.Path()). | ||||
| 			WithField("method", ctx.Method()). | ||||
| 			WithField("repo", repo). | ||||
| 			WithField("target", target). | ||||
| 			WithField("digest", digest). | ||||
| 			WithField("content-type", ctx.Get("Content-Type")). | ||||
| 			WithField("content", b.String()). | ||||
| 			Debug() | ||||
|  | ||||
| 		if err := m.Put(ctx.Request.Context(), repo, target, digest, &mf); err != nil { | ||||
| 			return rerr.Error(ctx, err) | ||||
| 		} | ||||
|  | ||||
| 		ctx.Set("Docker-Content-Digest", digest) | ||||
| 		return ctx.SendStatus(http.StatusCreated) | ||||
|  | ||||
| 	case http.MethodDelete: | ||||
| 		return ctx.SendStatus(http.StatusAccepted) | ||||
|  | ||||
| 	default: | ||||
| 		return rerr.Error(ctx, &rerr.RepositoryError{ | ||||
| 			Status:  http.StatusBadRequest, | ||||
| 			Code:    "METHOD_UNKNOWN", | ||||
| 			Message: "We don't understand your method + url", | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										57
									
								
								internal/handler/referrers.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								internal/handler/referrers.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,57 @@ | ||||
| package handler | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"github.com/loveuer/nf" | ||||
| 	"io" | ||||
| 	"net/http" | ||||
| 	"nf-repo/internal/interfaces" | ||||
| 	"nf-repo/internal/model" | ||||
| 	"nf-repo/internal/model/types" | ||||
| 	"nf-repo/internal/util/rerr" | ||||
| 	"strings" | ||||
| ) | ||||
|  | ||||
| func handleReferrers(ctx *nf.Ctx, m interfaces.ManifestHandler) error { | ||||
| 	if ctx.Method() != "GET" { | ||||
| 		return rerr.Error(ctx, &rerr.RepositoryError{ | ||||
| 			Status:  http.StatusBadRequest, | ||||
| 			Code:    "METHOD_UNKNOWN", | ||||
| 			Message: "We don't understand your method + url", | ||||
| 		}) | ||||
| 	} | ||||
|  | ||||
| 	elem := strings.Split(ctx.Path(), "/") | ||||
| 	elem = elem[1:] | ||||
| 	target := elem[len(elem)-1] | ||||
| 	repo := strings.Join(elem[1:len(elem)-2], "/") | ||||
|  | ||||
| 	var ( | ||||
| 		err error | ||||
| 		re  *rerr.RepositoryError | ||||
| 		im  *model.IndexManifest | ||||
| 	) | ||||
|  | ||||
| 	// Validate that incoming target is a valid digest | ||||
| 	if _, err := model.NewHash(target); err != nil { | ||||
| 		return rerr.Error(ctx, &rerr.RepositoryError{ | ||||
| 			Status:  http.StatusBadRequest, | ||||
| 			Code:    "UNSUPPORTED", | ||||
| 			Message: "Target must be a valid digest", | ||||
| 		}) | ||||
| 	} | ||||
|  | ||||
| 	if im, re = m.Referrers(ctx.Request.Context(), repo, target); re != nil { | ||||
| 		return rerr.Error(ctx, re) | ||||
| 	} | ||||
|  | ||||
| 	msg, _ := json.Marshal(im) | ||||
| 	ctx.Set("Content-Length", fmt.Sprint(len(msg))) | ||||
| 	ctx.Set("Content-Type", string(types.OCIImageIndex)) | ||||
| 	ctx.Status(http.StatusOK) | ||||
| 	_, err = io.Copy(ctx.RawWriter(), bytes.NewReader(msg)) | ||||
|  | ||||
| 	return err | ||||
| } | ||||
							
								
								
									
										46
									
								
								internal/handler/root.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								internal/handler/root.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,46 @@ | ||||
| package handler | ||||
|  | ||||
| import ( | ||||
| 	"github.com/loveuer/nf" | ||||
| 	"nf-repo/internal/interfaces" | ||||
| 	"nf-repo/internal/opt" | ||||
| ) | ||||
|  | ||||
| type blob struct { | ||||
| 	blobHandler   interfaces.BlobHandler | ||||
| 	uploadHandler interfaces.UploadHandler | ||||
| } | ||||
|  | ||||
| var ( | ||||
| 	b = &blob{} | ||||
| ) | ||||
|  | ||||
| func Root(bh interfaces.BlobHandler, uh interfaces.UploadHandler, mh interfaces.ManifestHandler) nf.HandlerFunc { | ||||
| 	b.blobHandler = bh | ||||
| 	b.uploadHandler = uh | ||||
| 	return func(c *nf.Ctx) error { | ||||
| 		if isBlob(c) { | ||||
| 			return handleBlobs(c) | ||||
| 		} | ||||
|  | ||||
| 		if isManifest(c) { | ||||
| 			return handleManifest(c, mh) | ||||
| 		} | ||||
|  | ||||
| 		if isTags(c) { | ||||
| 			return handleTags(c, mh) | ||||
| 		} | ||||
|  | ||||
| 		if isCatalog(c) { | ||||
| 			return handleCatalog(c, mh) | ||||
| 		} | ||||
|  | ||||
| 		if opt.ReferrersEnabled && isReferrers(c) { | ||||
| 			return handleReferrers(c, mh) | ||||
| 		} | ||||
|  | ||||
| 		c.Set("Docker-Distribution-API-Version", "registry/2.0") | ||||
|  | ||||
| 		return c.SendStatus(200) | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										50
									
								
								internal/handler/tags.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								internal/handler/tags.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,50 @@ | ||||
| package handler | ||||
|  | ||||
| import ( | ||||
| 	"github.com/loveuer/nf" | ||||
| 	"net/http" | ||||
| 	"nf-repo/internal/interfaces" | ||||
| 	"nf-repo/internal/model" | ||||
| 	"nf-repo/internal/util/rerr" | ||||
| 	"strings" | ||||
| ) | ||||
|  | ||||
| func handleTags(ctx *nf.Ctx, m interfaces.ManifestHandler) error { | ||||
| 	if ctx.Method() != "GET" { | ||||
| 		return rerr.Error(ctx, &rerr.RepositoryError{ | ||||
| 			Status:  http.StatusBadRequest, | ||||
| 			Code:    "METHOD_UNKNOWN", | ||||
| 			Message: "We don't understand your method + url", | ||||
| 		}) | ||||
| 	} | ||||
|  | ||||
| 	type Req struct { | ||||
| 		Last int `json:"last" query:"last"` | ||||
| 		N    int `json:"n" query:"n"` | ||||
| 	} | ||||
|  | ||||
| 	elem := strings.Split(ctx.Path(), "/") | ||||
| 	elem = elem[1:] | ||||
| 	repo := strings.Join(elem[1:len(elem)-2], "/") | ||||
|  | ||||
| 	var ( | ||||
| 		err  error | ||||
| 		req  = new(Req) | ||||
| 		re   *rerr.RepositoryError | ||||
| 		list *model.Tag | ||||
| 	) | ||||
|  | ||||
| 	if err = ctx.QueryParser(req); err != nil { | ||||
| 		return rerr.Error(ctx, &rerr.RepositoryError{ | ||||
| 			Status:  http.StatusBadRequest, | ||||
| 			Code:    "BAD_REQUEST", | ||||
| 			Message: err.Error(), | ||||
| 		}) | ||||
| 	} | ||||
|  | ||||
| 	if list, re = m.Tags(ctx.Request.Context(), repo, req.N, req.Last); err != nil { | ||||
| 		return rerr.Error(ctx, re) | ||||
| 	} | ||||
|  | ||||
| 	return ctx.JSON(list) | ||||
| } | ||||
							
								
								
									
										14
									
								
								internal/interfaces/blob.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								internal/interfaces/blob.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | ||||
| package interfaces | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"io" | ||||
| 	"nf-repo/internal/model" | ||||
| ) | ||||
|  | ||||
| type BlobHandler interface { | ||||
| 	Get(ctx context.Context, repo string, hash model.Hash) (io.ReadCloser, error) | ||||
| 	Stat(ctx context.Context, repo string, hash model.Hash) (int64, error) | ||||
| 	Put(ctx context.Context, repo string, hash model.Hash, rc io.ReadCloser) error | ||||
| 	Delete(ctx context.Context, repo string, hash model.Hash) error | ||||
| } | ||||
							
								
								
									
										113
									
								
								internal/interfaces/blobs/local.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								internal/interfaces/blobs/local.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,113 @@ | ||||
| package blobs | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"errors" | ||||
| 	"io" | ||||
| 	"nf-repo/internal/interfaces" | ||||
| 	"nf-repo/internal/model" | ||||
| 	"nf-repo/internal/opt" | ||||
| 	"os" | ||||
| 	"path" | ||||
| 	"sync" | ||||
| ) | ||||
|  | ||||
| type localHandler struct { | ||||
| 	base string | ||||
| 	sync.Mutex | ||||
| } | ||||
|  | ||||
| func (l *localHandler) path(hash model.Hash) string { | ||||
| 	//return path.Join(l.base, hash.Hex) | ||||
| 	dir := path.Join(l.base, hash.Hex[:2], hash.Hex[2:4]) | ||||
| 	_ = os.MkdirAll(dir, 0755) | ||||
| 	return path.Join(dir, hash.Hex) | ||||
| } | ||||
|  | ||||
| func (l *localHandler) Get(ctx context.Context, repo string, hash model.Hash) (io.ReadCloser, error) { | ||||
| 	var ( | ||||
| 		err error | ||||
| 		f   *os.File | ||||
| 	) | ||||
|  | ||||
| 	l.Lock() | ||||
| 	defer l.Unlock() | ||||
|  | ||||
| 	if f, err = os.Open(l.path(hash)); err != nil { | ||||
| 		if errors.Is(err, os.ErrNotExist) { | ||||
| 			return nil, opt.ErrNotFound | ||||
| 		} | ||||
|  | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return f, nil | ||||
| } | ||||
|  | ||||
| func (l *localHandler) Stat(ctx context.Context, repo string, hash model.Hash) (int64, error) { | ||||
| 	var ( | ||||
| 		err  error | ||||
| 		info os.FileInfo | ||||
| 	) | ||||
|  | ||||
| 	l.Lock() | ||||
| 	defer l.Unlock() | ||||
|  | ||||
| 	if info, err = os.Stat(l.path(hash)); err != nil { | ||||
| 		if errors.Is(err, os.ErrNotExist) { | ||||
| 			return 0, opt.ErrNotFound | ||||
| 		} | ||||
|  | ||||
| 		return 0, err | ||||
| 	} | ||||
|  | ||||
| 	return info.Size(), nil | ||||
| } | ||||
|  | ||||
| func (l *localHandler) Put(ctx context.Context, repo string, hash model.Hash, rc io.ReadCloser) error { | ||||
| 	var ( | ||||
| 		err error | ||||
| 		f   *os.File | ||||
| 	) | ||||
|  | ||||
| 	l.Lock() | ||||
| 	defer l.Unlock() | ||||
|  | ||||
| 	if f, err = os.OpenFile(l.path(hash), os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0644); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	if _, err = io.Copy(f, rc); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (l *localHandler) Delete(ctx context.Context, repo string, hash model.Hash) error { | ||||
| 	var ( | ||||
| 		err      error | ||||
| 		info     os.FileInfo | ||||
| 		filename = l.path(hash) | ||||
| 	) | ||||
|  | ||||
| 	l.Lock() | ||||
| 	defer l.Unlock() | ||||
|  | ||||
| 	if info, err = os.Stat(filename); err != nil { | ||||
| 		if errors.Is(err, os.ErrNotExist) { | ||||
| 			return opt.ErrNotFound | ||||
| 		} | ||||
|  | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	_ = info | ||||
|  | ||||
| 	return os.Remove(filename) | ||||
| } | ||||
|  | ||||
| func NewLocalBlobHandler(baseDir string) interfaces.BlobHandler { | ||||
| 	_ = os.MkdirAll(baseDir, 0755) | ||||
| 	return &localHandler{base: baseDir} | ||||
| } | ||||
							
								
								
									
										77
									
								
								internal/interfaces/blobs/mem.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								internal/interfaces/blobs/mem.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,77 @@ | ||||
| package blobs | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"context" | ||||
| 	"io" | ||||
| 	"nf-repo/internal/interfaces" | ||||
| 	"nf-repo/internal/model" | ||||
| 	"nf-repo/internal/opt" | ||||
| 	"sync" | ||||
| ) | ||||
|  | ||||
| type bytesCloser struct { | ||||
| 	*bytes.Reader | ||||
| } | ||||
|  | ||||
| func (r *bytesCloser) Close() error { | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| type memHandler struct { | ||||
| 	m    map[string][]byte | ||||
| 	lock sync.Mutex | ||||
| } | ||||
|  | ||||
| func NewMemBlobHandler() interfaces.BlobHandler { | ||||
| 	return &memHandler{ | ||||
| 		m: map[string][]byte{}, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (m *memHandler) Stat(_ context.Context, _ string, h model.Hash) (int64, error) { | ||||
| 	m.lock.Lock() | ||||
| 	defer m.lock.Unlock() | ||||
|  | ||||
| 	bs, found := m.m[h.String()] | ||||
| 	if !found { | ||||
| 		return 0, opt.ErrNotFound | ||||
| 	} | ||||
| 	return int64(len(bs)), nil | ||||
| } | ||||
|  | ||||
| func (m *memHandler) Get(_ context.Context, _ string, h model.Hash) (io.ReadCloser, error) { | ||||
| 	m.lock.Lock() | ||||
| 	defer m.lock.Unlock() | ||||
|  | ||||
| 	bs, found := m.m[h.String()] | ||||
| 	if !found { | ||||
| 		return nil, opt.ErrNotFound | ||||
| 	} | ||||
| 	return &bytesCloser{bytes.NewReader(bs)}, nil | ||||
| } | ||||
|  | ||||
| func (m *memHandler) Put(_ context.Context, _ string, h model.Hash, rc io.ReadCloser) error { | ||||
| 	m.lock.Lock() | ||||
| 	defer m.lock.Unlock() | ||||
|  | ||||
| 	defer rc.Close() | ||||
| 	all, err := io.ReadAll(rc) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	m.m[h.String()] = all | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (m *memHandler) Delete(_ context.Context, _ string, h model.Hash) error { | ||||
| 	m.lock.Lock() | ||||
| 	defer m.lock.Unlock() | ||||
|  | ||||
| 	if _, found := m.m[h.String()]; !found { | ||||
| 		return opt.ErrNotFound | ||||
| 	} | ||||
|  | ||||
| 	delete(m.m, h.String()) | ||||
| 	return nil | ||||
| } | ||||
							
								
								
									
										10
									
								
								internal/interfaces/db.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								internal/interfaces/db.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
| package interfaces | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"gorm.io/gorm" | ||||
| ) | ||||
|  | ||||
| type Database interface { | ||||
| 	TX(ctx context.Context) *gorm.DB | ||||
| } | ||||
							
								
								
									
										18
									
								
								internal/interfaces/manifest.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								internal/interfaces/manifest.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| package interfaces | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"io" | ||||
| 	"nf-repo/internal/model" | ||||
| 	"nf-repo/internal/util/rerr" | ||||
| ) | ||||
|  | ||||
| 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 | ||||
| 	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) | ||||
| 	Referrers(ctx context.Context, repo string, target string) (*model.IndexManifest, *rerr.RepositoryError) | ||||
| } | ||||
							
								
								
									
										234
									
								
								internal/interfaces/manifests/db.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										234
									
								
								internal/interfaces/manifests/db.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,234 @@ | ||||
| package manifests | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"context" | ||||
| 	"encoding/json" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"github.com/samber/lo" | ||||
| 	"github.com/sirupsen/logrus" | ||||
| 	"gorm.io/gorm" | ||||
| 	"io" | ||||
| 	"net/http" | ||||
| 	"nf-repo/internal/interfaces" | ||||
| 	"nf-repo/internal/model" | ||||
| 	"nf-repo/internal/util/rerr" | ||||
| 	"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 dbManifests struct { | ||||
| 	db interfaces.Database | ||||
| } | ||||
|  | ||||
| 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) | ||||
| 	) | ||||
|  | ||||
| 	if h, err = model.NewHash(target); err == nil { | ||||
| 		tx = tx.Where("digest", h.String()) | ||||
| 	} else { | ||||
| 		tx = tx.Where("repo", repo). | ||||
| 			Where("target", target) | ||||
| 	} | ||||
|  | ||||
| 	if err = tx. | ||||
| 		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), | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		return nil, "", rerr.ErrInternal(err) | ||||
| 	} | ||||
|  | ||||
| 	return io.NopCloser(bytes.NewReader(pm.Content)), pm.ContentType, nil | ||||
| } | ||||
|  | ||||
| func (m *dbManifests) Put(ctx context.Context, repo string, target string, digest string, mf *model.Manifest) *rerr.RepositoryError { | ||||
| 	var ( | ||||
| 		err error | ||||
| 		pm  = &PackageManifest{ | ||||
| 			Repo:        repo, | ||||
| 			Target:      target, | ||||
| 			Digest:      digest, | ||||
| 			ContentType: mf.ContentType, | ||||
| 			Content:     mf.Blob, | ||||
| 		} | ||||
| 	) | ||||
|  | ||||
| 	// todo on conflict | ||||
| 	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). | ||||
| 		Updates(map[string]any{ | ||||
| 			"repo":         repo, | ||||
| 			"target":       target, | ||||
| 			"digest":       digest, | ||||
| 			"content_type": mf.ContentType, | ||||
| 			"content":      mf.Blob, | ||||
| 		}). | ||||
| 		Error; err != nil { | ||||
| 		logrus. | ||||
| 			WithField("path", "dbManifests.Put.Updates"). | ||||
| 			WithField("err", err.Error()). | ||||
| 			Debug() | ||||
| 		return rerr.ErrInternal(err) | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (m *dbManifests) Delete(ctx context.Context, repo string, target string) *rerr.RepositoryError { | ||||
| 	var ( | ||||
| 		err error | ||||
| 	) | ||||
|  | ||||
| 	if err = m.db.TX(tools.Timeout(5)). | ||||
| 		Where("repo", repo). | ||||
| 		Where("target", target). | ||||
| 		Delete(&PackageManifest{}). | ||||
| 		Error; err != nil { | ||||
| 		return rerr.ErrInternal(err) | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (m *dbManifests) Catelog(ctx context.Context, limit int, last int) (*model.Catalog, *rerr.RepositoryError) { | ||||
| 	var ( | ||||
| 		err  error | ||||
| 		list = make([]*PackageManifest, 0) | ||||
| 	) | ||||
|  | ||||
| 	if err = m.db.TX(tools.Timeout(5)).Model(&PackageManifest{}). | ||||
| 		Order("updated_at"). | ||||
| 		Offset(last). | ||||
| 		Limit(limit). | ||||
| 		Find(&list). | ||||
| 		Error; err != nil { | ||||
| 		return nil, rerr.ErrInternal(err) | ||||
| 	} | ||||
|  | ||||
| 	return &model.Catalog{ | ||||
| 		Repos: lo.Map(list, func(item *PackageManifest, index int) string { | ||||
| 			return item.Repo | ||||
| 		}), | ||||
| 	}, nil | ||||
| } | ||||
|  | ||||
| func (m *dbManifests) Tags(ctx context.Context, repo string, limit, last int) (*model.Tag, *rerr.RepositoryError) { | ||||
| 	var ( | ||||
| 		err  error | ||||
| 		list = make([]*PackageManifest, 0) | ||||
| 	) | ||||
|  | ||||
| 	if err = m.db.TX(tools.Timeout(5)).Model(&PackageManifest{}). | ||||
| 		Where("repo", repo). | ||||
| 		Order("updated_at"). | ||||
| 		Offset(last). | ||||
| 		Limit(limit). | ||||
| 		Find(&list). | ||||
| 		Error; err != nil { | ||||
| 		return nil, rerr.ErrInternal(err) | ||||
| 	} | ||||
|  | ||||
| 	return &model.Tag{ | ||||
| 		Name: repo, | ||||
| 		Tags: lo.Map(list, func(item *PackageManifest, index int) string { | ||||
| 			return item.Target | ||||
| 		}), | ||||
| 	}, nil | ||||
| } | ||||
|  | ||||
| func (m *dbManifests) Referrers(ctx context.Context, repo string, target string) (*model.IndexManifest, *rerr.RepositoryError) { | ||||
| 	var ( | ||||
| 		err      error | ||||
| 		pm       = new(PackageManifest) | ||||
| 		manifest = &model.IndexManifest{} | ||||
| 		tx       = m.db.TX(tools.Timeout(5)).Model(pm) | ||||
| 	) | ||||
|  | ||||
| 	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 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), | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		logrus. | ||||
| 			WithField("path", "dbManifests.Referrers.Take"). | ||||
| 			WithField("repo", repo). | ||||
| 			WithField("target", target). | ||||
| 			WithField("err", err.Error()). | ||||
| 			Debug() | ||||
|  | ||||
| 		return nil, rerr.ErrInternal(err) | ||||
| 	} | ||||
|  | ||||
| 	if err = json.Unmarshal(pm.Content, manifest); err != nil { | ||||
| 		logrus. | ||||
| 			WithField("path", "dbManifests.Referrers.Unmarshal"). | ||||
| 			WithField("repo", repo). | ||||
| 			WithField("target", target). | ||||
| 			WithField("err", err.Error()). | ||||
| 			Debug() | ||||
| 		return nil, rerr.ErrInternal(err) | ||||
| 	} | ||||
|  | ||||
| 	return manifest, nil | ||||
| } | ||||
|  | ||||
| func NewManifestDBHandler(tx interfaces.Database) interfaces.ManifestHandler { | ||||
| 	var ( | ||||
| 		err error | ||||
| 	) | ||||
|  | ||||
| 	if err = tx.TX(tools.Timeout(5)).AutoMigrate(&PackageManifest{}); err != nil { | ||||
| 		logrus. | ||||
| 			WithField("path", "NewManifestDBHandler"). | ||||
| 			WithField("method", "AutoMigrate"). | ||||
| 			Panic(err) | ||||
| 	} | ||||
|  | ||||
| 	return &dbManifests{db: tx} | ||||
| } | ||||
							
								
								
									
										278
									
								
								internal/interfaces/manifests/mem.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										278
									
								
								internal/interfaces/manifests/mem.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,278 @@ | ||||
| package manifests | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"context" | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"github.com/sirupsen/logrus" | ||||
| 	"io" | ||||
| 	"net/http" | ||||
| 	"nf-repo/internal/interfaces" | ||||
| 	"nf-repo/internal/model" | ||||
| 	"nf-repo/internal/model/types" | ||||
| 	"nf-repo/internal/util/rerr" | ||||
| 	"sort" | ||||
| 	"strings" | ||||
| 	"sync" | ||||
| ) | ||||
|  | ||||
| type memManifest struct { | ||||
| 	sync.RWMutex | ||||
| 	m map[string]map[string]*model.Manifest | ||||
| } | ||||
|  | ||||
| func (m *memManifest) Referrers(ctx context.Context, repo string, target string) (*model.IndexManifest, *rerr.RepositoryError) { | ||||
| 	m.RLock() | ||||
| 	defer m.RUnlock() | ||||
|  | ||||
| 	digestToManifestMap, repoExists := m.m[repo] | ||||
| 	if !repoExists { | ||||
| 		return nil, &rerr.RepositoryError{ | ||||
| 			Status:  http.StatusNotFound, | ||||
| 			Code:    "NAME_UNKNOWN", | ||||
| 			Message: "Unknown name", | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	im := &model.IndexManifest{ | ||||
| 		SchemaVersion: 2, | ||||
| 		MediaType:     types.OCIImageIndex, | ||||
| 		Manifests:     []model.Descriptor{}, | ||||
| 	} | ||||
|  | ||||
| 	for digest, manifest := range digestToManifestMap { | ||||
| 		h, err := model.NewHash(digest) | ||||
| 		if err != nil { | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		var refPointer struct { | ||||
| 			Subject *model.Descriptor `json:"subject"` | ||||
| 		} | ||||
|  | ||||
| 		json.Unmarshal(manifest.Blob, &refPointer) | ||||
| 		if refPointer.Subject == nil { | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		referenceDigest := refPointer.Subject.Digest | ||||
| 		if referenceDigest.String() != target { | ||||
| 			continue | ||||
| 		} | ||||
| 		// At this point, we know the current digest references the target | ||||
| 		var imageAsArtifact struct { | ||||
| 			Config struct { | ||||
| 				MediaType string `json:"mediaType"` | ||||
| 			} `json:"config"` | ||||
| 		} | ||||
|  | ||||
| 		json.Unmarshal(manifest.Blob, &imageAsArtifact) | ||||
|  | ||||
| 		im.Manifests = append(im.Manifests, model.Descriptor{ | ||||
| 			MediaType:    types.MediaType(manifest.ContentType), | ||||
| 			Size:         int64(len(manifest.Blob)), | ||||
| 			Digest:       h, | ||||
| 			ArtifactType: imageAsArtifact.Config.MediaType, | ||||
| 		}) | ||||
|  | ||||
| 	} | ||||
|  | ||||
| 	return im, nil | ||||
| } | ||||
|  | ||||
| func (m *memManifest) Tags(ctx context.Context, repo string, limit int, last int) (*model.Tag, *rerr.RepositoryError) { | ||||
| 	m.RLock() | ||||
| 	defer m.RUnlock() | ||||
|  | ||||
| 	c, ok := m.m[repo] | ||||
| 	if !ok { | ||||
| 		return nil, &rerr.RepositoryError{ | ||||
| 			Status:  http.StatusNotFound, | ||||
| 			Code:    "NAME_UNKNOWN", | ||||
| 			Message: "Unknown name", | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	var tags []string | ||||
| 	for tag := range c { | ||||
| 		if !strings.Contains(tag, "sha256:") { | ||||
| 			tags = append(tags, tag) | ||||
| 		} | ||||
| 	} | ||||
| 	sort.Strings(tags) | ||||
|  | ||||
| 	// https://github.com/opencontainers/distribution-spec/blob/b505e9cc53ec499edbd9c1be32298388921bb705/detail.md#tags-paginated | ||||
| 	// Offset using last query parameter. | ||||
| 	//if last := ctx.Query("last"); last != "" { | ||||
| 	//	for i, t := range tags { | ||||
| 	//		if t > last { | ||||
| 	//			tags = tags[i:] | ||||
| 	//			break | ||||
| 	//		} | ||||
| 	//	} | ||||
| 	//} | ||||
| 	// | ||||
| 	//// Limit using n query parameter. | ||||
| 	//if ns := ctx.Query("n"); ns != "" { | ||||
| 	//	if n, err := strconv.Atoi(ns); err != nil { | ||||
| 	//		return rerr.Error(ctx, &rerr.RepositoryError{ | ||||
| 	//			Status:  http.StatusBadRequest, | ||||
| 	//			Code:    "BAD_REQUEST", | ||||
| 	//			Message: fmt.Sprintf("parsing n: %v", err), | ||||
| 	//		}) | ||||
| 	//	} else if n < len(tags) { | ||||
| 	//		tags = tags[:n] | ||||
| 	//	} | ||||
| 	//} | ||||
|  | ||||
| 	tagsToList := &model.Tag{ | ||||
| 		Name: repo, | ||||
| 		Tags: tags, | ||||
| 	} | ||||
|  | ||||
| 	return tagsToList, nil | ||||
| } | ||||
|  | ||||
| func (m *memManifest) Catelog(ctx context.Context, limit, last int) (*model.Catalog, *rerr.RepositoryError) { | ||||
| 	m.RLock() | ||||
| 	defer m.RUnlock() | ||||
|  | ||||
| 	var repos []string | ||||
| 	countRepos := 0 | ||||
|  | ||||
| 	// TODO: implement pagination | ||||
| 	for key := range m.m { | ||||
| 		if countRepos >= limit { | ||||
| 			break | ||||
| 		} | ||||
| 		countRepos++ | ||||
|  | ||||
| 		repos = append(repos, key) | ||||
| 	} | ||||
|  | ||||
| 	repositoriesToList := &model.Catalog{ | ||||
| 		Repos: 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. | ||||
| 	if types.MediaType(mf.ContentType).IsIndex() { | ||||
| 		if err := func() *rerr.RepositoryError { | ||||
| 			m.RLock() | ||||
| 			defer m.RUnlock() | ||||
|  | ||||
| 			im, err := model.ParseIndexManifest(bytes.NewReader(mf.Blob)) | ||||
| 			if err != nil { | ||||
| 				return &rerr.RepositoryError{ | ||||
| 					Status:  http.StatusBadRequest, | ||||
| 					Code:    "MANIFEST_INVALID", | ||||
| 					Message: err.Error(), | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			for _, desc := range im.Manifests { | ||||
| 				if !desc.MediaType.IsDistributable() { | ||||
| 					continue | ||||
| 				} | ||||
|  | ||||
| 				if desc.MediaType.IsIndex() || desc.MediaType.IsImage() { | ||||
| 					if _, found := m.m[repo][desc.Digest.String()]; !found { | ||||
| 						return &rerr.RepositoryError{ | ||||
| 							Status:  http.StatusNotFound, | ||||
| 							Code:    "MANIFEST_UNKNOWN", | ||||
| 							Message: fmt.Sprintf("Sub-manifest %q not found", desc.Digest), | ||||
| 						} | ||||
| 					} | ||||
| 				} else { | ||||
| 					// TODO: Probably want to do an existence check for blobs. | ||||
| 					logrus.Warnf("TODO: Check blobs for %q", desc.Digest) | ||||
| 				} | ||||
| 			} | ||||
| 			return nil | ||||
| 		}(); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	m.Lock() | ||||
| 	defer m.Unlock() | ||||
|  | ||||
| 	if _, ok := m.m[repo]; !ok { | ||||
| 		m.m[repo] = make(map[string]*model.Manifest, 2) | ||||
| 	} | ||||
|  | ||||
| 	// Allow future references by target (tag) and immutable digest. | ||||
| 	// See https://docs.docker.com/engine/reference/commandline/pull/#pull-an-image-by-digest-immutable-identifier. | ||||
| 	m.m[repo][digest] = mf | ||||
| 	m.m[repo][target] = mf | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (m *memManifest) Delete(ctx context.Context, repo string, target string) *rerr.RepositoryError { | ||||
| 	m.Lock() | ||||
| 	defer m.Unlock() | ||||
|  | ||||
| 	if _, ok := m.m[repo]; !ok { | ||||
| 		return &rerr.RepositoryError{ | ||||
| 			Status:  http.StatusNotFound, | ||||
| 			Code:    "NAME_UNKNOWN", | ||||
| 			Message: "Unknown name", | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	_, ok := m.m[repo][target] | ||||
| 	if !ok { | ||||
| 		return &rerr.RepositoryError{ | ||||
| 			Status:  http.StatusNotFound, | ||||
| 			Code:    "MANIFEST_UNKNOWN", | ||||
| 			Message: "Unknown manifest", | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	delete(m.m[repo], target) | ||||
|  | ||||
| 	if len(m.m[repo]) == 0 { | ||||
| 		delete(m.m, repo) | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (m *memManifest) Get(ctx context.Context, repo string, target string) (io.ReadCloser, string, *rerr.RepositoryError) { | ||||
| 	m.RLock() | ||||
| 	defer m.RUnlock() | ||||
|  | ||||
| 	c, ok := m.m[repo] | ||||
| 	if !ok { | ||||
| 		return nil, "", &rerr.RepositoryError{ | ||||
| 			Status:  http.StatusNotFound, | ||||
| 			Code:    "NAME_UNKNOWN", | ||||
| 			Message: "Unknown name", | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	f, ok := c[target] | ||||
| 	if !ok { | ||||
| 		return nil, "", &rerr.RepositoryError{ | ||||
| 			Status:  http.StatusNotFound, | ||||
| 			Code:    "MANIFEST_UNKNOWN", | ||||
| 			Message: "Unknown manifest", | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	reader := io.NopCloser(bytes.NewReader(f.Blob)) | ||||
|  | ||||
| 	return reader, f.ContentType, nil | ||||
| } | ||||
|  | ||||
| func NewManifestMemHandler() interfaces.ManifestHandler { | ||||
| 	return &memManifest{m: make(map[string]map[string]*model.Manifest)} | ||||
| } | ||||
							
								
								
									
										44
									
								
								internal/interfaces/tx/tx.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								internal/interfaces/tx/tx.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,44 @@ | ||||
| package tx | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"github.com/glebarez/sqlite" | ||||
| 	"github.com/sirupsen/logrus" | ||||
| 	"gorm.io/gorm" | ||||
| 	"nf-repo/internal/interfaces" | ||||
| ) | ||||
|  | ||||
| type tx struct { | ||||
| 	db *gorm.DB | ||||
| } | ||||
|  | ||||
| func (t *tx) TX(ctx context.Context) *gorm.DB { | ||||
| 	return t.db.Session(&gorm.Session{}).WithContext(ctx) | ||||
| } | ||||
|  | ||||
| func newTX(db *gorm.DB) interfaces.Database { | ||||
| 	return &tx{db: db} | ||||
| } | ||||
|  | ||||
| func Must(database interfaces.Database, err error) interfaces.Database { | ||||
| 	if err != nil { | ||||
| 		logrus. | ||||
| 			WithField("path", "tx.Must"). | ||||
| 			WithField("err", err.Error()). | ||||
| 			Panic() | ||||
| 	} | ||||
|  | ||||
| 	if database == nil { | ||||
| 		logrus. | ||||
| 			WithField("path", "tx.Must"). | ||||
| 			WithField("err", "database is nil"). | ||||
| 			Panic() | ||||
| 	} | ||||
|  | ||||
| 	return database | ||||
| } | ||||
|  | ||||
| func NewSqliteTX(filepath string) (interfaces.Database, error) { | ||||
| 	db, err := gorm.Open(sqlite.Open(filepath), &gorm.Config{}) | ||||
| 	return newTX(db), err | ||||
| } | ||||
							
								
								
									
										14
									
								
								internal/interfaces/uoload.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								internal/interfaces/uoload.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | ||||
| package interfaces | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"io" | ||||
| 	"nf-repo/internal/model" | ||||
| 	"nf-repo/internal/util/rerr" | ||||
| ) | ||||
|  | ||||
| type UploadHandler interface { | ||||
| 	UploadId() string | ||||
| 	Write(ctx context.Context, id string, reader io.ReadCloser, start, end int) (int, *rerr.RepositoryError) | ||||
| 	Done(ctx context.Context, bh BlobHandler, id string, closer io.ReadCloser, contentLength int, repo string, hash model.Hash) *rerr.RepositoryError | ||||
| } | ||||
							
								
								
									
										139
									
								
								internal/interfaces/uploads/local.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										139
									
								
								internal/interfaces/uploads/local.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,139 @@ | ||||
| package uploads | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"github.com/sirupsen/logrus" | ||||
| 	"io" | ||||
| 	"net/http" | ||||
| 	"nf-repo/internal/interfaces" | ||||
| 	"nf-repo/internal/model" | ||||
| 	"nf-repo/internal/util/rerr" | ||||
| 	"nf-repo/internal/verify" | ||||
| 	"os" | ||||
| 	"path" | ||||
| 	"sync" | ||||
| 	"time" | ||||
| ) | ||||
|  | ||||
| type localUploader struct { | ||||
| 	lock    sync.Mutex | ||||
| 	basedir string | ||||
| 	sizeMap map[string]int | ||||
| } | ||||
|  | ||||
| func (l *localUploader) UploadId() string { | ||||
| 	id := fmt.Sprintf("%d", time.Now().UnixNano()) | ||||
| 	l.lock.Lock() | ||||
| 	l.sizeMap[id] = 0 | ||||
| 	l.lock.Unlock() | ||||
|  | ||||
| 	return id | ||||
| } | ||||
|  | ||||
| func (l *localUploader) Write(ctx context.Context, id string, reader io.ReadCloser, start, end int) (int, *rerr.RepositoryError) { | ||||
| 	var ( | ||||
| 		err      error | ||||
| 		filename = path.Join(l.basedir, id) | ||||
| 		f        *os.File | ||||
| 		ok       bool | ||||
| 		flag     = os.O_CREATE | os.O_RDWR | os.O_TRUNC | ||||
| 		copied   int64 | ||||
| 	) | ||||
|  | ||||
| 	if _, ok = l.sizeMap[id]; !ok { | ||||
| 		return 0, &rerr.RepositoryError{ | ||||
| 			Status:  http.StatusRequestedRangeNotSatisfiable, | ||||
| 			Code:    "BLOB_UPLOAD_UNKNOWN", | ||||
| 			Message: "Your content range doesn't match what we have", | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if start > 0 { | ||||
| 		flag = os.O_APPEND | os.O_RDWR | ||||
|  | ||||
| 	} | ||||
|  | ||||
| 	if f, err = os.OpenFile(filename, flag, 0644); err != nil { | ||||
| 		return 0, rerr.ErrInternal(err) | ||||
| 	} | ||||
|  | ||||
| 	if copied, err = io.Copy(f, reader); err != nil { | ||||
| 		return 0, rerr.ErrInternal(err) | ||||
| 	} | ||||
|  | ||||
| 	reader.Close() | ||||
|  | ||||
| 	l.lock.Lock() | ||||
| 	l.sizeMap[id] += int(copied) | ||||
| 	l.lock.Unlock() | ||||
|  | ||||
| 	return l.sizeMap[id], nil | ||||
| } | ||||
|  | ||||
| func (l *localUploader) Done(ctx context.Context, bh interfaces.BlobHandler, id string, reader io.ReadCloser, contentLength int, repo string, hash model.Hash) *rerr.RepositoryError { | ||||
| 	size := verify.SizeUnknown | ||||
| 	if contentLength > 0 { | ||||
| 		size = l.sizeMap[id] + contentLength | ||||
| 	} | ||||
|  | ||||
| 	var ( | ||||
| 		err      error | ||||
| 		f        *os.File | ||||
| 		filename = path.Join(l.basedir, id) | ||||
| 	) | ||||
|  | ||||
| 	if f, err = os.OpenFile(filename, os.O_RDONLY, 0644); err != nil { | ||||
| 		return rerr.ErrInternal(err) | ||||
| 	} | ||||
|  | ||||
| 	in := io.NopCloser(io.MultiReader(f, reader)) | ||||
|  | ||||
| 	vrc, err := verify.ReadCloser(in, int64(size), hash) | ||||
| 	if err != nil { | ||||
| 		return rerr.ErrInternal(err) | ||||
| 	} | ||||
| 	defer vrc.Close() | ||||
|  | ||||
| 	if err := bh.Put(ctx, repo, hash, vrc); err != nil { | ||||
| 		if errors.As(err, &verify.Error{}) { | ||||
| 			logrus. | ||||
| 				WithField("path", "handleBlobs.Put"). | ||||
| 				WithField("repo", repo). | ||||
| 				WithField("hash", hash.String()). | ||||
| 				WithField("err", fmt.Sprintf("Digest mismatch: %v", err)).Debug() | ||||
| 			return rerr.ErrDigestMismatch | ||||
| 		} | ||||
| 		return rerr.ErrInternal(err) | ||||
| 	} | ||||
|  | ||||
| 	delete(l.sizeMap, id) | ||||
| 	f.Close() | ||||
|  | ||||
| 	if err = os.Remove(filename); err != nil { | ||||
| 		logrus. | ||||
| 			WithField("path", "localUploader.Done.Remove"). | ||||
| 			WithField("filename", filename). | ||||
| 			WithField("err", err.Error()). | ||||
| 			Warn() | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func NewLocalUploader(basedir string) interfaces.UploadHandler { | ||||
| 	var ( | ||||
| 		err error | ||||
| 	) | ||||
|  | ||||
| 	if err = os.MkdirAll(basedir, 0755); err != nil { | ||||
| 		logrus. | ||||
| 			WithField("path", "uploads.localUploader.NewLocalUploader"). | ||||
| 			WithField("basedir", basedir). | ||||
| 			WithField("err", err.Error()). | ||||
| 			Panic() | ||||
| 	} | ||||
|  | ||||
| 	return &localUploader{lock: sync.Mutex{}, basedir: basedir, sizeMap: make(map[string]int)} | ||||
| } | ||||
							
								
								
									
										101
									
								
								internal/interfaces/uploads/mem.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										101
									
								
								internal/interfaces/uploads/mem.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,101 @@ | ||||
| package uploads | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"context" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"github.com/sirupsen/logrus" | ||||
| 	"io" | ||||
| 	"net/http" | ||||
| 	"nf-repo/internal/interfaces" | ||||
| 	"nf-repo/internal/model" | ||||
| 	"nf-repo/internal/util/rerr" | ||||
| 	"nf-repo/internal/verify" | ||||
| 	"strconv" | ||||
| 	"sync" | ||||
| 	"time" | ||||
| ) | ||||
|  | ||||
| type memUploader struct { | ||||
| 	lock    sync.Mutex | ||||
| 	uploads map[string][]byte | ||||
| } | ||||
|  | ||||
| func (m *memUploader) UploadId() string { | ||||
| 	m.lock.Lock() | ||||
| 	m.lock.Unlock() | ||||
|  | ||||
| 	id := strconv.Itoa(int(time.Now().UnixNano())) | ||||
|  | ||||
| 	m.uploads[id] = []byte{} | ||||
|  | ||||
| 	return id | ||||
| } | ||||
|  | ||||
| func (m *memUploader) Write(ctx context.Context, id string, reader io.ReadCloser, start, end int) (int, *rerr.RepositoryError) { | ||||
| 	m.lock.Lock() | ||||
| 	defer m.lock.Unlock() | ||||
|  | ||||
| 	if start != len(m.uploads[id]) { | ||||
| 		return 0, &rerr.RepositoryError{ | ||||
| 			Status:  http.StatusRequestedRangeNotSatisfiable, | ||||
| 			Code:    "BLOB_UPLOAD_UNKNOWN", | ||||
| 			Message: "Your content range doesn't match what we have", | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	l := bytes.NewBuffer(m.uploads[id]) | ||||
| 	size, err := io.Copy(l, reader) | ||||
| 	if err != nil { | ||||
| 		return 0, rerr.ErrInternal(err) | ||||
| 	} | ||||
|  | ||||
| 	_ = size | ||||
|  | ||||
| 	m.uploads[id] = l.Bytes() | ||||
|  | ||||
| 	return len(m.uploads[id]), nil | ||||
| } | ||||
|  | ||||
| func (m *memUploader) Done(ctx context.Context, bh interfaces.BlobHandler, id string, reader io.ReadCloser, contentLength int, repo string, hash model.Hash) *rerr.RepositoryError { | ||||
| 	size := verify.SizeUnknown | ||||
| 	if contentLength > 0 { | ||||
| 		size = len(m.uploads[id]) + contentLength | ||||
| 	} | ||||
|  | ||||
| 	m.lock.Lock() | ||||
| 	defer m.lock.Unlock() | ||||
|  | ||||
| 	in := io.NopCloser(io.MultiReader(bytes.NewBuffer(m.uploads[id]), reader)) | ||||
|  | ||||
| 	vrc, err := verify.ReadCloser(in, int64(size), hash) | ||||
| 	if err != nil { | ||||
| 		return rerr.ErrInternal(err) | ||||
| 	} | ||||
| 	defer vrc.Close() | ||||
|  | ||||
| 	if err := bh.Put(ctx, repo, hash, vrc); err != nil { | ||||
| 		if errors.As(err, &verify.Error{}) { | ||||
| 			logrus. | ||||
| 				WithField("path", "handleBlobs.Put"). | ||||
| 				WithField("repo", repo). | ||||
| 				WithField("hash", hash.String()). | ||||
| 				WithField("err", fmt.Sprintf("Digest mismatch: %v", err)).Debug() | ||||
| 			return rerr.ErrDigestMismatch | ||||
| 		} | ||||
| 		return rerr.ErrInternal(err) | ||||
| 	} | ||||
|  | ||||
| 	m.uploads[id] = nil | ||||
| 	delete(m.uploads, id) | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func NewMemUploader() interfaces.UploadHandler { | ||||
| 	return &memUploader{ | ||||
| 		lock:    sync.Mutex{}, | ||||
| 		uploads: make(map[string][]byte), | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										5
									
								
								internal/model/catalog.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								internal/model/catalog.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| package model | ||||
|  | ||||
| type Catalog struct { | ||||
| 	Repos []string `json:"repositories"` | ||||
| } | ||||
							
								
								
									
										9
									
								
								internal/model/err.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								internal/model/err.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| package model | ||||
|  | ||||
| type RedirectError struct { | ||||
| 	// Location is the location to find the contents. | ||||
| 	Location string | ||||
|  | ||||
| 	// Code is the HTTP redirect status code to return to clients. | ||||
| 	Code int | ||||
| } | ||||
							
								
								
									
										76
									
								
								internal/model/hash.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								internal/model/hash.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,76 @@ | ||||
| package model | ||||
|  | ||||
| import ( | ||||
| 	"crypto" | ||||
| 	"encoding/hex" | ||||
| 	"fmt" | ||||
| 	"hash" | ||||
| 	"io" | ||||
| 	"strings" | ||||
| ) | ||||
|  | ||||
| func NewHash(s string) (Hash, error) { | ||||
| 	h := Hash{} | ||||
| 	if err := h.parse(s); err != nil { | ||||
| 		return Hash{}, err | ||||
| 	} | ||||
| 	return h, nil | ||||
| } | ||||
|  | ||||
| type Hash struct { | ||||
| 	// Algorithm holds the algorithm used to compute the hash. | ||||
| 	Algorithm string | ||||
|  | ||||
| 	// Hex holds the hex portion of the content hash. | ||||
| 	Hex string | ||||
| } | ||||
|  | ||||
| func (h *Hash) parse(unquoted string) error { | ||||
| 	parts := strings.Split(unquoted, ":") | ||||
| 	if len(parts) != 2 { | ||||
| 		return fmt.Errorf("cannot parse hash: %q", unquoted) | ||||
| 	} | ||||
|  | ||||
| 	rest := strings.TrimLeft(parts[1], "0123456789abcdef") | ||||
| 	if len(rest) != 0 { | ||||
| 		return fmt.Errorf("found non-hex character in hash: %c", rest[0]) | ||||
| 	} | ||||
|  | ||||
| 	hasher, err := Hasher(parts[0]) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	// Compare the hex to the expected size (2 hex characters per byte) | ||||
| 	if len(parts[1]) != hasher.Size()*2 { | ||||
| 		return fmt.Errorf("wrong number of hex digits for %s: %s", parts[0], parts[1]) | ||||
| 	} | ||||
|  | ||||
| 	h.Algorithm = parts[0] | ||||
| 	h.Hex = parts[1] | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (h Hash) String() string { | ||||
| 	return fmt.Sprintf("%s:%s", h.Algorithm, h.Hex) | ||||
| } | ||||
|  | ||||
| func Hasher(name string) (hash.Hash, error) { | ||||
| 	switch name { | ||||
| 	case "sha256": | ||||
| 		return crypto.SHA256.New(), nil | ||||
| 	default: | ||||
| 		return nil, fmt.Errorf("unsupported hash: %q", name) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func SHA256(r io.Reader) (Hash, int64, error) { | ||||
| 	hasher := crypto.SHA256.New() | ||||
| 	n, err := io.Copy(hasher, r) | ||||
| 	if err != nil { | ||||
| 		return Hash{}, 0, err | ||||
| 	} | ||||
| 	return Hash{ | ||||
| 		Algorithm: "sha256", | ||||
| 		Hex:       hex.EncodeToString(hasher.Sum(make([]byte, 0, hasher.Size()))), | ||||
| 	}, n, nil | ||||
| } | ||||
							
								
								
									
										39
									
								
								internal/model/manifest.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								internal/model/manifest.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,39 @@ | ||||
| package model | ||||
|  | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"io" | ||||
| 	"nf-repo/internal/model/types" | ||||
| ) | ||||
|  | ||||
| type Manifest struct { | ||||
| 	Blob        []byte `json:"blob"` | ||||
| 	ContentType string `json:"content_type"` | ||||
| } | ||||
|  | ||||
| type Descriptor struct { | ||||
| 	MediaType    types.MediaType   `json:"mediaType"` | ||||
| 	Size         int64             `json:"size"` | ||||
| 	Digest       Hash              `json:"digest"` | ||||
| 	Data         []byte            `json:"data,omitempty"` | ||||
| 	URLs         []string          `json:"urls,omitempty"` | ||||
| 	Annotations  map[string]string `json:"annotations,omitempty"` | ||||
| 	Platform     *Platform         `json:"platform,omitempty"` | ||||
| 	ArtifactType string            `json:"artifactType,omitempty"` | ||||
| } | ||||
|  | ||||
| type IndexManifest struct { | ||||
| 	SchemaVersion int64             `json:"schemaVersion"` | ||||
| 	MediaType     types.MediaType   `json:"mediaType,omitempty"` | ||||
| 	Manifests     []Descriptor      `json:"manifests"` | ||||
| 	Annotations   map[string]string `json:"annotations,omitempty"` | ||||
| 	Subject       *Descriptor       `json:"subject,omitempty"` | ||||
| } | ||||
|  | ||||
| func ParseIndexManifest(r io.Reader) (*IndexManifest, error) { | ||||
| 	im := IndexManifest{} | ||||
| 	if err := json.NewDecoder(r).Decode(&im); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return &im, nil | ||||
| } | ||||
							
								
								
									
										135
									
								
								internal/model/platform.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										135
									
								
								internal/model/platform.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,135 @@ | ||||
| package model | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"sort" | ||||
| 	"strings" | ||||
| ) | ||||
|  | ||||
| // Platform represents the target os/arch for an image. | ||||
| type Platform struct { | ||||
| 	Architecture string   `json:"architecture"` | ||||
| 	OS           string   `json:"os"` | ||||
| 	OSVersion    string   `json:"os.version,omitempty"` | ||||
| 	OSFeatures   []string `json:"os.features,omitempty"` | ||||
| 	Variant      string   `json:"variant,omitempty"` | ||||
| 	Features     []string `json:"features,omitempty"` | ||||
| } | ||||
|  | ||||
| func (p Platform) String() string { | ||||
| 	if p.OS == "" { | ||||
| 		return "" | ||||
| 	} | ||||
| 	var b strings.Builder | ||||
| 	b.WriteString(p.OS) | ||||
| 	if p.Architecture != "" { | ||||
| 		b.WriteString("/") | ||||
| 		b.WriteString(p.Architecture) | ||||
| 	} | ||||
| 	if p.Variant != "" { | ||||
| 		b.WriteString("/") | ||||
| 		b.WriteString(p.Variant) | ||||
| 	} | ||||
| 	if p.OSVersion != "" { | ||||
| 		b.WriteString(":") | ||||
| 		b.WriteString(p.OSVersion) | ||||
| 	} | ||||
| 	return b.String() | ||||
| } | ||||
|  | ||||
| // ParsePlatform parses a string representing a Platform, if possible. | ||||
| func ParsePlatform(s string) (*Platform, error) { | ||||
| 	var p Platform | ||||
| 	parts := strings.Split(strings.TrimSpace(s), ":") | ||||
| 	if len(parts) == 2 { | ||||
| 		p.OSVersion = parts[1] | ||||
| 	} | ||||
| 	parts = strings.Split(parts[0], "/") | ||||
| 	if len(parts) > 0 { | ||||
| 		p.OS = parts[0] | ||||
| 	} | ||||
| 	if len(parts) > 1 { | ||||
| 		p.Architecture = parts[1] | ||||
| 	} | ||||
| 	if len(parts) > 2 { | ||||
| 		p.Variant = parts[2] | ||||
| 	} | ||||
| 	if len(parts) > 3 { | ||||
| 		return nil, fmt.Errorf("too many slashes in platform spec: %s", s) | ||||
| 	} | ||||
| 	return &p, nil | ||||
| } | ||||
|  | ||||
| // Equals returns true if the given platform is semantically equivalent to this one. | ||||
| // The order of Features and OSFeatures is not important. | ||||
| func (p Platform) Equals(o Platform) bool { | ||||
| 	return p.OS == o.OS && | ||||
| 		p.Architecture == o.Architecture && | ||||
| 		p.Variant == o.Variant && | ||||
| 		p.OSVersion == o.OSVersion && | ||||
| 		stringSliceEqualIgnoreOrder(p.OSFeatures, o.OSFeatures) && | ||||
| 		stringSliceEqualIgnoreOrder(p.Features, o.Features) | ||||
| } | ||||
|  | ||||
| // Satisfies returns true if this Platform "satisfies" the given spec Platform. | ||||
| // | ||||
| // Note that this is different from Equals and that Satisfies is not reflexive. | ||||
| // | ||||
| // The given spec represents "requirements" such that any missing values in the | ||||
| // spec are not compared. | ||||
| // | ||||
| // For OSFeatures and Features, Satisfies will return true if this Platform's | ||||
| // fields contain a superset of the values in the spec's fields (order ignored). | ||||
| func (p Platform) Satisfies(spec Platform) bool { | ||||
| 	return satisfies(spec.OS, p.OS) && | ||||
| 		satisfies(spec.Architecture, p.Architecture) && | ||||
| 		satisfies(spec.Variant, p.Variant) && | ||||
| 		satisfies(spec.OSVersion, p.OSVersion) && | ||||
| 		satisfiesList(spec.OSFeatures, p.OSFeatures) && | ||||
| 		satisfiesList(spec.Features, p.Features) | ||||
| } | ||||
|  | ||||
| func satisfies(want, have string) bool { | ||||
| 	return want == "" || want == have | ||||
| } | ||||
|  | ||||
| func satisfiesList(want, have []string) bool { | ||||
| 	if len(want) == 0 { | ||||
| 		return true | ||||
| 	} | ||||
|  | ||||
| 	set := map[string]struct{}{} | ||||
| 	for _, h := range have { | ||||
| 		set[h] = struct{}{} | ||||
| 	} | ||||
|  | ||||
| 	for _, w := range want { | ||||
| 		if _, ok := set[w]; !ok { | ||||
| 			return false | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return true | ||||
| } | ||||
|  | ||||
| // stringSliceEqual compares 2 string slices and returns if their contents are identical. | ||||
| func stringSliceEqual(a, b []string) bool { | ||||
| 	if len(a) != len(b) { | ||||
| 		return false | ||||
| 	} | ||||
| 	for i, elm := range a { | ||||
| 		if elm != b[i] { | ||||
| 			return false | ||||
| 		} | ||||
| 	} | ||||
| 	return true | ||||
| } | ||||
|  | ||||
| // stringSliceEqualIgnoreOrder compares 2 string slices and returns if their contents are identical, ignoring order | ||||
| func stringSliceEqualIgnoreOrder(a, b []string) bool { | ||||
| 	if a != nil && b != nil { | ||||
| 		sort.Strings(a) | ||||
| 		sort.Strings(b) | ||||
| 	} | ||||
| 	return stringSliceEqual(a, b) | ||||
| } | ||||
							
								
								
									
										6
									
								
								internal/model/tag.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								internal/model/tag.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| package model | ||||
|  | ||||
| type Tag struct { | ||||
| 	Name string   `json:"name"` | ||||
| 	Tags []string `json:"tags"` | ||||
| } | ||||
							
								
								
									
										98
									
								
								internal/model/types/types.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										98
									
								
								internal/model/types/types.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,98 @@ | ||||
| // Copyright 2018 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 types holds common OCI media types. | ||||
| package types | ||||
|  | ||||
| // MediaType is an enumeration of the supported mime types that an element of an image might have. | ||||
| type MediaType string | ||||
|  | ||||
| // The collection of known MediaType values. | ||||
| const ( | ||||
| 	OCIContentDescriptor           MediaType = "application/vnd.oci.descriptor.v1+json" | ||||
| 	OCIImageIndex                  MediaType = "application/vnd.oci.image.index.v1+json" | ||||
| 	OCIManifestSchema1             MediaType = "application/vnd.oci.image.manifest.v1+json" | ||||
| 	OCIConfigJSON                  MediaType = "application/vnd.oci.image.config.v1+json" | ||||
| 	OCILayer                       MediaType = "application/vnd.oci.image.layer.v1.tar+gzip" | ||||
| 	OCILayerZStd                   MediaType = "application/vnd.oci.image.layer.v1.tar+zstd" | ||||
| 	OCIRestrictedLayer             MediaType = "application/vnd.oci.image.layer.nondistributable.v1.tar+gzip" | ||||
| 	OCIUncompressedLayer           MediaType = "application/vnd.oci.image.layer.v1.tar" | ||||
| 	OCIUncompressedRestrictedLayer MediaType = "application/vnd.oci.image.layer.nondistributable.v1.tar" | ||||
|  | ||||
| 	DockerManifestSchema1       MediaType = "application/vnd.docker.distribution.manifest.v1+json" | ||||
| 	DockerManifestSchema1Signed MediaType = "application/vnd.docker.distribution.manifest.v1+prettyjws" | ||||
| 	DockerManifestSchema2       MediaType = "application/vnd.docker.distribution.manifest.v2+json" | ||||
| 	DockerManifestList          MediaType = "application/vnd.docker.distribution.manifest.list.v2+json" | ||||
| 	DockerLayer                 MediaType = "application/vnd.docker.image.rootfs.diff.tar.gzip" | ||||
| 	DockerConfigJSON            MediaType = "application/vnd.docker.container.image.v1+json" | ||||
| 	DockerPluginConfig          MediaType = "application/vnd.docker.plugin.v1+json" | ||||
| 	DockerForeignLayer          MediaType = "application/vnd.docker.image.rootfs.foreign.diff.tar.gzip" | ||||
| 	DockerUncompressedLayer     MediaType = "application/vnd.docker.image.rootfs.diff.tar" | ||||
|  | ||||
| 	OCIVendorPrefix    = "vnd.oci" | ||||
| 	DockerVendorPrefix = "vnd.docker" | ||||
| ) | ||||
|  | ||||
| // IsDistributable returns true if a layer is distributable, see: | ||||
| // https://github.com/opencontainers/image-spec/blob/master/layer.md#non-distributable-layers | ||||
| func (m MediaType) IsDistributable() bool { | ||||
| 	switch m { | ||||
| 	case DockerForeignLayer, OCIRestrictedLayer, OCIUncompressedRestrictedLayer: | ||||
| 		return false | ||||
| 	} | ||||
| 	return true | ||||
| } | ||||
|  | ||||
| // IsImage returns true if the mediaType represents an image manifest, as opposed to something else, like an index. | ||||
| func (m MediaType) IsImage() bool { | ||||
| 	switch m { | ||||
| 	case OCIManifestSchema1, DockerManifestSchema2: | ||||
| 		return true | ||||
| 	} | ||||
| 	return false | ||||
| } | ||||
|  | ||||
| // IsIndex returns true if the mediaType represents an index, as opposed to something else, like an image. | ||||
| func (m MediaType) IsIndex() bool { | ||||
| 	switch m { | ||||
| 	case OCIImageIndex, DockerManifestList: | ||||
| 		return true | ||||
| 	} | ||||
| 	return false | ||||
| } | ||||
|  | ||||
| // IsConfig returns true if the mediaType represents a config, as opposed to something else, like an image. | ||||
| func (m MediaType) IsConfig() bool { | ||||
| 	switch m { | ||||
| 	case OCIConfigJSON, DockerConfigJSON: | ||||
| 		return true | ||||
| 	} | ||||
| 	return false | ||||
| } | ||||
|  | ||||
| func (m MediaType) IsSchema1() bool { | ||||
| 	switch m { | ||||
| 	case DockerManifestSchema1, DockerManifestSchema1Signed: | ||||
| 		return true | ||||
| 	} | ||||
| 	return false | ||||
| } | ||||
|  | ||||
| func (m MediaType) IsLayer() bool { | ||||
| 	switch m { | ||||
| 	case DockerLayer, DockerUncompressedLayer, OCILayer, OCILayerZStd, OCIUncompressedLayer, DockerForeignLayer, OCIRestrictedLayer, OCIUncompressedRestrictedLayer: | ||||
| 		return true | ||||
| 	} | ||||
| 	return false | ||||
| } | ||||
							
								
								
									
										12
									
								
								internal/opt/var.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								internal/opt/var.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | ||||
| package opt | ||||
|  | ||||
| import "errors" | ||||
|  | ||||
| const ( | ||||
| 	DefaultMaxSize   = 32 * 1024 * 1024 | ||||
| 	ReferrersEnabled = true | ||||
| ) | ||||
|  | ||||
| var ( | ||||
| 	ErrNotFound = errors.New("not found") | ||||
| ) | ||||
							
								
								
									
										55
									
								
								internal/util/rerr/rerr.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								internal/util/rerr/rerr.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,55 @@ | ||||
| package rerr | ||||
|  | ||||
| import ( | ||||
| 	"github.com/loveuer/nf" | ||||
| 	"net/http" | ||||
| ) | ||||
|  | ||||
| type RepositoryError struct { | ||||
| 	Status  int | ||||
| 	Code    string | ||||
| 	Message string | ||||
| } | ||||
|  | ||||
| func Error(c *nf.Ctx, err *RepositoryError) error { | ||||
| 	return c.Status(err.Status).JSON(nf.Map{ | ||||
| 		"errors": []nf.Map{ | ||||
| 			{ | ||||
| 				"code":    err.Code, | ||||
| 				"message": err.Message, | ||||
| 			}, | ||||
| 		}, | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func ErrInternal(err error) *RepositoryError { | ||||
| 	return &RepositoryError{ | ||||
| 		Status:  http.StatusInternalServerError, | ||||
| 		Code:    "INTERNAL_SERVER_ERROR", | ||||
| 		Message: err.Error(), | ||||
| 	} | ||||
| } | ||||
|  | ||||
| var ErrBlobUnknown = &RepositoryError{ | ||||
| 	Status:  http.StatusNotFound, | ||||
| 	Code:    "BLOB_UNKNOWN", | ||||
| 	Message: "Unknown blob", | ||||
| } | ||||
|  | ||||
| var ErrUnsupported = &RepositoryError{ | ||||
| 	Status:  http.StatusMethodNotAllowed, | ||||
| 	Code:    "UNSUPPORTED", | ||||
| 	Message: "Unsupported operation", | ||||
| } | ||||
|  | ||||
| var ErrDigestMismatch = &RepositoryError{ | ||||
| 	Status:  http.StatusBadRequest, | ||||
| 	Code:    "DIGEST_INVALID", | ||||
| 	Message: "digest does not match contents", | ||||
| } | ||||
|  | ||||
| var ErrDigestInvalid = &RepositoryError{ | ||||
| 	Status:  http.StatusBadRequest, | ||||
| 	Code:    "NAME_INVALID", | ||||
| 	Message: "invalid digest", | ||||
| } | ||||
							
								
								
									
										22
									
								
								internal/util/tools/ctx.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								internal/util/tools/ctx.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | ||||
| package tools | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"time" | ||||
| ) | ||||
|  | ||||
| func Timeout(seconds ...int) (ctx context.Context) { | ||||
| 	var ( | ||||
| 		duration time.Duration | ||||
| 	) | ||||
|  | ||||
| 	if len(seconds) > 0 && seconds[0] > 0 { | ||||
| 		duration = time.Duration(seconds[0]) * time.Second | ||||
| 	} else { | ||||
| 		duration = time.Duration(30) * time.Second | ||||
| 	} | ||||
|  | ||||
| 	ctx, _ = context.WithTimeout(context.Background(), duration) | ||||
|  | ||||
| 	return | ||||
| } | ||||
							
								
								
									
										121
									
								
								internal/verify/verify.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										121
									
								
								internal/verify/verify.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,121 @@ | ||||
| // 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 provides a ReadCloser that verifies content matches the | ||||
| // expected hash values. | ||||
| package verify | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"encoding/hex" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"hash" | ||||
| 	"io" | ||||
| 	"nf-repo/internal/and" | ||||
| 	"nf-repo/internal/model" | ||||
| ) | ||||
|  | ||||
| // SizeUnknown is a sentinel value to indicate that the expected size is not known. | ||||
| const SizeUnknown = -1 | ||||
|  | ||||
| type verifyReader struct { | ||||
| 	inner             io.Reader | ||||
| 	hasher            hash.Hash | ||||
| 	expected          model.Hash | ||||
| 	gotSize, wantSize int64 | ||||
| } | ||||
|  | ||||
| // Error provides information about the failed hash verification. | ||||
| type Error struct { | ||||
| 	got     string | ||||
| 	want    model.Hash | ||||
| 	gotSize int64 | ||||
| } | ||||
|  | ||||
| func (v Error) Error() string { | ||||
| 	return fmt.Sprintf("error verifying %s checksum after reading %d bytes; got %q, want %q", | ||||
| 		v.want.Algorithm, v.gotSize, v.got, v.want) | ||||
| } | ||||
|  | ||||
| // Read implements io.Reader | ||||
| func (vc *verifyReader) Read(b []byte) (int, error) { | ||||
| 	n, err := vc.inner.Read(b) | ||||
| 	vc.gotSize += int64(n) | ||||
| 	if err == io.EOF { | ||||
| 		if vc.wantSize != SizeUnknown && vc.gotSize != vc.wantSize { | ||||
| 			return n, fmt.Errorf("error verifying size; got %d, want %d", vc.gotSize, vc.wantSize) | ||||
| 		} | ||||
| 		got := hex.EncodeToString(vc.hasher.Sum(nil)) | ||||
| 		if want := vc.expected.Hex; got != want { | ||||
| 			return n, Error{ | ||||
| 				got:     vc.expected.Algorithm + ":" + got, | ||||
| 				want:    vc.expected, | ||||
| 				gotSize: vc.gotSize, | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	return n, err | ||||
| } | ||||
|  | ||||
| // ReadCloser wraps the given io.ReadCloser to verify that its contents match | ||||
| // the provided v1.Hash before io.EOF is returned. | ||||
| // | ||||
| // The reader will only be read up to size bytes, to prevent resource | ||||
| // exhaustion. If EOF is returned before size bytes are read, an error is | ||||
| // returned. | ||||
| // | ||||
| // A size of SizeUnknown (-1) indicates disables size verification when the size | ||||
| // is unknown ahead of time. | ||||
| func ReadCloser(r io.ReadCloser, size int64, h model.Hash) (io.ReadCloser, error) { | ||||
| 	w, err := model.Hasher(h.Algorithm) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	r2 := io.TeeReader(r, w) // pass all writes to the hasher. | ||||
| 	if size != SizeUnknown { | ||||
| 		r2 = io.LimitReader(r2, size) // if we know the size, limit to that size. | ||||
| 	} | ||||
| 	return &and.ReadCloser{ | ||||
| 		Reader: &verifyReader{ | ||||
| 			inner:    r2, | ||||
| 			hasher:   w, | ||||
| 			expected: h, | ||||
| 			wantSize: size, | ||||
| 		}, | ||||
| 		CloseFunc: r.Close, | ||||
| 	}, nil | ||||
| } | ||||
|  | ||||
| // Descriptor verifies that the embedded Data field matches the Size and Digest | ||||
| // fields of the given v1.Descriptor, returning an error if the Data field is | ||||
| // missing or if it contains incorrect data. | ||||
| func Descriptor(d model.Descriptor) error { | ||||
| 	if d.Data == nil { | ||||
| 		return errors.New("error verifying descriptor; Data == nil") | ||||
| 	} | ||||
|  | ||||
| 	h, sz, err := model.SHA256(bytes.NewReader(d.Data)) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	if h != d.Digest { | ||||
| 		return fmt.Errorf("error verifying Digest; got %q, want %q", h, d.Digest) | ||||
| 	} | ||||
| 	if sz != d.Size { | ||||
| 		return fmt.Errorf("error verifying Size; got %d, want %d", sz, d.Size) | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
							
								
								
									
										147
									
								
								internal/verify/verify_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										147
									
								
								internal/verify/verify_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,147 @@ | ||||
| // 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" | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"strings" | ||||
| 	"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)) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("v1.SHA256(%s) = %v", s, err) | ||||
| 	} | ||||
| 	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)) | ||||
| 	if err != nil { | ||||
| 		t.Fatal("ReadCloser() =", err) | ||||
| 	} | ||||
| 	if b, err := io.ReadAll(verified); err == nil { | ||||
| 		t.Errorf("ReadAll() = %q; want verification error", string(b)) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| 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)) | ||||
| 	if err != nil { | ||||
| 		t.Fatal("ReadCloser() =", err) | ||||
| 	} | ||||
| 	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)) | ||||
| 	if err != nil { | ||||
| 		t.Fatal("ReadCloser() =", 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) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										57
									
								
								internal/x/x_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								internal/x/x_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,57 @@ | ||||
| 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 | ||||
| } | ||||
							
								
								
									
										42
									
								
								main.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								main.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,42 @@ | ||||
| // https://github.com/google/go-containerregistry | ||||
|  | ||||
| package main | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"crypto/tls" | ||||
| 	"github.com/sirupsen/logrus" | ||||
| 	"nf-repo/internal/api" | ||||
| 	"nf-repo/internal/interfaces/blobs" | ||||
| 	"nf-repo/internal/interfaces/manifests" | ||||
| 	"nf-repo/internal/interfaces/tx" | ||||
| 	"nf-repo/internal/interfaces/uploads" | ||||
| ) | ||||
|  | ||||
| var ( | ||||
| 	tlsCfg *tls.Config | ||||
| 	bh     = blobs.NewLocalBlobHandler("images/layers") | ||||
| 	//uh     = uploads.NewMemUploader() | ||||
| 	uh = uploads.NewLocalUploader("images/uploads") | ||||
| 	//mh     = manifests.NewManifestMemHandler() | ||||
| 	mh = manifests.NewManifestDBHandler(tx.Must(tx.NewSqliteTX("data.sqlite"))) | ||||
| ) | ||||
|  | ||||
| func init() { | ||||
| 	logrus.SetFormatter(&logrus.JSONFormatter{TimestampFormat: "06/01/02 15:04:05"}) | ||||
| 	logrus.SetLevel(logrus.InfoLevel) | ||||
| 	logrus.SetReportCaller(true) | ||||
|  | ||||
| 	crt, err := tls.LoadX509KeyPair("etc/repo.me.crt", "etc/repo.me.key") | ||||
| 	if err != nil { | ||||
| 		panic(err) | ||||
| 	} | ||||
|  | ||||
| 	tlsCfg = &tls.Config{Certificates: []tls.Certificate{crt}} | ||||
| } | ||||
|  | ||||
| func main() { | ||||
| 	app := api.NewApi(context.TODO(), bh, uh, mh) | ||||
|  | ||||
| 	logrus.Fatal(app.RunTLS(":443", tlsCfg)) | ||||
| } | ||||
		Reference in New Issue
	
	Block a user