234 lines
5.7 KiB
Go
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)
|
|
}
|