wip: oci image management
This commit is contained in:
76
handler/blob.go
Normal file
76
handler/blob.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"gitea.loveuer.com/loveuer/cluster/controller"
|
||||
"gitea.loveuer.com/loveuer/cluster/internal/registry/storage"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
// GetBlob 获取 blob
|
||||
func GetBlob(store storage.Storage) fiber.Handler {
|
||||
return func(c fiber.Ctx) error {
|
||||
repo := c.Locals("repo_name")
|
||||
repoStr := ""
|
||||
if repo != nil {
|
||||
repoStr = repo.(string)
|
||||
}
|
||||
if repoStr == "" {
|
||||
repoStr = strings.TrimPrefix(c.Params("name"), "/")
|
||||
}
|
||||
digest := c.Locals("digest")
|
||||
digestStr := ""
|
||||
if digest != nil {
|
||||
digestStr = digest.(string)
|
||||
}
|
||||
|
||||
reader, size, err := controller.GetBlob(store, repoStr, digestStr)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
|
||||
"errors": []fiber.Map{
|
||||
{
|
||||
"code": "BLOB_UNKNOWN",
|
||||
"message": err.Error(),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
c.Set("Content-Type", "application/octet-stream")
|
||||
c.Set("Content-Length", strconv.FormatInt(size, 10))
|
||||
c.Set("Docker-Content-Digest", digestStr)
|
||||
return c.SendStream(reader, int(size))
|
||||
}
|
||||
}
|
||||
|
||||
// HeadBlob 检查 blob 是否存在
|
||||
func HeadBlob(store storage.Storage) fiber.Handler {
|
||||
return func(c fiber.Ctx) error {
|
||||
repo := c.Locals("repo_name")
|
||||
repoStr := ""
|
||||
if repo != nil {
|
||||
repoStr = repo.(string)
|
||||
}
|
||||
if repoStr == "" {
|
||||
repoStr = strings.TrimPrefix(c.Params("name"), "/")
|
||||
}
|
||||
digest := c.Locals("digest")
|
||||
digestStr := ""
|
||||
if digest != nil {
|
||||
digestStr = digest.(string)
|
||||
}
|
||||
|
||||
exists, size, err := controller.HeadBlob(store, repoStr, digestStr)
|
||||
if err != nil || !exists {
|
||||
return c.SendStatus(fiber.StatusNotFound)
|
||||
}
|
||||
|
||||
c.Set("Content-Length", strconv.FormatInt(size, 10))
|
||||
c.Set("Docker-Content-Digest", digestStr)
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
}
|
||||
}
|
||||
37
handler/blob_handler.go
Normal file
37
handler/blob_handler.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
|
||||
"gitea.loveuer.com/loveuer/cluster/internal/model"
|
||||
"gitea.loveuer.com/loveuer/cluster/internal/registry/storage"
|
||||
)
|
||||
|
||||
type blobHandlerImpl struct {
|
||||
store storage.Storage
|
||||
}
|
||||
|
||||
// NewBlobHandler creates a new blob handler
|
||||
func NewBlobHandler(store storage.Storage) BlobHandler {
|
||||
return &blobHandlerImpl{store: store}
|
||||
}
|
||||
|
||||
func (b *blobHandlerImpl) Get(ctx context.Context, repo string, h model.Hash) (io.ReadCloser, error) {
|
||||
reader, _, err := b.store.GetBlob(repo, h.String())
|
||||
return reader, err
|
||||
}
|
||||
|
||||
func (b *blobHandlerImpl) Put(ctx context.Context, repo string, h model.Hash, rc io.ReadCloser) error {
|
||||
defer rc.Close()
|
||||
return b.store.PutBlob(repo, h.String(), rc)
|
||||
}
|
||||
|
||||
func (b *blobHandlerImpl) Stat(ctx context.Context, repo string, h model.Hash) (int64, error) {
|
||||
return b.store.GetBlobSize(repo, h.String())
|
||||
}
|
||||
|
||||
func (b *blobHandlerImpl) Delete(ctx context.Context, repo string, h model.Hash) error {
|
||||
// TODO: implement delete
|
||||
return nil
|
||||
}
|
||||
8
handler/globals.go
Normal file
8
handler/globals.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package handler
|
||||
|
||||
// Global handlers
|
||||
var (
|
||||
blobHandler BlobHandler
|
||||
uploadHandler UploadHandler
|
||||
m ManifestHandler
|
||||
)
|
||||
34
handler/interfaces.go
Normal file
34
handler/interfaces.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
|
||||
"gitea.loveuer.com/loveuer/cluster/internal/rerr"
|
||||
"gitea.loveuer.com/loveuer/cluster/internal/model"
|
||||
)
|
||||
|
||||
// BlobHandler handles blob operations
|
||||
type BlobHandler interface {
|
||||
Get(ctx context.Context, repo string, h model.Hash) (io.ReadCloser, error)
|
||||
Put(ctx context.Context, repo string, h model.Hash, rc io.ReadCloser) error
|
||||
Stat(ctx context.Context, repo string, h model.Hash) (int64, error)
|
||||
Delete(ctx context.Context, repo string, h model.Hash) error
|
||||
}
|
||||
|
||||
// UploadHandler handles upload operations
|
||||
type UploadHandler interface {
|
||||
UploadId() string
|
||||
Write(ctx context.Context, sessionID string, r io.Reader, start, end int) (int, *rerr.RepositoryError)
|
||||
Done(ctx context.Context, blobHandler BlobHandler, sessionID string, r io.Reader, contentLength int, repo string, h model.Hash) *rerr.RepositoryError
|
||||
}
|
||||
|
||||
// ManifestHandler handles manifest operations
|
||||
type ManifestHandler interface {
|
||||
Get(ctx context.Context, repo, tag string) (io.ReadCloser, string, *rerr.RepositoryError)
|
||||
Put(ctx context.Context, repo, tag, digest string, mf *model.RepoSimpleManifest) error
|
||||
Delete(ctx context.Context, repo, tag string) error
|
||||
Tags(ctx context.Context, repo string, n, last int, prefix string) (*model.Tag, *rerr.RepositoryError)
|
||||
Catalog(ctx context.Context, n, last int, prefix string) (*model.Catalog, *rerr.RepositoryError)
|
||||
Referrers(ctx context.Context, repo, target string) (*model.IndexManifest, *rerr.RepositoryError)
|
||||
}
|
||||
153
handler/manifest.go
Normal file
153
handler/manifest.go
Normal file
@@ -0,0 +1,153 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"gitea.loveuer.com/loveuer/cluster/controller"
|
||||
"gitea.loveuer.com/loveuer/cluster/internal/registry/storage"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
// GetManifest 获取 manifest
|
||||
func GetManifest(store storage.Storage) fiber.Handler {
|
||||
return func(c fiber.Ctx) error {
|
||||
repo := c.Locals("repo_name")
|
||||
repoStr := ""
|
||||
if repo != nil {
|
||||
repoStr = repo.(string)
|
||||
}
|
||||
if repoStr == "" {
|
||||
repoStr = strings.TrimPrefix(c.Params("name"), "/")
|
||||
}
|
||||
reference := c.Locals("reference")
|
||||
referenceStr := ""
|
||||
if reference != nil {
|
||||
referenceStr = reference.(string)
|
||||
}
|
||||
|
||||
data, mediaType, digest, err := controller.GetManifest(store, repoStr, referenceStr)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
|
||||
"errors": []fiber.Map{
|
||||
{
|
||||
"code": "MANIFEST_UNKNOWN",
|
||||
"message": err.Error(),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
c.Set("Content-Type", mediaType)
|
||||
c.Set("Content-Length", strconv.FormatInt(int64(len(data)), 10))
|
||||
c.Set("Docker-Content-Digest", digest)
|
||||
return c.Send(data)
|
||||
}
|
||||
}
|
||||
|
||||
// PutManifest 推送 manifest
|
||||
func PutManifest(store storage.Storage) fiber.Handler {
|
||||
return func(c fiber.Ctx) error {
|
||||
repo := c.Locals("repo_name")
|
||||
repoStr := ""
|
||||
if repo != nil {
|
||||
repoStr = repo.(string)
|
||||
}
|
||||
if repoStr == "" {
|
||||
repoStr = c.Params("name")
|
||||
}
|
||||
reference := c.Locals("reference")
|
||||
referenceStr := ""
|
||||
if reference != nil {
|
||||
referenceStr = reference.(string)
|
||||
}
|
||||
|
||||
// 读取请求体
|
||||
data := c.Body()
|
||||
|
||||
// 获取 Content-Type
|
||||
mediaType := c.Get("Content-Type")
|
||||
if mediaType == "" {
|
||||
mediaType = "application/vnd.docker.distribution.manifest.v2+json"
|
||||
}
|
||||
|
||||
digest, err := controller.PutManifest(store, repoStr, referenceStr, data, mediaType)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||
"errors": []fiber.Map{
|
||||
{
|
||||
"code": "INTERNAL_ERROR",
|
||||
"message": err.Error(),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 返回 Location 和 Digest
|
||||
c.Set("Location", c.Path())
|
||||
c.Set("Docker-Content-Digest", digest)
|
||||
return c.SendStatus(fiber.StatusCreated)
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteManifest 删除 manifest
|
||||
func DeleteManifest(store storage.Storage) fiber.Handler {
|
||||
return func(c fiber.Ctx) error {
|
||||
repo := c.Locals("repo_name")
|
||||
repoStr := ""
|
||||
if repo != nil {
|
||||
repoStr = repo.(string)
|
||||
}
|
||||
if repoStr == "" {
|
||||
repoStr = c.Params("name")
|
||||
}
|
||||
reference := c.Locals("reference")
|
||||
referenceStr := ""
|
||||
if reference != nil {
|
||||
referenceStr = reference.(string)
|
||||
}
|
||||
|
||||
if err := controller.DeleteManifest(store, repoStr, referenceStr); err != nil {
|
||||
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
|
||||
"errors": []fiber.Map{
|
||||
{
|
||||
"code": "MANIFEST_UNKNOWN",
|
||||
"message": err.Error(),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return c.SendStatus(fiber.StatusAccepted)
|
||||
}
|
||||
}
|
||||
|
||||
// HeadManifest 检查 manifest 是否存在
|
||||
func HeadManifest(store storage.Storage) fiber.Handler {
|
||||
return func(c fiber.Ctx) error {
|
||||
repo := c.Locals("repo_name")
|
||||
repoStr := ""
|
||||
if repo != nil {
|
||||
repoStr = repo.(string)
|
||||
}
|
||||
if repoStr == "" {
|
||||
repoStr = strings.TrimPrefix(c.Params("name"), "/")
|
||||
}
|
||||
reference := c.Locals("reference")
|
||||
referenceStr := ""
|
||||
if reference != nil {
|
||||
referenceStr = reference.(string)
|
||||
}
|
||||
|
||||
exists, data, mediaType, digest, err := controller.HeadManifest(store, repoStr, referenceStr)
|
||||
if err != nil || !exists {
|
||||
return c.SendStatus(fiber.StatusNotFound)
|
||||
}
|
||||
|
||||
c.Set("Content-Type", mediaType)
|
||||
c.Set("Content-Length", strconv.FormatInt(int64(len(data)), 10))
|
||||
c.Set("Docker-Content-Digest", digest)
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
}
|
||||
}
|
||||
106
handler/manifest_handler.go
Normal file
106
handler/manifest_handler.go
Normal file
@@ -0,0 +1,106 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
|
||||
"gitea.loveuer.com/loveuer/cluster/internal/database"
|
||||
"gitea.loveuer.com/loveuer/cluster/internal/model"
|
||||
"gitea.loveuer.com/loveuer/cluster/internal/rerr"
|
||||
"gitea.loveuer.com/loveuer/cluster/internal/registry/storage"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type manifestHandlerImpl struct {
|
||||
store storage.Storage
|
||||
}
|
||||
|
||||
// NewManifestHandler creates a new manifest handler
|
||||
func NewManifestHandler(store storage.Storage) ManifestHandler {
|
||||
return &manifestHandlerImpl{store: store}
|
||||
}
|
||||
|
||||
func (m *manifestHandlerImpl) Get(ctx context.Context, repo, tag string) (io.ReadCloser, string, *rerr.RepositoryError) {
|
||||
data, mediaType, err := m.store.GetManifest(repo, tag)
|
||||
if err != nil {
|
||||
return nil, "", &rerr.RepositoryError{
|
||||
Status: 404,
|
||||
Code: "MANIFEST_UNKNOWN",
|
||||
Message: "manifest not found",
|
||||
}
|
||||
}
|
||||
return io.NopCloser(bytes.NewReader(data)), mediaType, nil
|
||||
}
|
||||
|
||||
func (m *manifestHandlerImpl) Put(ctx context.Context, repo, tag, digest string, mf *model.RepoSimpleManifest) error {
|
||||
return m.store.PutManifest(repo, tag, mf.Blob, mf.ContentType)
|
||||
}
|
||||
|
||||
func (m *manifestHandlerImpl) Delete(ctx context.Context, repo, tag string) error {
|
||||
return m.store.DeleteManifest(repo, tag)
|
||||
}
|
||||
|
||||
func (m *manifestHandlerImpl) Tags(ctx context.Context, repo string, n, last int, prefix string) (*model.Tag, *rerr.RepositoryError) {
|
||||
var repository model.RegistryRepository
|
||||
if err := database.DB.Where("name = ?", repo).First(&repository).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return &model.Tag{Name: repo, Tags: []string{}}, nil
|
||||
}
|
||||
return nil, rerr.ErrInternal(err)
|
||||
}
|
||||
|
||||
var tags []model.RegistryTag
|
||||
query := database.DB.Where("repository_id = ?", repository.ID)
|
||||
if prefix != "" {
|
||||
query = query.Where("name LIKE ?", prefix+"%")
|
||||
}
|
||||
if last > 0 {
|
||||
query = query.Where("id > ?", last)
|
||||
}
|
||||
query = query.Order("id ASC").Limit(n)
|
||||
|
||||
if err := query.Find(&tags).Error; err != nil {
|
||||
return nil, rerr.ErrInternal(err)
|
||||
}
|
||||
|
||||
tagNames := make([]string, len(tags))
|
||||
for i, tag := range tags {
|
||||
tagNames[i] = tag.Name
|
||||
}
|
||||
|
||||
return &model.Tag{Name: repo, Tags: tagNames}, nil
|
||||
}
|
||||
|
||||
func (m *manifestHandlerImpl) Catalog(ctx context.Context, n, last int, prefix string) (*model.Catalog, *rerr.RepositoryError) {
|
||||
var repositories []model.RegistryRepository
|
||||
query := database.DB
|
||||
if prefix != "" {
|
||||
query = query.Where("name LIKE ?", prefix+"%")
|
||||
}
|
||||
if last > 0 {
|
||||
query = query.Where("id > ?", last)
|
||||
}
|
||||
query = query.Order("id ASC").Limit(n)
|
||||
|
||||
if err := query.Find(&repositories).Error; err != nil {
|
||||
return nil, rerr.ErrInternal(err)
|
||||
}
|
||||
|
||||
repoNames := make([]string, len(repositories))
|
||||
for i, repo := range repositories {
|
||||
repoNames[i] = repo.Name
|
||||
}
|
||||
|
||||
return &model.Catalog{Repositories: repoNames}, nil
|
||||
}
|
||||
|
||||
func (m *manifestHandlerImpl) Referrers(ctx context.Context, repo, target string) (*model.IndexManifest, *rerr.RepositoryError) {
|
||||
// For now, return an empty index manifest
|
||||
return &model.IndexManifest{
|
||||
SchemaVersion: 2,
|
||||
MediaType: "application/vnd.oci.image.index.v1+json",
|
||||
Manifests: []model.IndexManifestEntry{},
|
||||
}, nil
|
||||
}
|
||||
113
handler/registry.go
Normal file
113
handler/registry.go
Normal file
@@ -0,0 +1,113 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"gitea.loveuer.com/loveuer/cluster/controller"
|
||||
"gitea.loveuer.com/loveuer/cluster/internal/registry/storage"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
func isBlob(c fiber.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 fiber.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 fiber.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 fiber.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 fiber.Ctx) bool {
|
||||
elems := strings.Split(c.Path(), "/")
|
||||
elems = elems[1:]
|
||||
if len(elems) < 4 {
|
||||
return false
|
||||
}
|
||||
return elems[len(elems)-2] == "referrers"
|
||||
}
|
||||
|
||||
// RegistryV2 handles all OCI Registry API v2 requests
|
||||
func RegistryV2(store storage.Storage) fiber.Handler {
|
||||
// Initialize handlers
|
||||
blobHandler = NewBlobHandler(store)
|
||||
uploadHandler = NewUploadHandler()
|
||||
m = NewManifestHandler(store)
|
||||
|
||||
return func(c fiber.Ctx) error {
|
||||
if isBlob(c) {
|
||||
return handleBlobs(c)
|
||||
}
|
||||
|
||||
if isManifest(c) {
|
||||
return handleManifest(c)
|
||||
}
|
||||
|
||||
if isTags(c) {
|
||||
return handleTags(c)
|
||||
}
|
||||
|
||||
if isCatalog(c) {
|
||||
return handleCatalog(c)
|
||||
}
|
||||
|
||||
if isReferrers(c) {
|
||||
return handleReferrers(c)
|
||||
}
|
||||
|
||||
// Handle root v2 endpoint
|
||||
if c.Path() == "/v2/" {
|
||||
c.Set("Docker-Distribution-API-Version", "registry/2.0")
|
||||
return c.SendStatus(200)
|
||||
}
|
||||
|
||||
c.Set("Docker-Distribution-API-Version", "registry/2.0")
|
||||
|
||||
log.Printf("[WARN] RegistryV2: unknown endpoint - path = %s, method = %s", c.Path(), c.Method())
|
||||
|
||||
return c.Status(404).JSON(fiber.Map{
|
||||
"errors": []fiber.Map{{"code": "NOT_FOUND", "message": "endpoint not found"}},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ListImages 返回所有仓库列表
|
||||
func ListImages(c fiber.Ctx) error {
|
||||
repos, err := controller.ListImages()
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(fiber.Map{"images": repos})
|
||||
}
|
||||
288
handler/registry_blob.go
Normal file
288
handler/registry_blob.go
Normal file
@@ -0,0 +1,288 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"gitea.loveuer.com/loveuer/cluster/internal/database"
|
||||
"gitea.loveuer.com/loveuer/cluster/internal/model"
|
||||
"gitea.loveuer.com/loveuer/cluster/internal/rerr"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var ErrNotFound = errors.New("not found")
|
||||
|
||||
func handleBlobs(c fiber.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 := strings.Join(elem[1:len(elem)-2], "/")
|
||||
|
||||
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",
|
||||
})
|
||||
}
|
||||
|
||||
size, err := blobHandler.Stat(c.Context(), repo, h)
|
||||
if errors.Is(err, ErrNotFound) {
|
||||
return rerr.Error(c, rerr.ErrBlobUnknown)
|
||||
} else if err != nil {
|
||||
return rerr.Error(c, rerr.ErrInternal(err))
|
||||
}
|
||||
|
||||
c.Set("Content-Length", fmt.Sprint(size))
|
||||
c.Set("Docker-Content-Digest", h.String())
|
||||
return c.Send(nil)
|
||||
|
||||
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",
|
||||
})
|
||||
}
|
||||
|
||||
size, err := blobHandler.Stat(c.Context(), repo, h)
|
||||
if errors.Is(err, ErrNotFound) {
|
||||
return rerr.Error(c, rerr.ErrBlobUnknown)
|
||||
} else if err != nil {
|
||||
return rerr.Error(c, rerr.ErrInternal(err))
|
||||
}
|
||||
|
||||
rc, err := blobHandler.Get(c.Context(), repo, h)
|
||||
if errors.Is(err, ErrNotFound) {
|
||||
return rerr.Error(c, rerr.ErrBlobUnknown)
|
||||
} else if err != 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),
|
||||
})
|
||||
}
|
||||
sr := io.NewSectionReader(ra, start, n)
|
||||
r = io.NopCloser(sr)
|
||||
} 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),
|
||||
})
|
||||
}
|
||||
lr := io.LimitReader(r, n)
|
||||
r = io.NopCloser(lr)
|
||||
}
|
||||
|
||||
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, r)
|
||||
return err
|
||||
|
||||
case http.MethodPost:
|
||||
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 := io.NopCloser(bytes.NewReader(c.Body()))
|
||||
defer vrc.Close()
|
||||
|
||||
if err = blobHandler.Put(c.Context(), repo, h, vrc); err != nil {
|
||||
return rerr.Error(c, rerr.ErrInternal(err))
|
||||
}
|
||||
c.Set("Docker-Content-Digest", h.String())
|
||||
return c.SendStatus(http.StatusCreated)
|
||||
}
|
||||
|
||||
id := uploadHandler.UploadId()
|
||||
|
||||
// Get or create repository
|
||||
var repository model.RegistryRepository
|
||||
if err := database.DB.Where("name = ?", repo).First(&repository).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
repository = model.RegistryRepository{Name: repo}
|
||||
if err := database.DB.Create(&repository).Error; err != nil {
|
||||
if strings.Contains(err.Error(), "UNIQUE constraint failed") {
|
||||
if err := database.DB.Where("name = ?", repo).First(&repository).Error; err != nil {
|
||||
return rerr.Error(c, rerr.ErrInternal(err))
|
||||
}
|
||||
} else {
|
||||
return rerr.Error(c, rerr.ErrInternal(err))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return rerr.Error(c, rerr.ErrInternal(err))
|
||||
}
|
||||
}
|
||||
|
||||
// Create upload session
|
||||
uploadPath := model.GetUploadPath("./x-storage", id)
|
||||
if err := os.MkdirAll(path.Dir(uploadPath), 0755); err != nil {
|
||||
return rerr.Error(c, rerr.ErrInternal(err))
|
||||
}
|
||||
|
||||
session := model.RegistryUploadSession{
|
||||
RepositoryID: repository.ID,
|
||||
SessionID: id,
|
||||
Path: uploadPath,
|
||||
Size: 0,
|
||||
}
|
||||
if err := database.DB.Create(&session).Error; err != nil {
|
||||
return rerr.Error(c, rerr.ErrInternal(err))
|
||||
}
|
||||
|
||||
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",
|
||||
})
|
||||
}
|
||||
|
||||
expectedEnd := start + len(c.Body()) - 1
|
||||
if end != expectedEnd {
|
||||
return rerr.Error(c, &rerr.RepositoryError{
|
||||
Status: http.StatusRequestedRangeNotSatisfiable,
|
||||
Code: "BLOB_UPLOAD_INVALID",
|
||||
Message: fmt.Sprintf("blob upload content range mismatch: expected end %d, got %d", expectedEnd, end),
|
||||
})
|
||||
}
|
||||
} else {
|
||||
end = start + len(c.Body()) - 1
|
||||
}
|
||||
|
||||
length, re := uploadHandler.Write(c.Context(), target, bytes.NewReader(c.Body()), start, end)
|
||||
if 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",
|
||||
})
|
||||
}
|
||||
|
||||
re := uploadHandler.Done(c.Context(), blobHandler, target, bytes.NewReader(c.Body()), len(c.Body()), repo, hash)
|
||||
if re != nil {
|
||||
return rerr.Error(c, re)
|
||||
}
|
||||
|
||||
c.Set("Docker-Content-Digest", hash.String())
|
||||
return c.SendStatus(http.StatusCreated)
|
||||
|
||||
default:
|
||||
return rerr.Error(c, &rerr.RepositoryError{
|
||||
Status: http.StatusBadRequest,
|
||||
Code: "METHOD_UNKNOWN",
|
||||
Message: "We don't understand your method + url",
|
||||
})
|
||||
}
|
||||
}
|
||||
33
handler/registry_catalog.go
Normal file
33
handler/registry_catalog.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"gitea.loveuer.com/loveuer/cluster/internal/rerr"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
func handleCatalog(c fiber.Ctx) error {
|
||||
if c.Method() != "GET" {
|
||||
return rerr.Error(c, &rerr.RepositoryError{
|
||||
Status: http.StatusBadRequest,
|
||||
Code: "METHOD_UNKNOWN",
|
||||
Message: "We don't understand your method + url",
|
||||
})
|
||||
}
|
||||
|
||||
nStr := c.Query("n")
|
||||
n := 10000
|
||||
if nStr != "" {
|
||||
n, _ = strconv.Atoi(nStr)
|
||||
}
|
||||
|
||||
list, re := m.Catalog(c.Context(), n, 0, "")
|
||||
if re != nil {
|
||||
return rerr.Error(c, re)
|
||||
}
|
||||
|
||||
return c.JSON(list)
|
||||
}
|
||||
92
handler/registry_manifest.go
Normal file
92
handler/registry_manifest.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"gitea.loveuer.com/loveuer/cluster/internal/model"
|
||||
"gitea.loveuer.com/loveuer/cluster/internal/rerr"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
func handleManifest(c fiber.Ctx) error {
|
||||
elem := strings.Split(c.Path(), "/")
|
||||
elem = elem[1:]
|
||||
target := elem[len(elem)-1]
|
||||
repo := strings.Join(elem[1:len(elem)-2], "/")
|
||||
|
||||
switch c.Method() {
|
||||
case http.MethodGet:
|
||||
reader, contentType, re := m.Get(c.Context(), repo, target)
|
||||
if re != nil {
|
||||
return rerr.Error(c, re)
|
||||
}
|
||||
|
||||
bs, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
return rerr.Error(c, rerr.ErrInternal(err))
|
||||
}
|
||||
|
||||
h, _, _ := model.SHA256(bytes.NewReader(bs))
|
||||
c.Set("Docker-Content-Digest", h.String())
|
||||
c.Set("Content-Type", contentType)
|
||||
c.Set("Content-Length", fmt.Sprint(len(bs)))
|
||||
return c.Send(bs)
|
||||
|
||||
case http.MethodHead:
|
||||
reader, contentType, re := m.Get(c.Context(), repo, target)
|
||||
if re != nil {
|
||||
return rerr.Error(c, re)
|
||||
}
|
||||
|
||||
bs, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
return rerr.Error(c, rerr.ErrInternal(err))
|
||||
}
|
||||
|
||||
h, _, _ := model.SHA256(bytes.NewReader(bs))
|
||||
c.Set("Docker-Content-Digest", h.String())
|
||||
c.Set("Content-Type", contentType)
|
||||
c.Set("Content-Length", fmt.Sprint(len(bs)))
|
||||
return c.Send(bs)
|
||||
|
||||
case http.MethodPut:
|
||||
buf := &bytes.Buffer{}
|
||||
if _, err := io.Copy(buf, bytes.NewReader(c.Body())); err != nil {
|
||||
return rerr.Error(c, rerr.ErrInternal(err))
|
||||
}
|
||||
|
||||
hash, _, err := model.SHA256(bytes.NewReader(buf.Bytes()))
|
||||
if err != nil {
|
||||
return rerr.Error(c, rerr.ErrInternal(err))
|
||||
}
|
||||
|
||||
digest := hash.String()
|
||||
|
||||
mf := model.RepoSimpleManifest{
|
||||
Blob: buf.Bytes(),
|
||||
ContentType: c.Get("Content-Type"),
|
||||
}
|
||||
|
||||
if err := m.Put(c.Context(), repo, target, digest, &mf); err != nil {
|
||||
return rerr.Error(c, rerr.ErrInternal(err))
|
||||
}
|
||||
|
||||
c.Set("Docker-Content-Digest", digest)
|
||||
return c.SendStatus(http.StatusCreated)
|
||||
|
||||
case http.MethodDelete:
|
||||
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",
|
||||
})
|
||||
}
|
||||
}
|
||||
32
handler/registry_referrers.go
Normal file
32
handler/registry_referrers.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"gitea.loveuer.com/loveuer/cluster/internal/rerr"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
func handleReferrers(c fiber.Ctx) error {
|
||||
if c.Method() != "GET" {
|
||||
return rerr.Error(c, &rerr.RepositoryError{
|
||||
Status: http.StatusBadRequest,
|
||||
Code: "METHOD_UNKNOWN",
|
||||
Message: "We don't understand your method + url",
|
||||
})
|
||||
}
|
||||
|
||||
elem := strings.Split(c.Path(), "/")
|
||||
elem = elem[1:]
|
||||
repo := strings.Join(elem[1:len(elem)-2], "/")
|
||||
target := elem[len(elem)-1]
|
||||
|
||||
index, re := m.Referrers(c.Context(), repo, target)
|
||||
if re != nil {
|
||||
return rerr.Error(c, re)
|
||||
}
|
||||
|
||||
return c.JSON(index)
|
||||
}
|
||||
49
handler/registry_tag.go
Normal file
49
handler/registry_tag.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"gitea.loveuer.com/loveuer/cluster/internal/rerr"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
func handleTags(c fiber.Ctx) error {
|
||||
if c.Method() != "GET" {
|
||||
return rerr.Error(c, &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(c.Path(), "/")
|
||||
elem = elem[1:]
|
||||
repo := strings.Join(elem[1:len(elem)-2], "/")
|
||||
|
||||
var req Req
|
||||
if err := c.Bind().Query(&req); err != nil {
|
||||
return rerr.Error(c, &rerr.RepositoryError{
|
||||
Status: http.StatusBadRequest,
|
||||
Code: "BAD_REQUEST",
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
if req.N <= 0 {
|
||||
req.N = 100
|
||||
}
|
||||
|
||||
list, re := m.Tags(c.Context(), repo, req.N, req.Last, "")
|
||||
if re != nil {
|
||||
return rerr.Error(c, re)
|
||||
}
|
||||
|
||||
return c.JSON(list)
|
||||
}
|
||||
173
handler/upload.go
Normal file
173
handler/upload.go
Normal file
@@ -0,0 +1,173 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"gitea.loveuer.com/loveuer/cluster/controller"
|
||||
"gitea.loveuer.com/loveuer/cluster/internal/registry/storage"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
// StartBlobUpload 开始 blob 上传
|
||||
func StartBlobUpload(store storage.Storage) fiber.Handler {
|
||||
return func(c fiber.Ctx) error {
|
||||
repo := c.Locals("repo_name")
|
||||
repoStr := ""
|
||||
if repo != nil {
|
||||
repoStr = repo.(string)
|
||||
}
|
||||
if repoStr == "" {
|
||||
repoStr = strings.TrimPrefix(c.Params("name"), "/")
|
||||
}
|
||||
|
||||
uuid, err := controller.StartBlobUpload(store, repoStr)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||
"errors": []fiber.Map{
|
||||
{
|
||||
"code": "INTERNAL_ERROR",
|
||||
"message": err.Error(),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 返回上传 URL
|
||||
location := c.Path() + "/" + uuid
|
||||
c.Set("Location", location)
|
||||
c.Set("Docker-Upload-UUID", uuid)
|
||||
c.Set("Range", "0-0")
|
||||
return c.SendStatus(fiber.StatusAccepted)
|
||||
}
|
||||
}
|
||||
|
||||
// PatchBlobUpload 上传 blob 数据块
|
||||
func PatchBlobUpload(store storage.Storage) fiber.Handler {
|
||||
return func(c fiber.Ctx) error {
|
||||
uuid := c.Locals("uuid")
|
||||
uuidStr := ""
|
||||
if uuid != nil {
|
||||
uuidStr = uuid.(string)
|
||||
}
|
||||
|
||||
// 获取 Range header
|
||||
rangeHeader := c.Get("Content-Range")
|
||||
if rangeHeader == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"errors": []fiber.Map{
|
||||
{
|
||||
"code": "BAD_REQUEST",
|
||||
"message": "Content-Range header required",
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 读取数据
|
||||
data := bytes.NewReader(c.Body())
|
||||
size, err := controller.PatchBlobUpload(store, uuidStr, data)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||
"errors": []fiber.Map{
|
||||
{
|
||||
"code": "INTERNAL_ERROR",
|
||||
"message": err.Error(),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
location := c.Path()
|
||||
c.Set("Location", location)
|
||||
c.Set("Docker-Upload-UUID", uuidStr)
|
||||
c.Set("Range", "0-"+strconv.FormatInt(size-1, 10))
|
||||
return c.SendStatus(fiber.StatusNoContent)
|
||||
}
|
||||
}
|
||||
|
||||
// PutBlobUpload 完成 blob 上传
|
||||
func PutBlobUpload(store storage.Storage) fiber.Handler {
|
||||
return func(c fiber.Ctx) error {
|
||||
repo := c.Locals("repo_name")
|
||||
repoStr := ""
|
||||
if repo != nil {
|
||||
repoStr = repo.(string)
|
||||
}
|
||||
if repoStr == "" {
|
||||
repoStr = strings.TrimPrefix(c.Params("name"), "/")
|
||||
}
|
||||
uuid := c.Locals("uuid")
|
||||
uuidStr := ""
|
||||
if uuid != nil {
|
||||
uuidStr = uuid.(string)
|
||||
}
|
||||
|
||||
// 获取 digest
|
||||
digest := c.Query("digest")
|
||||
if digest == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"errors": []fiber.Map{
|
||||
{
|
||||
"code": "BAD_REQUEST",
|
||||
"message": "digest query parameter required",
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 如果有请求体,先追加数据
|
||||
var data interface{} = nil
|
||||
if len(c.Body()) > 0 {
|
||||
data = bytes.NewReader(c.Body())
|
||||
}
|
||||
|
||||
location, err := controller.PutBlobUpload(store, repoStr, uuidStr, digest, data, c.Path())
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"errors": []fiber.Map{
|
||||
{
|
||||
"code": "BAD_REQUEST",
|
||||
"message": err.Error(),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
c.Set("Location", location)
|
||||
c.Set("Content-Length", "0")
|
||||
c.Set("Docker-Content-Digest", digest)
|
||||
return c.SendStatus(fiber.StatusCreated)
|
||||
}
|
||||
}
|
||||
|
||||
// GetBlobUpload 获取上传状态
|
||||
func GetBlobUpload(store storage.Storage) fiber.Handler {
|
||||
return func(c fiber.Ctx) error {
|
||||
uuid := c.Locals("uuid")
|
||||
uuidStr := ""
|
||||
if uuid != nil {
|
||||
uuidStr = uuid.(string)
|
||||
}
|
||||
|
||||
size, err := controller.GetBlobUpload(store, uuidStr)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
|
||||
"errors": []fiber.Map{
|
||||
{
|
||||
"code": "UPLOAD_UNKNOWN",
|
||||
"message": err.Error(),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
location := c.Path()
|
||||
c.Set("Location", location)
|
||||
c.Set("Docker-Upload-UUID", uuidStr)
|
||||
c.Set("Range", "0-"+strconv.FormatInt(size-1, 10))
|
||||
return c.SendStatus(fiber.StatusNoContent)
|
||||
}
|
||||
}
|
||||
99
handler/upload_handler.go
Normal file
99
handler/upload_handler.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"gitea.loveuer.com/loveuer/cluster/internal/database"
|
||||
"gitea.loveuer.com/loveuer/cluster/internal/model"
|
||||
"gitea.loveuer.com/loveuer/cluster/internal/rerr"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type uploadHandlerImpl struct{}
|
||||
|
||||
// NewUploadHandler creates a new upload handler
|
||||
func NewUploadHandler() UploadHandler {
|
||||
return &uploadHandlerImpl{}
|
||||
}
|
||||
|
||||
func (u *uploadHandlerImpl) UploadId() string {
|
||||
return uuid.New().String()
|
||||
}
|
||||
|
||||
func (u *uploadHandlerImpl) Write(ctx context.Context, sessionID string, r io.Reader, start, end int) (int, *rerr.RepositoryError) {
|
||||
// Get upload session
|
||||
var session model.RegistryUploadSession
|
||||
if err := database.DB.Where("session_id = ?", sessionID).First(&session).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return 0, &rerr.RepositoryError{
|
||||
Status: 404,
|
||||
Code: "BLOB_UPLOAD_UNKNOWN",
|
||||
Message: "upload session not found",
|
||||
}
|
||||
}
|
||||
return 0, rerr.ErrInternal(err)
|
||||
}
|
||||
|
||||
// Open file for writing
|
||||
file, err := os.OpenFile(session.Path, os.O_WRONLY|os.O_CREATE, 0644)
|
||||
if err != nil {
|
||||
return 0, rerr.ErrInternal(err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Seek to start position
|
||||
if _, err := file.Seek(int64(start), 0); err != nil {
|
||||
return 0, rerr.ErrInternal(err)
|
||||
}
|
||||
|
||||
// Write data
|
||||
n, err := io.CopyN(file, r, int64(end-start+1))
|
||||
if err != nil && err != io.EOF {
|
||||
return 0, rerr.ErrInternal(err)
|
||||
}
|
||||
|
||||
// Update session size
|
||||
session.Size = int64(start) + n
|
||||
if err := database.DB.Save(&session).Error; err != nil {
|
||||
return 0, rerr.ErrInternal(err)
|
||||
}
|
||||
|
||||
return int(n), nil
|
||||
}
|
||||
|
||||
func (u *uploadHandlerImpl) Done(ctx context.Context, blobHandler BlobHandler, sessionID string, r io.Reader, contentLength int, repo string, h model.Hash) *rerr.RepositoryError {
|
||||
// Get upload session
|
||||
var session model.RegistryUploadSession
|
||||
if err := database.DB.Where("session_id = ?", sessionID).First(&session).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return &rerr.RepositoryError{
|
||||
Status: 404,
|
||||
Code: "BLOB_UPLOAD_UNKNOWN",
|
||||
Message: "upload session not found",
|
||||
}
|
||||
}
|
||||
return rerr.ErrInternal(err)
|
||||
}
|
||||
|
||||
// Read from the uploaded file instead of the request body
|
||||
file, err := os.Open(session.Path)
|
||||
if err != nil {
|
||||
return rerr.ErrInternal(err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Store blob
|
||||
if err := blobHandler.Put(ctx, repo, h, file); err != nil {
|
||||
return rerr.ErrInternal(err)
|
||||
}
|
||||
|
||||
// Clean up upload session
|
||||
os.Remove(session.Path)
|
||||
database.DB.Delete(&session)
|
||||
|
||||
return nil
|
||||
}
|
||||
11
handler/version.go
Normal file
11
handler/version.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
// VersionCheck API 版本检查
|
||||
func VersionCheck(c fiber.Ctx) error {
|
||||
c.Set("Docker-Distribution-API-Version", "registry/2.0")
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
}
|
||||
Reference in New Issue
Block a user