package registry import ( "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "strings" "gitea.loveuer.com/loveuer/cluster/internal/model" "gitea.loveuer.com/loveuer/cluster/pkg/resp" "gitea.loveuer.com/loveuer/cluster/pkg/store" "github.com/gofiber/fiber/v3" "gorm.io/gorm" ) // isDigestFormat checks if a string is in digest format (e.g., sha256:abc123...) func isDigestFormat(s string) bool { parts := strings.SplitN(s, ":", 2) if len(parts) != 2 { return false } algo := parts[0] hash := parts[1] // Check algorithm if algo != "sha256" { // Could be extended to support other algorithms like sha512 return false } // Check that hash is a valid hex string of expected length (64 for sha256) if len(hash) != 64 { return false } // Verify it's all hex characters for _, r := range hash { if !((r >= '0' && r <= '9') || (r >= 'a' && r <= 'f')) { return false } } return true } // HandleManifest ?? manifest ???? // PUT /v2/{repo}/manifests/{tag} - ?? manifest // GET /v2/{repo}/manifests/{tag} - ?? manifest // DELETE /v2/{repo}/manifests/{tag} - ?? manifest func HandleManifest(c fiber.Ctx, db *gorm.DB, store store.Store) error { path := c.Path() method := c.Method() // ????: /v2/{repo}/manifests/{tag} // ??????????? "test/redis" pathWithoutV2 := strings.TrimPrefix(path, "/v2/") parts := strings.Split(pathWithoutV2, "/") if len(parts) < 2 { return resp.R404(c, "INVALID_PATH", nil, "invalid path") } // ?? "manifests" ??? manifestsIndex := -1 for i, part := range parts { if part == "manifests" { manifestsIndex = i break } } if manifestsIndex < 1 || manifestsIndex >= len(parts)-1 { return resp.R404(c, "INVALID_PATH", nil, "invalid path: manifests not found") } // ???? manifests ??????? repo := strings.Join(parts[:manifestsIndex], "/") // Strip registry_address prefix from repo if present var registryConfig model.RegistryConfig registryAddress := "" if err := db.Where("key = ?", "registry_address").First(®istryConfig).Error; err == nil { registryAddress = registryConfig.Value } if registryAddress != "" && strings.HasPrefix(repo, registryAddress+"/") { repo = strings.TrimPrefix(repo, registryAddress+"/") } // tag ? manifests ????? tag := parts[manifestsIndex+1] switch method { case "PUT": return handleManifestPut(c, db, store, repo, tag) case "GET": return handleManifestGet(c, db, store, repo, tag) case "HEAD": return handleManifestHead(c, db, store, repo, tag) case "DELETE": return handleManifestDelete(c, db, store, repo, tag) } return resp.R404(c, "NOT_FOUND", nil, "method not allowed") } // handleManifestPut ?? manifest func handleManifestPut(c fiber.Ctx, db *gorm.DB, store store.Store, repo string, tag string) error { // ?? manifest ?? content := c.Body() if len(content) == 0 { return resp.R400(c, "EMPTY_BODY", nil, "manifest content is empty") } // ?? digest hasher := sha256.New() hasher.Write(content) digest := "sha256:" + hex.EncodeToString(hasher.Sum(nil)) // ?? Content-Type mediaType := c.Get("Content-Type") if mediaType == "" { // ??? manifest ???? var mf map[string]interface{} if err := json.Unmarshal(content, &mf); err == nil { if mt, ok := mf["mediaType"].(string); ok { mediaType = mt } else { // ???? Docker manifest v2 mediaType = "application/vnd.docker.distribution.manifest.v2+json" } } else { mediaType = "application/vnd.docker.distribution.manifest.v2+json" } } // ?? manifest ???????? var manifestData map[string]interface{} if err := json.Unmarshal(content, &manifestData); err != nil { return resp.R400(c, "INVALID_MANIFEST", nil, "invalid manifest format") } // ?????? var repository model.Repository if err := db.Where("name = ?", repo).First(&repository).Error; err != nil { if err == gorm.ErrRecordNotFound { repository = model.Repository{Name: repo} if err := db.Create(&repository).Error; err != nil { return resp.R500(c, "", nil, err) } } else { return resp.R500(c, "", nil, err) } } // ?? manifest ????? if err := store.WriteManifest(c.Context(), digest, content); err != nil { return resp.R500(c, "", nil, err) } // ?? manifest ????? var manifest model.Manifest if err := db.Where("digest = ?", digest).First(&manifest).Error; err != nil { if err == gorm.ErrRecordNotFound { // ???? manifest ?? manifest = model.Manifest{ Repository: repo, Tag: tag, Digest: digest, MediaType: mediaType, Size: int64(len(content)), Content: content, } if err := db.Create(&manifest).Error; err != nil { return resp.R500(c, "", nil, err) } } else { return resp.R500(c, "", nil, err) } } else { // ???? manifest ? tag ?? manifest.Tag = tag manifest.Repository = repo if err := db.Save(&manifest).Error; err != nil { return resp.R500(c, "", nil, err) } } // ????? tag ?? var tagRecord model.Tag if err := db.Where("repository = ? AND tag = ?", repo, tag).First(&tagRecord).Error; err != nil { if err == gorm.ErrRecordNotFound { tagRecord = model.Tag{ Repository: repo, Tag: tag, Digest: digest, } if err := db.Create(&tagRecord).Error; err != nil { return resp.R500(c, "", nil, err) } } else { return resp.R500(c, "", nil, err) } } else { tagRecord.Digest = digest if err := db.Save(&tagRecord).Error; err != nil { return resp.R500(c, "", nil, err) } } // ????? c.Set("Location", fmt.Sprintf("/v2/%s/manifests/%s", repo, tag)) c.Set("Docker-Content-Digest", digest) c.Set("Content-Type", mediaType) c.Set("Content-Length", fmt.Sprintf("%d", len(content))) return c.SendStatus(201) } // handleManifestGet ?? manifest func handleManifestGet(c fiber.Ctx, db *gorm.DB, store store.Store, repo string, tag string) error { var manifest model.Manifest // ?? tag ?????????????????????? if isDigestFormat(tag) { // ?? digest ???????????? repository digest := tag // ?? manifest ??? if err := db.Where("digest = ?", digest).First(&manifest).Error; err != nil { if err == gorm.ErrRecordNotFound { return resp.R404(c, "MANIFEST_NOT_FOUND", nil, "manifest not found") } return resp.R500(c, "", nil, err) } // ???? manifest ?????????? repository ?? var tagRecord model.Tag if err := db.Where("repository = ? AND digest = ?", repo, digest).First(&tagRecord).Error; err != nil { if err == gorm.ErrRecordNotFound { return resp.R404(c, "MANIFEST_NOT_FOUND", nil, "manifest not found in this repository") } return resp.R500(c, "", nil, err) } } else { // ?? tag ???? tag ????????? var tagRecord model.Tag if err := db.Where("repository = ? AND tag = ?", repo, tag).First(&tagRecord).Error; err != nil { if err == gorm.ErrRecordNotFound { return resp.R404(c, "MANIFEST_NOT_FOUND", nil, "manifest not found") } return resp.R500(c, "", nil, err) } // ?? manifest ?? if err := db.Where("digest = ?", tagRecord.Digest).First(&manifest).Error; err != nil { if err == gorm.ErrRecordNotFound { return resp.R404(c, "MANIFEST_NOT_FOUND", nil, "manifest not found") } return resp.R500(c, "", nil, err) } } // Check Accept header if provided acceptHeader := c.Get("Accept") if acceptHeader != "" { // Parse Accept header to check if client accepts the manifest's media type acceptTypes := strings.Split(acceptHeader, ",") accepted := false for _, at := range acceptTypes { // Remove quality values (e.g., "application/vnd.docker.distribution.manifest.v2+json;q=0.9") mediaType := strings.TrimSpace(strings.Split(at, ";")[0]) if mediaType == manifest.MediaType || mediaType == "*/*" { accepted = true break } } if !accepted { // Check for wildcard or common Docker manifest types for _, at := range acceptTypes { mediaType := strings.TrimSpace(strings.Split(at, ";")[0]) if strings.Contains(mediaType, "manifest") || mediaType == "*/*" { accepted = true break } } } // Note: We still return the manifest even if not explicitly accepted, // as some clients may not send proper Accept headers } // Read manifest content content, err := store.ReadManifest(c.Context(), manifest.Digest) if err != nil { return resp.R500(c, "", nil, err) } // Set response headers c.Set("Content-Type", manifest.MediaType) c.Set("Content-Length", fmt.Sprintf("%d", len(content))) c.Set("Docker-Content-Digest", manifest.Digest) return c.Send(content) } // handleManifestHead ?? manifest ???? func handleManifestHead(c fiber.Ctx, db *gorm.DB, store store.Store, repo string, tag string) error { var manifest model.Manifest // ?? tag ?????????????????????? if isDigestFormat(tag) { // ?? digest ???????????? repository digest := tag // ?? manifest ??? if err := db.Where("digest = ?", digest).First(&manifest).Error; err != nil { if err == gorm.ErrRecordNotFound { return resp.R404(c, "MANIFEST_NOT_FOUND", nil, "manifest not found") } return resp.R500(c, "", nil, err) } // ???? manifest ?????????? repository ?? var tagRecord model.Tag if err := db.Where("repository = ? AND digest = ?", repo, digest).First(&tagRecord).Error; err != nil { if err == gorm.ErrRecordNotFound { return resp.R404(c, "MANIFEST_NOT_FOUND", nil, "manifest not found in this repository") } return resp.R500(c, "", nil, err) } } else { // ?? tag ???? tag ????????? var tagRecord model.Tag if err := db.Where("repository = ? AND tag = ?", repo, tag).First(&tagRecord).Error; err != nil { if err == gorm.ErrRecordNotFound { return resp.R404(c, "MANIFEST_NOT_FOUND", nil, "manifest not found") } return resp.R500(c, "", nil, err) } // ?? manifest ?? if err := db.Where("digest = ?", tagRecord.Digest).First(&manifest).Error; err != nil { if err == gorm.ErrRecordNotFound { return resp.R404(c, "MANIFEST_NOT_FOUND", nil, "manifest not found") } return resp.R500(c, "", nil, err) } } // ????? c.Set("Content-Type", manifest.MediaType) c.Set("Content-Length", fmt.Sprintf("%d", manifest.Size)) c.Set("Docker-Content-Digest", manifest.Digest) return c.SendStatus(200) } // handleManifestDelete ?? manifest func handleManifestDelete(c fiber.Ctx, db *gorm.DB, store store.Store, repo string, tag string) error { var digest string if isDigestFormat(tag) { // ?? digest ???????????? repository digest = tag // ???? manifest ?????????? repository ?? var tagRecord model.Tag if err := db.Where("repository = ? AND digest = ?", repo, digest).First(&tagRecord).Error; err != nil { if err == gorm.ErrRecordNotFound { return resp.R404(c, "MANIFEST_NOT_FOUND", nil, "manifest not found in this repository") } return resp.R500(c, "", nil, err) } // ???????? tag ??? manifest var count int64 if err := db.Model(&model.Tag{}).Where("digest = ?", digest).Count(&count).Error; err != nil { return resp.R500(c, "", nil, err) } // ??? tag ??????? manifest ?? if count == 0 { var manifest model.Manifest if err := db.Where("digest = ?", digest).First(&manifest).Error; err != nil { if err == gorm.ErrRecordNotFound { return resp.R404(c, "MANIFEST_NOT_FOUND", nil, "manifest not found") } return resp.R500(c, "", nil, err) } if err := db.Delete(&manifest).Error; err != nil { return resp.R500(c, "", nil, err) } } else { // ?? tag ????????????????? // ??? manifest ??????????? return resp.R400(c, "CANNOT_DELETE_DIGEST_REFERENCED_BY_TAGS", nil, "cannot delete manifest referenced by tags") } } else { // ?? tag ???? tag ????????? var tagRecord model.Tag if err := db.Where("repository = ? AND tag = ?", repo, tag).First(&tagRecord).Error; err != nil { if err == gorm.ErrRecordNotFound { return resp.R404(c, "MANIFEST_NOT_FOUND", nil, "manifest not found") } return resp.R500(c, "", nil, err) } digest = tagRecord.Digest // ?? tag ?? if err := db.Delete(&tagRecord).Error; err != nil { return resp.R500(c, "", nil, err) } // ???????? tag ??? manifest var count int64 if err := db.Model(&model.Tag{}).Where("digest = ?", digest).Count(&count).Error; err != nil { return resp.R500(c, "", nil, err) } // ?????? tag ????? manifest ?? if count == 0 { var manifest model.Manifest if err := db.Where("digest = ?", digest).First(&manifest).Error; err == nil { if err := db.Delete(&manifest).Error; err != nil { return resp.R500(c, "", nil, err) } } } } return c.SendStatus(202) }