Search/cache-images.go
2024-11-20 14:57:55 +01:00

234 lines
5.7 KiB
Go

package main
import (
"bytes"
"encoding/json"
"fmt"
"image"
"image/gif"
"image/jpeg"
"image/png"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"sync"
"github.com/chai2010/webp"
"golang.org/x/image/bmp"
"golang.org/x/image/tiff"
)
var (
cachingImages = make(map[string]*sync.Mutex)
cachingImagesMu sync.Mutex
cachingSemaphore = make(chan struct{}, 10) // Limit to 10 concurrent downloads
)
func cacheImage(imageURL, filename string) (string, error) {
cacheDir := "image_cache"
cachedImagePath := filepath.Join(cacheDir, filename)
// Check if the image is already cached
if _, err := os.Stat(cachedImagePath); err == nil {
return cachedImagePath, nil
}
// Ensure only one goroutine caches the same image
cachingImagesMu.Lock()
if _, exists := cachingImages[imageURL]; !exists {
cachingImages[imageURL] = &sync.Mutex{}
}
mu := cachingImages[imageURL]
cachingImagesMu.Unlock()
mu.Lock()
defer mu.Unlock()
// Double-check if the image was cached while waiting
if _, err := os.Stat(cachedImagePath); err == nil {
return cachedImagePath, nil
}
cachingSemaphore <- struct{}{} // Acquire a token
defer func() { <-cachingSemaphore }() // Release the token
// Download the image
resp, err := http.Get(imageURL)
if err != nil {
return "", err
}
defer resp.Body.Close()
// Read the image data into a byte slice
data, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
// Detect the content type
contentType := http.DetectContentType(data)
// If content type is HTML, skip caching
if strings.HasPrefix(contentType, "text/html") {
return "", fmt.Errorf("URL returned HTML content instead of an image: %s", imageURL)
}
// Handle SVG files directly
if contentType == "image/svg+xml" {
// Ensure the cache directory exists
if _, err := os.Stat(cacheDir); os.IsNotExist(err) {
os.Mkdir(cacheDir, os.ModePerm)
}
// Save the SVG file as-is
err = os.WriteFile(cachedImagePath, data, 0644)
if err != nil {
return "", err
}
// Clean up mutex
cachingImagesMu.Lock()
delete(cachingImages, imageURL)
cachingImagesMu.Unlock()
return cachedImagePath, nil
}
// Decode the image based on the content type
var img image.Image
switch contentType {
case "image/jpeg":
img, err = jpeg.Decode(bytes.NewReader(data))
case "image/png":
img, err = png.Decode(bytes.NewReader(data))
case "image/gif":
img, err = gif.Decode(bytes.NewReader(data))
case "image/webp":
img, err = webp.Decode(bytes.NewReader(data))
case "image/bmp":
img, err = bmp.Decode(bytes.NewReader(data))
case "image/tiff":
img, err = tiff.Decode(bytes.NewReader(data))
default:
return "", fmt.Errorf("unsupported image type: %s", contentType)
}
if err != nil {
return "", fmt.Errorf("failed to decode image: %v", err)
}
// Ensure the cache directory exists
if _, err := os.Stat(cacheDir); os.IsNotExist(err) {
os.Mkdir(cacheDir, os.ModePerm)
}
// Open the cached file for writing
outFile, err := os.Create(cachedImagePath)
if err != nil {
return "", err
}
defer outFile.Close()
// Encode the image to WebP and save
options := &webp.Options{Lossless: false, Quality: 80}
err = webp.Encode(outFile, img, options)
if err != nil {
return "", err
}
// Clean up mutex
cachingImagesMu.Lock()
delete(cachingImages, imageURL)
cachingImagesMu.Unlock()
return cachedImagePath, nil
}
func handleCachedImages(w http.ResponseWriter, r *http.Request) {
imageName := filepath.Base(r.URL.Path)
cacheDir := "image_cache"
cachedImagePath := filepath.Join(cacheDir, imageName)
if _, err := os.Stat(cachedImagePath); os.IsNotExist(err) {
// Serve placeholder image with no-store headers
placeholderPath := "static/images/placeholder.webp"
placeholderContentType := "image/webp"
// You can also check for SVG placeholder if needed
if strings.HasSuffix(imageName, ".svg") {
placeholderPath = "static/images/placeholder.svg"
placeholderContentType = "image/svg+xml"
}
w.Header().Set("Content-Type", placeholderContentType)
w.Header().Set("Cache-Control", "no-store, must-revalidate")
w.Header().Set("Pragma", "no-cache")
w.Header().Set("Expires", "0")
http.ServeFile(w, r, placeholderPath)
return
}
// Determine the content type based on the file extension
extension := strings.ToLower(filepath.Ext(cachedImagePath))
var contentType string
switch extension {
case ".svg":
contentType = "image/svg+xml"
case ".jpg", ".jpeg":
contentType = "image/jpeg"
case ".png":
contentType = "image/png"
case ".gif":
contentType = "image/gif"
case ".webp":
contentType = "image/webp"
default:
// Default to binary stream if unknown
contentType = "application/octet-stream"
}
w.Header().Set("Content-Type", contentType)
w.Header().Set("Cache-Control", "public, max-age=31536000") // Cache the image for 1 year
http.ServeFile(w, r, cachedImagePath)
}
func handleImageStatus(w http.ResponseWriter, r *http.Request) {
imageIDs := r.URL.Query().Get("image_ids")
ids := strings.Split(imageIDs, ",")
statusMap := make(map[string]string)
cacheDir := "image_cache"
printDebug("Received image status request for IDs: %v", ids)
printDebug("Status map: %v", statusMap)
for _, id := range ids {
// Check for different possible extensions
extensions := []string{".webp", ".svg"}
var cachedImagePath string
var found bool
for _, ext := range extensions {
filename := id + ext
path := filepath.Join(cacheDir, filename)
if _, err := os.Stat(path); err == nil {
cachedImagePath = "/image_cache/" + filename
found = true
break
}
}
if found {
statusMap[id] = cachedImagePath
} else {
// Image is not ready
statusMap[id] = ""
}
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(statusMap)
}