|
|
|
@@ -241,38 +241,37 @@ func (s *Service) GenerateSignedURL(
|
|
|
|
|
baseURL, path, sig, exp, req.Quality, req.FitMode), nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// loadCachedSource attempts to load source content from cache, returning nil
|
|
|
|
|
// if the cached data is unavailable or exceeds maxResponseSize.
|
|
|
|
|
func (s *Service) loadCachedSource(contentHash ContentHash) []byte {
|
|
|
|
|
reader, err := s.cache.GetSourceContent(contentHash)
|
|
|
|
|
// loadCachedSource opens source content from cache, without reading it, and
|
|
|
|
|
// returns it with its size; nil if the cached data is unavailable, empty or
|
|
|
|
|
// exceeds maxResponseSize.
|
|
|
|
|
func (s *Service) loadCachedSource(
|
|
|
|
|
contentHash ContentHash,
|
|
|
|
|
) (io.ReadCloser, int64) {
|
|
|
|
|
reader, size, err := s.cache.GetSourceContent(contentHash)
|
|
|
|
|
if err != nil {
|
|
|
|
|
s.log.Warn("failed to load cached source, fetching", "error", err)
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
return nil, 0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Bound the read to maxResponseSize to prevent unbounded memory use
|
|
|
|
|
// from unexpectedly large cached files.
|
|
|
|
|
limited := io.LimitReader(reader, s.maxResponseSize+1)
|
|
|
|
|
data, err := io.ReadAll(limited)
|
|
|
|
|
_ = reader.Close()
|
|
|
|
|
if size > s.maxResponseSize {
|
|
|
|
|
_ = reader.Close()
|
|
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
s.log.Warn("failed to read cached source, fetching", "error", err)
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if int64(len(data)) > s.maxResponseSize {
|
|
|
|
|
s.log.Warn("cached source exceeds max response size, discarding",
|
|
|
|
|
"hash", contentHash,
|
|
|
|
|
"max_bytes", s.maxResponseSize,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
return nil, 0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return data
|
|
|
|
|
if size == 0 {
|
|
|
|
|
_ = reader.Close()
|
|
|
|
|
|
|
|
|
|
return nil, 0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return reader, size
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// processFromSourceOrFetch processes an image, using cached source content
|
|
|
|
@@ -289,22 +288,27 @@ func (s *Service) processFromSourceOrFetch(
|
|
|
|
|
s.log.Warn("source lookup failed", "error", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var sourceData []byte
|
|
|
|
|
var (
|
|
|
|
|
source io.ReadCloser
|
|
|
|
|
sourceSize int64
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if contentHash != "" {
|
|
|
|
|
s.log.Debug("using cached source", "hash", contentHash)
|
|
|
|
|
sourceData = s.loadCachedSource(contentHash)
|
|
|
|
|
source, sourceSize = s.loadCachedSource(contentHash)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Fetch from upstream if we don't have source data or it's empty
|
|
|
|
|
if len(sourceData) == 0 {
|
|
|
|
|
if source == nil {
|
|
|
|
|
return s.fetchAndProcess(ctx, req, cacheKey)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Process using cached source; nothing was fetched from upstream
|
|
|
|
|
resp, err := s.processAndStore(
|
|
|
|
|
ctx, req, cacheKey, sourceData, int64(len(sourceData)),
|
|
|
|
|
)
|
|
|
|
|
defer func() { _ = source.Close() }()
|
|
|
|
|
|
|
|
|
|
// Process using cached source; nothing was fetched from upstream. The
|
|
|
|
|
// image processor reads the source only once it has a processing slot,
|
|
|
|
|
// so a request waiting for one holds none of it in memory.
|
|
|
|
|
resp, err := s.processAndStore(ctx, req, cacheKey, source, sourceSize)
|
|
|
|
|
|
|
|
|
|
return resp, 0, err
|
|
|
|
|
}
|
|
|
|
@@ -338,6 +342,10 @@ func (s *Service) fetchAndProcess(
|
|
|
|
|
return nil, 0, fmt.Errorf("upstream fetch failed: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Closing the body frees the upstream connection. It is closed only
|
|
|
|
|
// after processing, so the fetcher's connection limit also bounds the
|
|
|
|
|
// fetched sources held in memory while their requests wait for a
|
|
|
|
|
// processing slot.
|
|
|
|
|
defer func() { _ = fetchResult.Content.Close() }()
|
|
|
|
|
|
|
|
|
|
// Read and validate the source content
|
|
|
|
@@ -383,17 +391,20 @@ func (s *Service) fetchAndProcess(
|
|
|
|
|
// Continue even if caching fails
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
resp, err := s.processAndStore(ctx, req, cacheKey, sourceData, fetchBytes)
|
|
|
|
|
resp, err := s.processAndStore(
|
|
|
|
|
ctx, req, cacheKey, bytes.NewReader(sourceData), fetchBytes,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return resp, fetchBytes, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// processAndStore processes an image and stores the result.
|
|
|
|
|
// processAndStore processes the image read from source and stores the
|
|
|
|
|
// result.
|
|
|
|
|
func (s *Service) processAndStore(
|
|
|
|
|
ctx context.Context,
|
|
|
|
|
req *ImageRequest,
|
|
|
|
|
cacheKey VariantKey,
|
|
|
|
|
sourceData []byte,
|
|
|
|
|
source io.Reader,
|
|
|
|
|
fetchBytes int64,
|
|
|
|
|
) (*ImageResponse, error) {
|
|
|
|
|
// Process the image
|
|
|
|
@@ -406,7 +417,7 @@ func (s *Service) processAndStore(
|
|
|
|
|
FitMode: imageprocessor.FitMode(req.FitMode),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
processResult, err := s.processor.Process(ctx, bytes.NewReader(sourceData), processReq)
|
|
|
|
|
processResult, err := s.processor.Process(ctx, source, processReq)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("image processing failed: %w", err)
|
|
|
|
|
}
|
|
|
|
|