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:
@@ -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(®istryConfig).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 ????
|
||||
|
||||
Reference in New Issue
Block a user