diff --git a/internal/handlers/image.go b/internal/handlers/image.go index 89960e3..5dd9dcd 100644 --- a/internal/handlers/image.go +++ b/internal/handlers/image.go @@ -124,26 +124,26 @@ func (s *Handlers) HandleImage() http.HandlerFunc { w.Header().Set("ETag", resp.ETag) } - // Log cache status and timing + // Stream the response + w.WriteHeader(http.StatusOK) + + servedBytes, err := io.Copy(w, resp.Content) + if err != nil { + s.log.Error("failed to write response", + "error", err, + ) + } + + // Log cache status and timing after serving duration := time.Since(startTime) s.log.Info("image served", "cache_key", cacheKey, "cache_status", resp.CacheStatus, "duration_ms", duration.Milliseconds(), "format", req.Format, - "served_bytes", resp.ContentLength, + "served_bytes", servedBytes, "fetched_bytes", resp.FetchedBytes, ) - - // Stream the response - w.WriteHeader(http.StatusOK) - - _, err = io.Copy(w, resp.Content) - if err != nil { - s.log.Error("failed to write response", - "error", err, - ) - } } } diff --git a/internal/imgcache/cache.go b/internal/imgcache/cache.go index 2a6a628..f6b9d82 100644 --- a/internal/imgcache/cache.go +++ b/internal/imgcache/cache.go @@ -231,18 +231,18 @@ func (c *Cache) StoreSource( return contentHash, nil } -// StoreOutput stores processed output content. +// StoreOutput stores processed output content and returns the output hash. func (c *Cache) StoreOutput( ctx context.Context, req *ImageRequest, sourceMetadataID int64, content io.Reader, contentType string, -) error { +) (string, error) { // Store content outputHash, size, err := c.dstContent.Store(content) if err != nil { - return fmt.Errorf("failed to store output content: %w", err) + return "", fmt.Errorf("failed to store output content: %w", err) } cacheKey := CacheKey(req) @@ -254,7 +254,7 @@ func (c *Cache) StoreOutput( ON CONFLICT(content_hash) DO NOTHING `, outputHash, contentType, size) if err != nil { - return fmt.Errorf("failed to insert output content: %w", err) + return "", fmt.Errorf("failed to insert output content: %w", err) } _, err = c.db.ExecContext(ctx, ` @@ -266,7 +266,7 @@ func (c *Cache) StoreOutput( access_count = request_cache.access_count + 1 `, cacheKey, sourceMetadataID, outputHash, req.Size.Width, req.Size.Height, req.Format, req.Quality, req.FitMode) if err != nil { - return fmt.Errorf("failed to insert request cache: %w", err) + return "", fmt.Errorf("failed to insert request cache: %w", err) } // Update hot cache @@ -276,7 +276,7 @@ func (c *Cache) StoreOutput( c.hotCacheMu.Unlock() } - return nil + return outputHash, nil } // StoreNegative stores a negative cache entry for a failed fetch. diff --git a/internal/imgcache/cache_test.go b/internal/imgcache/cache_test.go index 904dd95..9319797 100644 --- a/internal/imgcache/cache_test.go +++ b/internal/imgcache/cache_test.go @@ -176,7 +176,7 @@ func TestCache_StoreAndLookup(t *testing.T) { // Store output content outputContent := []byte("fake webp data") - err = cache.StoreOutput(ctx, req, metaID, bytes.NewReader(outputContent), "image/webp") + _, err = cache.StoreOutput(ctx, req, metaID, bytes.NewReader(outputContent), "image/webp") if err != nil { t.Fatalf("StoreOutput() error = %v", err) } @@ -297,7 +297,7 @@ func TestCache_HotCache(t *testing.T) { metaID, _ := cache.GetSourceMetadataID(ctx, req) outputContent := []byte("output data") - err = cache.StoreOutput(ctx, req, metaID, bytes.NewReader(outputContent), "image/webp") + _, err = cache.StoreOutput(ctx, req, metaID, bytes.NewReader(outputContent), "image/webp") if err != nil { t.Fatalf("StoreOutput() error = %v", err) } @@ -343,15 +343,30 @@ func TestCache_GetOutput(t *testing.T) { // Store content sourceContent := []byte("source") fetchResult := &FetchResult{ContentType: "image/jpeg", Headers: map[string][]string{}} - _, _ = cache.StoreSource(ctx, req, bytes.NewReader(sourceContent), fetchResult) + _, err := cache.StoreSource(ctx, req, bytes.NewReader(sourceContent), fetchResult) + if err != nil { + t.Fatalf("StoreSource() error = %v", err) + } - metaID, _ := cache.GetSourceMetadataID(ctx, req) + metaID, err := cache.GetSourceMetadataID(ctx, req) + if err != nil { + t.Fatalf("GetSourceMetadataID() error = %v", err) + } outputContent := []byte("the actual output content") - _ = cache.StoreOutput(ctx, req, metaID, bytes.NewReader(outputContent), "image/webp") + outputHash, err := cache.StoreOutput(ctx, req, metaID, bytes.NewReader(outputContent), "image/webp") + if err != nil { + t.Fatalf("StoreOutput() error = %v", err) + } + if outputHash == "" { + t.Fatal("StoreOutput() returned empty hash") + } // Lookup to get hash - result, _ := cache.Lookup(ctx, req) + result, err := cache.Lookup(ctx, req) + if err != nil { + t.Fatalf("Lookup() error = %v", err) + } // Get output content reader, err := cache.GetOutput(result.OutputHash) diff --git a/internal/imgcache/service.go b/internal/imgcache/service.go index 283c3de..0905ece 100644 --- a/internal/imgcache/service.go +++ b/internal/imgcache/service.go @@ -88,10 +88,9 @@ func (s *Service) Get(ctx context.Context, req *ImageRequest) (*ImageResponse, e // Fall through to re-fetch } else { return &ImageResponse{ - Content: reader, - ContentLength: -1, // Unknown until read - ContentType: result.ContentType, - CacheStatus: CacheHit, + Content: reader, + ContentType: result.ContentType, + CacheStatus: CacheHit, }, nil } } @@ -152,27 +151,28 @@ func (s *Service) fetchAndProcess(ctx context.Context, req *ImageRequest) (*Imag return nil, fmt.Errorf("image processing failed: %w", err) } - // Read processed data to cache it - processedData, err := io.ReadAll(processResult.Content) - if err != nil { - return nil, fmt.Errorf("failed to read processed image: %w", err) - } - _ = processResult.Content.Close() - - // Store output content + // Store output content to cache metaID, err := s.cache.GetSourceMetadataID(ctx, req) - if err == nil { - err = s.cache.StoreOutput(ctx, req, metaID, bytes.NewReader(processedData), processResult.ContentType) - if err != nil { - s.log.Warn("failed to store output content", "error", err) - } + if err != nil { + return nil, fmt.Errorf("failed to get source metadata ID: %w", err) + } + + outputHash, err := s.cache.StoreOutput(ctx, req, metaID, processResult.Content, processResult.ContentType) + _ = processResult.Content.Close() + if err != nil { + return nil, fmt.Errorf("failed to store output content: %w", err) + } + + // Serve from the cached file on disk (same path as cache hits) + reader, err := s.cache.GetOutput(outputHash) + if err != nil { + return nil, fmt.Errorf("failed to read cached output: %w", err) } return &ImageResponse{ - Content: io.NopCloser(bytes.NewReader(processedData)), - ContentLength: int64(len(processedData)), - ContentType: processResult.ContentType, - FetchedBytes: int64(len(sourceData)), + Content: reader, + ContentType: processResult.ContentType, + FetchedBytes: int64(len(sourceData)), }, nil }