feat: add registry config, image upload/download, and OCI format support

Backend:
- Add registry_address configuration API (GET/POST)
- Add tar image upload with OCI and Docker format support
- Add image download with streaming optimization
- Fix blob download using c.Send (Fiber v3 SendStream bug)
- Add registry_address prefix stripping for all OCI v2 endpoints
- Add AGENTS.md for project documentation

Frontend:
- Add settings store with Snackbar notifications
- Add image upload dialog with progress bar
- Add download state tracking with multi-stage feedback
- Replace alert() with MUI Snackbar messages
- Display image names without registry_address prefix

🤖 Generated with [Qoder](https://qoder.com)
This commit is contained in:
loveuer
2025-11-10 16:28:58 +08:00
parent 29088a6b54
commit 9780a2b028
35 changed files with 3065 additions and 91 deletions

View File

@@ -6,6 +6,7 @@ import (
"encoding/hex"
"fmt"
"io"
"log"
"strconv"
"strings"
@@ -48,6 +49,16 @@ func HandleBlobs(c fiber.Ctx, db *gorm.DB, store store.Store) error {
// ???? blobs ???????
repo := strings.Join(parts[:blobsIndex], "/")
// Strip registry_address prefix from repo if present
var registryConfig model.RegistryConfig
registryAddress := ""
if err := db.Where("key = ?", "registry_address").First(&registryConfig).Error; err == nil {
registryAddress = registryConfig.Value
}
if registryAddress != "" && strings.HasPrefix(repo, registryAddress+"/") {
repo = strings.TrimPrefix(repo, registryAddress+"/")
}
// ???? parts??????????? parts[0] ? "blobs"
parts = parts[blobsIndex:]
@@ -285,42 +296,53 @@ func parseRangeHeader(rangeHeader string, size int64) (start, end int64, valid b
// handleBlobDownload ?? blob
func handleBlobDownload(c fiber.Ctx, db *gorm.DB, store store.Store, repo string, digest string) error {
log.Printf("[BlobDownload] Start: repo=%s, digest=%s", repo, digest)
// Check if blob exists
exists, err := store.BlobExists(c.Context(), digest)
if err != nil {
log.Printf("[BlobDownload] BlobExists error: %v", err)
return resp.R500(c, "", nil, err)
}
if !exists {
log.Printf("[BlobDownload] Blob not found: %s", digest)
return resp.R404(c, "BLOB_NOT_FOUND", nil, "blob not found")
}
// Get blob size
size, err := store.GetBlobSize(c.Context(), digest)
if err != nil {
log.Printf("[BlobDownload] GetBlobSize error: %v", err)
return resp.R500(c, "", nil, err)
}
log.Printf("[BlobDownload] Blob size: %d bytes", size)
// Read blob
reader, err := store.ReadBlob(c.Context(), digest)
if err != nil {
log.Printf("[BlobDownload] ReadBlob error: %v", err)
return resp.R500(c, "", nil, err)
}
defer reader.Close()
log.Printf("[BlobDownload] Reader opened successfully")
// Check for Range request
rangeHeader := c.Get("Range")
start, end, hasRange := parseRangeHeader(rangeHeader, size)
if hasRange {
log.Printf("[BlobDownload] Range request: %d-%d/%d", start, end, size)
// Handle Range request
// Seek to start position
if seeker, ok := reader.(io.Seeker); ok {
if _, err := seeker.Seek(start, io.SeekStart); err != nil {
log.Printf("[BlobDownload] Seek error: %v", err)
return resp.R500(c, "", nil, err)
}
} else {
// If not seekable, read and discard bytes
if _, err := io.CopyN(io.Discard, reader, start); err != nil {
log.Printf("[BlobDownload] CopyN discard error: %v", err)
return resp.R500(c, "", nil, err)
}
}
@@ -336,18 +358,32 @@ func handleBlobDownload(c fiber.Ctx, db *gorm.DB, store store.Store, repo string
c.Set("Docker-Content-Digest", digest)
c.Status(206) // Partial Content
// Send partial content
return c.SendStream(limitedReader)
log.Printf("[BlobDownload] Sending partial content")
// Read all content and send
content, err := io.ReadAll(limitedReader)
if err != nil {
log.Printf("[BlobDownload] ReadAll error: %v", err)
return resp.R500(c, "", nil, err)
}
return c.Send(content)
}
// Full blob download
log.Printf("[BlobDownload] Full blob download, setting headers")
c.Set("Content-Type", "application/octet-stream")
c.Set("Content-Length", fmt.Sprintf("%d", size))
c.Set("Accept-Ranges", "bytes")
c.Set("Docker-Content-Digest", digest)
// Send full blob stream
return c.SendStream(reader)
log.Printf("[BlobDownload] About to read all content, size=%d", size)
// Read all content and send
content, err := io.ReadAll(reader)
if err != nil {
log.Printf("[BlobDownload] ReadAll error: %v", err)
return resp.R500(c, "", nil, err)
}
log.Printf("[BlobDownload] Read %d bytes, sending...", len(content))
return c.Send(content)
}
// handleBlobHead ?? blob ????